diff --git a/build/0.16.zig b/build/0.16.zig index 9dc29152..f672acc7 100644 --- a/build/0.16.zig +++ b/build/0.16.zig @@ -1,7 +1,5 @@ const std = @import("std"); const Build = std.Build; -const ChildProcess = std.process.Child; - const log = std.log.scoped(.For_0_16_0); const version = "16"; @@ -13,25 +11,31 @@ pub fn build(b: *Build) void { // get target and optimize const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const io = b.graph.io; var lazy_path = b.path(relative_path); const full_path = lazy_path.getPath(b); // open dir - var dir = std.fs.openDirAbsolute(full_path, .{ .iterate = true }) catch |err| { - log.err("open 15 path failed, err is {}", .{err}); + var dir = std.Io.Dir.openDirAbsolute(io, full_path, .{ .iterate = true }) catch |err| { + log.err("open 16 path failed, err is {}", .{err}); std.process.exit(1); }; - defer dir.close(); + defer dir.close(io); // make a iterate for release ath var iterate = dir.iterate(); - while (iterate.next()) |val| { - if (val) |entry| { + while (iterate.next(io) catch |err| { + log.err("iterate examples_path failed, err is {}", .{err}); + std.process.exit(1); + }) |entry| { // get the entry name, entry can be file or directory - const output_name = std.mem.trimRight(u8, entry.name, ".zig"); + const output_name = if (std.mem.endsWith(u8, entry.name, ".zig")) + entry.name[0 .. entry.name.len - ".zig".len] + else + entry.name; if (entry.kind == .file) { // connect path const path = std.fs.path.join(b.allocator, &[_][]const u8{ relative_path, entry.name }) catch |err| { @@ -48,11 +52,11 @@ pub fn build(b: *Build) void { .optimize = optimize, }), }); - exe.linkLibC(); + exe.root_module.linkSystemLibrary("c", .{}); if (exe.root_module.resolved_target.?.result.os.tag == .windows and std.mem.eql(u8, "echo_tcp_server.zig", entry.name)) { std.log.info("link ws2_32 for {s}", .{entry.name}); - exe.linkSystemLibrary("ws2_32"); + exe.root_module.linkSystemLibrary("ws2_32", .{}); } // add to default install b.installArtifact(exe); @@ -75,8 +79,6 @@ pub fn build(b: *Build) void { } else if (entry.kind == .directory) { // build child process - var child = ChildProcess.init(&args, b.allocator); - // build cwd const cwd = std.fs.path.join(b.allocator, &[_][]const u8{ full_path, @@ -87,25 +89,19 @@ pub fn build(b: *Build) void { }; // open entry dir - const entry_dir = std.fs.openDirAbsolute(cwd, .{}) catch unreachable; - entry_dir.access("build.zig", .{}) catch { + const entry_dir = std.Io.Dir.openDirAbsolute(io, cwd, .{}) catch unreachable; + defer entry_dir.close(io); + + entry_dir.access(io, "build.zig", .{}) catch { log.err("not found build.zig in path {s}", .{cwd}); std.process.exit(1); }; - // set child cwd - // this api maybe changed in the future - child.cwd = cwd; - - // spawn and wait child process - _ = child.spawnAndWait() catch unreachable; + var child = std.process.spawn(io, .{ + .argv = &args, + .cwd = .{ .path = cwd }, + }) catch unreachable; + _ = child.wait(io) catch unreachable; } - } else { - // Stop endless loop - break; - } - } else |err| { - log.err("iterate examples_path failed, err is {}", .{err}); - std.process.exit(1); } } diff --git a/course/.vitepress/sidebar.ts b/course/.vitepress/sidebar.ts index f6aa15b6..945ed777 100644 --- a/course/.vitepress/sidebar.ts +++ b/course/.vitepress/sidebar.ts @@ -220,6 +220,14 @@ export default [ text: "版本说明", collapsed: true, items: [ + { + text: "0.16.0 升级指南", + link: "/update/upgrade-0.16.0", + }, + { + text: "0.16.0 版本说明", + link: "/update/0.16.0-description", + }, { text: "0.15.1 升级指南", link: "/update/upgrade-0.15.1", diff --git a/course/.vitepress/theme/config.ts b/course/.vitepress/theme/config.ts index 49d49919..08af1f81 100644 --- a/course/.vitepress/theme/config.ts +++ b/course/.vitepress/theme/config.ts @@ -1,3 +1,3 @@ -const version: string = "0.15.1"; +const version: string = "0.16.0"; export { version }; diff --git a/course/advanced/atomic.md b/course/advanced/atomic.md index 2a88ec70..64ebb469 100644 --- a/course/advanced/atomic.md +++ b/course/advanced/atomic.md @@ -148,5 +148,5 @@ outline: deep - **Mutex**:适合等待时间不确定或较长的场景,会让出 CPU 给其他线程 ::: warning ⚠️ 警告 -不当使用自旋等待会导致 CPU 资源浪费。如果等待时间较长或不确定,应使用 `std.Thread.Mutex` 等同步原语。 +不当使用自旋等待会导致 CPU 资源浪费。如果等待时间较长或不确定,应使用 `std.Io.Mutex` 这类会让出 CPU 的同步原语。 ::: diff --git a/course/advanced/interact-with-c.md b/course/advanced/interact-with-c.md index 9a2352ad..b14fafb9 100644 --- a/course/advanced/interact-with-c.md +++ b/course/advanced/interact-with-c.md @@ -44,7 +44,7 @@ C 语言共享类型通常是通过引入头文件实现,这点在 zig 中可 ::: info 🅿️ 提示 -注意:为了构建这个,我们需要引入 `libc`,可以在 `build.zig` 中添加 `exe.linkLibC` 函数,`exe` 是默认的构建变量。 +注意:为了构建这个,我们需要引入 `libc`。在 Zig 0.16 的构建脚本中,可以让对应模块链接 C 标准库,例如 `exe.root_module.linkSystemLibrary("c", .{})`。 或者我们可以手动执行构建:`zig build-exe source.zig -lc` diff --git a/course/advanced/memory_manage.md b/course/advanced/memory_manage.md index 50549b2a..a0fe7e98 100644 --- a/course/advanced/memory_manage.md +++ b/course/advanced/memory_manage.md @@ -42,7 +42,7 @@ outline: deep 这是一个用于调试的分配器,现阶段适用于在调试模式下使用该分配器,它的性能并不高! -这个分配器的目的不是为了性能,而是为了安全,它支持线程安全,安全检查,检查是否存在泄露等特性,这些特性均可手动配置是否开启。 +这个分配器的目的不是为了性能,而是为了安全。默认配置下它支持线程安全、安全检查、泄漏检测等能力,并且这些特性都可以按需配置。 <<<@/code/release/memory_manager.zig#DebugAllocator @@ -66,7 +66,7 @@ outline: deep ## `FixedBufferAllocator` -这个分配器是固定大小的内存缓冲区,无法扩容,常常在你需要缓冲某些东西时使用,注意默认情况下它不是线程安全的,但是存在着变体 [`ThreadSafeAllocator`](https://ziglang.org/documentation/master/std/#std.heap.ThreadSafeAllocator),使用 `ThreadSafeAllocator` 包裹一下它即可。 +这个分配器是固定大小的内存缓冲区,无法扩容,常常在你需要缓冲某些东西时使用。注意默认情况下它不是线程安全的;而在 Zig 0.16 中,`ThreadSafeAllocator` 已被移除。如果你需要跨线程共享同一个 `FixedBufferAllocator`,应当像示例那样在外层自行加锁,或者直接改用更适合并发场景的 `SmpAllocator`。 ::: code-group @@ -84,7 +84,7 @@ outline: deep ## `c_allocator` -这是纯粹的 C 的 `malloc`,它会直接尝试调用 C 库的内存分配,使用它需要在 `build.zig` 中添加上 `linkLibC` 功能: +这是纯粹的 C 的 `malloc`,它会直接尝试调用 C 库的内存分配。使用它时,需要在 `build.zig` 中让对应模块链接 libc,例如 `exe.root_module.linkSystemLibrary("c", .{})`: <<<@/code/release/memory_manager.zig#c_allocator diff --git a/course/advanced/result-location.md b/course/advanced/result-location.md index 80d28518..4c13a1c7 100644 --- a/course/advanced/result-location.md +++ b/course/advanced/result-location.md @@ -106,9 +106,9 @@ outline: deep <<<@/code/release/result-location.zig#stdlib_arraylist -### GeneralPurposeAllocator +### DebugAllocator -<<<@/code/release/result-location.zig#stdlib_gpa +<<<@/code/release/result-location.zig#stdlib_debug_allocator ## 字段和声明不可重名 diff --git a/course/advanced/type_cast.md b/course/advanced/type_cast.md index ed7465a4..64c25561 100644 --- a/course/advanced/type_cast.md +++ b/course/advanced/type_cast.md @@ -116,7 +116,7 @@ undefined 是一个神奇的值,它可以赋值给所有类型,代表这个 - [`@floatFromInt`](https://ziglang.org/documentation/master/#floatFromInt) 将整数显式强制转换为浮点数 - [`@intCast`](https://ziglang.org/documentation/master/#intCast) 在不同的整数类型中显式强制转换 - [`@intFromBool`](https://ziglang.org/documentation/master/#intFromBool) 将 `true` 转换为 `1`,`false` 转换为 `0` -- [`@intFromEnum`](https://ziglang.org/documentation/master/#intFromEnum) 根据整数值获取对应的联合标记或者枚举值 +- [`@intFromEnum`](https://ziglang.org/documentation/master/#intFromEnum) 获取枚举值或联合标记对应的整数值 - [`@intFromError`](https://ziglang.org/documentation/master/#intFromError) 获取对应错误的整数值 - [`@intFromFloat`](https://ziglang.org/documentation/master/#intFromFloat) 获取浮点数的整数部分 - [`@intFromPtr`](https://ziglang.org/documentation/master/#intFromPtr) 获取指针指向的地址(整数 `usize`),这在嵌入式开发和内核开发时很常用 diff --git a/course/basic/advanced_type/pointer.md b/course/basic/advanced_type/pointer.md index 65ef09fd..ae3c02d7 100644 --- a/course/basic/advanced_type/pointer.md +++ b/course/basic/advanced_type/pointer.md @@ -112,7 +112,7 @@ Zig 支持指针的加减运算,但建议在进行运算前,将指针转换 <<<@/code/release/pointer.zig#st_pointer -以上代码编译需要额外链接 libc,你只需要在你的 `build.zig` 中添加 `exe.linkLibC();` 即可。 +以上代码编译需要额外链接 `libc`。在 Zig 0.16 的构建脚本中,可以让对应模块链接 C 标准库,例如 `exe.root_module.linkSystemLibrary("c", .{})`。 ::: diff --git a/course/code/16/build_system/basic/src/main.zig b/course/code/16/build_system/basic/src/main.zig index 96d9a362..bf45938f 100644 --- a/course/code/16/build_system/basic/src/main.zig +++ b/course/code/16/build_system/basic/src/main.zig @@ -1,14 +1,15 @@ const std = @import("std"); -pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) +pub fn main(init: std.process.Init) !void { + const io = init.io; + // `std.debug.print` 会输出到标准错误。 std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); // stdout is for the actual output of your application, for example if you // are implementing gzip, then only the compressed bytes should be sent to // stdout, not any debugging messages. var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; try stdout.print("Run `zig build test` to run the tests.\n", .{}); diff --git a/course/code/16/build_system/build.zig b/course/code/16/build_system/build.zig index 5909232b..2cc7cd76 100644 --- a/course/code/16/build_system/build.zig +++ b/course/code/16/build_system/build.zig @@ -1,10 +1,9 @@ const std = @import("std"); -const ChildProcess = std.process.Child; - const args = [_][]const u8{ "zig", "build" }; pub fn build(b: *std.Build) !void { const optimize = b.standardOptimizeOption(.{}); + const io = b.graph.io; // #region crossTarget // 构建一个target const target_query = std.Target.Query{ @@ -36,57 +35,51 @@ pub fn build(b: *std.Build) !void { b.installArtifact(exe); - const full_path = try std.process.getCwdAlloc(b.allocator); + const full_path = try std.process.currentPathAlloc(io, b.allocator); + defer b.allocator.free(full_path); - var dir = std.fs.openDirAbsolute(full_path, .{ .iterate = true }) catch |err| { + var dir = std.Io.Dir.openDirAbsolute(io, full_path, .{ .iterate = true }) catch |err| { std.log.err("open path failed {s}, err is {}", .{ full_path, err }); std.process.exit(1); }; - defer dir.close(); + defer dir.close(io); var iterate = dir.iterate(); - while (iterate.next()) |val| { - if (val) |entry| { - // get the entry name, entry can be file or directory - const name = entry.name; - if (entry.kind == .directory) { - if (eqlu8(name, ".zig-cache") or eqlu8(name, "zig-out") or eqlu8(name, "zig-cache")) - continue; - - // build child process - var child = ChildProcess.init(&args, b.allocator); - - // build cwd - const cwd = std.fs.path.join(b.allocator, &[_][]const u8{ - full_path, - name, - }) catch |err| { - std.log.err("fmt path failed, err is {}", .{err}); - std.process.exit(1); - }; - - // open entry dir - const entry_dir = std.fs.openDirAbsolute(cwd, .{}) catch unreachable; - entry_dir.access("build.zig", .{}) catch { - std.log.err("not found build.zig in path {s}", .{cwd}); - std.process.exit(1); - }; - - // set child cwd - // this api maybe changed in the future - child.cwd = cwd; - - // spawn and wait child process - _ = child.spawnAndWait() catch unreachable; - } - } else { - // Stop endless loop - break; - } - } else |err| { + while (iterate.next(io) catch |err| { std.log.err("iterate examples_path failed, err is {}", .{err}); std.process.exit(1); + }) |entry| { + // get the entry name, entry can be file or directory + const name = entry.name; + if (entry.kind == .directory) { + if (eqlu8(name, ".zig-cache") or eqlu8(name, "zig-out") or eqlu8(name, "zig-cache")) + continue; + + // build cwd + const cwd = std.fs.path.join(b.allocator, &[_][]const u8{ + full_path, + name, + }) catch |err| { + std.log.err("fmt path failed, err is {}", .{err}); + std.process.exit(1); + }; + + // open entry dir + const entry_dir = std.Io.Dir.openDirAbsolute(io, cwd, .{}) catch unreachable; + defer entry_dir.close(io); + + entry_dir.access(io, "build.zig", .{}) catch { + std.log.err("not found build.zig in path {s}", .{cwd}); + std.process.exit(1); + }; + + var child = std.process.spawn(io, .{ + .argv = &args, + .cwd = .{ .path = cwd }, + }) catch unreachable; + _ = child.wait(io) catch unreachable; + } } } diff --git a/course/code/16/build_system/cli/src/main.zig b/course/code/16/build_system/cli/src/main.zig index 2e81a719..947ed515 100644 --- a/course/code/16/build_system/cli/src/main.zig +++ b/course/code/16/build_system/cli/src/main.zig @@ -1,14 +1,15 @@ const std = @import("std"); -pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) +pub fn main(init: std.process.Init) !void { + const io = init.io; + // `std.debug.print` 会输出到标准错误。 std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); // stdout is for the actual output of your application, for example if you // are implementing gzip, then only the compressed bytes should be sent to // stdout, not any debugging messages. var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; try stdout.print("Run `zig build test` to run the tests.\n", .{}); diff --git a/course/code/16/build_system/lib/build.zig b/course/code/16/build_system/lib/build.zig index e5edc582..2d0b4020 100644 --- a/course/code/16/build_system/lib/build.zig +++ b/course/code/16/build_system/lib/build.zig @@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void { }), }); - exe.linkLibrary(lib); + exe.root_module.linkLibrary(lib); b.installArtifact(exe); } diff --git a/course/code/16/build_system/lib/src/main.zig b/course/code/16/build_system/lib/src/main.zig index 734464bc..34185e55 100644 --- a/course/code/16/build_system/lib/src/main.zig +++ b/course/code/16/build_system/lib/src/main.zig @@ -1,14 +1,15 @@ const std = @import("std"); -pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) +pub fn main(init: std.process.Init) !void { + const io = init.io; + // `std.debug.print` 会输出到标准错误。 std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); // stdout is for the actual output of your application, for example if you // are implementing gzip, then only the compressed bytes should be sent to // stdout, not any debugging messages. var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; try stdout.print("Run `zig build test` to run the tests.\n", .{}); diff --git a/course/code/16/build_system/step/src/main.zig b/course/code/16/build_system/step/src/main.zig index 734464bc..34185e55 100644 --- a/course/code/16/build_system/step/src/main.zig +++ b/course/code/16/build_system/step/src/main.zig @@ -1,14 +1,15 @@ const std = @import("std"); -pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) +pub fn main(init: std.process.Init) !void { + const io = init.io; + // `std.debug.print` 会输出到标准错误。 std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); // stdout is for the actual output of your application, for example if you // are implementing gzip, then only the compressed bytes should be sent to // stdout, not any debugging messages. var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; try stdout.print("Run `zig build test` to run the tests.\n", .{}); diff --git a/course/code/16/build_system/system_lib/build.zig b/course/code/16/build_system/system_lib/build.zig index bf986458..55ef0097 100644 --- a/course/code/16/build_system/system_lib/build.zig +++ b/course/code/16/build_system/system_lib/build.zig @@ -20,13 +20,13 @@ pub fn build(b: *std.Build) void { if (target.result.os.tag == .windows) // 连接到系统的 ole32 - exe.linkSystemLibrary("ole32") + exe.root_module.linkSystemLibrary("ole32", .{}) else // 链接到系统的 libz - exe.linkSystemLibrary("z"); + exe.root_module.linkSystemLibrary("z", .{}); // 链接到 libc - exe.linkLibC(); + exe.root_module.linkSystemLibrary("c", .{}); b.installArtifact(exe); } diff --git a/course/code/16/build_system/test/src/main.zig b/course/code/16/build_system/test/src/main.zig index 96d9a362..bf45938f 100644 --- a/course/code/16/build_system/test/src/main.zig +++ b/course/code/16/build_system/test/src/main.zig @@ -1,14 +1,15 @@ const std = @import("std"); -pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) +pub fn main(init: std.process.Init) !void { + const io = init.io; + // `std.debug.print` 会输出到标准错误。 std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); // stdout is for the actual output of your application, for example if you // are implementing gzip, then only the compressed bytes should be sent to // stdout, not any debugging messages. var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; try stdout.print("Run `zig build test` to run the tests.\n", .{}); diff --git a/course/code/16/build_system/tinytetris/build.zig b/course/code/16/build_system/tinytetris/build.zig index ba101e52..dd977698 100644 --- a/course/code/16/build_system/tinytetris/build.zig +++ b/course/code/16/build_system/tinytetris/build.zig @@ -26,17 +26,17 @@ pub fn build(b: *std.Build) void { // 源代码路径(相对于build.zig) // 传递的 flags // 多个 C 源代码文件可以使用 addCSourceFiles - exe.addCSourceFile(.{ + exe.root_module.addCSourceFile(.{ .file = b.path("src/main.cc"), .flags = &.{}, }); - // 链接C++ 标准库 - // 同理对于 C 标准库可以使用 linkLibC - exe.linkLibCpp(); + // 链接 C++ 标准库 + // 同理对于 C 标准库可以使用 `exe.root_module.linkSystemLibrary("c", .{})` + exe.root_module.linkSystemLibrary("c++", .{}); // 链接系统库 ncurses - exe.linkSystemLibrary("ncurses"); + exe.root_module.linkSystemLibrary("ncurses", .{}); // 添加到顶级 install step 中作为依赖 b.installArtifact(exe); diff --git a/course/code/16/hello_world.zig b/course/code/16/hello_world.zig index 9c1a521b..35a6ec9f 100644 --- a/course/code/16/hello_world.zig +++ b/course/code/16/hello_world.zig @@ -1,12 +1,13 @@ -pub fn main() !void { +pub fn main(init: std.process.Init) !void { try One.main(); - try Two.main(); - try Three.main(); + try Two.main(init.io); + try Three.main(init.io); } +const std = @import("std"); + const One = struct { // #region one - const std = @import("std"); pub fn main() !void { std.debug.print("Hello, World!\n", .{}); } @@ -15,14 +16,13 @@ const One = struct { const Two = struct { // #region two - const std = @import("std"); - pub fn main() !void { + pub fn main(io: std.Io) !void { var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; var stderr_buffer: [1024]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer); const stderr = &stderr_writer.interface; try stdout.print("Hello {s}!\n", .{"out"}); @@ -35,27 +35,23 @@ const Two = struct { const Three = struct { // #region three - const std = @import("std"); - pub fn main() !void { + pub fn main(io: std.Io) !void { // 定义两个缓冲区 var stdout_buffer: [1024]u8 = undefined; // [!code focus] var stderr_buffer: [1024]u8 = undefined; // [!code focus] // 获取writer句柄// [!code focus] - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; // 获取writer句柄// [!code focus] - var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer); const stderr = &stderr_writer.interface; // 通过句柄写入buffer// [!code focus] try stdout.print("Hello {s}!\n", .{"out"}); // [!code focus] try stderr.print("Hello {s}!\n", .{"err"}); // [!code focus] - try stdout.flush(); - try stderr.flush(); - // 尝试刷新buffer// [!code focus] try stdout.flush(); // [!code focus] try stderr.flush(); // [!code focus] diff --git a/course/code/16/import_dependency_build/build.zig b/course/code/16/import_dependency_build/build.zig index 2e1e3913..c646ed7f 100644 --- a/course/code/16/import_dependency_build/build.zig +++ b/course/code/16/import_dependency_build/build.zig @@ -1,60 +1,53 @@ const std = @import("std"); -const ChildProcess = std.process.Child; - const args = [_][]const u8{ "zig", "build" }; pub fn build(b: *std.Build) !void { - const full_path = try std.process.getCwdAlloc(b.allocator); + const io = b.graph.io; + const full_path = try std.process.currentPathAlloc(io, b.allocator); + defer b.allocator.free(full_path); - var dir = std.fs.openDirAbsolute(full_path, .{ .iterate = true }) catch |err| { + var dir = std.Io.Dir.openDirAbsolute(io, full_path, .{ .iterate = true }) catch |err| { std.log.err("open path failed {s}, err is {}", .{ full_path, err }); std.process.exit(1); }; - defer dir.close(); + defer dir.close(io); var iterate = dir.iterate(); - while (iterate.next()) |val| { - if (val) |entry| { - // get the entry name, entry can be file or directory - const name = entry.name; - if (entry.kind == .directory) { - if (eqlu8(name, ".zig-cache") or eqlu8(name, "zig-out") or eqlu8(name, "zig-cache")) - continue; - - // build child process - var child = ChildProcess.init(&args, b.allocator); - - // build cwd - const cwd = std.fs.path.join(b.allocator, &[_][]const u8{ - full_path, - name, - }) catch |err| { - std.log.err("fmt path failed, err is {}", .{err}); - std.process.exit(1); - }; - - // open entry dir - const entry_dir = std.fs.openDirAbsolute(cwd, .{}) catch unreachable; - entry_dir.access("build.zig", .{}) catch { - std.log.err("not found build.zig in path {s}", .{cwd}); - std.process.exit(1); - }; - - // set child cwd - // this api maybe changed in the future - child.cwd = cwd; - - // spawn and wait child process - _ = child.spawnAndWait() catch unreachable; - } - } else { - // Stop endless loop - break; - } - } else |err| { + while (iterate.next(io) catch |err| { std.log.err("iterate examples_path failed, err is {}", .{err}); std.process.exit(1); + }) |entry| { + // get the entry name, entry can be file or directory + const name = entry.name; + if (entry.kind == .directory) { + if (eqlu8(name, ".zig-cache") or eqlu8(name, "zig-out") or eqlu8(name, "zig-cache")) + continue; + + // build cwd + const cwd = std.fs.path.join(b.allocator, &[_][]const u8{ + full_path, + name, + }) catch |err| { + std.log.err("fmt path failed, err is {}", .{err}); + std.process.exit(1); + }; + + // open entry dir + const entry_dir = std.Io.Dir.openDirAbsolute(io, cwd, .{}) catch unreachable; + defer entry_dir.close(io); + + entry_dir.access(io, "build.zig", .{}) catch { + std.log.err("not found build.zig in path {s}", .{cwd}); + std.process.exit(1); + }; + + var child = std.process.spawn(io, .{ + .argv = &args, + .cwd = .{ .path = cwd }, + }) catch unreachable; + _ = child.wait(io) catch unreachable; + } } } diff --git a/course/code/16/import_vcpkg/build.zig b/course/code/16/import_vcpkg/build.zig index bb4a649c..7475753a 100644 --- a/course/code/16/import_vcpkg/build.zig +++ b/course/code/16/import_vcpkg/build.zig @@ -16,13 +16,13 @@ const Build = struct { }); // #region c_import // 增加 include 搜索目录 - exe.addIncludePath(.{ .cwd_relative = "D:\\vcpkg\\installed\\windows-x64\\include" }); + exe.root_module.addIncludePath(.{ .cwd_relative = "D:\\vcpkg\\installed\\windows-x64\\include" }); // 增加 lib 搜索目录 - exe.addLibraryPath(.{ .cwd_relative = "D:\\vcpkg\\installed\\windows-x64\\lib" }); + exe.root_module.addLibraryPath(.{ .cwd_relative = "D:\\vcpkg\\installed\\windows-x64\\lib" }); // 链接标准c库 - exe.linkLibC(); + exe.root_module.linkSystemLibrary("c", .{}); // 链接第三方库gsl - exe.linkSystemLibrary("gsl"); + exe.root_module.linkSystemLibrary("gsl", .{}); // #endregion c_import b.installArtifact(exe); diff --git a/course/code/16/memory_manager.zig b/course/code/16/memory_manager.zig index eec3958f..52ae7f16 100644 --- a/course/code/16/memory_manager.zig +++ b/course/code/16/memory_manager.zig @@ -86,16 +86,19 @@ const ThreadSafeFixedBufferAllocator = struct { // 获取内存allocator const allocator = fba.allocator(); - // 使用 ThreadSafeAllocator 包裹, 你需要设置使用的内存分配器,还可以配置使用的mutex - var thread_safe_fba = std.heap.ThreadSafeAllocator{ .child_allocator = allocator }; + // Zig 0.16 移除了 ThreadSafeAllocator。 + // 如果需要在线程间共享 FixedBufferAllocator,需要自行保护临界区。 + var mutex: std.atomic.Mutex = .unlocked; - // 获取线程安全的内存allocator - const thread_safe_allocator = thread_safe_fba.allocator(); + while (!mutex.tryLock()) { + std.atomic.spinLoopHint(); + } + defer mutex.unlock(); // 申请内存 - const memory = try thread_safe_allocator.alloc(u8, 100); + const memory = try allocator.alloc(u8, 100); // 释放内存 - defer thread_safe_allocator.free(memory); + defer allocator.free(memory); } // #endregion ThreadSafeFixedBufferAllocator }; @@ -129,11 +132,11 @@ const ArenaAllocator = struct { pub fn main() !void { // 使用模型,一定要是变量,不能是常量 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; // 拿到一个allocator const allocator = gpa.allocator(); - // defer 用于执行general_purpose_allocator善后工作 + // defer 用于执行 debug allocator 善后工作 defer { const deinit_status = gpa.deinit(); diff --git a/course/code/16/package_management_importer/src/main.zig b/course/code/16/package_management_importer/src/main.zig index ef0882e1..5c0df21e 100644 --- a/course/code/16/package_management_importer/src/main.zig +++ b/course/code/16/package_management_importer/src/main.zig @@ -2,9 +2,10 @@ const std = @import("std"); const pe = @import("path_exporter"); const te = @import("tarball_exporter"); -pub fn main() !void { +pub fn main(init: std.process.Init) !void { + const io = init.io; var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; const str2: te.Str = .{ .str = "2" }; diff --git a/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/build.zig b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/build.zig new file mode 100644 index 00000000..27d76ae5 --- /dev/null +++ b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/build.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const Build = std.Build; +const Module = Build.Module; +const OptimizeMode = std.builtin.OptimizeMode; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const msgpack = b.addModule("msgpack", .{ + .root_source_file = b.path(b.pathJoin(&.{ "src", "msgpack.zig" })), + }); + + generateDocs(b, optimize, target); + + const test_step = b.step("test", "Run unit tests"); + + const msgpack_unit_tests = if (builtin.zig_version.minor == 14) b.addTest(.{ + .root_source_file = b.path(b.pathJoin(&.{ "src", "test.zig" })), + .target = target, + .optimize = optimize, + }) else b.addTest(.{ + .root_module = b.addModule("test", .{ + .root_source_file = b.path(b.pathJoin(&.{ "src", "test.zig" })), + .target = target, + .optimize = optimize, + }), + }); + msgpack_unit_tests.root_module.addImport("msgpack", msgpack); + const run_msgpack_tests = b.addRunArtifact(msgpack_unit_tests); + test_step.dependOn(&run_msgpack_tests.step); +} + +fn generateDocs(b: *Build, optimize: OptimizeMode, target: Build.ResolvedTarget) void { + const lib = if (builtin.zig_version.minor == 14) b.addObject(.{ + .name = "zig-msgpack", + .root_source_file = b.path(b.pathJoin(&.{ "src", "msgpack.zig" })), + .target = target, + .optimize = optimize, + }) else b.addObject(.{ + .name = "zig-msgpack", + .root_module = b.addModule("msgpack", .{ + .root_source_file = b.path(b.pathJoin(&.{ "src", "msgpack.zig" })), + .target = target, + .optimize = optimize, + }), + }); + + const docs_step = b.step("docs", "Emit docs"); + + const docs_install = b.addInstallDirectory(.{ + .source_dir = lib.getEmittedDocs(), + .install_dir = .prefix, + .install_subdir = "docs", + }); + + docs_step.dependOn(&docs_install.step); +} diff --git a/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/build.zig.zon b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/build.zig.zon new file mode 100644 index 00000000..0b1e9979 --- /dev/null +++ b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .zig_msgpack, + .version = "0.0.8", + .fingerprint = 0x14a3e10e78eefb7a, + .minimum_zig_version = "0.14.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/src/msgpack.zig b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/src/msgpack.zig new file mode 100644 index 00000000..15028c2f --- /dev/null +++ b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/src/msgpack.zig @@ -0,0 +1,1688 @@ +//! MessagePack implementation with zig +//! https://msgpack.org/ + +const std = @import("std"); +const builtin = @import("builtin"); + +const current_zig = builtin.zig_version; +const Allocator = std.mem.Allocator; +const comptimePrint = std.fmt.comptimePrint; +const native_endian = builtin.cpu.arch.endian(); + +const big_endian = switch (current_zig.minor) { + 11 => std.builtin.Endian.Big, + 12, 13, 14, 15, 16 => std.builtin.Endian.big, + else => @compileError("not support current version zig"), +}; +const little_endian = switch (current_zig.minor) { + 11 => std.builtin.Endian.Little, + 12, 13, 14, 15, 16 => std.builtin.Endian.little, + else => @compileError("not support current version zig"), +}; + +/// the Str Type +pub const Str = struct { + str: []const u8, + + /// get Str values + pub fn value(self: Str) []const u8 { + return self.str; + } +}; + +/// this is for encode str in struct +pub fn wrapStr(str: []const u8) Str { + return Str{ .str = str }; +} + +/// the Bin Type +pub const Bin = struct { + bin: []u8, + + /// get bin values + pub fn value(self: Bin) []u8 { + return self.bin; + } +}; + +/// this is wrapping for bin +pub fn wrapBin(bin: []u8) Bin { + return Bin{ .bin = bin }; +} + +/// the EXT Type +pub const EXT = struct { + type: i8, + data: []u8, +}; + +/// t is type, data is data +pub fn wrapEXT(t: i8, data: []u8) EXT { + return EXT{ + .type = t, + .data = data, + }; +} + +/// the Timestamp Type +/// Represents an instantaneous point on the time-line in the world +/// that is independent from time zones or calendars. +/// Maximum precision is nanoseconds. +pub const Timestamp = struct { + /// seconds since 1970-01-01 00:00:00 UTC + seconds: i64, + /// nanoseconds (0-999999999) + nanoseconds: u32, + + /// Create a new timestamp + pub fn new(seconds: i64, nanoseconds: u32) Timestamp { + return Timestamp{ + .seconds = seconds, + .nanoseconds = nanoseconds, + }; + } + + /// Create timestamp from seconds only (nanoseconds = 0) + pub fn fromSeconds(seconds: i64) Timestamp { + return Timestamp{ + .seconds = seconds, + .nanoseconds = 0, + }; + } + + /// Get total seconds as f64 (including fractional nanoseconds) + pub fn toFloat(self: Timestamp) f64 { + return @as(f64, @floatFromInt(self.seconds)) + @as(f64, @floatFromInt(self.nanoseconds)) / 1_000_000_000.0; + } +}; + +/// the map of payload +pub const Map = std.StringHashMap(Payload); + +/// Entity to store msgpack +/// +/// Note: The payload and its subvalues must have the same allocator +pub const Payload = union(enum) { + /// the error for Payload + pub const Errors = error{ + NotMap, + NotArr, + }; + + nil: void, + bool: bool, + int: i64, + uint: u64, + float: f64, + str: Str, + bin: Bin, + arr: []Payload, + map: Map, + ext: EXT, + timestamp: Timestamp, + + /// get array element + pub fn getArrElement(self: Payload, index: usize) !Payload { + if (self != .arr) { + return Errors.NotArr; + } + return self.arr[index]; + } + + /// get array length + pub fn getArrLen(self: Payload) !usize { + if (self != .arr) { + return Errors.NotArr; + } + return self.arr.len; + } + + /// get map's element + pub fn mapGet(self: Payload, key: []const u8) !?Payload { + if (self != .map) { + return Errors.NotMap; + } + return self.map.get(key); + } + + /// set array element + pub fn setArrElement(self: *Payload, index: usize, val: Payload) !void { + if (self.* != .arr) { + return Errors.NotArr; + } + self.arr[index] = val; + } + + /// put a new element to map payload + pub fn mapPut(self: *Payload, key: []const u8, val: Payload) !void { + if (self.* != .map) { + return Errors.NotMap; + } + const new_key = try self.map.allocator.alloc(u8, key.len); + @memcpy(new_key, key); + + if (try self.map.fetchPut(new_key, val)) |old| { + self.map.allocator.free(new_key); + old.value.free(self.map.allocator); + } + } + + /// get a NIL payload + pub fn nilToPayload() Payload { + return Payload{ + .nil = void{}, + }; + } + + /// get a bool payload + pub fn boolToPayload(val: bool) Payload { + return Payload{ + .bool = val, + }; + } + + /// get a int payload + pub fn intToPayload(val: i64) Payload { + return Payload{ + .int = val, + }; + } + + /// get a uint payload + pub fn uintToPayload(val: u64) Payload { + return Payload{ + .uint = val, + }; + } + + /// get a float payload + pub fn floatToPayload(val: f64) Payload { + return Payload{ + .float = val, + }; + } + + /// get a str payload + pub fn strToPayload(val: []const u8, allocator: Allocator) !Payload { + // alloca memory + const new_str = try allocator.alloc(u8, val.len); + // copy the val + @memcpy(new_str, val); + return Payload{ + .str = wrapStr(new_str), + }; + } + + /// get a bin payload + pub fn binToPayload(val: []const u8, allocator: Allocator) !Payload { + // alloca memory + const new_bin = try allocator.alloc(u8, val.len); + // copy the val + @memcpy(new_bin, val); + return Payload{ + .bin = wrapBin(new_bin), + }; + } + + /// get an array payload + pub fn arrPayload(len: usize, allocator: Allocator) !Payload { + const arr = try allocator.alloc(Payload, len); + for (0..len) |i| { + arr[i] = Payload.nilToPayload(); + } + return Payload{ + .arr = arr, + }; + } + + /// get a map payload + pub fn mapPayload(allocator: Allocator) Payload { + return Payload{ + .map = Map.init(allocator), + }; + } + + /// get an ext payload + pub fn extToPayload(t: i8, data: []const u8, allocator: Allocator) !Payload { + // alloca memory + const new_data = try allocator.alloc(u8, data.len); + // copy the val + @memcpy(new_data, data); + return Payload{ + .ext = wrapEXT(t, new_data), + }; + } + + /// get a timestamp payload + pub fn timestampToPayload(seconds: i64, nanoseconds: u32) Payload { + return Payload{ + .timestamp = Timestamp.new(seconds, nanoseconds), + }; + } + + /// get a timestamp payload from seconds only + pub fn timestampFromSeconds(seconds: i64) Payload { + return Payload{ + .timestamp = Timestamp.fromSeconds(seconds), + }; + } + + /// free the all memeory for this payload and sub payloads + /// the allocator is payload's allocator + pub fn free(self: Payload, allocator: Allocator) void { + switch (self) { + .str => { + const str = self.str; + allocator.free(str.value()); + }, + .bin => { + const bin = self.bin; + allocator.free(bin.value()); + }, + .ext => { + const ext = self.ext; + allocator.free(ext.data); + }, + .map => { + var map = self.map; + defer map.deinit(); + var itera = map.iterator(); + while (true) { + if (itera.next()) |entry| { + // free the key + defer allocator.free(entry.key_ptr.*); + entry.value_ptr.free(allocator); + } else { + break; + } + } + }, + .arr => { + const arr = self.arr; + defer allocator.free(arr); + for (0..arr.len) |i| { + arr[i].free(allocator); + } + }, + else => {}, + } + } + + /// get a i64 value from payload + /// Note: if the payload is not a int or the value is too large, it will return MsGPackError.INVALID_TYPE + pub fn getInt(self: Payload) !i64 { + return switch (self) { + .int => |val| val, + .uint => |val| { + if (val <= std.math.maxInt(i64)) { + return @intCast(val); + } + // TODO: we can not return this error + return MsGPackError.INVALID_TYPE; + }, + else => return MsGPackError.INVALID_TYPE, + }; + } + + /// get a u64 value from payload + /// Note: if the payload is not a uint or the value is negative, it will return MsGPackError.INVALID_TYPE + pub fn getUint(self: Payload) !u64 { + return switch (self) { + .int => |val| { + if (val >= 0) { + return @intCast(val); + } + // TODO: we can not return this error + return MsGPackError.INVALID_TYPE; + }, + .uint => |val| val, + else => return MsGPackError.INVALID_TYPE, + }; + } +}; + +/// markers +const Markers = enum(u8) { + POSITIVE_FIXINT = 0x00, + FIXMAP = 0x80, + FIXARRAY = 0x90, + FIXSTR = 0xa0, + NIL = 0xc0, + FALSE = 0xc2, + TRUE = 0xc3, + BIN8 = 0xc4, + BIN16 = 0xc5, + BIN32 = 0xc6, + EXT8 = 0xc7, + EXT16 = 0xc8, + EXT32 = 0xc9, + FLOAT32 = 0xca, + FLOAT64 = 0xcb, + UINT8 = 0xcc, + UINT16 = 0xcd, + UINT32 = 0xce, + UINT64 = 0xcf, + INT8 = 0xd0, + INT16 = 0xd1, + INT32 = 0xd2, + INT64 = 0xd3, + FIXEXT1 = 0xd4, + FIXEXT2 = 0xd5, + FIXEXT4 = 0xd6, + FIXEXT8 = 0xd7, + FIXEXT16 = 0xd8, + STR8 = 0xd9, + STR16 = 0xda, + STR32 = 0xdb, + ARRAY16 = 0xdc, + ARRAY32 = 0xdd, + MAP16 = 0xde, + MAP32 = 0xdf, + NEGATIVE_FIXINT = 0xe0, +}; + +/// A collection of errors that may occur when reading the payload +pub const MsGPackError = error{ + STR_DATA_LENGTH_TOO_LONG, + BIN_DATA_LENGTH_TOO_LONG, + ARRAY_LENGTH_TOO_LONG, + TUPLE_LENGTH_TOO_LONG, + MAP_LENGTH_TOO_LONG, + INPUT_VALUE_TOO_LARGE, + FIXED_VALUE_WRITING, + TYPE_MARKER_READING, + TYPE_MARKER_WRITING, + DATA_READING, + DATA_WRITING, + EXT_TYPE_READING, + EXT_TYPE_WRITING, + EXT_TYPE_LENGTH, + INVALID_TYPE, + LENGTH_READING, + LENGTH_WRITING, + INTERNAL, +}; + +/// Create an instance of msgpack_pack +pub fn Pack( + comptime WriteContext: type, + comptime ReadContext: type, + comptime WriteError: type, + comptime ReadError: type, + comptime writeFn: fn (context: WriteContext, bytes: []const u8) WriteError!usize, + comptime readFn: fn (context: ReadContext, arr: []u8) ReadError!usize, +) type { + return struct { + write_context: WriteContext, + read_context: ReadContext, + + const Self = @This(); + + /// init + pub fn init(writeContext: WriteContext, readContext: ReadContext) Self { + return Self{ + .write_context = writeContext, + .read_context = readContext, + }; + } + + /// wrap for writeFn + fn writeTo(self: Self, bytes: []const u8) !usize { + return writeFn(self.write_context, bytes); + } + + /// write one byte + fn writeByte(self: Self, byte: u8) !void { + const bytes = [_]u8{byte}; + const len = try self.writeTo(&bytes); + if (len != 1) { + return MsGPackError.LENGTH_WRITING; + } + } + + /// write data + fn writeData(self: Self, data: []const u8) !void { + const len = try self.writeTo(data); + if (len != data.len) { + return MsGPackError.LENGTH_WRITING; + } + } + + /// write type marker + fn writeTypeMarker(self: Self, comptime marker: Markers) !void { + switch (marker) { + .POSITIVE_FIXINT, .FIXMAP, .FIXARRAY, .FIXSTR, .NEGATIVE_FIXINT => { + const err_msg = comptimePrint("marker ({}) is wrong, the can not be write directly!", .{marker}); + @compileError(err_msg); + }, + else => {}, + } + try self.writeByte(@intFromEnum(marker)); + } + + /// write nil + fn writeNil(self: Self) !void { + try self.writeTypeMarker(Markers.NIL); + } + + /// write bool + fn writeBool(self: Self, val: bool) !void { + if (val) { + try self.writeTypeMarker(Markers.TRUE); + } else { + try self.writeTypeMarker(Markers.FALSE); + } + } + + /// write positive fix int + fn writePfixInt(self: Self, val: u8) !void { + if (val <= 0x7f) { + try self.writeByte(val); + } else { + return MsGPackError.INPUT_VALUE_TOO_LARGE; + } + } + + fn writeU8Value(self: Self, val: u8) !void { + try self.writeByte(val); + } + + /// write u8 int + fn writeU8(self: Self, val: u8) !void { + try self.writeTypeMarker(.UINT8); + try self.writeU8Value(val); + } + + fn writeU16Value(self: Self, val: u16) !void { + var arr: [2]u8 = undefined; + std.mem.writeInt(u16, &arr, val, big_endian); + + try self.writeData(&arr); + } + + /// write u16 int + fn writeU16(self: Self, val: u16) !void { + try self.writeTypeMarker(.UINT16); + try self.writeU16Value(val); + } + + fn writeU32Value(self: Self, val: u32) !void { + var arr: [4]u8 = undefined; + std.mem.writeInt(u32, &arr, val, big_endian); + + try self.writeData(&arr); + } + + /// write u32 int + fn writeU32(self: Self, val: u32) !void { + try self.writeTypeMarker(.UINT32); + try self.writeU32Value(val); + } + + fn writeU64Value(self: Self, val: u64) !void { + var arr: [8]u8 = undefined; + std.mem.writeInt(u64, &arr, val, big_endian); + + try self.writeData(&arr); + } + + /// write u64 int + fn writeU64(self: Self, val: u64) !void { + try self.writeTypeMarker(.UINT64); + try self.writeU64Value(val); + } + + /// write negative fix int + fn writeNfixInt(self: Self, val: i8) !void { + if (val >= -32 and val <= -1) { + try self.writeByte(@bitCast(val)); + } else { + return MsGPackError.INPUT_VALUE_TOO_LARGE; + } + } + + fn writeI8Value(self: Self, val: i8) !void { + try self.writeByte(@bitCast(val)); + } + + /// write i8 int + fn writeI8(self: Self, val: i8) !void { + try self.writeTypeMarker(.INT8); + try self.writeI8Value(val); + } + + fn writeI16Value(self: Self, val: i16) !void { + var arr: [2]u8 = undefined; + std.mem.writeInt(i16, &arr, val, big_endian); + + try self.writeData(&arr); + } + + /// write i16 int + fn writeI16(self: Self, val: i16) !void { + try self.writeTypeMarker(.INT16); + try self.writeI16Value(val); + } + + fn writeI32Value(self: Self, val: i32) !void { + var arr: [4]u8 = undefined; + std.mem.writeInt(i32, &arr, val, big_endian); + + try self.writeData(&arr); + } + + /// write i32 int + fn writeI32(self: Self, val: i32) !void { + try self.writeTypeMarker(.INT32); + try self.writeI32Value(val); + } + + fn writeI64Value(self: Self, val: i64) !void { + var arr: [8]u8 = undefined; + std.mem.writeInt(i64, &arr, val, big_endian); + + try self.writeData(&arr); + } + + /// write i64 int + fn writeI64(self: Self, val: i64) !void { + try self.writeTypeMarker(.INT64); + try self.writeI64Value(val); + } + + /// write uint + fn writeUint(self: Self, val: u64) !void { + if (val <= 0x7f) { + try self.writePfixInt(@intCast(val)); + } else if (val <= 0xff) { + try self.writeU8(@intCast(val)); + } else if (val <= 0xffff) { + try self.writeU16(@intCast(val)); + } else if (val <= 0xffffffff) { + try self.writeU32(@intCast(val)); + } else { + try self.writeU64(val); + } + } + + /// write int + fn writeInt(self: Self, val: i64) !void { + if (val >= 0) { + try self.writeUint(@intCast(val)); + } else if (val >= -32) { + try self.writeNfixInt(@intCast(val)); + } else if (val >= -128) { + try self.writeI8(@intCast(val)); + } else if (val >= -32768) { + try self.writeI16(@intCast(val)); + } else if (val >= -2147483648) { + try self.writeI32(@intCast(val)); + } else { + try self.writeI64(val); + } + } + + fn writeF32Value(self: Self, val: f32) !void { + const int: u32 = @bitCast(val); + var arr: [4]u8 = undefined; + std.mem.writeInt(u32, &arr, int, big_endian); + + try self.writeData(&arr); + } + + /// write f32 + fn writeF32(self: Self, val: f32) !void { + try self.writeTypeMarker(.FLOAT32); + try self.writeF32Value(val); + } + + fn writeF64Value(self: Self, val: f64) !void { + const int: u64 = @bitCast(val); + var arr: [8]u8 = undefined; + std.mem.writeInt(u64, &arr, int, big_endian); + + try self.writeData(&arr); + } + + /// write f64 + fn writeF64(self: Self, val: f64) !void { + try self.writeTypeMarker(.FLOAT64); + try self.writeF64Value(val); + } + + /// write float + fn writeFloat(self: Self, val: f64) !void { + const tmp_val = if (val < 0) 0 - val else val; + const min_f32 = std.math.floatMin(f32); + const max_f32 = std.math.floatMax(f32); + + if (tmp_val >= min_f32 and tmp_val <= max_f32) { + try self.writeF32(@floatCast(val)); + } else { + try self.writeF64(val); + } + } + + fn writeFixStrValue(self: Self, str: []const u8) !void { + try self.writeData(str); + } + + /// write fix str + fn writeFixStr(self: Self, str: []const u8) !void { + const len = str.len; + if (len > 0x1f) { + return MsGPackError.STR_DATA_LENGTH_TOO_LONG; + } + const header: u8 = @intFromEnum(Markers.FIXSTR) + @as(u8, @intCast(len)); + try self.writeByte(header); + try self.writeFixStrValue(str); + } + + fn writeStr8Value(self: Self, str: []const u8) !void { + const len = str.len; + try self.writeU8Value(@intCast(len)); + + try self.writeData(str); + } + + /// write str8 + fn writeStr8(self: Self, str: []const u8) !void { + const len = str.len; + if (len > 0xff) { + return MsGPackError.STR_DATA_LENGTH_TOO_LONG; + } + + try self.writeTypeMarker(.STR8); + try self.writeStr8Value(str); + } + + fn writeStr16Value(self: Self, str: []const u8) !void { + const len = str.len; + try self.writeU16Value(@intCast(len)); + + try self.writeData(str); + } + + /// write str16 + fn writeStr16(self: Self, str: []const u8) !void { + const len = str.len; + if (len > 0xffff) { + return MsGPackError.STR_DATA_LENGTH_TOO_LONG; + } + + try self.writeTypeMarker(.STR16); + + try self.writeStr16Value(str); + } + + fn writeStr32Value(self: Self, str: []const u8) !void { + const len = str.len; + try self.writeU32Value(@intCast(len)); + + try self.writeData(str); + } + + /// write str32 + fn writeStr32(self: Self, str: []const u8) !void { + const len = str.len; + if (len > 0xffff_ffff) { + return MsGPackError.STR_DATA_LENGTH_TOO_LONG; + } + + try self.writeTypeMarker(.STR32); + try self.writeStr32Value(str); + } + + /// write str + fn writeStr(self: Self, str: Str) !void { + const len = str.value().len; + if (len <= 0x1f) { + try self.writeFixStr(str.value()); + } else if (len <= 0xff) { + try self.writeStr8(str.value()); + } else if (len <= 0xffff) { + try self.writeStr16(str.value()); + } else { + try self.writeStr32(str.value()); + } + } + + /// write bin8 + fn writeBin8(self: Self, bin: []const u8) !void { + const len = bin.len; + if (len > 0xff) { + return MsGPackError.BIN_DATA_LENGTH_TOO_LONG; + } + + try self.writeTypeMarker(.BIN8); + + try self.writeStr8Value(bin); + } + + /// write bin16 + fn writeBin16(self: Self, bin: []const u8) !void { + const len = bin.len; + if (len > 0xffff) { + return MsGPackError.BIN_DATA_LENGTH_TOO_LONG; + } + + try self.writeTypeMarker(.BIN16); + + try self.writeStr16Value(bin); + } + + /// write bin32 + fn writeBin32(self: Self, bin: []const u8) !void { + const len = bin.len; + if (len > 0xffff_ffff) { + return MsGPackError.BIN_DATA_LENGTH_TOO_LONG; + } + + try self.writeTypeMarker(.BIN32); + + try self.writeStr32Value(bin); + } + + /// write bin + fn writeBin(self: Self, bin: Bin) !void { + const len = bin.value().len; + if (len <= 0xff) { + try self.writeBin8(bin.value()); + } else if (len <= 0xffff) { + try self.writeBin16(bin.value()); + } else { + try self.writeBin32(bin.value()); + } + } + + fn writeExtValue(self: Self, ext: EXT) !void { + try self.writeI8Value(ext.type); + try self.writeData(ext.data); + } + + fn writeFixExt1(self: Self, ext: EXT) !void { + if (ext.data.len != 1) { + return MsGPackError.EXT_TYPE_LENGTH; + } + try self.writeTypeMarker(.FIXEXT1); + + try self.writeExtValue(ext); + } + + fn writeFixExt2(self: Self, ext: EXT) !void { + if (ext.data.len != 2) { + return MsGPackError.EXT_TYPE_LENGTH; + } + try self.writeTypeMarker(.FIXEXT2); + try self.writeExtValue(ext); + } + + fn writeFixExt4(self: Self, ext: EXT) !void { + if (ext.data.len != 4) { + return MsGPackError.EXT_TYPE_LENGTH; + } + try self.writeTypeMarker(.FIXEXT4); + try self.writeExtValue(ext); + } + + fn writeFixExt8(self: Self, ext: EXT) !void { + if (ext.data.len != 8) { + return MsGPackError.EXT_TYPE_LENGTH; + } + try self.writeTypeMarker(.FIXEXT8); + try self.writeExtValue(ext); + } + + fn writeFixExt16(self: Self, ext: EXT) !void { + if (ext.data.len != 16) { + return MsGPackError.EXT_TYPE_LENGTH; + } + try self.writeTypeMarker(.FIXEXT16); + try self.writeExtValue(ext); + } + + fn writeExt8(self: Self, ext: EXT) !void { + if (ext.data.len > std.math.maxInt(u8)) { + return MsGPackError.EXT_TYPE_LENGTH; + } + + try self.writeTypeMarker(.EXT8); + try self.writeU8Value(@intCast(ext.data.len)); + try self.writeExtValue(ext); + } + + fn writeExt16(self: Self, ext: EXT) !void { + if (ext.data.len > std.math.maxInt(u16)) { + return MsGPackError.EXT_TYPE_LENGTH; + } + try self.writeTypeMarker(.EXT16); + try self.writeU16Value(@intCast(ext.data.len)); + try self.writeExtValue(ext); + } + + fn writeExt32(self: Self, ext: EXT) !void { + if (ext.data.len > std.math.maxInt(u32)) { + return MsGPackError.EXT_TYPE_LENGTH; + } + try self.writeTypeMarker(.EXT32); + try self.writeU32Value(@intCast(ext.data.len)); + try self.writeExtValue(ext); + } + + fn writeExt(self: Self, ext: EXT) !void { + const len = ext.data.len; + if (len == 1) { + try self.writeFixExt1(ext); + } else if (len == 2) { + try self.writeFixExt2(ext); + } else if (len == 4) { + try self.writeFixExt4(ext); + } else if (len == 8) { + try self.writeFixExt8(ext); + } else if (len == 16) { + try self.writeFixExt16(ext); + } else if (len <= std.math.maxInt(u8)) { + try self.writeExt8(ext); + } else if (len <= std.math.maxInt(u16)) { + try self.writeExt16(ext); + } else if (len <= std.math.maxInt(u32)) { + try self.writeExt32(ext); + } else { + return MsGPackError.EXT_TYPE_LENGTH; + } + } + + /// write timestamp + fn writeTimestamp(self: Self, timestamp: Timestamp) !void { + // According to MessagePack spec, timestamp uses extension type -1 + const TIMESTAMP_TYPE: i8 = -1; + + // timestamp 32 format: seconds fit in 32-bit unsigned int and nanoseconds is 0 + if (timestamp.nanoseconds == 0 and timestamp.seconds >= 0 and timestamp.seconds <= 0xffffffff) { + var data: [4]u8 = undefined; + std.mem.writeInt(u32, &data, @intCast(timestamp.seconds), big_endian); + const ext = EXT{ .type = TIMESTAMP_TYPE, .data = &data }; + try self.writeExt(ext); + return; + } + + // timestamp 64 format: seconds fit in 34-bit and nanoseconds <= 999999999 + if (timestamp.seconds >= 0 and (timestamp.seconds >> 34) == 0 and timestamp.nanoseconds <= 999999999) { + const data64: u64 = (@as(u64, timestamp.nanoseconds) << 34) | @as(u64, @intCast(timestamp.seconds)); + var data: [8]u8 = undefined; + std.mem.writeInt(u64, &data, data64, big_endian); + const ext = EXT{ .type = TIMESTAMP_TYPE, .data = &data }; + try self.writeExt(ext); + return; + } + + // timestamp 96 format: full range with signed 64-bit seconds and 32-bit nanoseconds + if (timestamp.nanoseconds <= 999999999) { + var data: [12]u8 = undefined; + std.mem.writeInt(u32, data[0..4], timestamp.nanoseconds, big_endian); + std.mem.writeInt(i64, data[4..12], timestamp.seconds, big_endian); + const ext = EXT{ .type = TIMESTAMP_TYPE, .data = &data }; + try self.writeExt(ext); + return; + } + + return MsGPackError.INVALID_TYPE; + } + + /// write payload + pub fn write(self: Self, payload: Payload) !void { + switch (payload) { + .nil => { + try self.writeNil(); + }, + .bool => |val| { + try self.writeBool(val); + }, + .int => |val| { + try self.writeInt(val); + }, + .uint => |val| { + try self.writeUint(val); + }, + .float => |val| { + try self.writeFloat(val); + }, + .str => |val| { + try self.writeStr(val); + }, + .bin => |val| { + try self.writeBin(val); + }, + .arr => |arr| { + const len = arr.len; + if (len <= 0xf) { + const header: u8 = @intFromEnum(Markers.FIXARRAY) + @as(u8, @intCast(len)); + try self.writeU8Value(header); + } else if (len <= 0xffff) { + try self.writeTypeMarker(.ARRAY16); + try self.writeU16Value(@as(u16, @intCast(len))); + } else if (len <= 0xffff_ffff) { + try self.writeTypeMarker(.ARRAY32); + try self.writeU32Value(@as(u32, @intCast(len))); + } else { + return MsGPackError.MAP_LENGTH_TOO_LONG; + } + for (arr) |val| { + try self.write(val); + } + }, + .map => |map| { + const len = map.count(); + if (len <= 0xf) { + const header: u8 = @intFromEnum(Markers.FIXMAP) + @as(u8, @intCast(len)); + try self.writeU8Value(header); + } else if (len <= 0xffff) { + try self.writeTypeMarker(.MAP16); + try self.writeU16Value(@intCast(len)); + } else if (len <= 0xffff_ffff) { + try self.writeTypeMarker(.MAP32); + try self.writeU32Value(@intCast(len)); + } else { + return MsGPackError.MAP_LENGTH_TOO_LONG; + } + var itera = map.iterator(); + while (itera.next()) |entry| { + try self.writeStr(wrapStr(entry.key_ptr.*)); + try self.write(entry.value_ptr.*); + } + }, + .ext => |ext| { + try self.writeExt(ext); + }, + .timestamp => |timestamp| { + try self.writeTimestamp(timestamp); + }, + } + } + + // TODO: add timestamp + + fn readFrom(self: Self, bytes: []u8) !usize { + return readFn(self.read_context, bytes); + } + + fn readByte(self: Self) !u8 { + var res = [1]u8{0}; + const len = try self.readFrom(&res); + + if (len != 1) { + return MsGPackError.LENGTH_READING; + } + + return res[0]; + } + + fn readData(self: Self, allocator: Allocator, len: usize) ![]u8 { + const data = try allocator.alloc(u8, len); + errdefer allocator.free(data); + const data_len = try self.readFrom(data); + + if (data_len != len) { + return MsGPackError.LENGTH_READING; + } + + return data; + } + + fn readTypeMarkerU8(self: Self) !u8 { + const val = try self.readByte(); + return val; + } + + fn markerU8To(_: Self, marker_u8: u8) Markers { + var val = marker_u8; + + if (val <= 0x7f) { + val = 0x00; + } else if (0x80 <= val and val <= 0x8f) { + val = 0x80; + } else if (0x90 <= val and val <= 0x9f) { + val = 0x90; + } else if (0xa0 <= val and val <= 0xbf) { + val = 0xa0; + } else if (0xe0 <= val and val <= 0xff) { + val = 0xe0; + } + + return @enumFromInt(val); + } + + fn readTypeMarker(self: Self) !Markers { + const val = try self.readTypeMarkerU8(); + return self.markerU8To(val); + } + + fn readBoolValue(_: Self, marker: Markers) !bool { + switch (marker) { + .TRUE => return true, + .FALSE => return false, + else => return MsGPackError.TYPE_MARKER_READING, + } + } + + fn readBool(self: Self) !bool { + const marker = try self.readTypeMarker(); + return self.readBoolValue(marker); + } + + fn readFixintValue(_: Self, marker_u8: u8) i8 { + return @bitCast(marker_u8); + } + + fn readI8Value(self: Self) !i8 { + const val = try self.readByte(); + return @bitCast(val); + } + + fn readV8Value(self: Self) !u8 { + return self.readByte(); + } + + fn readI16Value(self: Self) !i16 { + var buffer: [2]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 2) { + return MsGPackError.LENGTH_READING; + } + const val = std.mem.readInt(i16, &buffer, big_endian); + return val; + } + + fn readU16Value(self: Self) !u16 { + var buffer: [2]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 2) { + return MsGPackError.LENGTH_READING; + } + const val = std.mem.readInt(u16, &buffer, big_endian); + return val; + } + + fn readI32Value(self: Self) !i32 { + var buffer: [4]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 4) { + return MsGPackError.LENGTH_READING; + } + const val = std.mem.readInt(i32, &buffer, big_endian); + return val; + } + + fn readU32Value(self: Self) !u32 { + var buffer: [4]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 4) { + return MsGPackError.LENGTH_READING; + } + const val = std.mem.readInt(u32, &buffer, big_endian); + return val; + } + + fn readI64Value(self: Self) !i64 { + var buffer: [8]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 8) { + return MsGPackError.LENGTH_READING; + } + const val = std.mem.readInt(i64, &buffer, big_endian); + return val; + } + + fn readU64Value(self: Self) !u64 { + var buffer: [8]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 8) { + return MsGPackError.LENGTH_READING; + } + const val = std.mem.readInt(u64, &buffer, big_endian); + return val; + } + + fn readIntValue(self: Self, marker_u8: u8) !i64 { + const marker = self.markerU8To(marker_u8); + switch (marker) { + .NEGATIVE_FIXINT, .POSITIVE_FIXINT => { + const val = self.readFixintValue(marker_u8); + return val; + }, + .INT8 => { + const val = try self.readI8Value(); + return val; + }, + .UINT8 => { + const val = try self.readV8Value(); + return val; + }, + .INT16 => { + const val = try self.readI16Value(); + return val; + }, + .UINT16 => { + const val = try self.readU16Value(); + return val; + }, + .INT32 => { + const val = try self.readI32Value(); + return val; + }, + .UINT32 => { + const val = try self.readU32Value(); + return val; + }, + .INT64 => { + return self.readI64Value(); + }, + .UINT64 => { + const val = try self.readU64Value(); + if (val <= std.math.maxInt(i64)) { + return @intCast(val); + } + return MsGPackError.INVALID_TYPE; + }, + else => return MsGPackError.TYPE_MARKER_READING, + } + } + + fn readUintValue(self: Self, marker_u8: u8) !u64 { + const marker = self.markerU8To(marker_u8); + switch (marker) { + .POSITIVE_FIXINT => { + return marker_u8; + }, + .UINT8 => { + const val = try self.readV8Value(); + return val; + }, + .INT8 => { + const val = try self.readI8Value(); + if (val >= 0) { + return @intCast(val); + } + return MsGPackError.INVALID_TYPE; + }, + .UINT16 => { + const val = try self.readU16Value(); + return val; + }, + .INT16 => { + const val = try self.readI16Value(); + if (val >= 0) { + return @intCast(val); + } + return MsGPackError.INVALID_TYPE; + }, + .UINT32 => { + const val = try self.readU32Value(); + return val; + }, + .INT32 => { + const val = try self.readI32Value(); + if (val >= 0) { + return @intCast(val); + } + return MsGPackError.INVALID_TYPE; + }, + .UINT64 => { + return self.readU64Value(); + }, + .INT64 => { + const val = try self.readI64Value(); + if (val >= 0) { + return @intCast(val); + } + return MsGPackError.INVALID_TYPE; + }, + else => return MsGPackError.TYPE_MARKER_READING, + } + } + + fn readF32Value(self: Self) !f32 { + var buffer: [4]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 4) { + return MsGPackError.LENGTH_READING; + } + const val_int = std.mem.readInt(u32, &buffer, big_endian); + const val: f32 = @bitCast(val_int); + return val; + } + + fn readF64Value(self: Self) !f64 { + var buffer: [8]u8 = undefined; + const len = try self.readFrom(&buffer); + if (len != 8) { + return MsGPackError.LENGTH_READING; + } + const val_int = std.mem.readInt(u64, &buffer, big_endian); + const val: f64 = @bitCast(val_int); + return val; + } + + fn readFloatValue(self: Self, marker: Markers) !f64 { + switch (marker) { + .FLOAT32 => { + const val = try self.readF32Value(); + return val; + }, + .FLOAT64 => { + return self.readF64Value(); + }, + else => return MsGPackError.TYPE_MARKER_READING, + } + } + + fn readFixStrValue(self: Self, allocator: Allocator, marker_u8: u8) ![]const u8 { + const len: u8 = marker_u8 - @intFromEnum(Markers.FIXSTR); + const str = try self.readData(allocator, len); + + return str; + } + + fn readStr8Value(self: Self, allocator: Allocator) ![]const u8 { + const len = try self.readV8Value(); + const str = try self.readData(allocator, len); + + return str; + } + + fn readStr16Value(self: Self, allocator: Allocator) ![]const u8 { + const len = try self.readU16Value(); + const str = try self.readData(allocator, len); + + return str; + } + + fn readStr32Value(self: Self, allocator: Allocator) ![]const u8 { + const len = try self.readU32Value(); + const str = try self.readData(allocator, len); + + return str; + } + + fn readStrValue(self: Self, marker_u8: u8, allocator: Allocator) ![]const u8 { + const marker = self.markerU8To(marker_u8); + + switch (marker) { + .FIXSTR => { + return self.readFixStrValue(allocator, marker_u8); + }, + .STR8 => { + return self.readStr8Value(allocator); + }, + .STR16 => { + return self.readStr16Value(allocator); + }, + .STR32 => { + return self.readStr32Value(allocator); + }, + else => return MsGPackError.TYPE_MARKER_READING, + } + } + + fn readBin8Value(self: Self, allocator: Allocator) ![]u8 { + const len = try self.readV8Value(); + const bin = try self.readData(allocator, len); + + return bin; + } + + fn readBin16Value(self: Self, allocator: Allocator) ![]u8 { + const len = try self.readU16Value(); + const bin = try self.readData(allocator, len); + + return bin; + } + + fn readBin32Value(self: Self, allocator: Allocator) ![]u8 { + const len = try self.readU32Value(); + const bin = try self.readData(allocator, len); + + return bin; + } + + fn readBinValue(self: Self, marker: Markers, allocator: Allocator) ![]u8 { + switch (marker) { + .BIN8 => { + return self.readBin8Value(allocator); + }, + .BIN16 => { + return self.readBin16Value(allocator); + }, + .BIN32 => { + return self.readBin32Value(allocator); + }, + else => return MsGPackError.TYPE_MARKER_READING, + } + } + + fn readExtData(self: Self, allocator: Allocator, len: usize) !EXT { + const ext_type = try self.readI8Value(); + const data = try self.readData(allocator, len); + return EXT{ + .type = ext_type, + .data = data, + }; + } + + /// read ext value or timestamp if it's timestamp type (-1) + fn readExtValueOrTimestamp(self: Self, marker: Markers, allocator: Allocator) !Payload { + const TIMESTAMP_TYPE: i8 = -1; + + // First, check if this could be a timestamp format + if (marker == .FIXEXT4 or marker == .FIXEXT8 or marker == .EXT8) { + // Read and check length for EXT8 + var actual_len: usize = 0; + if (marker == .EXT8) { + actual_len = try self.readV8Value(); + if (actual_len != 12) { + // Not timestamp 96, read as regular EXT + const ext_type = try self.readI8Value(); + const ext_data = try allocator.alloc(u8, actual_len); + _ = try self.readFrom(ext_data); + return Payload{ .ext = EXT{ .type = ext_type, .data = ext_data } }; + } + } else if (marker == .FIXEXT4) { + actual_len = 4; + } else if (marker == .FIXEXT8) { + actual_len = 8; + } + + // Read the type + const ext_type = try self.readI8Value(); + + if (ext_type == TIMESTAMP_TYPE) { + // This is a timestamp + if (marker == .FIXEXT4) { + // timestamp 32 + const seconds = try self.readU32Value(); + return Payload{ .timestamp = Timestamp.new(@intCast(seconds), 0) }; + } else if (marker == .FIXEXT8) { + // timestamp 64 + const data64 = try self.readU64Value(); + const nanoseconds: u32 = @intCast(data64 >> 34); + const seconds: i64 = @intCast(data64 & 0x3ffffffff); + return Payload{ .timestamp = Timestamp.new(seconds, nanoseconds) }; + } else if (marker == .EXT8) { + // timestamp 96 + const nanoseconds = try self.readU32Value(); + const seconds = try self.readI64Value(); + return Payload{ .timestamp = Timestamp.new(seconds, nanoseconds) }; + } + } else { + // Not a timestamp, read as regular EXT + const ext_data = try allocator.alloc(u8, actual_len); + _ = try self.readFrom(ext_data); + return Payload{ .ext = EXT{ .type = ext_type, .data = ext_data } }; + } + } + + // Regular EXT processing + const val = try self.readExtValue(marker, allocator); + return Payload{ .ext = val }; + } + + /// try to read timestamp from ext data, return error if not timestamp + fn tryReadTimestamp(self: Self, marker: Markers, _: Allocator) !Timestamp { + const TIMESTAMP_TYPE: i8 = -1; + + switch (marker) { + .FIXEXT4 => { + // timestamp 32 format + const ext_type = try self.readI8Value(); + if (ext_type != TIMESTAMP_TYPE) { + return MsGPackError.INVALID_TYPE; + } + const seconds = try self.readU32Value(); + return Timestamp.new(@intCast(seconds), 0); + }, + .FIXEXT8 => { + // timestamp 64 format + const ext_type = try self.readI8Value(); + if (ext_type != TIMESTAMP_TYPE) { + return MsGPackError.INVALID_TYPE; + } + const data64 = try self.readU64Value(); + const nanoseconds: u32 = @intCast(data64 >> 34); + const seconds: i64 = @intCast(data64 & 0x3ffffffff); + return Timestamp.new(seconds, nanoseconds); + }, + .EXT8 => { + // timestamp 96 format (length should be 12) + const len = try self.readV8Value(); + if (len != 12) { + return MsGPackError.INVALID_TYPE; + } + const ext_type = try self.readI8Value(); + if (ext_type != TIMESTAMP_TYPE) { + return MsGPackError.INVALID_TYPE; + } + const nanoseconds = try self.readU32Value(); + const seconds = try self.readI64Value(); + return Timestamp.new(seconds, nanoseconds); + }, + else => { + return MsGPackError.INVALID_TYPE; + }, + } + } + + /// read timestamp from ext data + fn readTimestamp(self: Self, marker: Markers, _: Allocator) !Timestamp { + const TIMESTAMP_TYPE: i8 = -1; + + switch (marker) { + .FIXEXT4 => { + // timestamp 32 format + const ext_type = try self.readI8Value(); + if (ext_type != TIMESTAMP_TYPE) { + return MsGPackError.INVALID_TYPE; + } + const seconds = try self.readU32Value(); + return Timestamp.new(@intCast(seconds), 0); + }, + .FIXEXT8 => { + // timestamp 64 format + const ext_type = try self.readI8Value(); + if (ext_type != TIMESTAMP_TYPE) { + return MsGPackError.INVALID_TYPE; + } + const data64 = try self.readU64Value(); + const nanoseconds: u32 = @intCast(data64 >> 34); + const seconds: i64 = @intCast(data64 & 0x3ffffffff); + return Timestamp.new(seconds, nanoseconds); + }, + .EXT8 => { + // timestamp 96 format (length should be 12) + const len = try self.readV8Value(); + if (len != 12) { + return MsGPackError.INVALID_TYPE; + } + const ext_type = try self.readI8Value(); + if (ext_type != TIMESTAMP_TYPE) { + return MsGPackError.INVALID_TYPE; + } + const nanoseconds = try self.readU32Value(); + const seconds = try self.readI64Value(); + return Timestamp.new(seconds, nanoseconds); + }, + else => { + return MsGPackError.INVALID_TYPE; + }, + } + } + + fn readExtValue(self: Self, marker: Markers, allocator: Allocator) !EXT { + switch (marker) { + .FIXEXT1 => { + return self.readExtData(allocator, 1); + }, + .FIXEXT2 => { + return self.readExtData(allocator, 2); + }, + .FIXEXT4 => { + return self.readExtData(allocator, 4); + }, + .FIXEXT8 => { + return self.readExtData(allocator, 8); + }, + .FIXEXT16 => { + return self.readExtData(allocator, 16); + }, + .EXT8 => { + const len = try self.readV8Value(); + return self.readExtData(allocator, len); + }, + .EXT16 => { + const len = try self.readU16Value(); + return self.readExtData(allocator, len); + }, + .EXT32 => { + const len = try self.readU32Value(); + return self.readExtData(allocator, len); + }, + else => { + return MsGPackError.INVALID_TYPE; + }, + } + } + + /// read a payload, please use payload.free to free the memory + pub fn read(self: Self, allocator: Allocator) !Payload { + var res: Payload = undefined; + + const marker_u8 = try self.readTypeMarkerU8(); + const marker = self.markerU8To(marker_u8); + + switch (marker) { + // read nil + .NIL => { + res = Payload{ + .nil = void{}, + }; + }, + // read bool + .TRUE, .FALSE => { + const val = try self.readBoolValue(marker); + res = Payload{ + .bool = val, + }; + }, + // read uint + .POSITIVE_FIXINT, .UINT8, .UINT16, .UINT32, .UINT64 => { + const val = try self.readUintValue(marker_u8); + res = Payload{ + .uint = val, + }; + }, + // read int + .NEGATIVE_FIXINT, .INT8, .INT16, .INT32, .INT64 => { + const val = try self.readIntValue(marker_u8); + res = Payload{ + .int = val, + }; + }, + // read float + .FLOAT32, .FLOAT64 => { + const val = try self.readFloatValue(marker); + res = Payload{ + .float = val, + }; + }, + // read str + .FIXSTR, .STR8, .STR16, .STR32 => { + const val = try self.readStrValue(marker_u8, allocator); + errdefer allocator.free(val); + res = Payload{ + .str = wrapStr(val), + }; + }, + // read bin + .BIN8, .BIN16, .BIN32 => { + const val = try self.readBinValue(marker, allocator); + errdefer allocator.free(val); + res = Payload{ + .bin = wrapBin(val), + }; + }, + // read array + .FIXARRAY, .ARRAY16, .ARRAY32 => { + var len: usize = 0; + switch (marker) { + .FIXARRAY => { + len = marker_u8 - 0x90; + }, + .ARRAY16 => { + len = try self.readU16Value(); + }, + .ARRAY32 => { + len = try self.readU32Value(); + }, + else => { + return MsGPackError.INVALID_TYPE; + }, + } + + const arr = try allocator.alloc(Payload, len); + errdefer allocator.free(arr); + + for (0..len) |i| { + arr[i] = try self.read(allocator); + } + res = Payload{ + .arr = arr, + }; + }, + // read map + .FIXMAP, .MAP16, .MAP32 => { + var len: usize = 0; + switch (marker) { + .FIXMAP => { + len = marker_u8 - @intFromEnum(Markers.FIXMAP); + }, + .MAP16 => { + len = try self.readU16Value(); + }, + .MAP32 => { + len = try self.readU32Value(); + }, + else => { + return MsGPackError.INVALID_TYPE; + }, + } + + var map = Map.init(allocator); + for (0..len) |_| { + const str = try self.readStrValue( + try self.readTypeMarkerU8(), + allocator, + ); + const val = try self.read(allocator); + try map.put(str, val); + } + res = Payload{ + .map = map, + }; + }, + // read ext + .FIXEXT1, + .FIXEXT2, + .FIXEXT4, + .FIXEXT8, + .FIXEXT16, + .EXT8, + .EXT16, + .EXT32, + => { + const ext_result = try self.readExtValueOrTimestamp(marker, allocator); + res = ext_result; + }, + } + return res; + } + }; +} diff --git a/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/src/test.zig b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/src/test.zig new file mode 100644 index 00000000..1ba94dcc --- /dev/null +++ b/course/code/16/package_management_importer/zig-pkg/zig_msgpack-0.0.8-evvueB_ZAQBNRm7kdh1FslBxMvpu5WKvU2RrYhUY_Dne/src/test.zig @@ -0,0 +1,1910 @@ +const zig_std = @import("std"); +const std = struct { + pub const ArrayList = zig_std.ArrayList; + pub const fmt = zig_std.fmt; + pub const math = zig_std.math; + pub const mem = zig_std.mem; + pub const testing = zig_std.testing; + + pub const io = struct { + pub fn FixedBufferStream(comptime Buffer: type) type { + return struct { + buffer: Buffer, + pos: usize = 0, + + pub const WriteError = error{NoSpaceLeft}; + pub const ReadError = error{}; + + const Self = @This(); + + pub fn write(self: *Self, bytes: []const u8) WriteError!usize { + const buffer = self.buffer; + if (self.pos + bytes.len > buffer.len) return error.NoSpaceLeft; + @memcpy(buffer[self.pos .. self.pos + bytes.len], bytes); + self.pos += bytes.len; + return bytes.len; + } + + pub fn read(self: *Self, arr: []u8) ReadError!usize { + const buffer = self.buffer; + if (self.pos >= buffer.len) return 0; + const len = @min(arr.len, buffer.len - self.pos); + @memcpy(arr[0..len], buffer[self.pos .. self.pos + len]); + self.pos += len; + return len; + } + }; + } + + pub fn fixedBufferStream(buffer: anytype) FixedBufferStream([]u8) { + return .{ .buffer = buffer[0..] }; + } + }; +}; +const msgpack = @import("msgpack"); +const allocator = std.testing.allocator; +const expect = std.testing.expect; +const Payload = msgpack.Payload; + +fn u8eql(a: []const u8, b: []const u8) bool { + return std.mem.eql(u8, a, b); +} + +const bufferType = std.io.FixedBufferStream([]u8); + +const pack = msgpack.Pack( + *bufferType, + *bufferType, + bufferType.WriteError, + bufferType.ReadError, + bufferType.write, + bufferType.read, +); + +test "nil write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + try p.write(Payload{ .nil = void{} }); + const val = try p.read(allocator); + defer val.free(allocator); +} + +test "bool write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + const test_val_1 = false; + const test_val_2 = true; + + try p.write(.{ .bool = test_val_1 }); + try p.write(.{ .bool = test_val_2 }); + + const val_1 = try p.read(allocator); + defer val_1.free(allocator); + + const val_2 = try p.read(allocator); + defer val_2.free(allocator); + + try expect(val_1.bool == test_val_1); + try expect(val_2.bool == test_val_2); +} + +test "int/uint write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + const test_val_1: u8 = 21; + try p.write(.{ .uint = test_val_1 }); + const val_1 = try p.read(allocator); + defer val_1.free(allocator); + try expect(val_1.uint == test_val_1); + + const test_val_2: i8 = -6; + try p.write(.{ .int = test_val_2 }); + const val_2 = try p.read(allocator); + defer val_2.free(allocator); + try expect(val_2.int == test_val_2); +} + +test "float write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + const test_val: f64 = 3.5e+38; + try p.write(.{ .float = test_val }); + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.float == test_val); +} + +test "str write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + const test_str = "Hello, world!"; + const str_payload = try Payload.strToPayload(test_str, allocator); + defer str_payload.free(allocator); + try p.write(str_payload); + const val = try p.read(allocator); + defer val.free(allocator); + try expect(u8eql(test_str, val.str.value())); +} + +// In fact, the logic implemented by bin and str is basically the same +test "bin write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + // u8 bin + var test_bin = "This is a string that is more than 32 bytes long.".*; + try p.write(.{ .bin = msgpack.wrapBin(&test_bin) }); + const val = try p.read(allocator); + defer val.free(allocator); + try expect(u8eql(&test_bin, val.bin.value())); +} + +test "map write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + const str = "hello"; + + var test_val_1 = Payload.mapPayload(allocator); + var bin = [5]u8{ 1, 2, 3, 4, 5 }; + + try test_val_1.mapPut("nil", Payload.nilToPayload()); + try test_val_1.mapPut("id", Payload.uintToPayload(16)); + try test_val_1.mapPut("bool", Payload.boolToPayload(true)); + try test_val_1.mapPut("float", Payload.floatToPayload(0.5)); + try test_val_1.mapPut("str", try Payload.strToPayload(str, allocator)); + try test_val_1.mapPut("bin", try Payload.binToPayload(&bin, allocator)); + + var test_val_2 = Payload.mapPayload(allocator); + try test_val_2.mapPut("kk", Payload.intToPayload(-5)); + + try test_val_1.mapPut("ss", test_val_2); + + const test_val_3 = try Payload.arrPayload(5, allocator); + for (test_val_3.arr, 0..) |*v, i| { + v.* = Payload.uintToPayload(i); + } + + try test_val_1.mapPut("arr", test_val_3); + + defer test_val_1.free(allocator); + + try p.write(test_val_1); + + const val = try p.read(allocator); + defer val.free(allocator); + + try expect(val == .map); + try expect((try val.mapGet("nil")).? == .nil); + try expect((try val.mapGet("id")).?.uint == 16); + try expect((try val.mapGet("bool")).?.bool == true); + // Additional consideration needs + // to be given to the precision of floating point numbers + try expect((try val.mapGet("float")).?.float == 0.5); + try expect(u8eql(str, (try val.mapGet("str")).?.str.value())); + try expect(u8eql(&bin, (try val.mapGet("bin")).?.bin.value())); + try expect((try (try val.mapGet("ss")).?.mapGet("kk")).?.int == -5); + for ((try val.mapGet("arr")).?.arr, 0..) |v, i| { + try expect(v.uint == i); + } +} + +test "array write and read" { + // made test + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + const test_val = [5]u8{ 1, 2, 3, 4, 5 }; + var test_payload = try Payload.arrPayload(5, allocator); + defer test_payload.free(allocator); + for (test_val, 0..) |v, i| { + try test_payload.setArrElement(i, Payload.uintToPayload(v)); + } + + try p.write(test_payload); + const val = try p.read(allocator); + defer val.free(allocator); + + for (0..try val.getArrLen()) |i| { + const element = try val.getArrElement(i); + try expect(element.uint == test_val[i]); + } +} + +test "array16 write and read" { + var arr: [0xffff]u8 = std.mem.zeroes([0xffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + const test_val = [16]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; + var test_payload = try Payload.arrPayload(16, allocator); + defer test_payload.free(allocator); + + for (test_val, 0..) |v, i| { + try test_payload.setArrElement(i, Payload.uintToPayload(v)); + } + + try p.write(test_payload); + const val = try p.read(allocator); + defer val.free(allocator); + + try expect(arr[0] == 0xdc); + for (0..try val.getArrLen()) |i| { + const element = try val.getArrElement(i); + try expect(element.uint == test_val[i]); + } +} + +test "ext write and read" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init( + &write_buffer, + &read_buffer, + ); + + var test_data = [5]u8{ 1, 2, 3, 4, 5 }; + const test_type: u8 = 1; + + try p.write(.{ .ext = msgpack.EXT{ .type = test_type, .data = &test_data } }); + const val = try p.read(allocator); + defer val.free(allocator); + try expect(u8eql(&test_data, val.ext.data)); + try expect(test_type == val.ext.type); +} + +// Test error handling for Payload methods +test "payload error handling" { + // Test NotArr error + const not_arr_payload = Payload.nilToPayload(); + const arr_len_result = not_arr_payload.getArrLen(); + try expect(arr_len_result == Payload.Errors.NotArr); + + const arr_element_result = not_arr_payload.getArrElement(0); + try expect(arr_element_result == Payload.Errors.NotArr); + + var mut_not_arr = Payload.nilToPayload(); + const set_arr_result = mut_not_arr.setArrElement(0, Payload.nilToPayload()); + try expect(set_arr_result == Payload.Errors.NotArr); + + // Test NotMap error + const not_map_payload = Payload.nilToPayload(); + const map_get_result = not_map_payload.mapGet("test"); + try expect(map_get_result == Payload.Errors.NotMap); + + var mut_not_map = Payload.nilToPayload(); + const map_put_result = mut_not_map.mapPut("test", Payload.nilToPayload()); + try expect(map_put_result == Payload.Errors.NotMap); +} + +// Test boundary values for integers +test "integer boundary values" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test maximum positive values + const max_u8: u64 = 0xff; + try p.write(.{ .uint = max_u8 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.uint == max_u8); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const max_u16: u64 = 0xffff; + try p.write(.{ .uint = max_u16 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.uint == max_u16); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const max_u32: u64 = 0xffffffff; + try p.write(.{ .uint = max_u32 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.uint == max_u32); + } + + // Test minimum negative values + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const min_i8: i64 = -128; + try p.write(.{ .int = min_i8 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.int == min_i8); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const min_i16: i64 = -32768; + try p.write(.{ .int = min_i16 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.int == min_i16); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const min_i32: i64 = -2147483648; + try p.write(.{ .int = min_i32 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.int == min_i32); + } +} + +// Test different string sizes +test "string size boundaries" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test fixstr (31 bytes) + const fixstr_31_data = "a" ** 31; + const fixstr_31_payload = try Payload.strToPayload(fixstr_31_data, allocator); + defer fixstr_31_payload.free(allocator); + try p.write(fixstr_31_payload); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(u8eql(fixstr_31_data, val.str.value())); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test str8 (255 bytes) + const str8_255_data = "b" ** 255; + const str8_255_payload = try Payload.strToPayload(str8_255_data, allocator); + defer str8_255_payload.free(allocator); + try p.write(str8_255_payload); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(u8eql(str8_255_data, val.str.value())); + } +} + +// Test empty containers +test "empty containers" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test empty array + const empty_arr = try Payload.arrPayload(0, allocator); + defer empty_arr.free(allocator); + try p.write(empty_arr); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect((try val.getArrLen()) == 0); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test empty map + const empty_map = Payload.mapPayload(allocator); + defer empty_map.free(allocator); + try p.write(empty_map); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.map.count() == 0); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test empty string + const empty_str_payload = try Payload.strToPayload("", allocator); + defer empty_str_payload.free(allocator); + try p.write(empty_str_payload); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(u8eql("", val.str.value())); + } +} + +// Test different EXT sizes +test "ext different sizes" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test fixext1 + var ext1_data = [1]u8{0x42}; + try p.write(.{ .ext = msgpack.wrapEXT(1, &ext1_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 1); + try expect(u8eql(&ext1_data, val.ext.data)); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test fixext2 + var ext2_data = [2]u8{ 0x42, 0x43 }; + try p.write(.{ .ext = msgpack.wrapEXT(2, &ext2_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 2); + try expect(u8eql(&ext2_data, val.ext.data)); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test fixext4 + var ext4_data = [4]u8{ 0x42, 0x43, 0x44, 0x45 }; + try p.write(.{ .ext = msgpack.wrapEXT(3, &ext4_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 3); + try expect(u8eql(&ext4_data, val.ext.data)); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test fixext8 + var ext8_data = [8]u8{ 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49 }; + try p.write(.{ .ext = msgpack.wrapEXT(4, &ext8_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 4); + try expect(u8eql(&ext8_data, val.ext.data)); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test fixext16 + var ext16_data = [16]u8{ 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51 }; + try p.write(.{ .ext = msgpack.wrapEXT(5, &ext16_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 5); + try expect(u8eql(&ext16_data, val.ext.data)); + } +} + +// Test float precision +test "float precision" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test f32 range value + const f32_val: f64 = 3.14159; + try p.write(.{ .float = f32_val }); + { + const val = try p.read(allocator); + defer val.free(allocator); + // Check if value is approximately equal (due to f32 precision loss) + try expect(@abs(val.float - f32_val) < 0.0001); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test f64 value that exceeds f32 range + const f64_val: f64 = 1.7976931348623157e+308; // Near f64 max + try p.write(.{ .float = f64_val }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.float == f64_val); + } +} + +// Test payload utility methods +test "payload utility methods" { + // Test all ToPayload methods + try expect(Payload.nilToPayload() == .nil); + try expect(Payload.boolToPayload(true).bool == true); + try expect(Payload.intToPayload(-42).int == -42); + try expect(Payload.uintToPayload(42).uint == 42); + try expect(Payload.floatToPayload(3.14).float == 3.14); + + const str_payload = try Payload.strToPayload("test", allocator); + defer str_payload.free(allocator); + try expect(u8eql("test", str_payload.str.value())); + + const bin_payload = try Payload.binToPayload("binary", allocator); + defer bin_payload.free(allocator); + try expect(u8eql("binary", bin_payload.bin.value())); + + const ext_payload = try Payload.extToPayload(1, "extdata", allocator); + defer ext_payload.free(allocator); + try expect(ext_payload.ext.type == 1); + try expect(u8eql("extdata", ext_payload.ext.data)); + + const arr_payload = try Payload.arrPayload(3, allocator); + defer arr_payload.free(allocator); + try expect((try arr_payload.getArrLen()) == 3); + + const map_payload = Payload.mapPayload(allocator); + defer map_payload.free(allocator); + try expect(map_payload.map.count() == 0); +} + +// Test negative fixint range +test "negative fixint range" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test all values in negative fixint range (-32 to -1) + for (1..33) |i| { + const val: i64 = -@as(i64, @intCast(i)); + try p.write(.{ .int = val }); + } + + // Reset read buffer + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + for (1..33) |i| { + const expected: i64 = -@as(i64, @intCast(i)); + const result = try p.read(allocator); + defer result.free(allocator); + try expect(result.int == expected); + } +} + +// Test map with non-existent key +test "map operations" { + var test_map = Payload.mapPayload(allocator); + defer test_map.free(allocator); + + // Test getting non-existent key + const result = try test_map.mapGet("nonexistent"); + try expect(result == null); + + // Test putting and getting + try test_map.mapPut("key1", Payload.intToPayload(42)); + const value = try test_map.mapGet("key1"); + try expect(value != null); + try expect(value.?.int == 42); + + // Test overwriting existing key + try test_map.mapPut("key1", Payload.intToPayload(100)); + const new_value = try test_map.mapGet("key1"); + try expect(new_value != null); + try expect(new_value.?.int == 100); +} + +// Test array operations +test "array operations" { + var test_arr = try Payload.arrPayload(3, allocator); + defer test_arr.free(allocator); + + // Set all elements + for (0..3) |i| { + try test_arr.setArrElement(i, Payload.intToPayload(@intCast(i * 10))); + } + + // Get all elements + for (0..3) |i| { + const element = try test_arr.getArrElement(i); + try expect(element.int == @as(i64, @intCast(i * 10))); + } + + // Test array length + try expect((try test_arr.getArrLen()) == 3); +} + +// Test special float values +test "special float values" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test zero + try p.write(.{ .float = 0.0 }); + var val = try p.read(allocator); + defer val.free(allocator); + try expect(val.float == 0.0); + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test negative zero + try p.write(.{ .float = -0.0 }); + val = try p.read(allocator); + defer val.free(allocator); + try expect(val.float == -0.0); + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test very small positive number + const small_pos: f64 = 1e-100; + try p.write(.{ .float = small_pos }); + val = try p.read(allocator); + defer val.free(allocator); + try expect(val.float == small_pos); + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test very small negative number + const small_neg: f64 = -1e-100; + try p.write(.{ .float = small_neg }); + val = try p.read(allocator); + defer val.free(allocator); + try expect(val.float == small_neg); +} + +// Test Unicode strings +test "unicode strings" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test various Unicode strings + const unicode_strings = [_][]const u8{ + "Hello, 世界", + "🚀🌟✨", + "Привет мир", + "مرحبا بالعالم", + "こんにちは世界", + }; + + for (unicode_strings) |unicode_str| { + const unicode_payload = try Payload.strToPayload(unicode_str, allocator); + defer unicode_payload.free(allocator); + try p.write(unicode_payload); + } + + // Reset read buffer + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + for (unicode_strings) |expected| { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(u8eql(expected, val.str.value())); + } +} + +// Test nested structures +test "deeply nested structures" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Create deeply nested array [[[42]]] + var inner_arr = try Payload.arrPayload(1, allocator); + try inner_arr.setArrElement(0, Payload.intToPayload(42)); + + var middle_arr = try Payload.arrPayload(1, allocator); + try middle_arr.setArrElement(0, inner_arr); + + var outer_arr = try Payload.arrPayload(1, allocator); + try outer_arr.setArrElement(0, middle_arr); + + defer outer_arr.free(allocator); + + try p.write(outer_arr); + const val = try p.read(allocator); + defer val.free(allocator); + + // Navigate to deeply nested value + const level1 = try val.getArrElement(0); + const level2 = try level1.getArrElement(0); + const level3 = try level2.getArrElement(0); + try expect(try level3.getInt() == 42); +} + +// Test mixed type arrays +test "mixed type arrays" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + var mixed_arr = try Payload.arrPayload(6, allocator); + defer mixed_arr.free(allocator); + + try mixed_arr.setArrElement(0, Payload.nilToPayload()); + try mixed_arr.setArrElement(1, Payload.boolToPayload(true)); + try mixed_arr.setArrElement(2, Payload.intToPayload(-123)); + try mixed_arr.setArrElement(3, Payload.uintToPayload(456)); + try mixed_arr.setArrElement(4, Payload.floatToPayload(78.9)); + try mixed_arr.setArrElement(5, try Payload.strToPayload("mixed", allocator)); + + try p.write(mixed_arr); + const val = try p.read(allocator); + defer val.free(allocator); + + try expect((try val.getArrElement(0)) == .nil); + try expect((try val.getArrElement(1)).bool == true); + try expect((try val.getArrElement(2)).int == -123); + try expect((try val.getArrElement(3)).uint == 456); + // Use approximate comparison for float values due to f32 precision loss + try expect(@abs((try val.getArrElement(4)).float - 78.9) < 0.01); + try expect(u8eql("mixed", (try val.getArrElement(5)).str.value())); +} + +// Test large maps +test "large maps" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + var large_map = Payload.mapPayload(allocator); + defer large_map.free(allocator); + + // Store allocated keys to free them later + var keys: std.ArrayList([]u8) = .empty; + defer { + for (keys.items) |key| { + allocator.free(key); + } + keys.deinit(allocator); + } + + // Create a map with 20 entries (more than fixmap limit of 15) + for (0..20) |i| { + const key = try std.fmt.allocPrint(allocator, "key{d}", .{i}); + try keys.append(allocator, key); + try large_map.mapPut(key, Payload.intToPayload(@intCast(i))); + } + + try p.write(large_map); + const val = try p.read(allocator); + defer val.free(allocator); + + try expect(val.map.count() == 20); + + // Verify some entries + const value0 = try val.mapGet("key0"); + try expect(value0 != null); + try expect(try value0.?.getInt() == 0); + + const value19 = try val.mapGet("key19"); + try expect(value19 != null); + try expect(try value19.?.getInt() == 19); +} + +// Test array32 format +test "array32 write and read" { + // Create an array larger than 65535 elements would be too memory intensive + // Instead test the boundary where array32 format kicks in + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test with 65536 elements (0x10000), which should use array32 + const test_size = 0x10000; + var test_payload = try Payload.arrPayload(test_size, allocator); + defer test_payload.free(allocator); + + for (0..test_size) |i| { + try test_payload.setArrElement(i, Payload.uintToPayload(i % 256)); + } + + try p.write(test_payload); + const val = try p.read(allocator); + defer val.free(allocator); + + try expect((try val.getArrLen()) == test_size); + // Check first and last elements + try expect((try val.getArrElement(0)).uint == 0); + try expect((try val.getArrElement(test_size - 1)).uint == (test_size - 1) % 256); +} + +// Test bin16 and bin32 +test "bin16 and bin32 write and read" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test bin16 (256 bytes) + const test_bin16 = try allocator.alloc(u8, 256); + defer allocator.free(test_bin16); + for (test_bin16, 0..) |*byte, i| { + byte.* = @intCast(i % 256); + } + + try p.write(.{ .bin = msgpack.wrapBin(test_bin16) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.bin.value().len == 256); + try expect(val.bin.value()[0] == 0); + try expect(val.bin.value()[255] == 255); + } + + // Reset buffers for bin32 test + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test bin32 (65536 bytes) + const test_bin32 = try allocator.alloc(u8, 65536); + defer allocator.free(test_bin32); + for (test_bin32, 0..) |*byte, i| { + byte.* = @intCast(i % 256); + } + + try p.write(.{ .bin = msgpack.wrapBin(test_bin32) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.bin.value().len == 65536); + try expect(val.bin.value()[0] == 0); + try expect(val.bin.value()[65535] == 255); + } +} + +// Test str16 and str32 +test "str16 and str32 write and read" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test str16 (256 characters) + const str16_data = "x" ** 256; + const str16_payload = try Payload.strToPayload(str16_data, allocator); + defer str16_payload.free(allocator); + try p.write(str16_payload); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.str.value().len == 256); + try expect(u8eql(str16_data, val.str.value())); + } + + // Reset buffers for str32 test + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test str32 (65536 characters) + const str32_data = try allocator.alloc(u8, 65536); + defer allocator.free(str32_data); + @memset(str32_data, 'A'); + + const str32_payload = try Payload.strToPayload(str32_data, allocator); + defer str32_payload.free(allocator); + try p.write(str32_payload); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.str.value().len == 65536); + try expect(u8eql(str32_data, val.str.value())); + } +} + +// Test int64 and uint64 boundary values +test "int64 uint64 boundary values" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test max int64 + const max_i64: i64 = std.math.maxInt(i64); + try p.write(.{ .int = max_i64 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(try val.getInt() == max_i64); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test min int64 + const min_i64: i64 = std.math.minInt(i64); + try p.write(.{ .int = min_i64 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.int == min_i64); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test max uint64 + const max_u64: u64 = std.math.maxInt(u64); + try p.write(.{ .uint = max_u64 }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.uint == max_u64); + } +} + +// Test getUint method +test "getUint method" { + // Test uint payload + const uint_payload = Payload.uintToPayload(42); + try expect(try uint_payload.getUint() == 42); + + // Test positive int payload converted to uint + const pos_int_payload = Payload.intToPayload(24); + try expect(try pos_int_payload.getUint() == 24); + + // Test negative int payload should fail + const neg_int_payload = Payload.intToPayload(-5); + const result = neg_int_payload.getUint(); + try expect(result == msgpack.MsGPackError.INVALID_TYPE); + + // Test non-numeric payload should fail + const nil_payload = Payload.nilToPayload(); + const nil_result = nil_payload.getUint(); + try expect(nil_result == msgpack.MsGPackError.INVALID_TYPE); +} + +// Test NaN and Infinity float values +test "nan and infinity float values" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test positive infinity + try p.write(.{ .float = std.math.inf(f64) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(std.math.isInf(val.float)); + try expect(val.float > 0); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test negative infinity + try p.write(.{ .float = -std.math.inf(f64) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(std.math.isInf(val.float)); + try expect(val.float < 0); + } + + // Reset buffers + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test NaN + try p.write(.{ .float = std.math.nan(f64) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(std.math.isNan(val.float)); + } +} + +// Test EXT8, EXT16, EXT32 formats +test "ext8 ext16 ext32 formats" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test EXT8 (more than 16 bytes, up to 255) + const ext8_size = 100; + const ext8_data = try allocator.alloc(u8, ext8_size); + defer allocator.free(ext8_data); + for (ext8_data, 0..) |*byte, i| { + byte.* = @intCast(i % 256); + } + + try p.write(.{ .ext = msgpack.wrapEXT(10, ext8_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 10); + try expect(val.ext.data.len == ext8_size); + try expect(u8eql(ext8_data, val.ext.data)); + } + + // Reset buffers for EXT16 test + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test EXT16 (more than 255 bytes, up to 65535) + const ext16_size = 1000; + const ext16_data = try allocator.alloc(u8, ext16_size); + defer allocator.free(ext16_data); + for (ext16_data, 0..) |*byte, i| { + byte.* = @intCast(i % 256); + } + + try p.write(.{ .ext = msgpack.wrapEXT(20, ext16_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 20); + try expect(val.ext.data.len == ext16_size); + try expect(u8eql(ext16_data, val.ext.data)); + } + + // Reset buffers for EXT32 test + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test EXT32 (more than 65535 bytes, up to 4294967295) + const ext32_size = 70000; // Larger than 65535 + const ext32_data = try allocator.alloc(u8, ext32_size); + defer allocator.free(ext32_data); + for (ext32_data, 0..) |*byte, i| { + byte.* = @intCast(i % 256); + } + + try p.write(.{ .ext = msgpack.wrapEXT(30, ext32_data) }); + { + const val = try p.read(allocator); + defer val.free(allocator); + try expect(val.ext.type == 30); + try expect(val.ext.data.len == ext32_size); + // Check first and last few bytes to avoid memory intensive comparison + try expect(val.ext.data[0] == 0); + try expect(val.ext.data[ext32_size - 1] == (ext32_size - 1) % 256); + } +} + +// Test actual MAP32 format (more than 65535 entries) +test "actual map32 format" { + // Note: This test would be very memory intensive with 65536+ entries + // Instead, we test the boundary where map32 format would be used + // by verifying the implementation handles large maps correctly + + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test with a moderately large map (1000 entries) to ensure scalability + var large_map = Payload.mapPayload(allocator); + defer large_map.free(allocator); + + // Store allocated keys to free them later + var keys: std.ArrayList([]u8) = .empty; + defer { + for (keys.items) |key| { + allocator.free(key); + } + keys.deinit(allocator); + } + + // Create a map with 1000 entries (more than map16 threshold of 65535 would be too memory intensive) + for (0..1000) |i| { + const key = try std.fmt.allocPrint(allocator, "key{d:0>10}", .{i}); + try keys.append(allocator, key); + try large_map.mapPut(key, Payload.intToPayload(@intCast(i))); + } + + try p.write(large_map); + const val = try p.read(allocator); + defer val.free(allocator); + + try expect(val.map.count() == 1000); + + // Verify some entries exist and have correct values + const first_key = try std.fmt.allocPrint(allocator, "key{d:0>10}", .{0}); + defer allocator.free(first_key); + const last_key = try std.fmt.allocPrint(allocator, "key{d:0>10}", .{999}); + defer allocator.free(last_key); + + const first_value = try val.mapGet(first_key); + try expect(first_value != null); + try expect(try first_value.?.getInt() == 0); + + const last_value = try val.mapGet(last_key); + try expect(last_value != null); + try expect(try last_value.?.getInt() == 999); +} + +// Test edge cases and error conditions +test "edge cases and error conditions" { + var arr: [100]u8 = std.mem.zeroes([100]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test array index out of bounds + var test_arr = try Payload.arrPayload(3, allocator); + defer test_arr.free(allocator); + + // This should work + try test_arr.setArrElement(0, Payload.nilToPayload()); + try test_arr.setArrElement(1, Payload.boolToPayload(true)); + try test_arr.setArrElement(2, Payload.intToPayload(42)); + + try p.write(test_arr); + const val = try p.read(allocator); + defer val.free(allocator); + + try expect((try val.getArrLen()) == 3); + try expect((try val.getArrElement(0)) == .nil); + try expect((try val.getArrElement(1)).bool == true); + try expect(try (try val.getArrElement(2)).getInt() == 42); +} + +// Test EXT with negative type IDs +test "ext negative type ids" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test negative type ID (MessagePack spec allows -128 to 127) + var test_data = [4]u8{ 0x01, 0x02, 0x03, 0x04 }; + const negative_type: i8 = -42; + + try p.write(.{ .ext = msgpack.wrapEXT(negative_type, &test_data) }); + const val = try p.read(allocator); + defer val.free(allocator); + + try expect(val.ext.type == negative_type); + try expect(u8eql(&test_data, val.ext.data)); + + // Reset buffer and test minimum negative type ID + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const min_type: i8 = -128; + try p.write(.{ .ext = msgpack.wrapEXT(min_type, &test_data) }); + const val2 = try p.read(allocator); + defer val2.free(allocator); + + try expect(val2.ext.type == min_type); + try expect(u8eql(&test_data, val2.ext.data)); +} + +// Test format markers verification +test "format markers verification" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test nil marker + try p.write(Payload.nilToPayload()); + try expect(arr[0] == 0xc0); // NIL marker + + // Reset buffer + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + + // Test bool markers + try p.write(Payload.boolToPayload(true)); + try expect(arr[0] == 0xc3); // TRUE marker + + try p.write(Payload.boolToPayload(false)); + try expect(arr[1] == 0xc2); // FALSE marker + + // Reset buffer + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test uint8 marker + try p.write(Payload.uintToPayload(255)); + try expect(arr[0] == 0xcc); // UINT8 marker + + // Reset buffer + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test str8 marker (32+ bytes string uses str8) + const test_str32 = "a" ** 32; + const str_payload = try Payload.strToPayload(test_str32, allocator); + defer str_payload.free(allocator); + try p.write(str_payload); + try expect(arr[0] == 0xd9); // STR8 marker + + // Reset buffer + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test array16 marker (16+ elements uses array16) + var test_array = try Payload.arrPayload(16, allocator); + defer test_array.free(allocator); + for (0..16) |i| { + try test_array.setArrElement(i, Payload.nilToPayload()); + } + try p.write(test_array); + try expect(arr[0] == 0xdc); // ARRAY16 marker + + // Reset buffer + arr = std.mem.zeroes([0xffff_f]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test map16 marker (16+ entries uses map16) + var test_map = Payload.mapPayload(allocator); + defer test_map.free(allocator); + + var test_keys: std.ArrayList([]u8) = .empty; + defer { + for (test_keys.items) |key| { + allocator.free(key); + } + test_keys.deinit(allocator); + } + + for (0..16) |i| { + const key = try std.fmt.allocPrint(allocator, "k{d}", .{i}); + try test_keys.append(allocator, key); + try test_map.mapPut(key, Payload.nilToPayload()); + } + try p.write(test_map); + try expect(arr[0] == 0xde); // MAP16 marker +} + +// Test positive fixint boundary (0-127) +test "positive fixint boundary" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test boundary values for positive fixint (0-127) + const boundary_values = [_]u64{ 0, 1, 126, 127, 128 }; + + for (boundary_values) |val| { + try p.write(.{ .uint = val }); + } + + // Reset read buffer + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + for (boundary_values) |expected| { + const result = try p.read(allocator); + defer result.free(allocator); + try expect(result.uint == expected); + } +} + +// Test fixstr boundary (0-31 bytes) +test "fixstr boundary" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test different fixstr lengths + const test_strings = [_][]const u8{ + "", // 0 bytes + "a", // 1 byte + "hello", // 5 bytes + "a" ** 31, // 31 bytes (max fixstr) + "b" ** 32, // 32 bytes (should use str8) + }; + + for (test_strings) |test_str| { + const str_payload = try Payload.strToPayload(test_str, allocator); + defer str_payload.free(allocator); + try p.write(str_payload); + } + + // Reset read buffer + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + for (test_strings) |expected| { + const result = try p.read(allocator); + defer result.free(allocator); + try expect(u8eql(expected, result.str.value())); + } +} + +// Test fixarray boundary (0-15 elements) +test "fixarray boundary" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test different fixarray sizes + const test_sizes = [_]usize{ 0, 1, 5, 15, 16 }; + + for (test_sizes) |size| { + var test_payload = try Payload.arrPayload(size, allocator); + defer test_payload.free(allocator); + + for (0..size) |i| { + try test_payload.setArrElement(i, Payload.uintToPayload(i)); + } + + try p.write(test_payload); + } + + // Reset read buffer + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + for (test_sizes) |expected_size| { + const result = try p.read(allocator); + defer result.free(allocator); + try expect((try result.getArrLen()) == expected_size); + + for (0..expected_size) |i| { + const element = try result.getArrElement(i); + try expect(element.uint == i); + } + } +} + +// Test fixmap boundary (0-15 elements) +test "fixmap boundary" { + var arr: [0xffff_f]u8 = std.mem.zeroes([0xffff_f]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test different fixmap sizes + const test_sizes = [_]usize{ 0, 1, 5, 15, 16 }; + + for (test_sizes) |size| { + var test_map = Payload.mapPayload(allocator); + defer test_map.free(allocator); + + for (0..size) |i| { + const key = try std.fmt.allocPrint(allocator, "k{d}", .{i}); + defer allocator.free(key); + try test_map.mapPut(key, Payload.uintToPayload(i)); + } + + try p.write(test_map); + } + + // Reset read buffer + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + for (test_sizes) |expected_size| { + const result = try p.read(allocator); + defer result.free(allocator); + try expect(result.map.count() == expected_size); + + for (0..expected_size) |i| { + const key = try std.fmt.allocPrint(allocator, "k{d}", .{i}); + defer allocator.free(key); + const value = try result.mapGet(key); + try expect(value != null); + try expect(value.?.uint == i); + } + } +} + +// Test timestamp write and read +test "timestamp write and read" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test timestamp 32 (seconds only, nanoseconds = 0) + const timestamp32 = Payload.timestampFromSeconds(1234567890); + try p.write(timestamp32); + + // Reset read buffer + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val32 = try p.read(allocator); + defer val32.free(allocator); + try expect(val32 == .timestamp); + try expect(val32.timestamp.seconds == 1234567890); + try expect(val32.timestamp.nanoseconds == 0); + + // Test timestamp 64 (seconds + nanoseconds) + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const timestamp64 = Payload.timestampToPayload(1234567890, 123456789); + try p.write(timestamp64); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val64 = try p.read(allocator); + defer val64.free(allocator); + try expect(val64 == .timestamp); + try expect(val64.timestamp.seconds == 1234567890); + try expect(val64.timestamp.nanoseconds == 123456789); + + // Test timestamp 96 (negative seconds) + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const timestamp96 = Payload.timestampToPayload(-1234567890, 987654321); + try p.write(timestamp96); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val96 = try p.read(allocator); + defer val96.free(allocator); + try expect(val96 == .timestamp); + try expect(val96.timestamp.seconds == -1234567890); + try expect(val96.timestamp.nanoseconds == 987654321); +} + +// Test timestamp format markers +test "timestamp format markers" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // timestamp 32 should use FIXEXT4 (0xd6) + type(-1) + 4 bytes data + const timestamp32 = Payload.timestampFromSeconds(1000000000); + try p.write(timestamp32); + try expect(arr[0] == 0xd6); // FIXEXT4 + try expect(@as(i8, @bitCast(arr[1])) == -1); // timestamp type + + // Reset buffer for timestamp 64 + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // timestamp 64 should use FIXEXT8 (0xd7) + type(-1) + 8 bytes data + const timestamp64 = Payload.timestampToPayload(1000000000, 123456789); + try p.write(timestamp64); + try expect(arr[0] == 0xd7); // FIXEXT8 + try expect(@as(i8, @bitCast(arr[1])) == -1); // timestamp type + + // Reset buffer for timestamp 96 + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // timestamp 96 should use EXT8 (0xc7) + len(12) + type(-1) + 12 bytes data + const timestamp96 = Payload.timestampToPayload(-1000000000, 123456789); + try p.write(timestamp96); + try expect(arr[0] == 0xc7); // EXT8 + try expect(arr[1] == 12); // length + try expect(@as(i8, @bitCast(arr[2])) == -1); // timestamp type +} + +// Test timestamp utility methods +test "timestamp utility methods" { + // Test Timestamp.new + const ts1 = msgpack.Timestamp.new(1234567890, 123456789); + try expect(ts1.seconds == 1234567890); + try expect(ts1.nanoseconds == 123456789); + + // Test Timestamp.fromSeconds + const ts2 = msgpack.Timestamp.fromSeconds(9876543210); + try expect(ts2.seconds == 9876543210); + try expect(ts2.nanoseconds == 0); + + // Test toFloat + const ts3 = msgpack.Timestamp.new(1, 500000000); // 1.5 seconds + const float_val = ts3.toFloat(); + try expect(@abs(float_val - 1.5) < 0.000001); + + // Test payload creation methods + const payload1 = Payload.timestampToPayload(1000, 2000); + try expect(payload1 == .timestamp); + try expect(payload1.timestamp.seconds == 1000); + try expect(payload1.timestamp.nanoseconds == 2000); + + const payload2 = Payload.timestampFromSeconds(5000); + try expect(payload2 == .timestamp); + try expect(payload2.timestamp.seconds == 5000); + try expect(payload2.timestamp.nanoseconds == 0); +} + +// Test timestamp edge cases +test "timestamp edge cases" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test zero timestamp + const zero_ts = Payload.timestampFromSeconds(0); + try p.write(zero_ts); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val_zero = try p.read(allocator); + defer val_zero.free(allocator); + try expect(val_zero == .timestamp); + try expect(val_zero.timestamp.seconds == 0); + try expect(val_zero.timestamp.nanoseconds == 0); + + // Test maximum nanoseconds (999999999) + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const max_nano_ts = Payload.timestampToPayload(1000, 999999999); + try p.write(max_nano_ts); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val_max_nano = try p.read(allocator); + defer val_max_nano.free(allocator); + try expect(val_max_nano == .timestamp); + try expect(val_max_nano.timestamp.seconds == 1000); + try expect(val_max_nano.timestamp.nanoseconds == 999999999); + + // Test large positive seconds (near 32-bit limit) + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const large_ts = Payload.timestampFromSeconds(0xffffffff); + try p.write(large_ts); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val_large = try p.read(allocator); + defer val_large.free(allocator); + try expect(val_large == .timestamp); + try expect(val_large.timestamp.seconds == 0xffffffff); + try expect(val_large.timestamp.nanoseconds == 0); +} + +// Test timestamp boundary values +test "timestamp boundary values" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test 34-bit boundary for timestamp 64 format + // 2^34 - 1 = 17179869183 + const boundary_34bit = (1 << 34) - 1; + const ts_34bit = Payload.timestampToPayload(boundary_34bit, 123456789); + try p.write(ts_34bit); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val_34bit = try p.read(allocator); + defer val_34bit.free(allocator); + try expect(val_34bit == .timestamp); + try expect(val_34bit.timestamp.seconds == boundary_34bit); + try expect(val_34bit.timestamp.nanoseconds == 123456789); + + // Test seconds just over 34-bit boundary (should use timestamp 96) + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const over_34bit = (1 << 34); + const ts_over_34bit = Payload.timestampToPayload(over_34bit, 123456789); + try p.write(ts_over_34bit); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val_over_34bit = try p.read(allocator); + defer val_over_34bit.free(allocator); + try expect(val_over_34bit == .timestamp); + try expect(val_over_34bit.timestamp.seconds == over_34bit); + try expect(val_over_34bit.timestamp.nanoseconds == 123456789); + + // Test very large negative seconds + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const large_negative = -9223372036854775808; // i64 min + const ts_large_neg = Payload.timestampToPayload(large_negative, 999999999); + try p.write(ts_large_neg); + + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val_large_neg = try p.read(allocator); + defer val_large_neg.free(allocator); + try expect(val_large_neg == .timestamp); + try expect(val_large_neg.timestamp.seconds == large_negative); + try expect(val_large_neg.timestamp.nanoseconds == 999999999); +} + +// Test timestamp and EXT compatibility +test "timestamp and EXT compatibility" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test mixed timestamp and EXT data + const timestamp = Payload.timestampFromSeconds(1000000000); + try p.write(timestamp); + + // Write a regular EXT with type -1 but different length (should be treated as EXT, not timestamp) + var ext_data = [_]u8{ 0x01, 0x02, 0x03 }; + const ext_payload = try Payload.extToPayload(-1, &ext_data, allocator); + defer ext_payload.free(allocator); + try p.write(ext_payload); + + // Write another timestamp + const timestamp2 = Payload.timestampToPayload(2000000000, 500000000); + try p.write(timestamp2); + + // Read back and verify + read_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + const val1 = try p.read(allocator); + defer val1.free(allocator); + try expect(val1 == .timestamp); + try expect(val1.timestamp.seconds == 1000000000); + try expect(val1.timestamp.nanoseconds == 0); + + const val2 = try p.read(allocator); + defer val2.free(allocator); + try expect(val2 == .ext); + try expect(val2.ext.type == -1); + try expect(val2.ext.data.len == 3); + try expect(val2.ext.data[0] == 0x01); + try expect(val2.ext.data[1] == 0x02); + try expect(val2.ext.data[2] == 0x03); + + const val3 = try p.read(allocator); + defer val3.free(allocator); + try expect(val3 == .timestamp); + try expect(val3.timestamp.seconds == 2000000000); + try expect(val3.timestamp.nanoseconds == 500000000); +} + +// Test timestamp error handling +test "timestamp error handling" { + // Test invalid nanoseconds (> 999999999) - should return INVALID_TYPE + const invalid_nano_ts = msgpack.Timestamp.new(1000, 1000000000); // 1 billion nanoseconds + + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // This should fail to write + const result = p.write(Payload{ .timestamp = invalid_nano_ts }); + try std.testing.expectError(msgpack.MsGPackError.INVALID_TYPE, result); +} + +// Test timestamp format selection logic +test "timestamp format selection" { + var arr: [0xfffff]u8 = std.mem.zeroes([0xfffff]u8); + var write_buffer = std.io.fixedBufferStream(&arr); + var read_buffer = std.io.fixedBufferStream(&arr); + var p = pack.init(&write_buffer, &read_buffer); + + // Test format 32: seconds <= 0xffffffff and nanoseconds == 0 + const ts32_max = Payload.timestampFromSeconds(0xffffffff); + try p.write(ts32_max); + try expect(arr[0] == 0xd6); // FIXEXT4 + try expect(@as(i8, @bitCast(arr[1])) == -1); // timestamp type + + // Reset buffer + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test format 32 boundary: seconds = 0xffffffff + 1 should use format 64 + const ts64_min = Payload.timestampToPayload(0x100000000, 0); + try p.write(ts64_min); + try expect(arr[0] == 0xd7); // FIXEXT8 + try expect(@as(i8, @bitCast(arr[1])) == -1); // timestamp type + + // Reset buffer + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test format 64: nanoseconds != 0 but seconds fits in 34-bit + const ts64_nano = Payload.timestampToPayload(1000000000, 1); + try p.write(ts64_nano); + try expect(arr[0] == 0xd7); // FIXEXT8 + try expect(@as(i8, @bitCast(arr[1])) == -1); // timestamp type + + // Reset buffer + arr = std.mem.zeroes([0xfffff]u8); + write_buffer = std.io.fixedBufferStream(&arr); + p = pack.init(&write_buffer, &read_buffer); + + // Test format 96: negative seconds + const ts96_neg = Payload.timestampToPayload(-1, 123456789); + try p.write(ts96_neg); + try expect(arr[0] == 0xc7); // EXT8 + try expect(arr[1] == 12); // length + try expect(@as(i8, @bitCast(arr[2])) == -1); // timestamp type +} + +// Test timestamp precision and conversion +test "timestamp precision and conversion" { + // Test toFloat method precision + const ts1 = msgpack.Timestamp.new(1234567890, 123456789); + const float_val1 = ts1.toFloat(); + const expected1 = 1234567890.123456789; + try expect(@abs(float_val1 - expected1) < 0.000000001); + + // Test toFloat with zero nanoseconds + const ts2 = msgpack.Timestamp.new(1000, 0); + const float_val2 = ts2.toFloat(); + try expect(float_val2 == 1000.0); + + // Test toFloat with maximum nanoseconds + const ts3 = msgpack.Timestamp.new(0, 999999999); + const float_val3 = ts3.toFloat(); + const expected3 = 0.999999999; + try expect(@abs(float_val3 - expected3) < 0.000000001); + + // Test negative seconds with nanoseconds + const ts4 = msgpack.Timestamp.new(-1, 500000000); + const float_val4 = ts4.toFloat(); + const expected4 = -0.5; + try expect(@abs(float_val4 - expected4) < 0.000000001); +} diff --git a/course/code/16/result-location.zig b/course/code/16/result-location.zig new file mode 100644 index 00000000..638cce6f --- /dev/null +++ b/course/code/16/result-location.zig @@ -0,0 +1,390 @@ +const std = @import("std"); + +pub fn main() !void { + BasicInference.main(); + ResultTypeVariable.main(); + ResultTypeReturn.main(); + ResultTypeParam.main(); + ResultTypeFieldDefault.main(); + ResultLocationNested.main(); + DeclLiteralBasic.main(); + DeclLiteralFieldDefault.main(); + DeclLiteralFunction.main(); + // 以下示例需要 allocator,仅在测试中运行 + // try DeclLiteralErrorUnion.main(); + // try StdLibArrayList.main(); + try StdLibDebugAllocator.main(); +} + +const BasicInference = struct { + // #region basic_inference + const Point = struct { + x: i32, + y: i32, + }; + + pub fn main() void { + // 编译器从变量类型推断出 .{} 的具体类型 + const pt: Point = .{ .x = 10, .y = 20 }; + + // 等价于 + const pt2: Point = Point{ .x = 10, .y = 20 }; + + std.debug.print("pt: ({}, {}), pt2: ({}, {})\n", .{ pt.x, pt.y, pt2.x, pt2.y }); + } + // #endregion basic_inference +}; + +const ResultTypeVariable = struct { + // #region result_type_variable + const Color = struct { + r: u8, + g: u8, + b: u8, + }; + + pub fn main() void { + // 结果类型是 Color + const red: Color = .{ .r = 255, .g = 0, .b = 0 }; + std.debug.print("red: ({}, {}, {})\n", .{ red.r, red.g, red.b }); + } + // #endregion result_type_variable +}; + +const ResultTypeReturn = struct { + // #region result_type_return + const Vec2 = struct { + x: f32, + y: f32, + }; + + fn origin() Vec2 { + // 结果类型是 Vec2 + return .{ .x = 0, .y = 0 }; + } + + pub fn main() void { + const o = origin(); + std.debug.print("origin: ({d}, {d})\n", .{ o.x, o.y }); + } + // #endregion result_type_return +}; + +const ResultTypeParam = struct { + // #region result_type_param + const Size = struct { + width: u32, + height: u32, + }; + + fn calculateArea(size: Size) u64 { + return @as(u64, size.width) * size.height; + } + + pub fn main() void { + // 调用时,.{} 的结果类型是 Size + const area = calculateArea(.{ .width = 100, .height = 50 }); + std.debug.print("area: {}\n", .{area}); + } + // #endregion result_type_param +}; + +const ResultTypeFieldDefault = struct { + // #region result_type_field_default + const Config = struct { + timeout: u32 = 30, + retries: u8 = 3, + }; + + const Wrapper = struct { + // 字段类型是 Config,所以 .{} 的结果类型是 Config + config: Config = .{}, + }; + + pub fn main() void { + const w: Wrapper = .{}; + std.debug.print("timeout: {}, retries: {}\n", .{ w.config.timeout, w.config.retries }); + } + // #endregion result_type_field_default +}; + +const ResultLocationNested = struct { + // #region result_location_nested + const Inner = struct { + value: i32, + }; + + const Outer = struct { + inner: Inner, + name: []const u8, + }; + + pub fn main() void { + // 结果位置 Outer 传播到 inner 字段,使其结果类型为 Inner + const obj: Outer = .{ + .inner = .{ .value = 42 }, // 这里 .{} 的结果类型是 Inner + .name = "example", + }; + std.debug.print("inner.value: {}, name: {s}\n", .{ obj.inner.value, obj.name }); + } + // #endregion result_location_nested +}; + +const DeclLiteralBasic = struct { + // #region decl_literal_basic + const S = struct { + x: u32, + + // 类型内的常量声明 + const default: S = .{ .x = 123 }; + }; + + pub fn main() void { + // .default 会被解析为 S.default + const val: S = .default; + std.debug.print("val.x: {}\n", .{val.x}); + } + + test "decl literal" { + const val: S = .default; + try std.testing.expectEqual(123, val.x); + } + // #endregion decl_literal_basic +}; + +const DeclLiteralFieldDefault = struct { + // #region decl_literal_field_default + const Settings = struct { + x: u32, + y: u32, + + const default: Settings = .{ .x = 1, .y = 2 }; + const high_performance: Settings = .{ .x = 100, .y = 200 }; + }; + + const Application = struct { + // 使用声明字面量设置默认值 + settings: Settings = .default, + }; + + pub fn main() void { + const app1: Application = .{}; + std.debug.print("app1.settings: ({}, {})\n", .{ app1.settings.x, app1.settings.y }); + + // 也可以覆盖为其他预定义值 + const app2: Application = .{ .settings = .high_performance }; + std.debug.print("app2.settings: ({}, {})\n", .{ app2.settings.x, app2.settings.y }); + } + + test "decl literal in field default" { + const app1: Application = .{}; + try std.testing.expectEqual(1, app1.settings.x); + + const app2: Application = .{ .settings = .high_performance }; + try std.testing.expectEqual(100, app2.settings.x); + } + // #endregion decl_literal_field_default +}; + +const DeclLiteralFunction = struct { + // #region decl_literal_function + const Point = struct { + x: i32, + y: i32, + + fn init(val: i32) Point { + return .{ .x = val, .y = val }; + } + + fn offset(val: i32, dx: i32, dy: i32) Point { + return .{ .x = val + dx, .y = val + dy }; + } + }; + + pub fn main() void { + // .init(5) 等价于 Point.init(5) + const p1: Point = .init(5); + std.debug.print("p1: ({}, {})\n", .{ p1.x, p1.y }); + + const p2: Point = .offset(0, 10, 20); + std.debug.print("p2: ({}, {})\n", .{ p2.x, p2.y }); + } + + test "call function via decl literal" { + const p1: Point = .init(5); + try std.testing.expectEqual(5, p1.x); + try std.testing.expectEqual(5, p1.y); + + const p2: Point = .offset(0, 10, 20); + try std.testing.expectEqual(10, p2.x); + try std.testing.expectEqual(20, p2.y); + } + // #endregion decl_literal_function +}; + +const DeclLiteralErrorUnion = struct { + // #region decl_literal_error_union + const Buffer = struct { + data: std.ArrayListUnmanaged(u32), + + fn initCapacity(allocator: std.mem.Allocator, capacity: usize) !Buffer { + return .{ .data = try .initCapacity(allocator, capacity) }; + } + }; + + test "decl literal with error union" { + var buf: Buffer = try .initCapacity(std.testing.allocator, 10); + defer buf.data.deinit(std.testing.allocator); + + buf.data.appendAssumeCapacity(42); + try std.testing.expectEqual(42, buf.data.items[0]); + } + // #endregion decl_literal_error_union +}; + +const FaultyDefaultValues = struct { + // #region faulty_default_problem + /// `ptr` 指向 `[len]u32` + pub const BufferA = extern struct { + ptr: ?[*]u32 = null, + len: usize = 0, + }; + + // 看起来是空 buffer + var empty_buf: BufferA = .{}; + + // 但用户可以只覆盖部分字段,导致不一致的状态! + var bad_buf: BufferA = .{ .len = 10 }; // ptr 是 null,但 len 是 10 + // #endregion faulty_default_problem +}; + +const FaultyDefaultSolution = struct { + // #region faulty_default_solution + /// `ptr` 指向 `[len]u32` + pub const BufferB = extern struct { + ptr: ?[*]u32, + len: usize, + + // 通过声明提供预定义的有效状态 + pub const empty: BufferB = .{ .ptr = null, .len = 0 }; + }; + + // 安全地创建空 buffer + var empty_buf: BufferB = .empty; + + // 如果要手动指定值,必须同时指定所有字段 + // var custom_buf: BufferB = .{ .ptr = some_ptr, .len = 10 }; + // #endregion faulty_default_solution +}; + +const StdLibArrayList = struct { + // #region stdlib_arraylist + const Container = struct { + // 使用 .empty 而不是 .{} + list: std.ArrayListUnmanaged(i32) = .empty, + }; + + test "ArrayListUnmanaged with decl literal" { + var c: Container = .{}; + defer c.list.deinit(std.testing.allocator); + + try c.list.append(std.testing.allocator, 1); + try c.list.append(std.testing.allocator, 2); + + try std.testing.expectEqual(2, c.list.items.len); + } + // #endregion stdlib_arraylist +}; + +const StdLibDebugAllocator = struct { + // #region stdlib_debug_allocator + pub fn main() !void { + // DebugAllocator 在 0.16 中提供了 .init 声明 + var gpa: std.heap.DebugAllocator(.{}) = .init; + defer _ = gpa.deinit(); + + const allocator = gpa.allocator(); + const ptr = try allocator.alloc(u8, 100); + defer allocator.free(ptr); + + std.debug.print("allocated {} bytes\n", .{ptr.len}); + } + + test "debug allocator with decl literal" { + var gpa: std.heap.DebugAllocator(.{}) = .init; + defer _ = gpa.deinit(); + + const allocator = gpa.allocator(); + const ptr = try allocator.alloc(u8, 100); + defer allocator.free(ptr); + + try std.testing.expectEqual(100, ptr.len); + } + // #endregion stdlib_debug_allocator +}; + +const NamingConflict = struct { + // #region naming_conflict + // 错误:字段和声明同名(此代码无法编译) + // const Bad = struct { + // Value: u32, // 字段 + // const Value = 100; // 声明 - 编译错误! + // }; + + // 正确:遵循命名约定 + const Good = struct { + value: u32, // 字段使用 snake_case + const Value = 100; // 声明使用 PascalCase + }; + // #endregion naming_conflict +}; + +test "basic inference" { + const Point = BasicInference.Point; + const pt: Point = .{ .x = 10, .y = 20 }; + try std.testing.expectEqual(10, pt.x); + try std.testing.expectEqual(20, pt.y); +} + +test "decl literal basic" { + const S = DeclLiteralBasic.S; + const val: S = .default; + try std.testing.expectEqual(123, val.x); +} + +test "decl literal field default" { + const Application = DeclLiteralFieldDefault.Application; + const app1: Application = .{}; + try std.testing.expectEqual(1, app1.settings.x); +} + +test "decl literal function" { + const Point = DeclLiteralFunction.Point; + const p1: Point = .init(5); + try std.testing.expectEqual(5, p1.x); +} + +test "decl literal error union" { + const Buffer = DeclLiteralErrorUnion.Buffer; + var buf: Buffer = try .initCapacity(std.testing.allocator, 10); + defer buf.data.deinit(std.testing.allocator); + buf.data.appendAssumeCapacity(42); + try std.testing.expectEqual(42, buf.data.items[0]); +} + +test "stdlib arraylist" { + const Container = StdLibArrayList.Container; + var c: Container = .{}; + defer c.list.deinit(std.testing.allocator); + try c.list.append(std.testing.allocator, 1); + try std.testing.expectEqual(1, c.list.items.len); +} + +test "stdlib debug allocator" { + var gpa: std.heap.DebugAllocator(.{}) = .init; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + const ptr = try allocator.alloc(u8, 100); + defer allocator.free(ptr); + try std.testing.expectEqual(100, ptr.len); +} diff --git a/course/code/16/struct.zig b/course/code/16/struct.zig index 48ce2a8b..33dca038 100644 --- a/course/code/16/struct.zig +++ b/course/code/16/struct.zig @@ -140,7 +140,7 @@ const SelfReference3 = struct { // #region more_self_reference3 const std = @import("std"); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; // #region deault_self_reference3 const User = struct { diff --git a/course/code/release b/course/code/release index 99b5cf48..c3e11da9 120000 --- a/course/code/release +++ b/course/code/release @@ -1 +1 @@ -./15 \ No newline at end of file +./16 \ No newline at end of file diff --git a/course/engineering/build-system.md b/course/engineering/build-system.md index f308297b..bdaff7de 100644 --- a/course/engineering/build-system.md +++ b/course/engineering/build-system.md @@ -154,7 +154,7 @@ Project-Specific Options: 通常,二进制可执行程序的构建结果会输出在 `zig-out/bin` 下,而链接库的构建结果会输出在 `zig-out/lib` 下。 -如果要连接到系统的库,则使用 `exe.linkSystemLibrary`,Zig 内部借助 pkg-config 实现该功能。示例: +如果要连接到系统的库,在 Zig 0.16 的模块化构建 API 中通常使用 `exe.root_module.linkSystemLibrary`,Zig 内部借助 pkg-config 实现该功能。类似地,链接其他库或添加 C/C++ 源文件时,也通常是操作 `root_module`。示例: <<<@/code/release/build_system/system_lib/build.zig @@ -266,9 +266,9 @@ zig 的构建系统还允许我们执行一些额外的命令,例如根据 jso ::: info 🅿️ 提示 -关于头文件的引入,可以使用 `addIncludePath` +关于头文件的引入,可以使用 `root_module.addIncludePath` -针对多个 C 源代码文件,zig 提供了函数 `addCSourceFiles` 用于便捷地添加多个源文件。 +针对多个 C 源代码文件,zig 提供了 `root_module.addCSourceFiles` 用于便捷地添加多个源文件。 ::: diff --git a/course/engineering/package_management.md b/course/engineering/package_management.md index 61033983..2206feb1 100644 --- a/course/engineering/package_management.md +++ b/course/engineering/package_management.md @@ -21,12 +21,9 @@ zig 当前并没有一个中心化存储库,包可以来自任何来源,无 - `name`:当前你所开发的包的名字 - `version`:包的版本,使用 [Semantic Version](https://semver.org/)。 - `fingerprint`: 该值为校验和,它与包的名字有关,使用 `zig build` 时会告诉你应该填什么。 -- `dependencies`:依赖项,内部是一连串的匿名结构体,字段 - `dep_name` 是依赖包的名字, - `url` 是源代码地址, - `hash` 是对应的 hash(源文件内容的 hash), - `path`是不使用源码包而是本地目录时目录的路径。 - 当使用目录方法导入包时就不能使用`url`和`hash`,反之同理。 +- `dependencies`:依赖项,内部是一个匿名结构体;每个字段名就是依赖包名,字段值则是该依赖的配置。 + 常见配置项包括 `url`、`hash` 和 `path`。 + 当使用源码包时填写 `url` 和 `hash`;当使用本地目录时填写 `path`,两种方式不能混用。 - `paths`:显式声明包含的源文件,包含所有则指定为空。 ::: info 🅿️ 提示 diff --git a/course/hello-world.md b/course/hello-world.md index f2152503..e999dd3e 100644 --- a/course/hello-world.md +++ b/course/hello-world.md @@ -42,7 +42,7 @@ Zig 默认可导入三个核心模块: - **参数**:第二个参数是一个元组,你可以将其理解为一个匿名结构体。 ::: warning ⚠️ 注意 -`std.debug.print` 主要用于调试,不推荐在生产环境中使用。因为它会将信息打印到 `stderr`,且在某些构建模式下可能会被编译器优化掉。 +`std.debug.print` 主要用于调试,不推荐在生产环境中作为正式输出接口使用。因为它会将信息打印到 `stderr`,更适合调试信息而不是程序的正常输出。 这只是一个入门示例,接下来我们将探讨更“正确”的打印方式。 ::: @@ -52,7 +52,7 @@ Zig 默认可导入三个核心模块: “打印 Hello, World”看似简单,但在 Zig 中,它能引导我们思考一些底层设计。 -Zig 本身没有内置的 `@print()` 函数,输出功能通常由标准库的 `log` 和 `io` 包提供。`std.debug.print` 是一个特例,主要用于调试。 +Zig 本身没有内置的 `@print()` 函数,输出功能通常由标准库的 `log` 和 `Io` 包提供。`std.debug.print` 是一个特例,主要用于调试。 让我们看一个更规范的例子(**但请注意,此代码同样不建议直接用于生产环境**): @@ -65,9 +65,9 @@ Zig 本身没有内置的 `@print()` 函数,输出功能通常由标准库的 这段代码分别向 `stdout` 和 `stderr` 输出了信息。 - `stdout` (标准输出):用于输出程序的正常信息。写入 `stdout` 的操作可能会失败。 -- `stderr` (标准错误):用于输出错误信息。我们通常假定写入 `stderr` 的操作不会失败(由操作系统保证)。 +- `stderr` (标准错误):用于输出错误信息。写入 `stderr` 同样可能失败,因此示例里也使用了 `try` 来处理错误。 -我们通过 `std.io` 模块获取了标准输出和标准错误的 `writer`,它们提供了 `print` 方法,可以将格式化的字符串写入对应的 I/O 流。 +我们通过 `std.Io.File.stdout()` 和 `std.Io.File.stderr()` 获取标准输出和标准错误,并基于传入的 `io` 实例创建 `writer`。示例中的 `io` 一般来自入口函数 `main(init: std.process.Init)` 中的 `init.io`。 ### 考虑性能:使用缓冲区 @@ -77,17 +77,17 @@ Zig 本身没有内置的 `@print()` 函数,输出功能通常由标准库的 <<<@/code/release/hello_world.zig#three -通过 `std.io.bufferedWriter`,我们为 `stdout` 和 `stderr` 的 `writer` 添加了缓冲功能,从而提高了性能。 +这里直接为 `stdout` 和 `stderr` 各自提供了一块固定缓冲区,`writer` 会先把内容写入缓冲区,再在 `flush` 时真正输出,从而减少系统调用。 ## 更进一步:线程安全 以上代码在单线程环境下工作良好,但在多线程环境中,多个线程同时调用 `print` 可能会导致输出内容交错混乱。 -为了保证线程安全,我们需要为 `writer` 添加锁。 +为了保证线程安全,通常需要在共享 `writer` 的外层自行做同步。 -你可以使用 `std.Thread.Mutex` 来实现一个线程安全的 `writer`。 +在 Zig 0.16 中,常见做法是使用 `std.Io.Mutex`(需要 `Io` 实例)或基于 `std.atomic.Mutex` 的轻量自旋锁,根据具体场景选择。 -我们鼓励你阅读[标准库源码](https://ziglang.org/documentation/master/std/#std.Thread.Mutex)来深入了解其工作原理。 +我们鼓励你阅读[标准库源码](https://ziglang.org/documentation/master/std/#std.Io.Mutex)来深入了解其工作原理。 ## 了解更多 diff --git a/course/update/0.16.0-description.md b/course/update/0.16.0-description.md new file mode 100644 index 00000000..542bdf06 --- /dev/null +++ b/course/update/0.16.0-description.md @@ -0,0 +1,739 @@ +--- +outline: deep +comments: false +showVersion: false +--- + +# `0.16.0` + +2026/4/13,`0.16.0` 发布,历时 8 个月,有 244 位贡献者参与,一共进行了 1183 次提交! + +如果要用一句话概括这个版本,那就是:**`0.16.0` 把上一轮预告过的大量基础设施重构真正落地了。** + +`0.15.x` 还在为 `std.Io`、增量编译和新的工具链架构铺路,到了 `0.16.0`,这些方向已经进入可以大规模体验的阶段:I/O 统一为接口、`main` 可以直接拿到 `io` 和 `gpa`、增量编译进一步可用、新 ELF linker 开始接入默认流程,同时语言层也继续清理历史设计。 + +## 目标支持 + +`0.16.0` 在目标支持上的一个重要变化,是 Zig 对“哪些平台值得持续投入工程质量”这件事变得更明确了。 + +比较值得注意的点有: + +- `aarch64-freebsd`、`aarch64-netbsd`、`loongarch64-linux`、`powerpc64le-linux`、`s390x-linux`、`x86_64-freebsd`、`x86_64-netbsd`、`x86_64-openbsd` 这些目标现在都会在 Zig 的 CI 中原生测试(OSUOSL 提供了 AArch64 和 Power ISA 硬件,IBM 提供了 z/Architecture 硬件) +- 新增 `aarch64-maccatalyst` 与 `x86_64-maccatalyst` 的交叉编译支持。这部分支持几乎是“免费”得到的,因为 Zig 自带的 `libSystem.tbd` 本来就提供了对应符号 +- 新增 `loongarch32-linux` 的初始支持,不过当前仍不支持 libc,LLVM 也仍把这个目标的 ABI 视为不稳定,只有走 `std.os.linux` 的纯 syscall 程序才能跑 +- Alpha、KVX、MicroBlaze、OpenRISC、PA-RISC、SuperH 等架构加入了基础支持。这些目标当前要求使用 Zig 的 C 后端(配合 GCC),或者外部的 LLVM/Clang fork +- Oracle Solaris、IBM AIX、z/OS 支持被移除——这些专有系统取系统头文件本身就不容易,影响了贡献的可审计性;`illumos` 是开源的 OpenSolaris fork,不受影响,仍然保留支持 +- 栈回溯支持进一步扩大,几乎所有主流目标在崩溃或使用 `DebugAllocator` 时都能得到更可靠的 stack trace +- 一批主要影响弱内存序架构(AArch64 尤其在没有 LSE 的情况下、LoongArch、Power ISA)以及不常见 page size 的标准库 bug 被修复 +- 影响 big-endian 主机的若干标准库与编译器 bug 被修复;big-endian ARM 目标在 ARMv6+ 下现在会输出 BE8 目标文件,而不是过去那套 BE32 + +对普通用户来说,这意味着 Zig 在常见 Linux / BSD / macOS / Windows 目标上的“可用性底线”又往前推了一步;而对于比较边缘的平台,官方也更清晰地区分了“支持”“实验性支持”和“不再支持”。 + +### Tier 系统 + +Zig 把对各目标的支持程度划成四档(Tier 1 最高),具体含义如下: + +- **Tier 1**:所有非实验性语言特性都已知正常工作;编译器可以**不依赖 LLVM** 直接为该目标生成机器码 +- **Tier 2**:标准库的跨平台抽象覆盖了该目标;该目标具备调试信息能力,因此在断言失败 / crash 时能给出 stack trace;交叉编译时可获得 libc;CI 在每次推送时都会跑模块测试 +- **Tier 3**:编译器可通过 LLVM 为该目标生成机器码;链接器可生成对象文件、库与可执行文件;该目标在 LLVM 中不被视为实验性 +- **Tier 4**:编译器只能通过 LLVM 为该目标生成汇编源码 + +`Tier 1` 的长期目标是“没有任何被禁用的测试”——这一条也会变成 Zig 1.0 之后版本的硬性要求。 + +### 其他附加目标 + +除了 Tier 1–4 这套划分之外,Zig 对下面这些目标也有不同程度的支持,但 tier 系统本身并不完全适用: + +`aarch64-driverkit`、`aarch64(_be)-freestanding`、`aarch64-uefi`、`alpha-freestanding`、`amdgcn-amdhsa`、`amdgcn-amdpal`、`amdgcn-mesa3d`、`arc(eb)-freestanding`、`arm(eb)-freestanding`、`arm-3ds`、`arm-uefi`、`arm-vita`、`avr-freestanding`、`bpf(eb,el)-freestanding`、`csky-freestanding`、`hexagon-freestanding`、`hppa(64)-freestanding`、`kalimba-freestanding`、`kvx-freestanding`、`lanai-freestanding`、`loongarch(32,64)-freestanding`、`loongarch(32,64)-uefi`、`m68k-freestanding`、`microblaze(el)-freestanding`、`mips(64)(el)-freestanding`、`mipsel-psp`、`msp430-freestanding`、`nvptx(64)-cuda`、`nvptx(64)-nvcl`、`or1k-freestanding`、`powerpc(64)(le)-freestanding`、`powerpc64-ps3`、`propeller-freestanding`、`riscv(32,64)(be)-freestanding`、`riscv(32,64)-uefi`、`s390x-freestanding`、`sh(eb)-freestanding`、`sparc(64)-freestanding`、`spirv(32,64)-opencl`、`spirv(32,64)-opengl`、`spirv(32,64)-vulkan`、`thumb(eb)-freestanding`、`ve-freestanding`、`wasm(32,64)-emscripten`、`wasm(32,64)-freestanding`、`wasm(32,64)-wasi`、`x86(_64)-driverkit`、`x86(_64)-elfiamcu`、`x86(_64)-freestanding`、`x86(_64)-uefi`、`xcore-freestanding`、`xtensa-freestanding`。 + +这些目标常见于嵌入式、unikernel、固件、内核扩展、GPU 计算、UEFI、各类游戏机/掌机、WebAssembly 宿主等场景。 + +## 系统最低版本要求 + +| 操作系统(Operating System) | 最低版本要求(Minimum Version) | +| :--------------------------- | :-----------------------------: | +| DragonFly BSD | 6.0 | +| FreeBSD | 14.0 | +| Linux | 5.10 | +| NetBSD | 10.1 | +| OpenBSD | 7.8 | +| macOS | 13.0 | +| Windows | 10 | + +## 语言变动 + +### `switch` 继续补齐语义 + +`switch` 是这一轮里继续被打磨的语言特性之一。现在,`packed struct` 和 `packed union` 可以直接作为 prong item,比较规则按照 backing integer 来做;同时,decl literals、需要结果类型的表达式、union tag capture 等场景也获得了更一致的支持。 + +这类变更本身并不一定会让旧代码报错,但它明显减少了过去一些“语言明明应该支持、但实现上还没补齐”的边角问题。 + +### `@cImport` 正式进入“迁移期” + +`0.16.0` 仍然保留了 `@cImport`,但已经明确将其标记为 deprecated。官方方向是把 C 头文件翻译迁到构建系统中,通过 `build.zig` 里的 `addTranslateC` 生成模块,再在 Zig 代码里使用 `@import("c")`。 + +这和 Zig 未来“逐步把对 LLVM / Clang 的库级依赖转向进程级依赖”的方向是一致的。 + +同时,`translate-c` 的实现现在已经从 `libclang` 切换到了 Aro / translate-c 方案。对大多数用户来说这是透明的,但如果你升级后发现 C 头文件翻译行为有差异,它更可能是实现 bug,而不是新的预期行为。 + +### `@Type` 被拆分为多个独立内建函数 + +这是 `0.16.0` 最明显的语言级 breaking change 之一。`@Type` 被移除,原来依赖它造类型的元编程代码,需要迁移到新的内建函数: + +- `@EnumLiteral()` +- `@Int()` +- `@Tuple()` +- `@Pointer()` +- `@Fn()` +- `@Struct()` +- `@Union()` +- `@Enum()` + +这项改动的核心目标,是让“构造类型”这件事更直观,也让常见场景不必再绕一层 `std.meta.Int`、`std.meta.Tuple` 之类的辅助函数。 + +### packed / extern 相关规则更严格 + +这次发布继续收紧了位级布局和 ABI 边界的隐式行为: + +- `packed union` 现在要求更明确的 backing integer 语义 +- `packed struct` / `packed union` 不再允许直接放指针字段 +- `extern` 场景下,`enum` 与 `packed` 类型不能再依赖隐式推断的底层整数类型 + +从设计上看,这些限制的方向非常统一:**凡是会影响 ABI 或精确内存布局的内容,Zig 都更倾向于要求你显式写出来。** + +### 向量语义进一步收紧 + +`0.16.0` 禁止了运行时向量索引,同时也不再鼓励通过旧式内存强转在数组和向量之间来回转换。简单来说,向量更明确地被当成“值语义上的 SIMD 数据”,而不是“碰巧可以按数组方式随便访问的内存”。 + +另外,小整数类型在“绝对不会丢精度”的前提下,现在可以安全地隐式转换为浮点类型,这也让数值代码更顺手了一些。 + +### 类型解析与依赖环错误大幅重做 + +`0.16.0` 还重做了编译器内部的类型解析流程。这个改动的影响非常深: + +- 许多以前会误报 dependency loop 的代码现在可以正常工作 +- 增量编译和普通编译之间的一致性明显增强 +- 一小部分本来就存在真实依赖环的代码,现在会更早、更明确地报错 + +如果你升级后遇到以前没见过的 dependency loop,先别急着回退版本。因为 `0.16.0` 的错误报告已经能更清楚地指出环路是怎么形成的,通常只要打断其中一条依赖即可。 + +### 一元浮点内建会向下转发结果类型 + +`@sqrt`、`@sin`、`@cos`、`@tan`、`@exp`、`@exp2`、`@log`、`@log2`、`@log10`、`@floor`、`@ceil`、`@trunc`、`@round` 现在都会把外层的结果类型向内转发。意味着以前必须借中间变量才能写的式子(例如 `const x: f64 = @sqrt(@floatFromInt(N));`)现在可以直接成立。这条改动是配合“为做游戏开发改善 ergonomics”的更大计划。 + +### 浮点取整可以直接产出整数 + +`@floor`、`@ceil`、`@round`、`@trunc` 现在不仅会做取整,还能在结果类型是整数时直接给出整数值,省掉一层 `@intFromFloat`。 + +### 局部变量地址不能再被返回 + +返回局部变量地址(`return &local;`)这个新手常见错误现在直接编译报错。 + +这件事的难点其实在于:返回一个无效指针本身是合法的(例如 `fn foo() *i32 { return undefined; }`),非法只发生在解引用之后;甚至 `fn bar() noreturn { unreachable; // equivalent to foo().* }` 也是合法函数。Zig 的解法是“句法层面的较真”:所有“不需要类型检查就能 trivially 降级到 `return undefined` 的表达式”都被禁止,要求你直接写规范的 `return undefined`。`return &local;` 就属于这一类——编译器会给出 `error: returning address of expired local variable 'x'` 这样的清晰诊断,并标出 `var x` 是“declared runtime-known here”。 + +修复方式无非三种:返回值、由调用方传入 buffer、改用堆分配。后续还会有更多类似性质的编译错误被加进来。 + +### Lazy 字段分析 + +`struct` / `union` / `enum` / `opaque` 现在只有在真正需要它的尺寸或字段类型时才会被解析。把类型当作命名空间使用,或者只是构造非解引用指针 `*T`,都不会再触发 `T` 的字段解析。这条改动直接消除了“引用一下 `std.Io.Writer` 就把整个 `std.Io` 的 vtable 都拉进来”这类问题,进而减少了无谓 codegen 和体积膨胀。 + +### 指向 comptime-only 类型的指针不再是 comptime-only + +`comptime_int` 是 comptime-only 类型,但 `*comptime_int` / `[]comptime_int` 现在不是。最直观的例子是函数指针:`*const fn () void` 是运行时类型——你不能在运行时解引用它,但它本身可以在运行时存在。配合反射,这意味着 `[]const std.builtin.Type.StructField` 现在可以直接传给运行时函数,并通过它读出每个字段的 `name`。 + +### `*u8` 与 `*align(1) u8` 不再是同一类型 + +这两个类型现在被视为不同类型(之前 `==` 比较为真)。**两者依然可以互相转换,包括嵌套在指针里**,所以日常用法几乎不需要变。可以理解为类似 `u32` 与 `c_uint` 的差别——技术上不同,行为一致。 + +### Zero-bit tuple 字段不再隐式 `comptime` + +`0.14` 引入的“tuple 中 zero-bit 字段隐式变成 `comptime` 字段”这条规则被回滚。对绝大多数代码无影响——zero-bit 字段的值依然是 comptime-known 的——只有直接读 `StructField.is_comptime` 反射或依赖类型等价性的代码需要调整。 + +### Reworked Byval Syntax Lowering + +编译器前端早期为了减少中间指令尝试用“byval”语义降级表达式,但这个实验带来了数组访问性能问题、显式拷贝下的意外别名、退化场景代码质量极差等一系列 issue。`0.16.0` 改成全程 byref 降级,只在最后一次 load 时才取值,从根上修掉这些问题。 + +## 标准库 + +### I/O 作为 Interface + +这是 `0.16.0` 最重头的内容,没有之一。 + +从这个版本开始,所有输入输出相关能力都围绕 `std.Io` 展开。更准确地说,凡是可能阻塞控制流,或会引入非确定性的操作,都被纳入了 `Io` 的抽象边界内。 + +当前官方提供了几种典型实现: + +- `Io.Threaded`:基于线程,功能最完整,也是从 `0.15.x` 升级时最接近旧行为的实现 +- `Io.Evented`:仍在实验阶段,用来推动接口演进 +- `Io.Uring` / `Io.Kqueue` / `Io.Dispatch`:分别基于 Linux io_uring、kqueue 和 macOS Grand Central Dispatch 的概念验证实现,尚未完整 +- `Io.failing`:用于模拟“不支持任何操作”的环境 + +围绕它,标准库引入了整套新的任务和并发抽象: + +- `Future` +- `Group` +- `Batch` +- `Select` +- `Queue(T)` +- `Clock` / `Duration` / `Timestamp` / `Timeout` 等时间度量类型 +- 统一的 cancelation 模型 + +这不仅是 API 改名,而是 Zig 对“并发 I/O 应该怎样进入语言生态”给出的新答案。文件系统、网络、进程、同步原语、定时器等能力,都围绕这套接口重新组织了。 + +#### Future + +Future 是构建在函数之上的任务级抽象。 + +`io.async(fn, args)` 会返回一个 `Future(T)`,其中 `T` 是被调用函数的返回类型。`async` 表达的是“这次调用与其他逻辑相互独立”——也就是 **asynchrony**。因此创建任务是不会失败的,并且能在缺乏并发机制的精简 `Io` 实现上工作(这种实现可以在 `async` 调用里直接同步执行那个函数)。 + +`io.concurrent(fn, args)` 与 `io.async` 类似,但语义上要求“必须并发执行才能保证正确性”。这必然涉及内存分配,所以可能返回 `error.ConcurrencyUnavailable`。 + +`Future(T)` 提供两个方法: + +- `await`:阻塞控制流直到任务完成,并返回函数的返回值 +- `cancel`:和 `await` 相同,但额外请求 `Io` 实现去打断该操作并返回 `error.Canceled`。绝大多数 I/O 操作的错误集合里现在都包含 `error.Canceled` + +为了避免资源泄漏并优雅处理 cancelation,推荐这种写法: + +```zig +var foo_future = io.async(foo, .{args}); +defer if (foo_future.cancel(io)) |resource| resource.deinit() else |_| {} + +var bar_future = io.async(bar, .{args}); +defer if (bar_future.cancel(io)) |resource| resource.deinit() else |_| {} + +const foo_result = try foo_future.await(io); +const bar_result = try bar_future.await(io); +``` + +如果函数没有返回需要释放的资源,那个 `if` 可以简化成 `_ = foo.cancel(io) catch {}`;如果返回 `void`,丢弃也可以省掉。但 `cancel` 这一步是必要的,因为它在出错(包括 `error.Canceled`)时也会释放 async 任务本身占用的资源。 + +#### Group + +当多个任务有相同生命周期时,`Group` 是更合适的选择。它的开销是 O(1):开 N 个任务也只需要常数级的额外空间。 + +下面是一个用 `Group.async` 实现 sleep sort 的例子: + +```zig +const std = @import("std"); +const Io = std.Io; + +test "sleep sort" { + const io = std.testing.io; + + const rng_impl: std.Random.IoSource = .{ .io = io }; + const rng = rng_impl.interface(); + + var array: [10]i32 = undefined; + for (&array) |*elem| elem.* = rng.uintLessThan(u16, 1000); + + var sorted: [10]i32 = undefined; + var index: std.atomic.Value(usize) = .init(0); + + var group: Io.Group = .init; + defer group.cancel(io); + + for (&array) |elem| group.async(io, sleepAppend, .{ io, &sorted, &index, elem }); + + try group.await(io); + + for (sorted[0 .. sorted.len - 1], sorted[1..]) |a, b| { + try std.testing.expect(a <= b); + } +} + +fn sleepAppend(io: Io, result: []i32, i_ptr: *std.atomic.Value(usize), elem: i32) !void { + try io.sleep(.fromMilliseconds(elem), .awake); + result[i_ptr.fetchAdd(1, .monotonic)] = elem; +} +``` + +#### Cancelation + +> 顺便注意拼写:标准命名采用单 `l` 的 `cancelation`。 + +和 for 循环里的“提前 break”是同一类问题——一旦多个任务并发执行,就会遇到“某个任务已经完成或失败,其它任务的结果或副作用对全局已经不再重要,甚至需要回滚”的场景。 + +`Future` / `Group` / `Batch` 都支持发起 cancelation 请求。请求可能被确认,也可能不会。一旦被确认,对应的 I/O 操作会返回 `error.Canceled`。即便是 `Io.Threaded` 也支持这套机制——它通过给线程发信号让阻塞 syscall 返回 `EINTR`,再以此为契机检查 cancelation 请求并决定是否重试。 + +只有“发起这次取消请求”的代码自己才能安全地忽略 `error.Canceled`。其它情况下处理这个错误有三种方式(按常见程度排序): + +1. 把它向上传播 +2. 收到后调用 `io.recancel()`,再选择不传播——这会重新激活 cancelation 请求,下一个检查点会有机会再次确认 +3. 用 `io.swapCancelProtection()` 把它变成 unreachable + +cancel 在语义上等同于 await + 取消请求。也就是说,请求取消之后任务可能依然成功完成,你仍然能拿到返回值——这意味着诸如已分配的资源等副作用必须被正确处理: + +```zig +const std = @import("std"); +const Io = std.Io; + +test "trivial cancel demo" { + const io = std.testing.io; + + var file_task = io.async(Io.Dir.openFile, .{ .cwd(), io, "hello.txt", .{} }); + defer if (file_task.cancel(io)) |file| file.close(io) else |_| {}; +} +``` + +由于 `await` 和 `cancel` 都是幂等的,最常见的写法是“创建任务后立刻 `defer` 取消”,这样无论函数从哪里返回都能保证并发任务被回收。 + +通常 Zig 程序员不需要主动写代码支持 cancelation,因为 `error.Canceled` 已经被烘焙进所有可取消 I/O 操作的错误集合。但如果有长时间运行的 CPU 密集任务需要响应取消,可以显式调用 `io.checkCancel` 添加额外的 cancelation point。 + +#### Batch + +`Batch` 是更底层的并发原语,它在 `Operation` 这一层提供并发能力。优点是高效且可移植,缺点是抽象起来更难——尤其当你需要在操作之间穿插一些自定义逻辑时。 + +未来大部分文件系统和网络功能预计都会迁到基于 `Operation` 的实现上,进而都能配合 `Batch` 使用,并通过 `operateTimeout` 给**任意** I/O 操作加超时。当前能用 Batch 跑的 Operation 列表是: + +- `FileReadStreaming` +- `FileWriteStreaming` +- `DeviceIoControl` +- `NetReceive` + +而 Future 是同样思想在“函数”层面的版本——更灵活、更顺手,但在使用 `concurrent` 时需要分配任务内存并可能返回 `error.ConcurrencyUnavailable`,使用 `async` 时也可能引入意外的阻塞操作。 + +简单说:写最优、可复用的代码时,如果你只是想同时做几件事,`Batch` 更合适;如果绕开它会让你重新发明 future,那就用 Future。也可以先用 Future 写出来,之后再把热点路径改造成 Batch 来减少任务开销。 + +### `Juicy Main` + +为了配合新的 `std.Io`,`main` 也获得了一个很实用的新入口:`std.process.Init`。 + +只要把 `main` 写成: + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const io = init.io; + _ = gpa; + _ = io; +} +``` + +你就能直接拿到: + +- `gpa` +- `io` +- `arena` +- `environ_map` +- `preopens` +- `minimal.args` / `minimal.environ` + +这让应用入口第一次真正成了“进程上下文的注入点”。对于应用开发来说,这个改动的体感甚至不亚于 `std.Io` 本身。 + +### 环境变量和进程参数不再是全局状态 + +和 `Juicy Main` 配套的另一项重要变化,是环境变量和进程参数都不再被鼓励当成全局状态来访问。 + +现在,环境变量原则上只存在于应用入口的 `Init` 里;需要使用它们的函数,应当显式接收需要的值,或者接收 `*const std.process.Environ.Map`。 + +这个方向很符合 Zig 一贯的设计哲学:尽量少依赖隐式的全局上下文,让副作用和依赖关系都显式体现在函数签名里。 + +### 线程与分配器模型继续更新 + +围绕新的 `std.Io`,标准库的并发相关设施也继续收敛: + +- `std.Thread.Pool` 被移除,官方建议迁移到 `std.Io.async` / `std.Io.Group.async` +- `std.heap.ArenaAllocator` 变成了 thread-safe 且 lock-free +- `std.heap.ThreadSafeAllocator` 被移除 + +如果把这些变化放在一起看,会发现 Zig 正在逐步放弃一些“靠包装器补线程安全”的旧路子,转而更偏向于:让真正需要并发的基础组件自己具备合适的并发语义。 + +### 文件系统、路径与容器 API 持续整理 + +除了 `std.Io` 大迁移之外,这次标准库还有很多看起来零碎、但真实影响升级体验的整理工作: + +- `std.io` 继续收敛到 `std.Io` +- `std.fs` 的一批常用入口迁到 `std.Io.Dir` / `std.Io.File` +- `std.process.getCwd*` 改名为 `currentPath*` +- `fs.path.relative` 变成纯函数,需要显式传入上下文 +- `File.Stat.atime` 变成可选值 +- `std.mem` 里 “index of” 系列统一更名为 “find” +- 一批容器继续向 unmanaged 方向迁移,`PriorityQueue` / `PriorityDequeue` 的命名也更统一了 + +这些调整单看都不算大新闻,但合在一起,就是一次很典型的 Zig 式“去历史包袱”整理。 + +### 调试信息与栈追踪重做 + +`0.16.0` 重做了一批和调试信息相关的标准库 API,特别是栈追踪(stack trace)。这件事的真正动机是:**在没有帧指针(例如 libc 用 `-fomit-frame-pointer` 编译)的前提下,依然能做到“快又安全”的栈展开**——既不需要为每个栈帧检查地址是否越界、又能避开过去那些“崩溃时去做栈展开反而再次崩溃”的边角情况。 + +这其实是个比想象中复杂得多的问题,因为它真正的解法是 unwind information,而不同目标对 unwind 信息的编码方式各不相同。Zig 的标准库以前虽然也支持过 unwind 信息,但实现 buggy 又不完整,性能也常常拖后腿。`0.16.0` 之后,标准库默认会优先使用基于 unwind 的“安全”栈展开;和原来基于帧指针的展开相比,性能开销在大多数场景下是可以接受的。 + +这条改动同样是“为做游戏开发改善 Zig 体验”的一部分。 + +### 跨进程进度上报支持 Windows + +`std.Progress` 现在可以在 Windows 上跨进程上报进度,子进程的进度会自动反映到父进程的进度树里。同时,最大节点名称长度也从 40 提升到 120。 + +这件事看起来很小,但对“构建系统跑大量子工具”的工作流来说体感差异很明显:以前 Windows 下子工具只能各自打印日志,现在可以像 Linux / macOS 一样汇聚到统一的进度面板。 + +### Windows 网络不再依赖 `ws2_32.dll` + +`0.16.0` 把 Windows 上所有网络 API 直接迁到了 AFD(Ancillary Function Driver),不再走 `ws2_32.dll`。 + +这个改动一次性带来了几个实际收益: + +- 修掉了一批历史 bug +- 让 cancelation 和 Batch 在网络操作上正确工作 +- 避开了 `ws2_32.dll` 内部的性能陷阱——例如它会为每个 socket handle 维护一个完全多余的 hash table,需要分配和同步,而不是简单地在 `accept` 时把 socket mode 和 protocol 一起传下去 + +简单一句话:Windows 上网络 API 现在更稳、更快,也更适配 `std.Io` 的并发模型。 + +### 完成向 NtDll 的迁移 + +`0.16.0` 之后,Windows 上**几乎所有标准库功能**都直接基于最低层级、稳定的 syscall 接口实现。剩下还会显式调用 Windows DLL 的 extern 函数只剩这些: + +- `kernel32.CreateProcessW` +- `crypt32` 一组:`CertOpenStore` / `CertCloseStore` / `CertEnumCertificatesInStore` / `CertFreeCertificateContext` / `CertAddEncodedCertificateToStore` / `CertOpenSystemStoreW` / `CertGetCertificateChain` / `CertFreeCertificateChain` / `CertVerifyCertificateChainPolicy` + +这套迁移直接修掉了一批 Windows 上的 bug、性能问题和缺失功能,让 Zig 程序在 Windows 上比许多其它语言更精简、更可靠。Batch 与 Cancelation 这两套在 Windows 上能用并且实现高效,就是这次迁移直接带来的结果。 + +如果你想兼容 XP 这类更老的 Windows 版本,或者就是更倾向于走 kernel32 这种较高层的 DLL,官方建议作为社区方案做一份不依赖 NtDll 的第三方 `Io` 实现。`CreateProcessW` 与 `crypt32` 这一组短期内不会再继续迁移。 + +### Deflate 压缩器与解压缩简化 + +`std.compress.flate` 这一轮**新增了从零实现的 deflate 压缩器**:以 writer 缓冲区作为历史窗口、用链式哈希表寻找匹配,token 累积到阈值后整块输出。除此之外,还提供了两种额外的 writer: + +- `Raw`:完全只输出 store block(即未压缩字节),借助数据向量高效发送 block header 与数据 +- `Huffman`:只做 Huffman 压缩,不做匹配 + +这两者因为不需要保留历史,可以更直接地利用新的 writer 语义。 + +`token` 中的字面量与距离编码参数也被重做:参数现在是数学方法推导出来的,更昂贵的那部分依然走查表(`ReleaseSmall` 例外)。 + +解压侧的 bit 读取也大幅简化,充分利用了底层 reader 可以 peek 的能力,并修掉了若干和 limit 处理相关的 bug。 + +#### 与 zlib 的对比 + +性能与压缩比层面,简单概括是: + +- **压缩比**:默认压缩级别下 zlib 比 Zig 的实现高 1.00%,最高压缩级别下高 0.77%——zlib 选取的匹配略有不同,但匹配字节总数其实更少,未来还可以继续打磨向 zlib 看齐 +- **默认级别压缩性能**:Zig 的 std-deflate 比 zlib 大约**快 9.7%**,CPU 周期少 9.8%,cache miss 与分支误预测都明显更少;instruction 数因为算法选择不同会高约 18.9%,但被更友好的访存模式抵消 +- **最高级别压缩性能**:和 zlib 持平(差异在 1% 以内) +- **解压性能(vs 上一版)**:新解压实现比 0.15.x 大约**快 9.5%**,CPU 周期与指令数都减少约 10%,分支误预测下降约 18% + +换句话说,Zig `0.16.0` 自带的 deflate 在常见场景下已经能在性能上和 zlib 打平甚至略胜,压缩比仍然是后续的优化方向。 + +### `std.crypto` 新增 AES-SIV / AES-GCM-SIV + +过去 Zig 标准库一直缺少“可抗 nonce 重用”的 AEAD 方案;`0.16.0` 把这一缺口补上了: + +- **AES-SIV**:在密钥包装(key wrapping)这类场景里特别有用 +- **AES-GCM-SIV**:在嵌入式目标上尤其合适 + +这两个原语都是“nonce reuse-resistant”AEAD 的事实标准选择。 + +### `std.crypto` 新增 Ascon-AEAD / Ascon-Hash / Ascon-CHash + +Ascon 是 NIST 为轻量级密码学标准化的一系列构造。Zig 标准库以前已经提供了 Ascon 置换本身,但建立在置换之上的高层构造一直被刻意推迟,等待 NIST 发布最终规范。 + +NIST SP 800-232 已经正式发布之后,Zig `0.16.0` 把这一组高层构造一次性补齐: + +- `Ascon-AEAD`:AEAD 加密 +- `Ascon-Hash`:定长哈希 +- `Ascon-CHash`:可定制化的哈希 + +对嵌入式 / IoT 场景这是一项久违的能力补齐。 + +### Windows 标准库实现继续下沉 + +Windows 也是 `0.16.0` 里非常有意思的一条线,前面几节已经覆盖了核心内容: + +- 网络 API 不再依赖 `ws2_32.dll`,而是直接基于 AFD 实现 +- 标准库基本完成向 NtDll 收敛,仅剩 `CreateProcessW` 与一组 `crypt32` 函数 +- `std.Progress` 现在也支持 Windows 下的跨进程进度上报 +- inline caller 现在会从 PDB 调试信息里被解析;如果 debug info 模糊,会把所有候选 caller 都打印出来;error return trace 也会在所有平台上包含 inline caller + +这些工作虽然对多数用户不可见,但会真实影响程序的健壮性、性能,以及 cancelation / batch 模型在 Windows 上的完整度。 + +## 构建系统 + +### 依赖目录改到项目本地 `zig-pkg` + +从 `0.16.0` 开始,依赖包会被拉取到项目根目录旁边的 `zig-pkg` 目录,而不是继续使用过去那种全局解压缓存模式。 + +这个变化的好处很直接: + +- 你可以更方便地阅读、搜索、修改依赖源码 +- 可以更自然地把依赖目录换成本地 git clone +- IDE 也更容易直接索引整棵依赖树 + +### `zig build --fork` + +构建系统新增了 `--fork=[path]` 参数,可以让你临时用本地目录里的 fork 覆盖依赖树中的匹配包。 + +这对生态 breakage 的排查非常有帮助:你可以在不改版本元数据的前提下,直接调试一整串依赖之间的兼容问题。 + +### 依赖元数据更严格 + +`0.16.0` 还提高了 `build.zig.zon` 的要求: + +- 缺少 `fingerprint` 会直接失败 +- `name` 不能再用字符串,必须写成 enum literal +- 旧 hash 格式支持已被移除 + +这意味着旧项目在升级时,最好顺手检查一遍所有依赖元数据,而不是等到 `zig build` 报错再逐个补。 + +### 新增测试超时与错误输出样式 + +`zig build` 新增了几项很适合日常开发的参数: + +- `--test-timeout` +- `--error-style` +- `--multiline-errors` + +同时,旧的 `--prominent-compile-errors` 被移除了,对应的新写法是 `--error-style minimal`。 + +### 临时文件 API 被重构 + +`Build.makeTempPath` 和 `RemoveDir` step 都被清理掉了,新的推荐路径是: + +- `Build.addTempFiles` +- `Build.addMutateFiles` +- `Build.tmpPath` + +这项重构背后的核心思路,是把“临时目录”“可变文件”“缓存语义”这些东西从一开始就表达清楚,而不是让旧 API 在 configure 阶段偷偷做一堆文件系统副作用。 + +## Compiler + +### `translate-c` 改用 Aro + +编译器内部的 `translate-c` 现在基于 Aro / translate-c,而不是 `libclang`。这一改动直接从编译器源码树里删掉了 5,940 行 C++(剩余 3,763 行),也让 Zig 离“把对 LLVM 的库级依赖换成对 Clang 的进程级依赖”又近了一步。 + +对普通用户来说,这件事大体上是非破坏性的——但也确实是“一个 C 编译器替换成另一个”,所以如果你升级后发现某个 C 头文件翻译结果不一致,请按 bug 反馈。 + +实现是“懒加载”的:第一次遇到 `@cImport` 时再从源码构建。 + +### LLVM 后端 + +LLVM 后端这一轮的进展有几条: + +- **实验性支持增量编译**——这并不会加速 “LLVM Emit Object” 这一步(LLVM 自己负责的部分我们做不了什么),但加速了 Zig 编译器侧生成 LLVM bitcode 的过程,因此当你的代码本身就有编译错误时,你能在 LLVM 后端下也获得近乎瞬时的反馈 +- LLVM bitcode 体积下降 3-7% +- 在某些情况下编译速度略提升约 3% +- 修掉了零字节 payload union 的调试信息 +- 调试信息现在对所有类型都包含正确的名字 +- error set 类型现在以 enum 方式 lower,使得 error 名字在运行时可见 + +LLVM 后端目前通过了 2004 / 2010(100%)行为测试。Matthew 还实验过把 tagged union 和 error union 用 DWARF 的 variant 类型表达,让调试器只显示“当前激活的字段”——但 LLDB 对 variant 类型的支持只有在语言被标成 Rust 时才启用,因此暂未落地。这条路径未来下游若改善还可能再走。 + +后续还会继续推进 LLVM 后端的并行化:让多个线程同时为不同函数生成 LLVM IR,再由一个 “linker” 线程合并。 + +### 重做 byval 语法降级 + +编译器前端早期为了减少中间指令尝试用“byval”语义降级表达式,但这个实验带来了三类典型问题:数组访问性能问题、显式拷贝下的意外别名、退化场景代码质量极差。 + +`0.16.0` 改成全程 byref 降级,只在最后一次 load 时才取值,从根上修掉这些问题。 + +### 类型解析重构 + +前面在“语言变动”里提过,`0.16.0` 大幅重做了类型解析。这件事的真正动机有两个:简化 Zig 语言规范的撰写,以及修掉一大批和增量编译相关的编译器 bug。 + +新规则总体上比旧规则**更宽松**:以前能 work 的代码大多数仍然能 work,一些以前会因为 dependency loop 报错的代码现在反而能正常编译。但它并不是“严格更宽松”——例如下面这种结构体在自身对齐查询里依赖 `@This()` 的写法,从 `0.16.0` 起会被识别为依赖环并直接报错: + +```zig +const S = struct { + foo: [*]align(@alignOf(@This())) u8, +}; +``` + +不过这次配套的错误信息也清晰了许多。例如某条长度为 3 的依赖环——“S 默认字段值用了 default_val,default_val 用了 other_val,other_val 又通过 `@typeInfo(S)` 回到 S”——错误输出会把这三跳依次列出来,并提示“破坏其中任何一条都能解开环”。 + +这件事对编译器本身还有两个非常重要的连锁收益: + +- 依赖环报错更可解释 +- 增量编译和普通编译之间的一致性更强 + +这也是为什么你会发现,本版本许多看似分散的改动,最后都会回到“为了更可靠的增量编译”这个主题上。 + +### 增量编译继续前进 + +增量编译让 Zig 编译器只重新编译自上次构建以来真正改动的代码——小改动可以从“秒 / 分钟”降到“毫秒”。`0.16.0` 这一轮里它的进步非常具体: + +- **避免“过度分析”**——大部分场景下,编译器不再因为某个改动牵连出本不需要重建的代码。在 Zig 编译器自己身上做实验:以前会几乎重新编译整个编译器的改动,现在能在毫秒级完成。这里的关键,是“类型解析重做”让编译器内部依赖图变成了无环图(dependency loop 除外) +- 增量编译和非增量编译之间,不再因为对方触发 dependency loop 而表现不一致。这是上一版里最大的不一致来源 +- 在 ELF 目标上,自托管后端 + `-fincremental` 时**默认启用新的 ELF linker**——更快、对增量编译的支持也更稳 +- 整体稳定性明显提升,崩溃和误编译比上一版少得多 +- LLVM 后端现在也支持增量编译 + +官方现在明确鼓励大家实际使用: + +```sh +zig build -fincremental --watch +``` + +哪怕只是“近乎瞬时拿到编译错误”,多数用户也会被它实际的提速程度震惊到。当然,它仍然有已知 bug、甚至可能包含误编译;所以 `0.16.0` 里增量编译依然不是默认开启。 + +### x86 后端 + +`0.16.0` 里 x86 自托管后端: + +- 修复 11 个 bug +- 常量 `memcpy` 的代码生成更优 + +和 LLVM 后端相比,x86 后端通过的行为测试更多、编译速度显著更快、调试信息更优,机器码质量稍逊;它仍然是 Debug 模式下的默认后端。 + +### aarch64 后端 + +仍处于开发中。这个版本周期内,由于 `std.Io` 带来的标准库 churn,aarch64 后端的进度暂时放慢;当前在跑行为测试时会崩溃。等标准库这边的 churn 结束之后,进度会重新拾起。 + +### WebAssembly 后端 + +目前通过了 1813 / 1970(约 92%)项行为测试(与 LLVM 后端做对比)。 + +### 不再依赖 LLVM 生成 .def 导入库 + +`0.16.0` 把“从 .def 文件生成导入库”这件事从 LLVM 那边切了出来——具体面向的是 Zig 自带的 MinGW-w64 .def 文件。新实现大体参考 LLVM 的 `COFFModuleDefinition.cpp` 与 `COFFImportFile.cpp`。 + +这条同样是“把对 LLVM 的库级依赖转向对 Clang 的进程级依赖”这条长期路线的一环。 + +### for 循环安全检查的代码生成改进 + +针对 slice 的 for 循环,安全检查相关的代码生成减少了大约 **30%**。这意味着典型的循环代码在 Debug 构建里不仅仍然带越界检查,体积也明显小于上一版。 + +## 链接器(Linker) + +### 新 ELF Linker + +`0.16.0` 的新 ELF linker 可以通过 `-fnew-linker` 显式启用,或者在 build 脚本里设置 `exe.use_new_linker = true`。更重要的是:**在 `-fincremental` 且目标是 ELF 时,它现在会默认启用。** + +官方给出的一组数据点非常直观——对 Zig 编译器本体先做一次完整构建,然后做一次单行改动,再做第二次单行改动: + +- 旧 linker:`14s`、`194ms`、`191ms` +- 新 linker:`14s`、`65ms`、`64ms`(快约 66%) +- 完全跳过链接:`14s`、`62ms`、`62ms`(快约 68%) + +这也意味着过去那种专门暴露 `-Dno-bin`、只求快速拿到编译错误的工作流,**收益已经几乎可以忽略**——你大可以让 codegen 和链接一直开着,构建结束顺手拿到一个可执行文件。 + +不过要注意,新 linker 目前**还没补齐旧 linker / LLD 的能力**,例如生成物还缺少 DWARF 信息。所以它已经够快、够值得试,但还没有到“所有场景都能无脑切换”的程度。当新 linker 功能完全对齐之后,旧 linker 会被删掉,LLD 也会从依赖里移除。 + +## Fuzzer(模糊测试器) + +### `Smith` 取代 `[]const u8` + +Fuzz 测试接口是 `0.16.0` 里另一个会直接影响用户代码的 breaking change。过去 fuzz test 习惯接收 `[]const u8` 输入;现在统一改成 `*std.testing.Smith`,由它来生成结构化值。 + +`Smith` 提供的基础方法包括: + +- `value`:生成任意类型的值 +- `eos`:生成 end-of-stream 标记,且保证“最终一定会返回 `true`” +- `bytes`:填充字节数组 +- `slice`:填充缓冲区的一部分并给出长度 + +每个方法都有支持权重的对应版本,权重通过 `[]const Smith.Weight` 表达,可以用来: + +- 让“有意思”的值出现得更频繁 +- 减少不必要的工作量 +- 限制可选值的范围 + +值得注意的是,权重只能用于能放进 64 位的类型;空权重切片意味着所有值的权重为零,因此都不会被选中。除此之外还提供: + +- `baselineWeights`:为某个类型构造覆盖所有可能值的权重集 +- `boolWeighted` / `eosSimpleWeighted`:方便地为 `true` / `false` 配权重 +- `valueRangeAtMost` / `valueRangeLessThan`:只生成某个范围内的值 + +每个方法还有一个接收 hash 参数的对应版本——同 hash 的值更倾向于一起被变异。常规方法本身已经基于 callee 返回地址生成 hash,因此你通常不需要手动调用这一组,主要是 inline 的场景下才会用到。 + +### 多进程、多核与 crash dump + +除了接口变化之外,fuzzer 本身也更强了: + +- **多进程 / 多核**:现在可以利用多核,受 `-j` 构建选项控制;受限模糊(limited fuzzing)仍然只用一个核 +- **无限模式**:当提供多个 fuzz test 时,fuzzer 会在它们之间切换并**优先运行更“有产出”的测试**;随着时间推移,已经被反复探索过的测试会被分得越来越少的预算 +- **崩溃输入会自动落盘**:crash 信息中会指出落盘文件路径,便于配合 `std.testing.FuzzInputOptions.corpus` 与 `@embedFile` 复现 + +### 配套的 AST Smith 已经替 zig fmt 找到 20 个 bug + +新的 Smith 接口本身已经被用来构建一个 “AST Smith”——专门生成随机但合法的 AST。把它丢给 `zig fmt`(再加上一些更早期的简单随机源码测试),一共发现并修复了 **20 个独立 bug**,其中一部分是新发现的。 + +它还顺手修了几个“PEG 与 parser 不一致”的问题,例如以前 tuple 不能包含以 `extern` 或 `inline` 起头的类型——`const T = struct { u64, extern struct { a: u64 }, u32 }` 在以前会直接报错。 + +## Bug 修复 + +本轮发布周期内,Zig 一共关闭了 **345 个 bug 报告**。许多 bug 是在本周期内被引入又被修复的;为了简洁,绝大多数 bug 修复并未单独列出。 + +### 这个版本仍然包含已知 bug + +官方明确写在 release notes 里—— + +> Zig 仍然存在已知 bug、误编译(miscompilation)和回归(regression)。 + +对稍微复杂一点的项目,使用 `0.16.x` 仍然意味着要准备好参与开发流程:提交最小复现、跟踪 issue、必要时切版本验证。 + +这并不意外。因为 `0.16.0` 本质上是一个“大迁移版本”,它把很多还在演进中的长期工程方向一次性推到了用户面前。Zig 1.0 之后,Tier 1 支持会获得一项额外要求:bug policy。 + +## 工具链(Toolchain) + +### LLVM 21 + +`0.16.0` 升级到了 LLVM `21.1.0`,覆盖了 Clang(即 `zig cc`)、libc++、libc++abi、libunwind、libtsan 等组件。 + +#### 为绕过回归,loop vectorization 被全局关闭 + +这里有一个非常值得注意的 caveat:LLVM 21 上游存在一个严重回归,会在常见配置下**误编译 Zig 编译器自身**。试图通过禁用某些 CPU 特性来绕过太脆弱,所以 Zig 在 `0.16.x` 中**完全禁用了 loop vectorization**。这会让某些代码生成结果比理想情况更保守,但它仍然比“在常见配置下误编译 Zig 编译器自身”要好得多。 + +这个 bug 已经向上游报告并修复,但写文档时,修复尚未被 cherry-pick 到 LLVM 22.x 分支。因此官方预计这个性能回退不止会影响 `0.16.x`,还会延续到 `0.17.x`,大概率要等到 `0.18.x` 才会彻底解决。 + +### musl 1.2.5 + +`0.16.0` 分发 musl 1.2.5 + 回移的安全修复。上游已经标了 1.2.6,未来某个 Zig 版本会跟进。 + +由于很多函数被迁到了 `zig libc`,相比上一版**少分发了 331 个 musl C 源文件**(剩 1,206 个)。这意味着如果你遇到 musl libc 相关 bug,应当先报到 Zig 的 issue tracker 而不是 musl。 + +另外,`0.16.0` 不受 `CVE-2026-40200` 影响——musl 的 `qsort` / `qsort_r` 已经不再被使用。 + +### glibc 2.43 + +交叉编译时现在可以选择 glibc 2.43。 + +### Linux 6.19 headers + +随 Zig 分发的 Linux 内核头文件升级到 6.19。 + +### macOS 26.4 headers + +随 Zig 分发的 macOS 系统头文件升级到 26.4。 + +### MinGW-w64 + +`0.16.0` 继续分发 MinGW-w64 commit `38c8142f660b6ba11e7c408f2de1e9f8bfaf839e`。但很多函数已经迁到 `zig libc`:本版相比上一版**少分发 99 个 MinGW-w64 C 源文件**(剩 398 个)。同样地,相关 bug 应当先报到 Zig 这边而不是 MinGW-w64。 + +### FreeBSD 15.0 libc + +交叉编译时现在可以选择 FreeBSD libc 15.0。 + +### WASI libc + +`0.16.0` 升级到 WASI libc commit `c89896107d7b57aef69dcadede47409ee4f702ee`。 + +虽然很多函数已经迁到 `zig libc`,但因为新版 WASI libc 添加了 pthread shim,且 WASI libc 大部分源文件其实和 musl 共享,**WASI libc 自身的源文件数反而从 196 增加到 228**。 + +### zig libc + +`zig libc` 这一轮继续吞并原来来自 musl、MinGW-w64、WASI libc 的一部分 C 实现。Zig 分发的 C 源文件总数从 `2,270` 降到了 `1,873`,减少了约 `17%`。 + +这里尤其值得一提的是:**很多数学函数,以及 `malloc` 系列函数**,现在都已经进入 `zig libc` 的实现范围。这条进展受益于 Szabolcs Nagy 提供的 [libc-test](https://wiki.musl-libc.org/libc-test.html)。 + +### zig cc + +`zig cc` / `zig c++` 现在基于 Clang `21.1.8`。这意味着 Zig 在“C / C++ 工具链外壳”这条线上,也继续和整体 LLVM 版本一同推进。本周期内一共修复了 **9 个 zig cc 相关 bug**。 + +### 交叉编译时支持动态链接的 OpenBSD libc + +Zig 现在允许交叉编译到 OpenBSD 7.8+:和 glibc 类似,会提供动态 libc 的 stub library,并附带全部 libc 头文件以及大多数系统头文件。 + +## 路线图(Roadmap) + +官方对 `0.17.0` 的规划很明确:这是一个相对短周期版本,主要目标是升级到 LLVM `22`,并完成“把构建执行阶段和 `build.zig` 配置阶段分离”的工作。 + +在这之后,更长期的大方向仍然是: + +- **继续完成并稳定语言本身** +- **做完 aarch64 后端**,并让它成为 Debug 模式默认后端 +- **继续增强链接器**,减少对 LLD 的依赖,并服务增量编译 +- **继续增强内置 fuzzer**,让它和 AFL 等业界最先进的 fuzzer 处于同一水平线 +- **继续把对 LLVM 的依赖,从“链接库”转向“调用 Clang 进程”** + +如果说 `0.15.x` 还是“这些方向马上就要影响到你了”,那 `0.16.0` 就是“它们已经开始真正影响你的日常开发了”。 diff --git a/course/update/upgrade-0.16.0.md b/course/update/upgrade-0.16.0.md new file mode 100644 index 00000000..6fbd0c7f --- /dev/null +++ b/course/update/upgrade-0.16.0.md @@ -0,0 +1,1642 @@ +--- +outline: deep +showVersion: false +--- + +本篇文档将介绍如何从 `0.15.1` 版本升级到 `0.16.0`。 + +`0.16.0` 是 Zig 近几个版本里破坏性变更最集中的一次更新之一。最大的主题是:**I/O 被统一到了新的 `std.Io` 接口**,同时语言层也清理了 `@Type`、`@cImport`、`packed` / `extern` 类型规则,以及一批历史 API。 + +## 语言变动 + +### `switch` 能力继续增强 + +`packed struct` 和 `packed union` 现在可以直接作为 switch 的 prong item,并且**比较规则完全基于其 backing integer**——和 packed 类型自身的相等比较保持一致: + +```zig +const U = packed union(u2) { + a: i2, + b: u2, +}; + +const u: U = .{ .a = -1 }; +switch (u) { + .{ .b = 3 } => {}, + else => unreachable, +} +``` + +这一版本里 switch 还新增了以下能力: + +- decl literals 以及其他需要结果类型的表达式(例如 `@enumFromInt`)现在可以直接作为 switch prong item +- 联合类型的 tag capture 不再只限于 `inline` prong——所有 prong 都可以使用 +- 如果 prong body 是 `=> comptime unreachable`,prong item 中允许出现“当前 error 集合里并不存在的错误” +- **prong capture 不再允许被全部丢弃**——这是一条破坏性改动,如果你之前在 prong 里把所有 capture 都写成了 `_`,需要至少保留一个真实变量名 + +bug 修复方面: + +- 大量 “one-possible-value” 类型上的 switch 相关 bug 被修复 +- 在 error 上做 switch 时,关于 unreachable `else` prong 的规则现在适用于**所有**对 error 的 switch,而不仅是 `switch_block_err_union`,且基于 AST 正确判断 +- 对 `void` 做 switch 时,不再无条件要求 `else` prong +- lazy values 在与 prong item 比较前会被正确求值 +- 不同形式的 switch 语句(带 / 不带 label)求值顺序现在保持一致 + +绝大多数代码不需要主动迁移,但 prong capture 全部丢弃的这条规则会让一小部分写法直接编译报错。 + +### `packed union` 的相等比较 + +`packed union` 现在支持直接相等比较(基于 backing integer),不再需要绕到 backing integer 显式 `@bitCast` 再比。这与上面 switch 中“按 backing integer 比较”的语义是一致的。 + +### `@cImport` 开始迁移到构建系统 + +`@cImport` 在 `0.16.0` 里还没有被移除,但已经被正式标记为 deprecated。官方建议开始把 C 头文件翻译工作迁移到 `build.zig` 中,通过 `addTranslateC` 生成模块,再在 Zig 代码里 `@import("c")`。 + +旧写法: + +```zig +pub const c = @cImport({ + @cInclude("stdio.h"); + @cInclude("math.h"); + @cInclude("stdlib.h"); +}); + +const c = @import("c.zig").c; +``` + +新写法: + +`c.h` + +```c +#include +#include +#include +``` + +`build.zig` + +```zig +const translate_c = b.addTranslateC(.{ + .root_source_file = b.path("src/c.h"), + .target = target, + .optimize = optimize, +}); +translate_c.linkSystemLibrary("glfw", .{}); +translate_c.linkSystemLibrary("epoxy", .{}); + +const exe = b.addExecutable(.{ + .name = "example", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ + .name = "c", + .module = translate_c.createModule(), + }, + }, + }), +}); +``` + +```zig +const c = @import("c"); +``` + +这样翻译出来的 C 代码与以前用 `@cImport` 的结果是一致的;如果需要更细的翻译参数控制,可以把官方 `translate-c` 包作为显式依赖加入。 + +如果你升级到 `0.16.0` 后发现同一份 C 头文件翻译结果和以前不一致,也不用急着怀疑自己。因为 `translate-c` 的底层实现已经从 `libclang` 切换到了 Aro,这类差异更应该视为 bug 并反馈给 Zig。 + +### `@Type` 被拆分为独立的类型构造内建 + +这是 `0.16.0` 最重要的语言级 breaking change 之一。`@Type` 被移除了,原本依赖 `@Type(.{ ... })` 或 `std.meta.*` 造类型的代码,需要迁移到新的内建函数。 + +新增的核心内建包括: + +- `@EnumLiteral()` +- `@Int()` +- `@Tuple()` +- `@Pointer()` +- `@Fn()` +- `@Struct()` +- `@Union()` +- `@Enum()` + +常见迁移: + +```zig +@Type(.enum_literal) +``` + +⬇️ + +```zig +@EnumLiteral() +``` + +```zig +@Type(.{ .int = .{ .signedness = .unsigned, .bits = 10 } }) +``` + +⬇️ + +```zig +@Int(.unsigned, 10) +``` + +```zig +std.meta.Tuple(&.{ u32, [2]f64 }) +``` + +⬇️ + +```zig +@Tuple(&.{ u32, [2]f64 }) +``` + +为了简化语言,**不再支持反射出带 `comptime` 字段的 tuple 类型**。 + +`@Pointer` 等价于 `@Type(.{ .pointer = ... })`,但配合新的 `std.builtin.Type.Pointer.Attributes` 类型——后者借助 struct 字段默认值,使得用法更接近字面 pointer 类型语法: + +```zig +@Type(.{ .pointer = .{ + .size = .one, + .is_const = true, + .is_volatile = false, + .alignment = @alignOf(u32), + .address_space = .generic, + .child = u32, + .is_allowzero = false, + .sentinel_ptr = null, +} }) +``` + +⬇️ + +```zig +@Pointer(.one, .{ .@"const" = true }, u32, null) +``` + +```zig +@Type(.{ .pointer = .{ + .size = .many, + .is_const = false, + .is_volatile = false, + .alignment = 1, + .address_space = .generic, + .child = u64, + .is_allowzero = false, + .sentinel_ptr = &@as(u64, 0), +} }) +``` + +⬇️ + +```zig +@Pointer(.many, .{ .@"align" = 1 }, u64, 0) +``` + +`@Fn` 等价于 `@Type(.{ .@"fn" = ... })`。参数分两段:第一段是所有参数类型,第二段是“参数属性”(目前只有 `noalias` 这个 flag): + +```zig +@Type(.{ .@"fn" = .{ + .calling_convention = .c, + .is_generic = false, + .is_var_args = true, + .return_type = u32, + .params = &.{.{ + .is_generic = false, + .is_noalias = false, + .type = f64, + }, .{ + .is_generic = false, + .is_noalias = true, + .type = *const anyopaque, + }}, +} }) +``` + +⬇️ + +```zig +@Fn( + &.{ f64, *const anyopaque }, + &.{ .{}, .{ .@"noalias" = true } }, + u32, + .{ .@"callconv" = .c, .varargs = true }, +) +``` + +这是几个新 builtin 中用 “struct of arrays” 风格接收参数的代表。这种风格的好处是“给所有元素一个统一默认值”非常容易——比如想给所有参数用默认属性 `.{}`,用 `&@splat(.{})` 即可: + +```zig +@Fn(param_types, &@splat(.{}), ReturnType, .{ .@"callconv" = .c }) +``` + +`@Struct` 同样采用 “struct of arrays”:字段名、字段类型、字段属性各自传一组数组,属性里包含 alignment、`comptime` 标志、字段默认值: + +```zig +@Type(.{ .@"struct" = .{ + .layout = .@"extern", + .fields = &.{.{ + .name = "foo", + .type = [2]f64, + .default_value_ptr = null, + .is_comptime = false, + .alignment = 1, + }, .{ + .name = "bar", + .type = u32, + .default_value_ptr = &@as(u32, 123), + .is_comptime = true, + .alignment = @alignOf(u32), + }}, + .decls = &.{}, + .is_tuple = false, +} }) +``` + +⬇️ + +```zig +@Struct( + .@"extern", + null, + &.{ "foo", "bar" }, + &.{ [2]f64, u32 }, + &.{ + .{ .@"align" = 1 }, + .{ .@"comptime" = true, .default_value_ptr = &@as(u32, 123) }, + }, +) +``` + +同样可以用 `&@splat(.{})` 表达“所有字段都用默认属性”,甚至连 field types 都可以——例如要构造一个所有字段都是 `FieldType`,并且字段名跟 `MyEnum` 的枚举名一致的 struct: + +```zig +const MyStruct = @Struct(.auto, null, std.meta.fieldNames(MyEnum), &@splat(FieldType), &@splat(.{})); +``` + +`@Union` 用法与 `@Struct` 类似: + +```zig +@Type(.{ .@"union" = .{ + .layout = .auto, + .tag_type = MyEnum, + .fields = &.{.{ + .name = "foo", + .type = i64, + .alignment = @alignOf(i64), + }, .{ + .name = "bar", + .type = f64, + .alignment = @alignOf(f64), + }}, + .decls = &.{}, +} }) +``` + +⬇️ + +```zig +@Union( + .auto, + MyEnum, + &.{ "foo", "bar" }, + &.{ i64, f64 }, + &@splat(.{}), +) +``` + +`@Enum` 和 `@Struct` 风格相近,但接收的是字段 **tag 值** 数组而不是字段类型数组: + +```zig +@Type(.{ .@"enum" = .{ + .tag_type = u32, + .fields = &.{.{ + .name = "foo", + .value = 0, + }, .{ + .name = "bar", + .value = 1, + }}, + .decls = &.{}, + .is_exhaustive = true, +} }) +``` + +⬇️ + +```zig +@Enum( + u32, + .exhaustive, + &.{ "foo", "bar" }, + &.{ 0, 1 }, +) +``` + +需要注意的是,**这套新 builtin 里没有 `@Float`**——因为运行时浮点类型只有 5 种,在用户代码里实现这件事很轻松;如果非要从位数构造浮点类型,可以用 `std.meta.Float`。 + +如果你的项目大量依赖元编程,这一项往往是升级时最先爆出来的报错来源。建议先全局搜索 `@Type(` 和 `std.meta.`,再逐个迁移。 + +### 小整数类型现在可以安全地隐式转换为浮点 + +如果某个整数类型的所有可能值,都能被目标浮点类型精确表示,那么现在可以直接发生隐式 coercion。 + +旧写法: + +```zig +var foo_int: u24 = 123; +var foo_float: f32 = @floatFromInt(foo_int); +``` + +新写法: + +```zig +var foo_int: u24 = 123; +var foo_float: f32 = foo_int; +``` + +注意这只适用于“不会丢精度”的情况。像 `u25 -> f32` 这种仍然需要显式写 `@floatFromInt`。 + +### 运行时向量索引被禁止 + +此前很多人会把向量当成“可在运行时索引的数组”来用。`0.16.0` 不再允许这种写法。 + +旧写法: + +```zig +for (0..vector_len) |i| { + _ = vector[i]; +} +``` + +新写法: + +```zig +const vector_type = @typeInfo(@TypeOf(vector)).vector; +const array: [vector_type.len]vector_type.child = vector; + +for (&array) |elem| { + _ = elem; +} +``` + +如果你确实需要逐项遍历向量,请先把它显式 coercion 成数组,再做索引或遍历。 + +### 数组与向量不再支持旧式内存强转 + +`0.16.0` 不再鼓励通过 `@ptrCast` 在数组内存和向量内存之间来回转换。如果你之前是在做同构数据的值级转换,请直接使用 coercion: + +```zig +const arr: [4]i32 = .{ 1, 2, 3, 4 }; +const vec: @Vector(4, i32) = arr; +const back: [4]i32 = vec; +``` + +如果你的类型外层还包了一层 `error!` 或其他容器类型,先解包,再在内部做数组和向量之间的转换。 + +### 不再允许返回局部变量地址 + +下面这种初学者常见错误,现在会直接给出明确的编译错误: + +```zig +fn foo() *i32 { + var x: i32 = 1234; + return &x; +} +``` + +报错形式如下: + +```sh +test.zig:3:13: error: returning address of expired local variable 'x' + return &x; + ^ +test.zig:2:9: note: declared runtime-known here + var x: i32 = 1234; + ^ +``` + +这个变化背后有点小故事。返回无效指针本身在 Zig 里是合法的——非法只发生在解引用: + +```zig +fn foo() *i32 { + return undefined; +} +``` + +甚至下面这种“无条件触发非法行为”的函数也是合法的: + +```zig +fn bar() noreturn { + unreachable; // equivalent to foo().* +} +``` + +也就是说 `bar()` 的语义等价于 `unreachable`。那要怎么把“返回局部变量地址”变成编译错误?答案是“句法层面的较真”:编译器禁止所有“不需要类型检查就能 trivially 降级为 `return undefined` 的表达式”,理由是这种代码应当写成规范的 `return undefined`。`return &x;`(其中 `x` 是局部变量)正属于这一类。 + +如果你在升级后遇到这类错误,正确修复方式通常是三种之一: + +- 直接返回值,而不是返回指针 +- 让调用方传入 buffer / 输出参数 +- 改用堆分配,并明确约定释放责任 + +后续官方计划继续以同样的思路加入更多类似的编译错误。 + +### `packed` 与 `extern` 规则更严格 + +#### `packed union` 需要明确 backing integer,并保证各字段 bit size 一致 + +以前 Zig 对 `packed union` 的位级布局有一些隐式推断。`0.16.0` 开始要求它更明确。 + +旧写法: + +```zig +const U = packed union { + x: u8, + y: u16, +}; +``` + +新写法: + +```zig +const U = packed union(u16) { + x: packed struct(u16) { + data: u8, + padding: u8 = 0, + }, + y: u16, +}; +``` + +总结一下这条规则:如果你需要 `packed union`,就请明确写出 backing integer,并保证每个字段都能映射到同样大小的位表示。 + +之前 `packed struct(T)` 已经可以指定 backing integer,但 `packed union(T)` 不行;`0.16.0` 把这条限制也去掉了。下面是用普通声明语法和用 `@Union` builtin 同时构造同一个 packed union 的例子: + +```zig +// 用普通声明语法 +const Split16 = packed union(u16) { + raw: MaybeSigned16, + split: packed struct { low: u8, high: u8 }, +}; + +// 用 `@Union` builtin +const MaybeSigned16 = @Union( + .@"packed", + u16, // backing integer + &.{ "unsigned", "signed" }, + &.{ u16, i16 }, + &@splat(.{}), +); + +test "use packed union type with explicit backing integer" { + const u: Split16 = .{ .raw = .{ .unsigned = 0xFFFE } }; + try testing.expectEqual(-2, u.raw.signed); + try testing.expectEqual(0xFE, u.split.low); + try testing.expectEqual(0xFF, u.split.high); +} +``` + +由于下面 “`extern` 场景必须显式指定 tag type / backing type” 这条规则的存在,现在某些场景下显式指定 packed union 的 backing integer 是必须的。 + +#### `packed struct` / `packed union` 不再允许指针字段 + +如果你过去把指针直接塞进 `packed` 类型里,现在需要改成整数保存地址: + +```zig +const addr: usize = @intFromPtr(ptr); +const ptr_again: *T = @ptrFromInt(addr); +``` + +这项变更的核心原因是:很多目标平台里,指针并不只是“裸地址位”,而 `packed` 类型又承诺了精确的位级布局,因此两者不再兼容。 + +#### `extern` 场景下必须显式指定 tag type / backing type + +`enum`、`packed struct`、`packed union` 只要被用于 `extern` / `export` 场景,就不能再依赖隐式推断的底层整数类型。 + +旧写法: + +```zig +const Enum = enum { a, b, c, d }; +const PackedStruct = packed struct { a: u4, b: u4 }; +const PackedUnion = packed union { a: u8, b: i8 }; +``` + +新写法: + +```zig +const Enum = enum(u8) { a, b, c, d }; +const PackedStruct = packed struct(u8) { a: u4, b: u4 }; +const PackedUnion = packed union(u8) { a: u8, b: i8 }; +``` + +如果你的类型需要跨 ABI 边界导出,请把 tag type 或 backing type 明确写出来,不要再依赖编译器推断。 + +### 浮点取整内建现在可以直接产出整数 + +`@floor`、`@ceil`、`@round`、`@trunc` 现在可以直接把浮点值转成整数值。 + +旧写法: + +```zig +const x: i32 = @intFromFloat(@round(value)); +``` + +新写法: + +```zig +const x: i32 = @round(value); +``` + +这项改动本身不一定会让旧代码报错,但会让很多“先取整、再转整数”的写法明显简化。 + +### 一元浮点内建会向下转发结果类型 + +`@sqrt`、`@sin`、`@cos`、`@tan`、`@exp`、`@exp2`、`@log`、`@log2`、`@log10`、`@floor`、`@ceil`、`@trunc`、`@round` 现在都会把外层的结果类型向内转发,于是下面这种过去无法直接写的式子在 `0.16.0` 是合法的: + +```zig +const x: f64 = @sqrt(@floatFromInt(N)); +``` + +之前 `@sqrt` 不会把 `f64` 这个结果类型传给内层的 `@floatFromInt`,所以你必须手工加一层中间变量。这项改动不会让旧代码报错,但能消除大量样板。 + +### `*u8` 与 `*align(1) u8` 不再是同一个类型 + +在 `0.16.0` 之前,Zig 把这两者视为完全相同的类型(甚至 `==` 比较都为真)。从这个版本开始,它们变成了两个不同的类型。 + +不过**两者仍然可以互相强转,包括嵌套在指针里的“in-memory coercion”**,所以日常用法绝大多数情况下不需要任何改动。可以把这种区分理解为类似 `u32` 与 `c_uint` 的关系:技术上不同,但行为一致。 + +只有在你显式比较 `@TypeOf(...)` 的相等性、或者依赖 `@typeInfo` 反射时,才需要顺手处理一下。 + +### Zero-bit tuple 字段不再被隐式标记为 `comptime` + +`0.14` 时引入的“tuple 中 zero-bit 字段自动变成 `comptime` 字段”这个隐式规则在 `0.16.0` 被回滚。也就是说: + +```zig +const S = struct { void }; +@typeInfo(S).@"struct".fields[0].is_comptime +// 0.15.x 下为 true,0.16.0 下为 false +``` + +这个改动几乎不会影响任何真实代码,因为 zero-bit 字段的值依然是 comptime-known 的。但如果你直接读 `std.builtin.StructField.is_comptime`,或者依赖“带 / 不带 `comptime` 的 tuple 互为同一类型”的写法,就需要相应调整。 + +### 字段分析变成 lazy + +引入新的 `std.Io` 接口之后,官方注意到一个问题:只要把某个类型当成命名空间使用,它的字段也会被分析。例如代码里用了 `std.Io.Writer`,就会把 `std.Io` 的整个 vtable 拉进来——某些场景下这甚至会引入不必要的 codegen,让产物体积明显膨胀。 + +`0.16.0` 把 `struct`(提醒一下,`.zig` 文件本身就是 struct)、`union`、`enum`、`opaque` 改成只在“需要它的尺寸”或“需要它的某个字段类型”时才解析字段。这意味着: + +- 把类型当作命名空间引用时,不会触发字段解析 +- 即便构造非解引用指针 `*T`,只要从不实际取尺寸或字段,`T` 也不会被解析 + +这是一项内部行为优化,对绝大多数代码透明。但如果你以前刻意依赖“引用一个类型就能强迫它被解析”这种隐式行为(例如某些 trait 风格的元编程),可能需要显式触发字段解析(比如调用 `@sizeOf(T)` 或访问一个具体字段)。这条改动是 `Reworked Type Resolution` 的一部分。 + +### 指向 comptime-only 类型的指针不再是 comptime-only + +虽然 `comptime_int` 是 comptime-only 类型,但在 `0.16.0` 里: + +- `*comptime_int` 不是 comptime-only +- `[]comptime_int` 也不是 comptime-only + +最直观的例子是函数指针:`*const fn () void` 是运行时类型——你不能在运行时解引用它,因为元素类型 `fn () void` 是 comptime-only;但这个指针本身可以在运行时存在。也就是说,这类指针“可以在运行时存在,但只能在编译期解引用”。 + +这条规则在反射场景下意外地有用。比如你拿到一个 `[]const std.builtin.Type.StructField`,想把每个字段的 `name` 传给运行时代码。 + +旧写法:先把 `name` 抽成一个 `[]const []const u8`,再把后者传给运行时函数。 + +新写法:可以直接把 `[]const std.builtin.Type.StructField` 传给运行时函数。这个函数自然不能在运行时从切片里加载一个 `StructField`(毕竟 `StructField` 是 comptime-only),但**可以**加载它的 `name` 字段——因为 `name` 的类型是运行时类型! + +这条改动同样是 `Reworked Type Resolution` 的一部分。 + +### Reworked Byval 语法降级 + +这是一条编译器内部的改动,但因为它直接影响 `Forbid Runtime Vector Indexes` 等语义,所以值得在升级时知道。 + +编译器前端早期为了减少中间指令数,曾经尝试用“byval”语义降级表达式。这个实验最后被认定为失败,因为它带来了: + +- 数组访问的性能问题 +- 显式拷贝下出现意料之外的别名 +- 退化场景里代码质量极差 + +`0.16.0` 改成全程“byref”降级,只在最终一次 load 时才取值。这不仅修掉了上面这些问题,也是为什么本版本会顺手禁止运行时向量索引(详见前面对应小节)。 + +对应用代码而言,这条改动总体是无感的——但如果你以前观察到“某些数组 / 向量代码有奇怪的别名表现”,升级到 `0.16.0` 后大概率会自动消失。 + +### 类型解析规则被重做,依赖环错误会更清晰 + +`0.16.0` 彻底重做了编译器内部的类型解析流程。结果是: + +- 大多数以前能工作的代码会继续工作 +- 一些以前会莫名其妙报 dependency loop 的代码,现在反而能正常工作 +- 也有一小部分旧代码会因为真正的依赖环而在 `0.16.0` 开始报错 + +最常见的两类“现在会报错”的写法: + +1. **结构体在自身字段上做对齐查询**: + + ```zig + const S = struct { + foo: [*]align(@alignOf(@This())) u8, + }; + ``` + + `@alignOf(@This())` 需要先知道 `S` 的对齐,但 `S` 的对齐又依赖这个字段的对齐——直接构成依赖环,这版编译器会直接给出明确的错误:`type 'S' depends on itself for alignment query here`。 + +2. **结构体默认字段值与 `@typeInfo` 反射形成环**: + + ```zig + const S = struct { x: u32 = default_val }; + const default_val = other_val; + const other_val = @typeInfo(S).@"struct".fields.len; + ``` + + 编译器现在会按依赖顺序把每一跳都列出来,并提示“破坏其中任何一条都能解开环”。 + +这类问题没有统一的机械式修法,但 `0.16.0` 的错误信息比以前清楚很多,通常你只需要打断这条环上的任意一条依赖即可。如果实在排查不出来,建议加入 Zig 社区交流。 + +## 标准库 + +### 标准库总览:新增 / 移除 / 错误集合改名 + +具体迁移点之外,先把 `0.16.0` 在标准库根命名空间一级的几类总体调整列出来,方便你在升级时一次性扫一遍。 + +新增: + +- `Io.Dir.renamePreserve`:不会替换目标文件的 rename 操作 +- `Io.net.Socket.createPair` + +直接移除(不再有替代): + +- `SegmentedList` +- `meta.declList` +- `Io.GenericWriter` / `Io.AnyWriter` / `Io.null_writer` +- `Io.CountingReader` +- `Thread.Mutex.Recursive` + +错误集合改名 / 合并: + +- `error.RenameAcrossMountPoints` ➡️ `error.CrossDevice` +- `error.NotSameFileSystem` ➡️ `error.CrossDevice` +- `error.SharingViolation` ➡️ `error.FileBusy` +- `error.EnvironmentVariableNotFound` ➡️ `error.EnvironmentVariableMissing` +- `std.Io.Dir.rename` 现在返回 `error.DirNotEmpty` 而不是 `error.PathAlreadyExists` + +其它零散调整: + +- `fmt.Formatter` ➡️ `fmt.Alt` +- `fmt.format` ➡️ `std.Io.Writer.print` +- `fmt.FormatOptions` ➡️ `fmt.Options` +- `fmt.bufPrintZ` ➡️ `fmt.bufPrintSentinel` +- `compress`:`lzma` / `lzma2` / `xz` 已迁到 `Io.Reader` / `Io.Writer` +- `DynLib`:Windows 支持被移除——用户应该直接使用 `LoadLibraryExW` 和 `GetProcAddress`(实际多数人本来就是这么做的) +- `math.sign`:现在返回能容纳所有可能值的最小整数类型 +- Windows 上现在会自动触发拉取根证书 +- `tar.extract`:对路径穿越进行清理 +- `BitSet` / `EnumSet`:`initEmpty` / `initFull` 改为 decl literal + +### `std.Io` 成为新的核心 I/O 抽象 + +这是 `0.16.0` 最大的标准库变更。现在,凡是“可能阻塞控制流”或“会引入非确定性”的能力,基本都要求显式接收一个 `std.Io` 实例。 + +这意味着下面这些领域都围绕 `std.Io` 重新组织了: + +- 文件系统 +- 网络 +- 进程 +- 同步原语 +- 定时器与睡眠 +- 一部分流式读写接口 + +对于从 `0.15.x` 升级的项目来说,如果你只是想先获得和以前类似的行为,通常可以从 `Io.Threaded` 开始: + +```zig +var threaded: std.Io.Threaded = .init_single_threaded; +const io = threaded.io(); +``` + +但这只是临时过渡方案。更理想的做法,仍然是把 `io: std.Io` 作为参数一路向下传递,或者放到你的应用上下文里统一管理。 + +测试代码则建议优先使用 `std.testing.io`。 + +### `main` 现在可以直接接收 `std.process.Init` + +`0.16.0` 给 `main` 函数增加了一个非常实用的新入口。你可以直接在参数里拿到已经初始化好的常用资源: + +- `init.gpa` +- `init.io` +- `init.arena` +- `init.environ_map` +- `init.preopens` + +新写法示例: + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + try std.Io.File.stdout().writeStreamingAll(io, "Hello, world!\n"); +} +``` + +`main` 现在有三种合法形态: + +- `pub fn main() ...`:仍然合法,但拿不到参数和环境变量 +- `pub fn main(init: std.process.Init.Minimal) ...`:只拿到原始 `argv` / `environ` +- `pub fn main(init: std.process.Init) ...`:拿到完整预初始化资源 + +### 环境变量与命令行参数不再是全局状态 + +`0.16.0` 之后,环境变量和进程参数被明确收敛到 `main` 的初始化参数里,不再鼓励像以前一样把它们当作“随处可取的全局状态”。 + +读取参数: + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init.Minimal) void { + var args = init.args.iterate(); + while (args.next()) |arg| { + std.log.info("arg: {s}", .{arg}); + } +} +``` + +读取环境变量: + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + for (init.environ_map.keys(), init.environ_map.values()) |key, value| { + std.log.info("env: {s}={s}", .{ key, value }); + } +} +``` + +如果你的库函数仍然需要环境变量,请改成显式传参,或者显式接收 `*const std.process.Environ.Map`。 + +### 进程 API 的入口被重新整理 + +围绕新的 `std.Io`,进程相关 API 也发生了明显变化。 + +启动子进程: + +```zig +var child = std.process.Child.init(argv, gpa); +child.stdin_behavior = .Pipe; +child.stdout_behavior = .Pipe; +child.stderr_behavior = .Pipe; +try child.spawn(io); +``` + +⬇️ + +```zig +var child = try std.process.spawn(io, .{ + .argv = argv, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, +}); +``` + +运行并捕获输出: + +```zig +const result = try std.process.run(allocator, io, .{ + .argv = argv, +}); +``` + +替换当前进程镜像: + +```zig +const err = std.process.execv(arena, argv); +``` + +⬇️ + +```zig +const err = std.process.replace(io, .{ .argv = argv }); +``` + +### 网络 API 全面迁到 `std.Io` + +`std.net` 下的所有网络 API 都被迁到了 `std.Io`,统一通过 `Io` 实例发起调用。Windows 下的网络实现也彻底改成直接基于 AFD,不再依赖 `ws2_32.dll`,因此 cancelation 和 Batch 在 Windows 上也能正确工作。 + +需要注意: + +- `Io.Evented` 目前**还没有实现网络**,依赖 evented 网络的代码暂时只能选 `Io.Threaded` 或其它实现 +- `Io.net` 当前**也还缺乏非 IP 协议的能力** + +如果你过去使用 `std.net.Stream` / `std.net.tcpConnectToHost` 等 API,请按相同思路改为接收 `io: std.Io` 并使用 `std.Io.net.*`;具体迁移建议关注后续标准库文档,因为这部分接口还在快速演进中。 + +### `std.Thread.Pool` 被移除 + +`std.Thread.Pool` 已经从标准库中移除。最常见的迁移方向,是改用 `std.Io.async` 或 `std.Io.Group.async`。 + +如果你过去用的是“提交一组任务,然后等待全部结束”的模式,通常可以这样迁移: + +```zig +fn doAllTheWork(io: std.Io) !void { + var group: std.Io.Group = .init; + errdefer group.cancel(io); + + group.async(io, doSomeWork, .{ io, &group, first_work_item }); + try group.await(io); +} +``` + +另外要特别注意:如果你的旧代码里除了 `Thread.Pool` 之外,还用了 `Thread.Mutex`、`Thread.Condition`、`Thread.ResetEvent` 等同步原语,那么升级到 `std.Io` 时,它们也应该一起迁到对应的 `std.Io.*` 类型。 + +### 同步原语全面迁到 `std.Io` + +`0.16.0` 中,绝大多数同步原语都从 `std.Thread` 迁到了 `std.Io` 命名空间。这样做的核心原因是:**被同步的代码必须能与应用所选的 I/O 实现正确集成**——只有这样,等待行为才能跟随当前 `Io` 实现走。例如,在 `Io.Threaded` 下争用的 mutex 会阻塞线程;在 `Io.Evented` 下则会切换栈而不是阻塞当前线程。这些同步原语同样会正确接入新的 cancelation 模型。 + +需要注意的是,纯 lock-free 的同步原语并不需要接入 `std.Io`。 + +完整迁移表: + +- `std.Thread.ResetEvent` ➡️ `std.Io.Event` +- `std.Thread.WaitGroup` ➡️ `std.Io.Group` +- `std.Thread.Futex` ➡️ `std.Io.Futex` +- `std.Thread.Mutex` ➡️ `std.Io.Mutex` +- `std.Thread.Condition` ➡️ `std.Io.Condition` +- `std.Thread.Semaphore` ➡️ `std.Io.Semaphore` +- `std.Thread.RwLock` ➡️ `std.Io.RwLock` +- `std.once` 已被移除,建议直接避免全局变量,或者自行实现等价逻辑 + +### 随机数与熵 API 接入 `std.Io` + +随机数生成相关接口在 `0.16.0` 也被收敛到了 `std.Io`:日常熵直接通过 `io.random` 获取,而需要绕开进程内 RNG 状态、强制走 syscall 的“安全熵”则改用 `io.randomSecure`。 + +`std.crypto.random.bytes` 迁移: + +```zig +var buffer: [123]u8 = undefined; +std.crypto.random.bytes(&buffer); +``` + +⬇️ + +```zig +var buffer: [123]u8 = undefined; +io.random(&buffer); +``` + +需要 `std.Random` 接口时: + +```zig +const rng = std.crypto.random; +``` + +⬇️ + +```zig +const rng_impl: std.Random.IoSource = .{ .io = io }; +const rng = rng_impl.interface(); +``` + +`posix.getrandom` 也统一走 `io.random`: + +```zig +var buffer: [64]u8 = undefined; +posix.getrandom(&buffer); +``` + +⬇️ + +```zig +var buffer: [64]u8 = undefined; +io.random(&buffer); +``` + +另外,`std.Options.crypto_always_getrandom` 和 `std.Options.crypto_fork_safety` 这两个全局选项也被移除了,对应能力变成了 `io.random` / `io.randomSecure` 两条不同的 API 路径。 + +### 时间 API 迁到 `std.Io` + +时间相关接口也并入 `std.Io`,并允许查询时钟分辨率(可能失败)。`error.Unexpected` 和 `error.ClockUnsupported` 因此从超时和时钟读取的错误集合里被剔除——分辨率被视为无穷大,由用户自行通过 `Clock.resolution` 单独检查。 + +迁移表: + +- `std.time.Instant` ➡️ `std.Io.Timestamp` +- `std.time.Timer` ➡️ `std.Io.Timestamp` +- `std.time.timestamp` ➡️ `std.Io.Timestamp.now` + +### `{D}` 格式说明符被移除 + +为了配合新的 `std.Io.Duration` 类型并增强类型安全,`{D}` 这个旧的 duration 格式说明符已经被移除: + +```zig +writer.print("{D}", .{ns}); +``` + +⬇️ + +```zig +writer.print("{f}", .{std.Io.Duration{ .nanoseconds = ns }}); +``` + +### 调试栈追踪 API 重做 + +`0.16.0` 重做了一批调试相关 API,特别是栈追踪。核心目标是:**在不依赖帧指针(如 libc 用 `-fomit-frame-pointer` 编译)的情况下,也能实现快速且不会因为越界访问而崩溃的栈展开**。这其实是个很复杂的问题,真正的解法是 unwind information,而不同目标对 unwind 信息的编码方式各不相同;旧实现既 buggy 又不完整,而且常常拖慢性能。 + +从这个版本开始,标准库**默认使用基于 unwind 信息的“安全”栈展开**;和原来基于帧指针的展开相比,性能开销在大多数场景下是可以接受的。 + +主要 API 变化: + +- `captureStackTrace` ➡️ `captureCurrentStackTrace` +- `dumpStackTraceFromBase` ➡️ `dumpCurrentStackTrace` +- `walkStackWindows` ➡️ `captureCurrentStackTrace` +- `writeStackTraceWindows` ➡️ `writeCurrentStackTrace` +- `std.debug.StackIterator` 现在是内部 API,已从公开导出中移除 + +新 API 签名示例: + +```zig +/// 把已经捕获的栈追踪写入 `t`,并附上源码位置。 +pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void { ... } + +/// 捕获当前栈追踪到 `addr_buf`。`addr_buf` 的生命周期需要不短于返回的 StackTrace。 +pub noinline fn captureCurrentStackTrace( + options: StackUnwindOptions, + addr_buf: []usize, +) StackTrace { ... } +``` + +`StackUnwindOptions` 提供了几个常用选项: + +- `first_address`:忽略到指定返回地址前的所有栈帧(典型用法是把 panic handler 自身从 trace 里抹掉) +- `context`:从指定的 `cpu_context.Native` 而不是当前栈顶开始展开(典型用法是从信号处理函数里打印 trace) +- `allow_unsafe_unwind`:作为最后手段,允许使用可能崩溃的展开策略;默认为 `false` + +绝大多数情况下用 `captureCurrentStackTrace` 就够了;如果需要打印当前栈,对应的还有 `writeCurrentStackTrace` / `dumpCurrentStackTrace`。`StackIterator` 已经不再适合直接使用,如果你以前依赖它,可以考虑直接用 `std.debug.SelfInfo` 提供的更底层 API;后者还可以通过定义 `@import("root").debug.SelfInfo` 替换实现,从而让栈追踪在标准库不直接支持的目标(甚至 freestanding 目标)上也能工作。 + +### `ucontext_t` 与相关类型 / 函数被移除 + +`std.posix` 不再提供 `ucontext_t` 系列绑定。原因有两条: + +- 用 `ucontext.h` 函数做非局部控制流的能力本来就不在 Zig 支持范围内,且 POSIX 已弃用,musl 也不再提供 +- 用 `ucontext_t` 在信号处理函数里读取机器状态这件事,标准库做得很差——这类类型在不同架构下变化很快,标准库一直没跟上 + +如果你的代码确实需要在信号处理里访问机器状态,建议自行定义贴合具体场景的 `ucontext_t` / `mcontext_t`。`std.debug.cpu_context.signal_context_t` 也在这一版本里相应调整。 + +### `std.io` 进一步收敛到 `std.Io` + +这一轮更新里,`GenericReader`、`AnyReader`、`FixedBufferStream` 等历史接口继续退出。 + +常见映射: + +- `std.io` ➡️ `std.Io` +- `std.Io.GenericReader` ➡️ `std.Io.Reader` +- `std.Io.AnyReader` ➡️ `std.Io.Reader` +- `std.leb.readUleb128` ➡️ `std.Io.Reader.takeLeb128` +- `std.leb.readIleb128` ➡️ `std.Io.Reader.takeLeb128` + +读取固定缓冲区: + +```zig +var fbs = std.io.fixedBufferStream(data); +const reader = fbs.reader(); +``` + +⬇️ + +```zig +var reader: std.Io.Reader = .fixed(data); +``` + +写入固定缓冲区: + +```zig +var fbs = std.io.fixedBufferStream(buffer); +const writer = fbs.writer(); +``` + +⬇️ + +```zig +var writer: std.Io.Writer = .fixed(buffer); +``` + +### 文件系统和路径 API 有一批实用迁移点 + +`fs` 全部 API 都迁到了 `Io`。和 0.15 那次 “writergate” 不同,这次虽然 breaking 范围很大,但绝大多数迁移机械、不需要特别多的判断。最典型的形态就是给原本无参的方法加一个 `io`: + +```zig +file.close(); +``` + +⬇️ + +```zig +file.close(io); +``` + +升级 diff 可能很长,但每一处都很容易看懂。 + +新增 API: + +- `Io.Dir.hardLink` +- `Io.Dir.Reader` +- `Io.Dir.setFilePermissions` +- `Io.Dir.setFileOwner` +- `Io.File.NLink` + +无对应替代被直接移除的 API: + +- `fs.realpathZ` / `fs.realpathW` / `fs.realpathW2` +- `fs.makeDirAbsoluteZ` / `fs.deleteDirAbsoluteZ` / `fs.openDirAbsoluteZ` +- `fs.renameAbsoluteZ` / `fs.renameZ` +- `fs.deleteTreeAbsolute` +- `fs.symLinkAbsoluteW` +- `fs.Dir.realpathZ` / `fs.Dir.realpathW` / `fs.Dir.realpathW2` +- `fs.Dir.deleteFileZ` / `fs.Dir.deleteFileW` / `fs.Dir.deleteDirZ` / `fs.Dir.deleteDirW` +- `fs.Dir.renameZ` / `fs.Dir.renameW` +- `fs.Dir.symLinkWasi` / `fs.Dir.symLinkZ` / `fs.Dir.symLinkW` +- `fs.Dir.readLinkWasi` / `fs.Dir.readLinkZ` / `fs.Dir.readLinkW` +- `fs.Dir.adaptToNewApi` / `fs.Dir.adaptFromNewApi` +- `fs.File.isCygwinPty` +- `fs.File.adaptToNewApi` / `fs.File.adaptFromNewApi` + +重命名 / 迁移过的 API(节选最常用的部分): + +| 0.15.x | 0.16.0 | +| --- | --- | +| `fs.Dir` | `std.Io.Dir` | +| `fs.File` | `std.Io.File` | +| `fs.cwd` | `std.Io.Dir.cwd` | +| `fs.copyFileAbsolute` | `std.Io.Dir.copyFileAbsolute` | +| `fs.makeDirAbsolute` | `std.Io.Dir.createDirAbsolute` | +| `fs.deleteDirAbsolute` | `std.Io.Dir.deleteDirAbsolute` | +| `fs.openDirAbsolute` | `std.Io.Dir.openDirAbsolute` | +| `fs.openFileAbsolute` | `std.Io.Dir.openFileAbsolute` | +| `fs.accessAbsolute` | `std.Io.Dir.accessAbsolute` | +| `fs.createFileAbsolute` | `std.Io.Dir.createFileAbsolute` | +| `fs.deleteFileAbsolute` | `std.Io.Dir.deleteFileAbsolute` | +| `fs.renameAbsolute` | `std.Io.Dir.renameAbsolute` | +| `fs.readLinkAbsolute` | `std.Io.Dir.readLinkAbsolute` | +| `fs.symLinkAbsolute` | `std.Io.Dir.symLinkAbsolute` | +| `fs.realpath` | `std.Io.Dir.realPathFileAbsolute` | +| `fs.realpathAlloc` | `std.Io.Dir.realPathFileAbsoluteAlloc` | +| `fs.rename` | `std.Io.Dir.rename` | +| `fs.has_executable_bit` | `std.Io.File.Permissions.has_executable_bit` | +| `fs.defaultWasiCwd` | `std.os.defaultWasiCwd` | +| `fs.openSelfExe` | `std.process.openExecutable` | +| `fs.selfExePath` | `std.process.executablePath` | +| `fs.selfExePathAlloc` | `std.process.executablePathAlloc` | +| `fs.selfExeDirPath` | `std.process.executableDirPath` | +| `fs.selfExeDirPathAlloc` | `std.process.executableDirPathAlloc` | +| `fs.Dir.setAsCwd` | `std.process.setCurrentDir` | +| `fs.Dir.realpath` | `std.Io.Dir.realPathFile` | +| `fs.Dir.realpathAlloc` | `std.Io.Dir.realPathFileAlloc` | +| `fs.Dir.makeDir` | `std.Io.Dir.createDir` | +| `fs.Dir.makePath` | `std.Io.Dir.createDirPath` | +| `fs.Dir.makeOpenDir` | `std.Io.Dir.createDirPathOpen` | +| `fs.Dir.atomicSymLink` | `std.Io.Dir.symLinkAtomic` | +| `fs.Dir.chmod` | `std.Io.Dir.setPermissions` | +| `fs.Dir.chown` | `std.Io.Dir.setOwner` | +| `fs.File.Mode` | `std.Io.File.Permissions` | +| `fs.File.PermissionsWindows` | `std.Io.File.Permissions` | +| `fs.File.PermissionsUnix` | `std.Io.File.Permissions` | +| `fs.File.default_mode` | `std.Io.File.Permissions.default_file` | +| `fs.File.getOrEnableAnsiEscapeSupport` | `std.Io.File.enableAnsiEscapeCodes` | +| `fs.File.setEndPos` | `std.Io.File.setLength` | +| `fs.File.getEndPos` | `std.Io.File.length` | +| `fs.File.seekTo` / `seekBy` / `seekFromEnd` | `std.Io.File.Reader.seekTo` / `Reader.seekBy` / `Writer.seekTo` | +| `fs.File.getPos` | `std.Io.File.Reader.logicalPos` / `std.Io.Writer.logicalPos` | +| `fs.File.mode` | `std.Io.File.stat().permissions.toMode` | +| `fs.File.chmod` | `std.Io.File.setPermissions` | +| `fs.File.chown` | `std.Io.File.setOwner` | +| `fs.File.updateTimes` | `std.Io.File.setTimestamps` / `setTimestampsNow` | +| `fs.File.read` / `readv` | `std.Io.File.readStreaming` | +| `fs.File.pread` / `preadv` | `std.Io.File.readPositional` | +| `fs.File.preadAll` | `std.Io.File.readPositionalAll` | +| `fs.File.write` / `writev` | `std.Io.File.writeStreaming` | +| `fs.File.pwrite` / `pwritev` | `std.Io.File.writePositional` | +| `fs.File.writeAll` | `std.Io.File.writeStreamingAll` | +| `fs.File.pwriteAll` | `std.Io.File.writePositionalAll` | +| `fs.File.copyRange` / `copyRangeAll` | `std.Io.File.writer` | + +这一表里许多函数除了改名,还顺手在签名里塞了一个 `io: std.Io` 参数。 + +另外这些三个老的命名空间常量被 deprecated: + +- `fs.path` ➡️ `std.Io.Dir.path` +- `fs.max_path_bytes` ➡️ `std.Io.Dir.max_path_bytes` +- `fs.max_name_bytes` ➡️ `std.Io.Dir.max_name_bytes` + +#### `readFileAlloc` + +旧写法: + +```zig +const contents = try std.fs.cwd().readFileAlloc(allocator, file_name, 1234); +``` + +新写法: + +```zig +const contents = try std.Io.Dir.cwd().readFileAlloc(io, file_name, allocator, .limited(1234)); +``` + +注意新的限制语义更严格:到达上限本身也会报错,错误名也从 `FileTooBig` 变成了 `StreamTooLong`。 + +#### `readToEndAlloc` + +旧写法: + +```zig +const contents = try file.readToEndAlloc(allocator, 1234); +``` + +新写法: + +```zig +var file_reader = file.reader(&.{}); +const contents = try file_reader.interface.allocRemaining(allocator, .limited(1234)); +``` + +#### 当前目录 API 更名 + +旧写法: + +```zig +std.process.getCwd(buffer) +std.process.getCwdAlloc(allocator) +``` + +新写法: + +```zig +std.process.currentPath(io, buffer) +std.process.currentPathAlloc(io, allocator) +``` + +#### `fs.path.relative` 变成纯函数 + +旧写法: + +```zig +const relative = try std.fs.path.relative(gpa, from, to); +defer gpa.free(relative); +``` + +新写法: + +```zig +const cwd_path = try std.process.currentPathAlloc(io, gpa); +defer gpa.free(cwd_path); + +const relative = try std.fs.path.relative(gpa, cwd_path, environ_map, from, to); +defer gpa.free(relative); +``` + +也就是说,`relative` 不再自己偷偷读取当前工作目录和环境变量,而是要求你把这些上下文显式传进去。 + +#### `File.Stat.atime` 现在是可选值 + +读取访问时间: + +```zig +stat.atime +``` + +⬇️ + +```zig +stat.atime orelse return error.FileAccessTimeUnavailable +``` + +设置时间戳: + +```zig +try file.setTimestamps(io, src_stat.atime, src_stat.mtime); +``` + +⬇️ + +```zig +try file.setTimestamps(io, .{ + .access_timestamp = .init(src_stat.atime), + .modify_timestamp = .init(src_stat.mtime), +}); +``` + +#### 选择性遍历目录树 + +旧的 `Dir.walk` 没法跳过特定子目录。`0.16.0` 新增了 `walkSelectively`,每次进入新目录都需要显式 `enter`,从而避免对被跳过目录做无谓的 open/close syscall。 + +```zig +var walker = try dir.walk(gpa); +defer walker.deinit(); + +while (try walker.next(io)) |entry| { + // ... +} +``` + +⬇️ + +```zig +var walker = try dir.walkSelectively(gpa); +defer walker.deinit(); + +while (try walker.next(io)) |entry| { + if (failsFilter(entry)) continue; + if (entry.kind == .directory) { + try walker.enter(io, entry); + } + // ... +} +``` + +另外 `Walker.Entry` 增加了 `depth` 函数,`Walker` 与 `SelectiveWalker` 都增加了 `leave`,便于在遍历到一半时跳出当前子目录。 + +#### `fs.path` 对 Windows 路径处理更一致 + +`std.fs.path` 全部函数都更正确地处理 Windows 的 UNC、"rooted" 和 drive-relative 路径。API 上的具体变化: + +- `windowsParsePath` / `diskDesignator` / `diskDesignatorWindows` ➡️ `parsePath` / `parsePathWindows` / `parsePathPosix` +- 新增 `getWin32PathType` +- `componentIterator` / `ComponentIterator.init` 不再返回错误 + +#### `File.MemoryMap` 语义收紧 + +内存映射的指针内容现在被定义为只在显式 sync point 后同步,这让基于普通文件操作的回退实现成为合法选择,也允许 evented I/O 用 evented 文件 I/O 来实现 sync point。 + +技术上这是 breaking change:positional 文件读写的错误集合更窄;在 WASI 上现在会正确返回 `error.IsDir` 而不是 `error.NotOpenForReading`。 + +#### 内存锁定 / 保护 API 迁到 `std.process` + +`mmap` / `mprotect` 的标志现在改为类型安全的结构体字段: + +```zig +std.posix.PROT.READ | std.posix.PROT.WRITE, +``` + +⬇️ + +```zig +.{ .READ = true, .WRITE = true }, +``` + +`mlock` 系列也搬到了 `std.process`: + +```zig +try std.posix.mlock(); +try std.posix.mlock2(slice, std.posix.MLOCK_ONFAULT); +try std.posix.mlockall(slice, std.posix.MCL_CURRENT|std.posix.MCL_FUTURE); +``` + +⬇️ + +```zig +try std.process.lockMemory(slice, .{}); +try std.process.lockMemory(slice, .{ .on_fault = true }); +try std.process.lockMemoryAll(.{ .current = true, .future = true }); +``` + +#### "Preopens" 迁到 `std.process` + +WASI 上预先打开的文件句柄从 `std.fs.wasi.Preopens` 搬到了 `std.process.Preopens`: + +```zig +const wasi_preopens: std.fs.wasi.Preopens = try .preopensAlloc(arena); +``` + +⬇️ + +```zig +const preopens: std.process.Preopens = try .init(arena); +``` + +或者直接通过前面 `Juicy Main` 部分介绍的 `std.process.Init.preopens` 拿到。在非 WASI 系统上数据类型是 `void`——你不用就不会付出代价。 + +#### `atomicFile` 重构为 `createFileAtomic` + +这次重构主要动机是把 `std.crypto.random` 的调用挪到 `std.Io.VTable` 之下(具体是 `std.Io.File.Atomic.init` 里那一处),同时顺手在 Linux 上接入了 `O_TMPFILE`——也就是“创建一个无名 fd,操作完后再 link 到目标位置;如果进程提前结束,OS 会自动回收,不留临时垃圾”这套机制。 + +不过 `O_TMPFILE` 在内核 / libc 这一层有不少坑(`linkat()` 缺 `AT_REPLACE`、错误码为 `EISDIR`/`ENOENT` 这种反直觉行为等),所以当前实现是“能用上 `O_TMPFILE` 的场景用,其余场景仍然走原来的随机文件名 + `renameat()`”。这套封装让 Zig 端不用感知差异,将来 OS 修了 bug 直接受益。 + +旧写法: + +```zig +var buffer: [1024]u8 = undefined; +var atomic_file = try dest_dir.atomicFile(io, dest_path, .{ + .permissions = actual_permissions, + .write_buffer = &buffer, +}); +defer atomic_file.deinit(); + +// do something with atomic_file.file_writer; + +try atomic_file.flush(); +try atomic_file.renameIntoPlace(); +``` + +⬇️ + +```zig +var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{ + .permissions = actual_permissions, + .make_path = true, + .replace = true, +}); +defer atomic_file.deinit(io); + +var buffer: [1024]u8 = undefined; // 仅在没有直接 fd-to-fd 通路时使用 +var file_writer = atomic_file.file.writer(io, &buffer); + +// do something with file_writer + +try file_writer.flush(); +try atomic_file.replace(io); // 或者把上面的 .replace 改为 false,再调用 link() +``` + +另外这一版还新增了 `std.Io.File.hardLink` API(目前仅 Linux)——它是 `O_TMPFILE` 没有 replace 语义时把 fd 物化为常规文件的必备工具。 + +#### 其他值得顺手处理的文件系统改动 + +- `fs.getAppDataDir` 已被移除,应用应自行决定“应用数据目录”的策略;可考虑第三方包 `known-folders` +- `Io.Writer.Allocating` 新增了 `alignment: std.mem.Alignment` 字段(运行时已知对齐,配合 Allocator API 的 raw 函数变体使用) + +### `std.posix` 和 `std.os.windows` 的中层 API 被移除 + +这次标准库很明确地砍掉了很多“中不溜”的系统接口。如果你升级后是在 `std.posix` 或 `std.os.windows` 里踩雷,官方建议只选两条路: + +- 往上走,改用 `std.Io` +- 往下走,直接使用 `std.posix.system` + +也就是说,Zig 不再想长期维护那批半高层、半底层的历史包装函数。 + +### `std.mem` 的 “index of” 系列统一更名为 “find” + +`std.mem` 现在统一使用 `find` 作为“查找子串位置”的概念名称,并新增了 `cut`、`cutPrefix`、`cutSuffix`、`cutScalar`、`cutLast`、`cutLastScalar` 等函数。 + +如果你项目里大量用了 `indexOf` / `lastIndexOf` / `indexOfScalar` 这类 API,可以统一按新的 `find*` 命名规则做搜索替换。 + +### 容器继续向 unmanaged 方向收敛 + +这部分延续了 `0.14` 和 `0.15` 的趋势:标准库越来越倾向于“容器本身不持有 allocator,把 allocator 显式传给需要分配的方法”。 + +这次比较关键的变化有: + +- 新增 `heap.MemoryPoolUnmanaged` / `heap.MemoryPoolAlignedUnmanaged` / `heap.MemoryPoolExtraUnmanaged` +- `PriorityDequeue` 不再持有 `Allocator` 字段 +- `PriorityQueue` 不再持有 `Allocator` 字段 +- `ArrayHashMap`、`AutoArrayHashMap`、`StringArrayHashMap` 被移除 +- `AutoArrayHashMapUnmanaged` ➡️ `std.array_hash_map.Auto` +- `StringArrayHashMapUnmanaged` ➡️ `std.array_hash_map.String` +- `ArrayHashMapUnmanaged` ➡️ `std.array_hash_map.Custom` +- `PriorityQueue` 和 `PriorityDequeue` 都继续往 `.empty` / `push` / `pop` 风格迁移 + +`PriorityQueue` 现在可以通过 `.empty` 初始化(不再需要 `init` 方法),最小堆和最大堆只需要换比较函数即可: + +```zig +fn lessThan(context: void, a: u32, b: u32) Order { + _ = context; + return std.math.order(a, b); +} +const MinHeap = std.PriorityQueue(u32, void, lessThan); +var queue: MinHeap = .empty; +``` + +```zig +fn greaterThan(context: void, a: u32, b: u32) Order { + _ = context; + return std.math.order(a, b).invert(); +} +const MaxHeap = std.PriorityQueue(u32, void, greaterThan); +var queue: MaxHeap = .empty; +``` + +常见重命名: + +- `init` ➡️ `initContext` +- `add` ➡️ `push` +- `addUnchecked` ➡️ `pushUnchecked` +- `addSlice` ➡️ `pushSlice` +- `remove` ➡️ `pop` +- `removeOrNull` ➡️ `pop` +- `removeIndex` ➡️ `popIndex` + +`PriorityDequeue` 的改动整体跟随 `Deque`:含 `add` 的方法改名为 `push`,含 `remove` 的方法改名为 `pop`;`popMinOrNull` / `popMaxOrNull` 与 `popMin` / `popMax` 合并(功能不变);默认字段值通过 `.empty` 常量而不是 `init()` 方法初始化。 + +常见重命名: + +- `init` ➡️ `.empty` +- `add` ➡️ `push` +- `addSlice` ➡️ `pushSlice` +- `addUnchecked` ➡️ `pushUnchecked` +- `removeMinOrNull` / `removeMin` ➡️ `popMin` +- `removeMaxOrNull` / `removeMax` ➡️ `popMax` +- `removeIndex` ➡️ `popIndex` + +### 分配器与并发模型继续调整 + +有两条需要直接注意: + +- `std.heap.ArenaAllocator` 现在变成了 thread-safe 且 lock-free。它在单线程场景下的性能与旧实现相当,在最多 7 线程并发的场景下,则比“旧实现 + `ThreadSafeAllocator` 包装”略快;同样地,未来 `std.heap.DebugAllocator` 也会朝这个方向走 +- `std.heap.ThreadSafeAllocator` 被移除——“mutex 包一层 allocator”这种实现既必然要求 `Io` 实例又通常很慢,已经被官方视为反模式 + +如果你的旧代码是“在外层包一层 `ThreadSafeAllocator`”,现在应改为直接选用本身适合并发场景的 allocator,或者改造调用结构,避免再依赖这层包装器。 + +### Deflate 压缩与解压重做 + +`std.compress.flate` 这一轮**新增了从零实现的 deflate 压缩器**: + +- 默认 writer:以 writer 缓冲区作为历史窗口、用链式哈希表寻找匹配,token 累积到阈值后整块输出 +- 新增 `Raw` writer:完全只输出 store block(即未压缩字节),借助数据向量高效发送 block header 与数据 +- 新增 `Huffman` writer:只做 Huffman 压缩,不做匹配 + +`Raw` 与 `Huffman` 因为不需要保留历史,可以更直接地利用新的 writer 语义。 + +`token` 中的字面量与距离编码参数也被重做:参数现在是数学方法推导出来的,更昂贵的那部分依然走查表(`ReleaseSmall` 例外)。 + +解压侧的 bit 读取也大幅简化,充分利用了底层 reader 可以 peek 的能力,并修掉了若干和 limit 处理相关的 bug。 + +性能数据(与 zlib 对比,越快越好): + +- **默认级别压缩**:std-deflate 比 zlib 快约 **9.7%**,cache miss 与分支误预测都明显更少 +- **最高级别压缩**:与 zlib 持平(差异 1% 以内) +- **解压(vs 上一版 std)**:新实现快约 **9.5%**,CPU 周期与指令数都减少约 10% + +压缩比层面,zlib 在默认级别下高约 1.00%,最高级别下高约 0.77%——这是后续打磨的方向。 + +### `std.crypto` 新增 AES-SIV 与 AES-GCM-SIV + +针对 nonce 重用敏感的场景,`std.crypto` 现在内置了: + +- **AES-SIV**:在密钥包装(key wrapping)这类场景里特别有用 +- **AES-GCM-SIV**:在嵌入式 / 受限目标上尤其合适 + +如果你的项目以前不得不靠第三方 crate 提供这两个原语,现在可以直接换到标准库版本。 + +### `std.crypto` 新增 Ascon-AEAD / Ascon-Hash / Ascon-CHash + +NIST SP 800-232 已经发布之后,Zig 标准库一次性补齐基于 Ascon 置换的高层构造: + +- `Ascon-AEAD`:AEAD 加密 +- `Ascon-Hash`:定长哈希 +- `Ascon-CHash`:可定制化的哈希 + +之前 Ascon 置换本身已经在标准库里,但建立在它之上的高层构造一直被刻意推迟到 NIST 最终规范公布。现在终于可以在标准库内直接使用。 + +### `std.Progress` 支持 Windows 跨进程上报 + +`std.Progress` 现在支持在 Windows 下报告子进程的进度,子进程的进度会自动反映到父进程的进度树中。同时,最大节点名称长度从 40 提升到 120。 + +如果你以前因为 Windows 下 progress 不能跨进程聚合而做了 workaround,可以删掉那部分代码。 + +### Windows 上网络 API 不再依赖 `ws2_32.dll` + +Windows 上所有网络 API 现在直接基于 AFD 实现,不再走 `ws2_32.dll`。这意味着: + +- 一批网络相关历史 bug 被修复 +- cancelation 与 Batch 在 Windows 上能正确生效 +- 避开了 `ws2_32.dll` 的性能陷阱(例如 socket handle 旁挂的 hash table) + +对应用层代码这通常是透明的,但如果你以前手写了 `extern "ws2_32"` 的绑定来补标准库不足,现在可以重新评估是否还需要继续维护这部分代码。 + +### Windows 上向 NtDll 的迁移基本完成 + +`0.16.0` 之后,Windows 上几乎所有标准库功能都直接基于最低层级的稳定 syscall API 实现。仍会调用 Windows DLL 的 extern 函数仅剩: + +- `kernel32.CreateProcessW` +- `crypt32` 一组:`CertOpenStore` / `CertCloseStore` / `CertEnumCertificatesInStore` / `CertFreeCertificateContext` / `CertAddEncodedCertificateToStore` / `CertOpenSystemStoreW` / `CertGetCertificateChain` / `CertFreeCertificateChain` / `CertVerifyCertificateChainPolicy` + +短期内不打算继续迁移这两组。如果你需要在 XP 这类老 Windows 上运行,或者更倾向走高层 DLL,建议作为社区项目实现一个不依赖 NtDll 的第三方 `Io`。 + +### 各目标上的栈回溯能力进一步扩大 + +`0.16.0` 在“几乎所有真正在用的目标”上都补齐了崩溃和 `DebugAllocator` 场景下的栈追踪能力。Windows 下打印 stack trace 时,inline caller 会从 PDB debug info 中被解析;如果 debug info 模糊,所有候选 caller 都会被打印出来。error return trace 现在在所有平台上都包含 inline caller。 + +这一切都是“为做游戏开发改善 Zig 体验”这条更大计划的一部分,对应用代码通常透明,但意味着遇到崩溃时你能拿到更多有用信息。 + +### Fuzz 测试接口改成 `std.testing.Smith` + +如果你的项目用到了 fuzz 测试,这也是一个直接的 breaking change。 + +旧写法: + +```zig +const std = @import("std"); + +fn fuzzTest(_: void, input: []const u8) !void { + var sum: u64 = 0; + for (input) |b| sum += b; + try std.testing.expect(sum != 1234); +} +``` + +新写法: + +```zig +const std = @import("std"); + +fn fuzzTest(_: void, smith: *std.testing.Smith) !void { + var sum: u64 = 0; + while (!smith.eosWeightedSimple(7, 1)) { + sum += smith.value(u8); + } + try std.testing.expect(sum != 1234); +} +``` + +同时,`0.16.0` 的 fuzzer 还支持了多进程、多核利用和崩溃输入落盘。如果你本来就依赖 fuzz 测试,这次升级是值得顺手把整套流程一起更新的。 + +## 构建系统 + +### 依赖目录改到项目本地 `zig-pkg` + +从 `0.16.0` 开始,依赖包不再解压到全局缓存目录下,而是会被拉到项目根目录旁边的 `zig-pkg` 目录中。 + +这带来两个直接结果: + +- 调试依赖更方便,能直接 grep、编辑、替换 +- 你通常不应该把 `zig-pkg` 提交进仓库,但如果团队确实想这么做,也不是不允许 + +### `build.zig.zon` 的依赖信息更严格 + +这次升级后,如果依赖缺少 `fingerprint`,或者 `name` 还在用字符串而不是 enum literal,`zig build` 会直接失败。 + +另外,旧的 hash 格式也已经不再支持。也就是说,`0.15.x` 时还能勉强工作的某些老 `build.zig.zon`,到 `0.16.0` 可能需要顺手全部更新一遍。 + +### 可以通过 `zig build --fork` 临时覆写依赖 + +新加入的 `--fork=[path]` 允许你在不改 `build.zig.zon` 的前提下,临时把整棵依赖树中的某个包替换成本地目录里的 fork。 + +```sh +zig build --fork=/path/to/your-package +``` + +这对排查生态 breakage、联调依赖、离线开发都很有帮助。 + +### 新的错误输出与测试超时选项 + +`0.16.0` 的 `zig build` 新增或调整了这些常用参数: + +- `--test-timeout`:为每个 Zig 单元测试设置超时 +- `--error-style`:控制构建错误输出样式 +- `--multiline-errors`:控制多行错误信息展示方式 + +其中,旧的 `--prominent-compile-errors` 已被移除。对应的新写法是: + +```sh +zig build --error-style minimal +``` + +如果你平时配合 `--watch` 或增量编译工作流,`verbose_clear` / `minimal_clear` 这两种 error style 会比较顺手。 + +`--error-style` 与 `--multiline-errors` 都额外支持通过环境变量 `ZIG_BUILD_ERROR_STYLE` / `ZIG_BUILD_MULTILINE_ERRORS` 设默认值,便于你在 shell 配置里一次性写好。`--multiline-errors` 可选值为 `indent`(新默认)、`newline`、`none`,分别对应“后续行缩进对齐第一行”“在第一行前补一个换行让所有行从首列起头”“原样输出不做处理”。 + +另外,Zig 不再读取 `ZIG_BTRFS_WORKAROUND` 这个旧环境变量——上游 Linux 那边的 bug 早已修复([#17095](https://github.com/ziglang/zig/issues/17095))。 + +### 临时文件 API 被重构 + +这次构建系统还清理了旧的临时目录 API: + +- `Build.makeTempPath` 被移除 +- `RemoveDir` step 被移除 + +迁移方向是: + +- 用 `Build.addTempFiles` 创建非缓存的临时文件目录 +- 用 `Build.addMutateFiles` 表达“会修改文件”的流程 +- 用 `Build.tmpPath` 作为便捷入口 + +如果你以前是在 configure 阶段预先创建临时目录,再在 make 阶段清理,`0.16.0` 之后应该把这套逻辑迁到新的 `WriteFile` / temp files API 上。 + +### `builtin.subsystem` 被移除,`Target.SubSystem` 也迁了位置 + +如果你的代码依赖 `std.builtin.subsystem`,现在需要重新设计:真正的 subsystem 直到链接阶段才知道,编译期再去猜它并不可靠。 + +另外,`std.Target.SubSystem` 被移动到了 `std.zig.Subsystem`。旧名字目前仍有 deprecated alias,可暂时过渡,但新代码最好直接跟着新命名走。 + +### 增量编译与新 ELF linker 继续前进 + +`0.16.0` 里,增量编译已经明显比 `0.15.x` 更实用了: + +- LLVM 后端也开始支持增量编译 +- 在 ELF 目标上,`-fincremental` 会默认启用新的 ELF linker +- 对很多项目来说,`-Dno-bin` 的收益已经不再明显 + +常见工作流现在可以直接写成: + +```sh +zig build -fincremental --watch +``` + +不过也要注意: + +- 增量编译依然不是默认开启 +- 依然存在已知 bug 和误编译 +- 新 ELF linker 还不完整,例如目前生成物仍然缺少 DWARF 信息 + +换句话说,`0.16.0` 的增量编译已经值得日常试用,但如果你碰到诡异问题,仍要记得先排除它。