From 4c1d361243b0046d45c19dffb3e6e6d524958a3e Mon Sep 17 00:00:00 2001 From: Lukas Lalinsky Date: Sun, 4 Oct 2026 12:41:03 +0200 Subject: [PATCH] Support Zig 0.17 alongside 0.16 Zig 0.17 replaced the `fields` and `decls` slices in `@typeInfo` with parallel `field_names`/`field_types`/`field_attrs`/`decl_names` slices. Add an internal compat module that presents the 0.16 shape on both versions and route struct/union reflection through it. Also replace `std.meta.Int` with `@Int`, `StaticBitSet.initEmpty()` with `.empty`, and the removed `**` operator in tests with `@splat`. CI now runs on 0.16.0 and 0.17.0. --- .github/workflows/build.yml | 6 +++- CHANGELOG.md | 5 +++ README.md | 2 ++ src/compat.zig | 71 +++++++++++++++++++++++++++++++++++++ src/float.zig | 4 +-- src/msgpack.zig | 13 +++---- src/skip.zig | 2 +- src/struct.zig | 19 +++++----- src/union.zig | 17 ++++----- 9 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 src/compat.zig diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3d959a..6f4a423 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b21fa..ad68b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 6043ddd..902ab0c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/compat.zig b/src/compat.zig new file mode 100644 index 0000000..2ca48b4 --- /dev/null +++ b/src/compat.zig @@ -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; + }; +} diff --git a/src/float.zig b/src/float.zig index 0544df1..a476ed2 100644 --- a/src/float.zig +++ b/src/float.zig @@ -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))); diff --git a/src/msgpack.zig b/src/msgpack.zig index 45ddfe9..450d2a9 100644 --- a/src/msgpack.zig +++ b/src/msgpack.zig @@ -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; @@ -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; @@ -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(); @@ -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"); } } diff --git a/src/skip.zig b/src/skip.zig index 20a5ac8..b4145db 100644 --- a/src/skip.zig +++ b/src/skip.zig @@ -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 diff --git a/src/struct.zig b/src/struct.zig index a3fc5a3..a3ce398 100644 --- a/src/struct.zig +++ b/src/struct.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const compat = @import("compat.zig"); const hdrs = @import("headers.zig"); const NonOptional = @import("utils.zig").NonOptional; @@ -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); @@ -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)) { @@ -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)); @@ -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); @@ -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; @@ -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; diff --git a/src/union.zig b/src/union.zig index b76ea08..bef44e3 100644 --- a/src/union.zig +++ b/src/union.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const compat = @import("compat.zig"); const hdrs = @import("headers.zig"); const NonOptional = @import("utils.zig").NonOptional; @@ -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.?; @@ -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.?; @@ -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); @@ -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)); } @@ -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; @@ -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)) {