Migrate to Zig 0.16: std.Io transports, minimum version 0.16.0 (closes #55)#60
Conversation
…#55) Zig 0.16 moved networking, HTTP, sleep, and clocks behind the new std.Io interface, which broke the entire transport layer at compile time (hidden until now by lazy analysis, since unit tests never touch network code paths). New src/runtime.zig (eth.runtime) keeps every public signature unchanged by constructing a default Io internally (std.Io.Threaded.global_single_threaded — synchronous on the calling thread, matching the old blocking behavior), and provides milliTimestamp()/sleepMs() replacements for the removed std APIs. - http_transport, flashbots, sse_transport: http.Client now takes io - ws_transport: std.Io.net Stream/IpAddress/HostName, Io reader/writer framing (readVec to preserve recv-like short reads), TLS client rebuilt against the new init options (bundle, entropy, realtime_now) - ws_client, retry_provider, wallet, sse: sleeps and clocks via runtime - mnemonic: entropy via Io random (latent break, now pinned by a test) - keccak: comptime branch quota raised for 0.16's stdlib permutation - bench/examples: timers, args, stdout migrated; three pre-existing example bugs fixed (wrong file name in build.zig, missing try, unhandled optional) - CI matrix 0.15.2 -> 0.16.0 (master unchanged); README/CONTRIBUTING prerequisites and build.zig.zon minimum_zig_version updated Breaking: minimum supported Zig is now 0.16.0. Verified on 0.16.0 and 0.17.0-dev.813: build, unit tests, fmt, and 23/23 Anvil integration tests (HTTP, WS subscriptions, LogWatcher).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR upgrades eth.zig to support Zig 0.16 by introducing a new ChangesZig 0.16 Compatibility Migration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/ws_transport.zig (1)
406-446:⚠️ Potential issue | 🔴 CriticalFix Zig 0.16 TLS
Client.Optionsfields ininitTls()
.realtime_nowdoesn’t match Zig 0.16’sstd.crypto.tls.Client.Options; supply the correct.realtime_now_secondsvalue as ani64of seconds (convertstd.Io.Clock.now(.real, io)from ns to seconds).- Populate
entropyusingstd.Io.randomSecure(or the required secure-entropy API for Zig 0.16) rather thanio.random.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ws_transport.zig` around lines 406 - 446, In initTls, fix the Zig 0.16 TLS Options usage: replace the incorrect .realtime_now field with .realtime_now_seconds and pass an i64 seconds value by converting std.Io.Clock.now(.real, io) from nanoseconds to seconds (e.g., cast/divide to i64 seconds), and populate the entropy buffer using the secure RNG API (use std.Io.randomSecure or the Zig 0.16 secure-entropy helper) instead of io.random so the entropy array is filled with cryptographically secure bytes before calling std.crypto.tls.Client.init (refer to the entropy variable and state.tls_client initialization).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/02_check_balance.zig`:
- Around line 14-16: The defer currently discards the leak-check result from
gpa.deinit(); update the cleanup to assert or otherwise handle the returned
Check so memory leaks are noticed: call gpa.deinit() in the defer and verify its
result (e.g., via std.debug.assert on the returned value) instead of ignoring
it; target the DebugAllocator instance named gpa and its gpa.deinit() call and
preserve the existing gpa.allocator() usage.
In `@src/runtime.zig`:
- Around line 3-18: The function defaultIo currently returns
std.Io.Threaded.global_single_threaded.io(), which relies on an undocumented
global; instead construct the single-threaded Io using
std.Io.Threaded.init_single_threaded and return its .io() so it uses the
documented initializer. Replace references to
std.Io.Threaded.global_single_threaded in defaultIo with a local init via
std.Io.Threaded.init_single_threaded(...) and return the resulting threaded io()
from that instance (ensure you call the documented init API on the Threaded type
and use the instance’s .io()).
- Around line 20-27: milliTimestamp currently calls std.Io.Clock.now(.real,
defaultIo()) but ignores its Error!std.Io.Timestamp result; change
milliTimestamp to propagate the error by updating its signature to return
Error!i64 (or Error!i64 alias) and use try on std.Io.Clock.now(.real,
defaultIo()) before calling Timestamp.toMilliseconds() (i.e., const ts = try
std.Io.Clock.now(.real, defaultIo()); return ts.toMilliseconds();), so
Error!Timestamp is handled properly.
---
Outside diff comments:
In `@src/ws_transport.zig`:
- Around line 406-446: In initTls, fix the Zig 0.16 TLS Options usage: replace
the incorrect .realtime_now field with .realtime_now_seconds and pass an i64
seconds value by converting std.Io.Clock.now(.real, io) from nanoseconds to
seconds (e.g., cast/divide to i64 seconds), and populate the entropy buffer
using the secure RNG API (use std.Io.randomSecure or the Zig 0.16 secure-entropy
helper) instead of io.random so the entropy array is filled with
cryptographically secure bytes before calling std.crypto.tls.Client.init (refer
to the entropy variable and state.tls_client initialization).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1d1f7df9-a667-473f-89bc-57cefc318cf2
📒 Files selected for processing (30)
.github/workflows/ci.ymlCHANGELOG.mdCONTRIBUTING.mdREADME.mdbench/bench.zigbench/keccak_bench_cli.zigbench/keccak_compare.zigbench/u256_bench.zigbuild.zig.zonexamples/01_derive_address.zigexamples/02_check_balance.zigexamples/03_sign_message.zigexamples/04_send_transaction.zigexamples/05_read_erc20.zigexamples/06_hd_wallet.zigexamples/07_selectors.zigexamples/build.zigexamples/build.zig.zonsrc/flashbots.zigsrc/http_transport.zigsrc/keccak.zigsrc/mnemonic.zigsrc/retry_provider.zigsrc/root.zigsrc/runtime.zigsrc/sse_transport.zigsrc/wallet.zigsrc/ws_client.zigsrc/ws_transport.zigtests/integration_tests.zig
| var gpa: std.heap.DebugAllocator(.{}) = .init; | ||
| defer _ = gpa.deinit(); | ||
| const allocator = gpa.allocator(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for DebugAllocator usage patterns across the codebase and verify API
echo "Searching for DebugAllocator usage patterns..."
rg -n "DebugAllocator" --type zig -C3
echo -e "\nSearching for GeneralPurposeAllocator (to see if it was removed)..."
rg -n "GeneralPurposeAllocator" --type zig -C2Repository: StrobeLabs/eth.zig
Length of output: 1988
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Listing repo files (sanity check)..."
ls -la
echo "Verifying file exists..."
git ls-files examples/02_check_balance.zig || true
ls -la examples || true
echo "Showing examples/02_check_balance.zig (top ~80 lines)..."
sed -n '1,80p' examples/02_check_balance.zig
echo -e "\nSearching for allocator types across Zig files..."
rg -n "DebugAllocator|GeneralPurposeAllocator" --type zig -SRepository: StrobeLabs/eth.zig
Length of output: 1958
🌐 Web query:
Zig 0.16 std.heap.DebugAllocator(.{}) .init API defer deinit usage
💡 Result:
In Zig 0.16, std.heap.GeneralPurposeAllocator has been renamed to std.heap.DebugAllocator [1][2]. To use DebugAllocator correctly, you must instantiate it, obtain an allocator interface, and ensure deinit is called to perform leak detection [3][4][5]. The standard pattern is to use defer to ensure deinit is called upon exiting the scope [3][4]. Usage Pattern: zig const std = `@import`("std"); pub fn main() !void { // 1. Instantiate DebugAllocator with your desired configuration var gpa: std.heap.DebugAllocator(.{}) = .init; // 2. Use defer to ensure deinit is called at the end of the scope. // deinit() returns a status (e.g., .ok or .leak). defer std.debug.assert(gpa.deinit() == .ok); // 3. Obtain the standard Allocator interface to perform allocations const allocator = gpa.allocator(); // Now use the allocator as usual const memory = try allocator.alloc(u8, 1024); defer allocator.free(memory); } Key Details: - Instantiation: std.heap.DebugAllocator(.{}) is a function that returns a type, which you then initialize [6]. Using .init (e.g., var gpa: ... = .init;) is the recommended way to instantiate it with default configuration [6][7]. - Deinitialization: The deinit() method performs critical cleanup and, if safety is enabled (default), checks for memory leaks [1][8][5]. It returns a std.heap.Check enum, typically .ok or .leak [9][1]. Using std.debug.assert(gpa.deinit() == .ok) is a common idiom in tests or main functions to panic if a leak is detected [3][4]. - Scope: Because DebugAllocator maintains internal state, it must remain in scope for the entire duration of any allocations performed via its allocator() interface [10]. Failure to call deinit prevents the allocator from reporting leaks and may result in improper cleanup of internal metadata [5].
Citations:
- 1: ziglang/zig@cd99ab3
- 2: Question: zig 0.16 is out of dev, will the book be updated? pedropark99/zig-book#230
- 3: https://ziglang.org/learn/samples/
- 4: https://zig.guide/standard-library/allocators/
- 5: https://ziggit.dev/t/how-do-i-know-when-i-should-call-deinit/7684
- 6: https://ziggit.dev/t/confusing-syntax-for-declaring-allocators-in-zig/13098
- 7: https://github.com/ziglang/zig/blob/master/lib/std/heap/debug_allocator.zig
- 8: https://tgmatos.dev/defeating-memory-leaks-with-zig-allocators/
- 9: https://github.com/ziglang/zig/blob/master/lib/std/heap.zig
- 10: https://ziggit.dev/t/debugallocator-in-shared-library/9136
Review: std.heap.DebugAllocator(.{}) usage + deinit handling in examples/02_check_balance.zig
std.heap.DebugAllocator(.{}) = .initmatches the standard Zig pattern for instantiatingDebugAllocator(and then callinggpa.allocator()).defer _ = gpa.deinit();correctly ensures cleanup runs, but it discards thedeinit()leak-check result—consider using something likedefer std.debug.assert(gpa.deinit() == .ok);(or otherwise handling the returnedCheck) so leaks don’t go unnoticed.- Using
DebugAllocatorfor an example is reasonable since it helps catch leaks/misuse during the example’s execution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/02_check_balance.zig` around lines 14 - 16, The defer currently
discards the leak-check result from gpa.deinit(); update the cleanup to assert
or otherwise handle the returned Check so memory leaks are noticed: call
gpa.deinit() in the defer and verify its result (e.g., via std.debug.assert on
the returned value) instead of ignoring it; target the DebugAllocator instance
named gpa and its gpa.deinit() call and preserve the existing gpa.allocator()
usage.
| /// Default `std.Io` runtime used by the library. | ||
| /// | ||
| /// Zig 0.16 moved networking, HTTP, sleeping, and clocks behind the `std.Io` | ||
| /// interface. To keep the public eth.zig API unchanged (no `Io` parameter on | ||
| /// transports), the library constructs a default blocking implementation | ||
| /// internally. All operations the library performs through this `Io` are | ||
| /// synchronous and run on the calling thread, matching the pre-0.16 behavior | ||
| /// of the blocking std.net / std.http APIs. | ||
| /// | ||
| /// Callers that need a custom event loop should drive the library from their | ||
| /// own threads; the transports remain blocking by design. | ||
| pub fn defaultIo() std.Io { | ||
| // `global_single_threaded` performs every operation synchronously on the | ||
| // calling thread and never allocates, spawns threads, or requires deinit. | ||
| return std.Io.Threaded.global_single_threaded.io(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify std.Io.Threaded.global_single_threaded usage patterns in the codebase
rg -n -C3 'std\.Io\.Threaded\.global_single_threaded' --type=zigRepository: StrobeLabs/eth.zig
Length of output: 1988
🌐 Web query:
Zig 0.16 std.Io.Threaded.global_single_threaded documentation and usage
💡 Result:
In Zig 0.16, std.Io is a new interface used to abstract I/O and blocking operations [1][2]. The standard library provides std.Io.Threaded as a thread-pool-based implementation for this interface [3][4]. Usage of init_single_threaded The identifier init_single_threaded is a static constant within std.Io.Threaded used to initialize an I/O implementation that does not support task-level concurrency or cancellation [5][2]. You can create an Io instance for single-threaded use as follows: var threaded: std.Io.Threaded =.init_single_threaded; const io = threaded.io; This approach is intended as a convenience or fallback for code that does not require concurrency [1][2]. The official documentation advises against using this as a default pattern in application design [2][6]. Instead, the application's main function should be responsible for constructing the Io instance, which should then be passed down as a parameter to the code that requires it [1][2]. Regarding global_single_threaded There is no public-facing API named global_single_threaded documented for general application use [5][7]. While some internal components (such as specific debug machinery) may reference a global instance in source code [8], it is not intended for use by library or application code. Developers should construct their own Io instances or accept them as arguments [1][2]. Recommendations If you do not need concurrency, using.init_single_threaded is technically valid but should be used sparingly [2]. If you need task-level concurrency, use.init(gpa,.{}) instead, which supports thread-pool-based operations [1][2]. Avoid using global variables for I/O; explicit passing of the Io instance is the idiomatic pattern in Zig 0.16 [1][2].
Citations:
- 1: https://ziggit.dev/t/std-io-overview/14994
- 2: https://ziglang.org/download/0.16.0/release-notes.html
- 3: std: Introduce
IoInterface ziglang/zig#25592 - 4: https://lalinsky.com/2026/05/11/async-io-in-zig-016-today.html
- 5: https://github.com/ziglang/zig/blob/738d2be9/lib/std/Io/Threaded.zig
- 6: https://web.archive.org/web/3/https%3A%2F%2Fziglang.org%2Fdownload%2F0.16.0%2Frelease-notes.html
- 7: https://github.com/ziglang/zig/blob/738d2be9/lib/std/Io.zig
- 8: https://ziggit.dev/t/building-wasm-library-with-zig-0-16-0/14957
Avoid std.Io.Threaded.global_single_threaded; use the documented single-threaded initializer instead
Zig 0.16’s std.Io.Threaded documentation describes initialization via init_single_threaded (and init(...) for concurrent cases); it does not present global_single_threaded as a documented public API. Relying on std.Io.Threaded.global_single_threaded can break across Zig versions—construct the single-threaded std.Io.Threaded via std.Io.Threaded.init_single_threaded and return its .io().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime.zig` around lines 3 - 18, The function defaultIo currently
returns std.Io.Threaded.global_single_threaded.io(), which relies on an
undocumented global; instead construct the single-threaded Io using
std.Io.Threaded.init_single_threaded and return its .io() so it uses the
documented initializer. Replace references to
std.Io.Threaded.global_single_threaded in defaultIo with a local init via
std.Io.Threaded.init_single_threaded(...) and return the resulting threaded io()
from that instance (ensure you call the documented init API on the Threaded type
and use the instance’s .io()).
There was a problem hiding this comment.
Checked against the 0.16.0 stdlib sources: global_single_threaded is a documented public API (pub const global_single_threaded: *Threaded, lib/std/Io/Threaded.zig:1704). It is preferred here over init_single_threaded because .io() requires a stable *Threaded pointer to mutable state; the global provides exactly that without allocation or deinit.
There was a problem hiding this comment.
@koko1123, you're right — I stand corrected. The direct stdlib source verification is definitive: global_single_threaded is a pub const at lib/std/Io/Threaded.zig:1704 and is part of the documented public API. The technical reasoning is also sound: since std.Io.Threaded.io() returns an std.Io that holds a *Threaded pointer back to the owning instance, a stack-local init_single_threaded would produce a dangling pointer once the caller returns, whereas the global sidesteps that entirely with no allocation or deinit required. The current implementation is correct.
✏️ Learnings added
Learnt from: koko1123
Repo: StrobeLabs/eth.zig PR: 60
File: src/runtime.zig:3-18
Timestamp: 2026-06-10T13:36:19.173Z
Learning: In Zig 0.16.0, `std.Io.Threaded.global_single_threaded` is a documented public API (`pub const global_single_threaded: *Threaded` at `lib/std/Io/Threaded.zig:1704`). It is the correct choice for obtaining a default blocking single-threaded `std.Io` in library code (e.g. `std.Io.Threaded.global_single_threaded.io()`), because `std.Io.Threaded.io()` returns an `std.Io` that holds a `*Threaded` back-pointer, so a stack-local `init_single_threaded` would produce a dangling pointer. The global provides a stable pointer without allocation or deinit and is preferred over `init_single_threaded` in this context.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| /// Wall-clock time in milliseconds since the Unix epoch. | ||
| /// | ||
| /// Replacement for `std.time.milliTimestamp()`, which was removed in | ||
| /// Zig 0.16. Deadline-style APIs in this library (for example | ||
| /// `WsTransport.readMessageDeadline`) use this time base. | ||
| pub fn milliTimestamp() i64 { | ||
| return std.Io.Clock.now(.real, defaultIo()).toMilliseconds(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for other Clock.now usage patterns in the migrated codebase
rg -n 'std\.Io\.Clock\.now' --type=zig -C2Repository: StrobeLabs/eth.zig
Length of output: 1988
🌐 Web query:
Zig 0.16 std.Io.Clock API documentation
💡 Result:
In Zig 0.16, the std.Io Clock API lives under std.Io.Clock (not std.io / std.time.Timer). The authoritative docs entry point is the 0.16.0 standard library docs, which includes std.Io and std.Io.Clock: https://ziglang.org/documentation/0.16.0/std/ [1] What you can rely on from the 0.16.0 std library source: 1) std.Io.Clock provides three built-in clock kinds (enum Clock): real, awake, and (identical to awake but with intent to include suspended time) [2]. The source comments define their semantics (POSIX/Unix epoch for real; monotonic behavior guarantees for awake; etc.) [2]. 2) Example usage from an external help thread shows calling Clock.awake.now(io), then calling untilNow(io,.awake) and converting the result to nanoseconds. This demonstrates the intended method style around std.Io.Clock [3]. Practical navigation: - Open the Zig 0.16 std library docs page [1] and search within it for “Io.Clock” or “Clock”. - If the web docs don’t show the exact method list, use the std library source for the definitive definition of std.Io.Clock in lib/std/Io.zig [2] (or an unpacked Zig 0.16 tarball copy, which also includes lib/std/Io.zig) [4]. If you want, tell me whether you’re looking for Clock.now / untilNow / sleep/timeout integration, and I’ll extract the exact signatures and related types (Duration, Timestamp, Timeout) from the 0.16.0 std.Io.Clock implementation in lib/std/Io.zig.
Citations:
- 1: https://ziglang.org/documentation/0.16.0/std/
- 2: https://github.com/ziglang/zig/blob/738d2be9/lib/std/Io.zig
- 3: https://ziggit.dev/t/0-16-0-std-time-timer/14941
- 4: https://fossies.org/linux/misc/zig-0.16.0.tar.xz/zig-0.16.0/lib/std/Io.zig?M=751
🌐 Web query:
Zig 0.16 std.Io.Clock now signature toMilliseconds return type i64
💡 Result:
In Zig 0.16, the std.Io.Clock.now signature and behavior have evolved as part of the broader move of time-related functionality into the std.Io interface [1][2][3]. The now function is a method on a Clock (e.g., std.Io.Clock.real or std.Io.Clock.awake), which requires an io instance as an argument [4][5][2]. It returns a std.Io.Timestamp struct, not a raw integer [4][2]. The std.Io.Timestamp struct includes a method named toMilliseconds(), which returns an i64 [6]. This method calculates the milliseconds by performing a truncated division of the timestamp's internal nanoseconds by std.time.ns_per_ms [6]. Example usage: const timestamp = std.Io.Clock.real.now(io); const ms: i64 = timestamp.toMilliseconds;
Citations:
- 1: https://ziggit.dev/t/0-16-0-std-time-timer/14941
- 2: https://ziggit.dev/t/what-happened-to-timestamp-and-what-actually-is-clock/14310
- 3: https://codeberg.org/ziglang/zig/pulls/31086
- 4: https://ziggit.dev/t/what-is-the-resolution-of-std-time-timer-and-is-it-good-enough/4678
- 5: https://cookbook.ziglang.cc/03-01-elapsed-time/
- 6: https://git.medv.io/zig/commit/19c9062840225cc927db105ba1454a0e189901fa.html
🌐 Web query:
Zig 0.16 lib/std/Io.zig "pub fn now" std.Io.Clock real now signature
💡 Result:
In Zig 0.16, the std.Io interface provides structured time management through the std.Io.Clock and std.Io.Timestamp types [1][2][3]. The function to retrieve the current time via an Io instance is defined as: pub fn now(clock: Clock, io: Io) Error!Io.Timestamp This function is accessed through the Clock type (e.g., std.Io.Clock.real.now(io)) or as a method on a Clock instance [1][4]. Key details: 1. Usage: You generally call it using a specific clock instance (like Clock.real or Clock.awake) provided by the std.Io namespace [4]. For example: const now_ts = try std.Io.Clock.real.now(io); 2. Clock Types: std.Io.Clock defines different timing sources. Clock.real is typically used for wall-clock time, while other variants like Clock.awake are available for monotonic or specific runtime tracking [4]. 3. Migration: In Zig 0.16, older time functions like std.time.timestamp have been replaced by this Io-based interface [4][5]. The new API emphasizes type safety and requires passing an Io instance to handle the time retrieval, reflecting the new Io interface design in 0.16 [2][3]. 4. Helper Methods: The resulting timestamp object (std.Io.Timestamp) provides methods for duration calculations, such as untilNow(io, clock), which replaces the need for separate timer objects in many use cases [6][4].
Citations:
- 1: https://github.com/ziglang/zig/blob/738d2be9/lib/std/Io.zig
- 2: std: Introduce
IoInterface ziglang/zig#25592 - 3: https://ziggit.dev/t/std-io-overview/14994
- 4: https://ziggit.dev/t/what-happened-to-timestamp-and-what-actually-is-clock/14310
- 5: https://tangled.org/zzstoatzz.io/typeahead/commit/daeb32381146a5a4437ea1b45721ac743a276bc3
- 6: https://ziggit.dev/t/0-16-0-std-time-timer/14941
🌐 Web query:
Zig 0.16 lib/std/Io.zig "toMilliseconds" std.Io.Timestamp returns i64
💡 Result:
In Zig 0.16, std.Io.Timestamp does indeed provide a toMilliseconds function that returns an i64 [1]. The implementation is defined within the std.Io.Timestamp struct in lib/std/Io.zig and uses integer division to convert the nanosecond-based timestamp into milliseconds [1]: pub fn toMilliseconds(t: Timestamp) i64 { return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_ms)); } This conversion relies on std.time.ns_per_ms (the number of nanoseconds in a millisecond) [1]. The Timestamp struct internally stores time as nanoseconds, typically in an i96 field, to maintain precision, and the toMilliseconds method performs truncation during the conversion to the i64 return type [1]. Similar conversion methods, such as toSeconds (also returning i64) and toNanoseconds (returning i96), are also available for Timestamp [1].
Citations:
Fix milliTimestamp() Zig 0.16 std.Io.Clock usage: handle Error!Timestamp
std.Io.Clock.now(.real, defaultIo()) returns Error!std.Io.Timestamp, so it must be try/catch’d before calling toMilliseconds(); .real and Timestamp.toMilliseconds() (→ i64) match the intended wall-clock milliseconds representation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/runtime.zig` around lines 20 - 27, milliTimestamp currently calls
std.Io.Clock.now(.real, defaultIo()) but ignores its Error!std.Io.Timestamp
result; change milliTimestamp to propagate the error by updating its signature
to return Error!i64 (or Error!i64 alias) and use try on std.Io.Clock.now(.real,
defaultIo()) before calling Timestamp.toMilliseconds() (i.e., const ts = try
std.Io.Clock.now(.real, defaultIo()); return ts.toMilliseconds();), so
Error!Timestamp is handled properly.
There was a problem hiding this comment.
In 0.16.0, std.Io.Clock.now(clock: Clock, io: Io) returns Io.Timestamp directly, not an error union (lib/std/Io.zig:778). The code compiles and the unit test pinning milliTimestamp passes on both 0.16.0 and 0.17.0-dev.
There was a problem hiding this comment.
@koko1123, you're right — my mistake. std.Io.Clock.now(clock, io) returns Io.Timestamp directly (no error union), so no try is needed and the current implementation is correct. Thanks for pointing to line 778 of lib/std/Io.zig and confirming it compiles and passes tests on both 0.16.0 and 0.17.0-dev.
✏️ Learnings added
Learnt from: koko1123
Repo: StrobeLabs/eth.zig PR: 60
File: src/runtime.zig:20-27
Timestamp: 2026-06-10T13:36:13.514Z
Learning: In Zig 0.16.0, `std.Io.Clock.now(clock: Clock, io: Io)` returns `Io.Timestamp` directly (not an error union `Error!Io.Timestamp`). Therefore, calling it does not require `try` or `catch`. Example correct usage: `return std.Io.Clock.now(.real, defaultIo()).toMilliseconds();` where `toMilliseconds()` returns `i64`. This is confirmed at lib/std/Io.zig line 778 and verified to compile on both Zig 0.16.0 and 0.17.0-dev.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
What
Makes Zig 0.16.0 the minimum supported version and migrates the entire transport layer to the new
std.Iointerface. Closes #55.Why
Zig 0.16's I/O overhaul moved net/http/sleep/clocks behind
std.Io:std.http.Clientrequires aniofield,std.netandstd.Thread.sleepare gone. The transports did not compile on 0.16 — invisible until now because unit tests never analyze network code paths (a user hit this in #55).How
src/runtime.zig(new,eth.runtime): public API stays unchanged — a default Io is constructed internally (std.Io.Threaded.global_single_threaded, synchronous on the calling thread, zero allocation, matching the old blocking behavior). ProvidesmilliTimestamp()/sleepMs()for the removed std APIs.std.Io.netStream/IpAddress/HostName, Io reader/writer framing — usingreadVecfor recv-like short reads (readSliceShortnow blocks until full, which hung the WS handshake; caught by integration tests) — and the TLS client rebuilt against the new init options (bundle + entropy + realtime_now).http.Clientgains.io; SSE reconnect sleeps via runtime.0.15.2→0.16.0(master kept); fmt/build jobs pinned to 0.16.0.Breaking
Minimum Zig version is now 0.16.0 (0.15.2 support dropped). This will ship as v0.5.0.
Verification (both 0.16.0 and 0.17.0-dev.813)
zig build/zig build test/zig fmt --check: passzig build integration-testagainst Anvil: 23/23 pass on each compiler (HTTP transport, WS subscriptions, LogWatcher)examples/: 15/15 build steps succeed on 0.16Note for after merge: the repo ruleset's required status check
Test / Zig 0.15.2 / ubuntu-latestmust be renamed toTest / Zig 0.16.0 / ubuntu-latest.Closes #55
🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
New Features
eth.runtimemodule exposing:defaultIo()for I/O configuration,milliTimestamp()for wall-clock millisecond timestamps, andsleepMs()for blocking sleep operations.