Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ on:
jobs:
build:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
zig-version: ['0.16.0', '0.17.0']
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
version: ${{ matrix.zig-version }}
- run: zig build test --summary all
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Support for Zig 0.17, alongside 0.16

## [0.9.0] - 2026-09-05

### Changed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ There are multiple options on how to encode struct fields, in order to generate

## Installation

Requires Zig 0.16 or 0.17.

1) Add msgpack.zig as a dependency in your `build.zig.zon`:

```bash
Expand Down
71 changes: 71 additions & 0 deletions src/compat.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Type reflection helpers that work on both Zig 0.16 and 0.17.
//!
//! Zig 0.17 changed `@typeInfo` for containers from a slice of field structs
//! (`fields`, `decls`) to parallel slices (`field_names`, `field_types`,
//! `field_attrs`, `decl_names`). These helpers present the 0.16 shape on both.

const std = @import("std");

const new_type_info = @hasField(std.builtin.Type.Struct, "field_names");

pub const StructField = if (new_type_info) struct {
name: [:0]const u8,
type: type,
default_value_ptr: ?*const anyopaque,
is_comptime: bool,
alignment: ?usize,

pub inline fn defaultValue(comptime sf: StructField) ?sf.type {
const dp: *const sf.type = @ptrCast(@alignCast(sf.default_value_ptr orelse return null));
return dp.*;
}
} else std.builtin.Type.StructField;

pub const UnionField = if (new_type_info) struct {
name: [:0]const u8,
type: type,
alignment: ?usize,
} else std.builtin.Type.UnionField;

pub fn structFields(comptime T: type) []const StructField {
if (!new_type_info) return @typeInfo(T).@"struct".fields;
return comptime blk: {
const info = @typeInfo(T).@"struct";
var fields: [info.field_names.len]StructField = undefined;
for (&fields, info.field_names, info.field_types, info.field_attrs) |*field, name, Field, attrs| {
field.* = .{
.name = name,
.type = Field,
.default_value_ptr = attrs.default_value_ptr,
.is_comptime = attrs.@"comptime",
.alignment = attrs.@"align",
};
}
const final = fields;
break :blk &final;
};
}

pub fn unionFields(comptime T: type) []const UnionField {
if (!new_type_info) return @typeInfo(T).@"union".fields;
return comptime blk: {
const info = @typeInfo(T).@"union";
var fields: [info.field_names.len]UnionField = undefined;
for (&fields, info.field_names, info.field_types, info.field_attrs) |*field, name, Field, attrs| {
field.* = .{ .name = name, .type = Field, .alignment = attrs.@"align" };
}
const final = fields;
break :blk &final;
};
}

pub fn declNames(comptime T: type) []const [:0]const u8 {
if (new_type_info) return @typeInfo(T).@"struct".decl_names;
return comptime blk: {
const decls = @typeInfo(T).@"struct".decls;
var names: [decls.len][:0]const u8 = undefined;
for (&names, decls) |*name, decl| name.* = decl.name;
const final = names;
break :blk &final;
};
}
4 changes: 2 additions & 2 deletions src/float.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ pub fn packFloat(writer: *std.Io.Writer, comptime T: type, value_or_maybe_null:
else => unreachable,
};

const IntType = std.meta.Int(.unsigned, @bitSizeOf(TargetType));
const IntType = @Int(.unsigned, @bitSizeOf(TargetType));
const int_value = @as(IntType, @bitCast(@as(TargetType, @floatCast(value))));
return packHeaderAndInt(writer, header, IntType, int_value);
}

pub fn readFloatValue(reader: *std.Io.Reader, comptime SourceFloat: type, comptime TargetFloat: type) !TargetFloat {
const IntType = std.meta.Int(.unsigned, @bitSizeOf(SourceFloat));
const IntType = @Int(.unsigned, @bitSizeOf(SourceFloat));
const int_value = try takeInt(reader, IntType);

return @floatCast(@as(SourceFloat, @bitCast(int_value)));
Expand Down
13 changes: 7 additions & 6 deletions src/msgpack.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;

const compat = @import("compat.zig");
const NoAllocator = @import("utils.zig").NoAllocator;

pub const getNullSize = @import("null.zig").getNullSize;
Expand Down Expand Up @@ -391,7 +392,7 @@ test "decode a string value larger than the reader buffer" {
// `readSliceShort`, which hands the reader the caller's destination rather
// than asking it to buffer.
const Msg = struct { s: []const u8 };
const long = "a" ** 200;
const long = &@as([200]u8, @splat('a'));
const value = Msg{ .s = long };

var encoded: [256]u8 = undefined;
Expand All @@ -412,7 +413,7 @@ test "skip a value larger than the reader buffer" {
// `error.EndOfStream` even though the bytes were available.
var encoded: [512]u8 = undefined;
var writer = std.Io.Writer.fixed(&encoded);
try packString(&writer, "a" ** 200);
try packString(&writer, &@as([200]u8, @splat('a')));
try packInt(&writer, u8, 42);
const bytes = writer.buffered();

Expand Down Expand Up @@ -584,12 +585,12 @@ test "unpacker readUnion" {

fn assertCovered(comptime T: type, comptime covered: []const []const u8) void {
comptime {
for (@typeInfo(T).@"struct".decls) |decl| {
if (@typeInfo(@TypeOf(@field(T, decl.name))) != .@"fn") continue;
for (compat.declNames(T)) |decl_name| {
if (@typeInfo(@TypeOf(@field(T, decl_name))) != .@"fn") continue;
for (covered) |name| {
if (std.mem.eql(u8, name, decl.name)) break;
if (std.mem.eql(u8, name, decl_name)) break;
} else {
@compileError(@typeName(T) ++ "." ++ decl.name ++
@compileError(@typeName(T) ++ "." ++ decl_name ++
" is not exercised by the smoke test; add a call for it");
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/skip.zig
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ test "skipAny: extension types" {
&[_]u8{ 0xd5, 0x01, 0xff, 0xff }, // fixext2
&[_]u8{ 0xd6, 0x01, 1, 2, 3, 4 }, // fixext4
&[_]u8{ 0xd7, 0x01, 1, 2, 3, 4, 5, 6, 7, 8 }, // fixext8
&[_]u8{ 0xd8, 0x01 } ++ &[_]u8{0xab} ** 16, // fixext16
&[_]u8{ 0xd8, 0x01 } ++ &@as([16]u8, @splat(0xab)), // fixext16
&[_]u8{ 0xc7, 0x03, 0x01, 1, 2, 3 }, // ext8
&[_]u8{ 0xc8, 0x00, 0x03, 0x01, 1, 2, 3 }, // ext16
&[_]u8{ 0xc9, 0x00, 0x00, 0x00, 0x03, 0x01, 1, 2, 3 }, // ext32
Expand Down
19 changes: 8 additions & 11 deletions src/struct.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const compat = @import("compat.zig");
const hdrs = @import("headers.zig");

const NonOptional = @import("utils.zig").NonOptional;
Expand Down Expand Up @@ -61,7 +62,7 @@ pub const default_struct_format = StructFormat{
},
};

fn isStructFieldUsed(field: std.builtin.Type.StructField, value: anytype, opts: StructAsMapOptions) bool {
fn isStructFieldUsed(field: compat.StructField, value: anytype, opts: StructAsMapOptions) bool {
const field_type_info = @typeInfo(field.type);
const field_value = @field(value, field.name);

Expand All @@ -84,7 +85,7 @@ fn isStructFieldUsed(field: std.builtin.Type.StructField, value: anytype, opts:
return true;
}

fn countUsedStructFields(comptime fields: []const std.builtin.Type.StructField, value: anytype, opts: StructAsMapOptions) u16 {
fn countUsedStructFields(comptime fields: []const compat.StructField, value: anytype, opts: StructAsMapOptions) u16 {
var used_field_count: u16 = 0;
inline for (fields) |field| {
if (isStructFieldUsed(field, value, opts)) {
Expand All @@ -99,8 +100,7 @@ fn strPrefix(src: []const u8, len: usize) []const u8 {
}

pub fn packStructAsMap(writer: *std.Io.Writer, comptime T: type, value: T, comptime opts: StructAsMapOptions) !void {
const type_info = @typeInfo(T);
const fields = type_info.@"struct".fields;
const fields = comptime compat.structFields(T);
const FieldEnum = std.meta.FieldEnum(T);

try packMapHeader(writer, countUsedStructFields(fields, value, opts));
Expand Down Expand Up @@ -128,8 +128,7 @@ pub fn packStructAsMap(writer: *std.Io.Writer, comptime T: type, value: T, compt
}

pub fn packStructAsArray(writer: *std.Io.Writer, comptime T: type, value: T, comptime opts: StructAsArrayOptions) !void {
const type_info = @typeInfo(T);
const fields = type_info.@"struct".fields;
const fields = comptime compat.structFields(T);

try packArrayHeader(writer, fields.len);

Expand Down Expand Up @@ -167,11 +166,10 @@ pub fn packStruct(writer: *std.Io.Writer, comptime T: type, value_or_maybe_null:

pub fn unpackStructFromMapBody(reader: *std.Io.Reader, allocator: std.mem.Allocator, comptime T: type, field_count: u16, comptime opts: StructAsMapOptions) !T {
const Type = NonOptional(T);
const type_info = @typeInfo(Type);
const fields = type_info.@"struct".fields;
const fields = comptime compat.structFields(Type);
const FieldEnum = std.meta.FieldEnum(Type);

var fields_seen = std.bit_set.StaticBitSet(fields.len).initEmpty();
var fields_seen = std.bit_set.StaticBitSet(fields.len).empty;

var result: Type = undefined;

Expand Down Expand Up @@ -269,8 +267,7 @@ pub fn unpackStructAsArray(reader: *std.Io.Reader, allocator: std.mem.Allocator,
try unpackArrayHeader(reader, u16);

const Type = NonOptional(T);
const type_info = @typeInfo(Type);
const fields = type_info.@"struct".fields;
const fields = comptime compat.structFields(Type);

if (len != fields.len) {
return error.InvalidFormat;
Expand Down
17 changes: 7 additions & 10 deletions src/union.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const compat = @import("compat.zig");
const hdrs = @import("headers.zig");

const NonOptional = @import("utils.zig").NonOptional;
Expand Down Expand Up @@ -66,8 +67,7 @@ fn strPrefix(src: []const u8, len: usize) []const u8 {
}

pub fn packUnionAsMap(writer: *std.Io.Writer, comptime T: type, value: T, comptime opts: UnionAsMapOptions) !void {
const type_info = @typeInfo(T);
const fields = type_info.@"union".fields;
const fields = comptime compat.unionFields(T);

const TagType = @typeInfo(T).@"union".tag_type.?;

Expand All @@ -92,8 +92,7 @@ pub fn packUnionAsMap(writer: *std.Io.Writer, comptime T: type, value: T, compti
}

pub fn packUnionAsTagged(writer: *std.Io.Writer, comptime T: type, value: T, comptime opts: UnionAsTaggedOptions) !void {
const type_info = @typeInfo(T);
const fields = type_info.@"union".fields;
const fields = comptime compat.unionFields(T);

const TagType = @typeInfo(T).@"union".tag_type.?;

Expand All @@ -102,7 +101,7 @@ pub fn packUnionAsTagged(writer: *std.Io.Writer, comptime T: type, value: T, com
const field_value = @field(value, field.name);
const field_type_info = @typeInfo(field.type);

const field_count = if (field_type_info == .@"struct") field_type_info.@"struct".fields.len else 0;
const field_count = if (field_type_info == .@"struct") comptime compat.structFields(field.type).len else 0;

try packMapHeader(writer, field_count + 1);

Expand All @@ -120,7 +119,7 @@ pub fn packUnionAsTagged(writer: *std.Io.Writer, comptime T: type, value: T, com
}

if (field_type_info == .@"struct") {
inline for (field_type_info.@"struct".fields) |struct_field| {
inline for (comptime compat.structFields(field.type)) |struct_field| {
try packStringLiteral(writer, struct_field.name);
try packAny(writer, @field(field_value, struct_field.name));
}
Expand Down Expand Up @@ -164,8 +163,7 @@ pub fn unpackUnionAsMap(reader: *std.Io.Reader, allocator: std.mem.Allocator, co
}

const Type = NonOptional(T);
const type_info = @typeInfo(Type);
const fields = type_info.@"union".fields;
const fields = comptime compat.unionFields(Type);

var result: Type = undefined;

Expand Down Expand Up @@ -222,8 +220,7 @@ pub fn unpackUnionAsTagged(reader: *std.Io.Reader, allocator: std.mem.Allocator,
}

const Type = NonOptional(T);
const type_info = @typeInfo(Type);
const fields = type_info.@"union".fields;
const fields = comptime compat.unionFields(Type);

const tag_field_name = try unpackStringBorrowed(reader);
if (!eqlLiteral(opts.tag_field, tag_field_name)) {
Expand Down
Loading