A simple and efficient MessagePack and MessagePack-RPC library for Zig.
zig-msgpack requires Zig 0.17.0 or later.
Run zig fetch to add zig-msgpack to your build.zig.zon:
# Add from git repository
zig fetch --save git+https://github.com/justinhj/zig-msgpack.git
# Or add a specific tagged release
# Replace `<VERSION>` with the version you want to use
# See: https://github.com/justinhj/zig-msgpack/releases
zig fetch --save https://github.com/justinhj/zig-msgpack/archive/refs/tags/<VERSION>.tar.gzThen add the following to build.zig:
const msgpack = b.dependency("zig_msgpack", .{});
exe.root_module.addImport("zig_msgpack", msgpack.module("zig_msgpack"));- Full MessagePack specification support
- Positive and negative fixints, int 8..64, uint 8..64
- Floating point numbers (float 32, float 64)
- Booleans and nil
- Strings and binary buffers
- Nested arrays and key-value maps
- Extension types (fixext 1..16, ext 8..32)
- One-shot deserialization (
msgpack.unpack) and serialization (msgpack.pack) - High-performance streaming unpacker (
msgpack.Unpacker)- True non-recursive state machine with explicit container frame stack
-
$O(N)$ linear-time parsing across arbitrary chunk boundaries without re-parsing previously received data - Memory-safe teardown: zero memory leaks when deinitializing midway through an incomplete message
- Configurable
max_depthto protect against stack-overflow and DoS attacks from malicious deeply nested payloads
- Flexible in-memory builder (
msgpack.Packer) as well as directWriterpacking functions - MessagePack-RPC support (
msgpack.rpc)- Request, response, and notification serialization
- Typed RPC message parsing (
Request,Response,Notification) projected over unpacked objects without redundant allocations - Transport-agnostic session manager (
RpcSession)
The simplest way to deserialize MessagePack data is to call the msgpack.unpack function.
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Packed MessagePack bytes for: ["hello", 42]
const data = [_]u8{ 0x92, 0xa5, 'h', 'e', 'l', 'l', 'o', 0x2a };
const obj = try msgpack.unpack(allocator, &data);
defer msgpack.freeObject(allocator, obj);
switch (obj) {
.array => |items| {
std.debug.print("Array with {} items:\n", .{items.len});
std.debug.print(" [0] = {s}\n", .{items[0].string});
std.debug.print(" [1] = {}\n", .{items[1].integer});
},
else => {},
}
}
const msgpack = @import("zig_msgpack");
const std = @import("std");msgpack.unpack returns a MsgPackObject, which is a tagged union of all supported MessagePack
types. If the object contains dynamically allocated nodes (such as strings, binary data, arrays, or
maps), call msgpack.freeObject(allocator, obj) to recursively free all associated memory.
The simplest way to serialize a MsgPackObject into bytes is to call msgpack.pack.
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var text = "hello world".*;
const obj = msgpack.MsgPackObject{ .string = &text };
const bytes = try msgpack.pack(allocator, obj);
defer allocator.free(bytes);
std.debug.print("Serialized {} bytes\n", .{bytes.len});
}
const msgpack = @import("zig_msgpack");
const std = @import("std");For building MessagePack payloads incrementally without having to construct a full MsgPackObject
tree first, msgpack.Packer provides a dynamic byte builder:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var packer = msgpack.Packer.init(allocator);
defer packer.deinit();
// Pack an array of 2 elements: ["ping", 1]
try packer.packArrayHeader(2);
try packer.packString("ping");
try packer.packUInt(1);
const slice = packer.getSlice();
std.debug.print("Packed payload len: {}\n", .{slice.len});
}
const msgpack = @import("zig_msgpack");
const std = @import("std");The underlying functions (msgpack.packer.packNil, packInt, packString, packObject, etc.)
also accept any arbitrary Zig Writer directly.
When reading data incrementally from a network socket or stream where messages arrive in chunks,
msgpack.Unpacker buffers input in an internal ring buffer and parses messages using a non-recursive
state machine. It resumes parsing exactly where it left off on each subsequent chunk, guaranteeing
Unpacker.init accepts options to configure buffer size and nesting security limits:
max_buffer_size: Internal ring buffer capacity (default: 1MB).max_depth: Maximum container nesting depth allowed before returningerror.MaxDepthExceeded(default: 128).
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var unpacker = try msgpack.Unpacker.init(allocator, .{
.max_buffer_size = 64 * 1024, // 64KB
.max_depth = 64, // Limit recursion depth
});
defer unpacker.deinit();
// Feed chunks into unpacker as they arrive from socket/stream
try unpacker.feed(&[_]u8{ 0x92, 0xa4 }); // array header + start of string
try unpacker.feed(&[_]u8{ 't', 'e', 's', 't', 0x2a }); // rest of string + int 42
while (unpacker.next()) |obj| {
defer msgpack.freeObject(allocator, obj);
std.debug.print("Unpacked complete object: {}\n", .{@tagName(obj)});
} else |err| switch (err) {
error.NoMessage, error.Incomplete => {
// Need more data from socket/stream
},
else => return err,
}
}
const msgpack = @import("zig_msgpack");
const std = @import("std");For long-running loops (such as socket clients or daemon event loops), you can achieve maximum throughput and avoid memory fragmentation by using two allocators:
- A persistent general-purpose allocator (GPA) for the
Unpacker's internal ring buffer and parser infrastructure. - A transient
std.heap.ArenaAllocatorpassed tounpacker.nextAlloc(arena.allocator())to decode message trees.
Calling arena.reset(.retain_capacity) at the end of each message cycle frees all message strings, arrays, and maps in
// Persistent stream buffer lives in GPA
var unpacker = try msgpack.Unpacker.init(gpa, .{});
defer unpacker.deinit();
// Transient arena for message objects
var cycle_arena = std.heap.ArenaAllocator.init(gpa);
defer cycle_arena.deinit();
while (connected) {
// Reset arena at the end of each iteration; .retain_capacity keeps memory
// pages warm and avoids OS syscalls in steady state
defer _ = cycle_arena.reset(.retain_capacity);
const alloc = cycle_arena.allocator();
// 1. Pack request using the arena
var packer = msgpack.Packer.init(alloc);
// ... pack and send ...
// 2. Unpack response directly into the arena
const obj = try unpacker.nextAlloc(alloc);
handleMessage(obj);
// No need to call msgpack.freeObject() or packer.deinit()!
// All message memory is wiped automatically in O(1) time.
}zig-msgpack includes helpers and data structures for building MessagePack-RPC clients and
servers (such as communicating with Neovim over a Unix socket or pipe):
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var session = msgpack.RpcSession.init(allocator);
defer session.deinit();
var packer = msgpack.Packer.init(allocator);
defer packer.deinit();
// Pack an RPC request: method "nvim_eval", params ["2 + 2"]
var expr = "2 + 2".*;
const params = [_]msgpack.MsgPackObject{
.{ .string = &expr },
};
const msgid = try session.packRequest(&packer, "nvim_eval", ¶ms);
_ = msgid;
// Parse incoming RPC message
const obj = try msgpack.unpack(allocator, packer.getSlice());
defer msgpack.freeObject(allocator, obj);
const rpc_msg = try msgpack.rpc.parseMessage(obj);
switch (rpc_msg) {
.request => |req| {
std.debug.print("RPC Request #{}: {s}\n", .{ req.msgid, req.method });
},
.response => |res| {
std.debug.print("RPC Response #{}\n", .{ res.msgid });
},
.notification => |notif| {
std.debug.print("RPC Notification: {s}\n", .{ notif.method });
},
}
}
const msgpack = @import("zig_msgpack");
const std = @import("std");The repository includes runnable examples in the examples/ directory:
# Dump and pretty-print a MessagePack file
zig build run -- input.msgpack
# Or run the dumpmsgpack example directly
zig build run-dumpmsgpack -- input.msgpack
# Run the Neovim RPC example (requires a running Neovim instance listening on a socket)
zig build run-hellonvim -- /tmp/nvim.sockTo compile all example executables into zig-out/bin/:
zig build examples