Skip to content

Migrate to Zig 0.16: std.Io transports, minimum version 0.16.0 (closes #55)#60

Merged
koko1123 merged 1 commit into
mainfrom
feat/zig-0.16
Jun 10, 2026
Merged

Migrate to Zig 0.16: std.Io transports, minimum version 0.16.0 (closes #55)#60
koko1123 merged 1 commit into
mainfrom
feat/zig-0.16

Conversation

@koko1123

@koko1123 koko1123 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

What

Makes Zig 0.16.0 the minimum supported version and migrates the entire transport layer to the new std.Io interface. Closes #55.

Why

Zig 0.16's I/O overhaul moved net/http/sleep/clocks behind std.Io: std.http.Client requires an io field, std.net and std.Thread.sleep are 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). Provides milliTimestamp() / sleepMs() for the removed std APIs.
  • ws_transport (largest): std.Io.net Stream/IpAddress/HostName, Io reader/writer framing — using readVec for recv-like short reads (readSliceShort now 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_transport / flashbots / sse_transport: http.Client gains .io; SSE reconnect sleeps via runtime.
  • ws_client / retry_provider / wallet: clocks, backoff sleeps, and RNG via the Io interface.
  • mnemonic.generate: entropy via Io random (latent 0.16 break, now pinned by a new test).
  • bench / examples: timers, args iterators, stdout writers migrated; three pre-existing never-compiled example bugs fixed.
  • CI: test matrix 0.15.20.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: pass
  • zig build integration-test against Anvil: 23/23 pass on each compiler (HTTP transport, WS subscriptions, LogWatcher)
  • examples/: 15/15 build steps succeed on 0.16

Note for after merge: the repo ruleset's required status check Test / Zig 0.15.2 / ubuntu-latest must be renamed to Test / Zig 0.16.0 / ubuntu-latest.

Closes #55

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores

    • Updated minimum Zig version requirement to 0.16.0 in CI workflows, build configuration, and documentation.
  • New Features

    • Introduced eth.runtime module exposing: defaultIo() for I/O configuration, milliTimestamp() for wall-clock millisecond timestamps, and sleepMs() for blocking sleep operations.

…#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).
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eth-zig Ready Ready Preview, Comment Jun 10, 2026 12:23pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR upgrades eth.zig to support Zig 0.16 by introducing a new runtime module that provides blocking I/O and timing abstractions replacing removed standard library APIs. The library's public APIs remain unchanged; the refactoring is internal only. Zig version requirements are bumped to 0.16.0 across CI, manifests, and documentation.

Changes

Zig 0.16 Compatibility Migration

Layer / File(s) Summary
Runtime abstraction module and re-export
src/runtime.zig, src/root.zig
New runtime.zig module provides defaultIo() for std.Io context, milliTimestamp() for wall-clock milliseconds, and sleepMs() for blocking sleep. Tests verify timestamp plausibility and immediate return on zero-sleep. Root module re-exports runtime for public use.
Version requirement bumps across CI, manifests, and documentation
.github/workflows/ci.yml, CHANGELOG.md, CONTRIBUTING.md, README.md, build.zig.zon, examples/build.zig.zon
CI jobs, manifests, and developer documentation updated from Zig 0.15.2 to 0.16.0 throughout, declaring the breaking change in changelog.
HTTP, SSE, Flashbots transports and integration test
src/http_transport.zig, src/sse_transport.zig, src/flashbots.zig, tests/integration_tests.zig
HTTP, SSE, and Flashbots transports now initialize std.http.Client with runtime.defaultIo() for I/O configuration. Integration test Anvil check migrates from POSIX socket calls to std.Io.net.IpAddress.parse(...).connect(...) and explicit stream close.
Internal timing and randomness updates for wallet, retry, mnemonic, and WebSocket client
src/wallet.zig, src/retry_provider.zig, src/mnemonic.zig, src/ws_client.zig
Wallet receipt polling uses runtime.sleepMs(1_000). Retry provider seeds PRNG via runtime.defaultIo().random and sleeps via runtime.sleepMs(). Mnemonic entropy generation uses runtime.defaultIo().random(). WebSocket client keepalive timing and nonce generation migrate to runtime.milliTimestamp() and runtime.defaultIo().random().
WebSocket transport I/O and type structure overhaul
src/ws_transport.zig
WsTransport struct gains io: std.Io field; stream changes from std.net.Stream to std.Io.net.Stream. TlsState reader/writer types and buffer sizes updated for std.Io interfaces. Connection uses std.Io.net APIs. Plain read/write refactored to create transient reader/writer objects via stream interfaces. Deadline handling updates to use runtime.milliTimestamp().
Example files stdout writer and allocator updates
examples/01_derive_address.zig, examples/02_check_balance.zig, examples/03_sign_message.zig, examples/04_send_transaction.zig, examples/05_read_erc20.zig, examples/06_hd_wallet.zig, examples/07_selectors.zig, examples/build.zig
All example files update stdout to use std.Io.File.stdout().writerStreaming(eth.runtime.defaultIo(), ...), switch allocators to DebugAllocator, and add error handling for parseEther(). Example build file references renamed selector example.
Benchmark custom Timer and I/O updates
bench/bench.zig, bench/keccak_bench_cli.zig, bench/keccak_compare.zig, bench/u256_bench.zig
Benchmark files implement local Timer structs using std.Io.Clock.now() to replace removed std.time.Timer. Stdout writers update to std.Io.File.stdout().writerStreaming(...). Allocators switch to DebugAllocator. CLI benchmark main signature accepts std.process.Init.Minimal.
Keccak comptime quota adjustment
src/keccak.zig
Comptime branch-quota increased from 10000 to 100000 in keccak hash() to avoid quota overflows during short input comptime evaluation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • StrobeLabs/eth.zig#48: SSE transport implementation shares file modifications with this PR's migration of SSE client initialization and reconnect backoff to the new runtime module.
  • StrobeLabs/eth.zig#45: Retry provider refactoring overlaps with this PR's migration of retry backoff PRNG seeding and sleep timing to the runtime abstraction.
  • StrobeLabs/eth.zig#51: WebSocket transport deadline and keepalive enhancements are extended by this PR's refactoring of timing references to use runtime.milliTimestamp() throughout.

Poem

🐰 A rabbit hops through standards old,
From 0.15 to 0.16 bold,
With std.Io and runtime shines,
New interfaces cross all lines,
No breaking changes seen outside—
Just graceful evolution, with pride! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: migrating to Zig 0.16 with std.Io transports and setting minimum version to 0.16.0. It is concise and directly summarizes the primary focus of the changeset.
Linked Issues check ✅ Passed The PR fully addresses issue #55's request for Zig 0.16 compatibility. The implementation includes updating minimum version to 0.16.0, migrating transports to std.Io, and adding runtime shims (eth.runtime) to maintain public APIs.
Out of Scope Changes check ✅ Passed All changes are directly aligned with Zig 0.16 migration requirements. Updates to benchmarks, examples, allocators, and compilation reflect necessary adaptations to Zig 0.16 APIs (std.Io, removal of std.time.Timer, etc.). No extraneous functionality was introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/zig-0.16

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

Fix Zig 0.16 TLS Client.Options fields in initTls()

  • .realtime_now doesn’t match Zig 0.16’s std.crypto.tls.Client.Options; supply the correct .realtime_now_seconds value as an i64 of seconds (convert std.Io.Clock.now(.real, io) from ns to seconds).
  • Populate entropy using std.Io.randomSecure (or the required secure-entropy API for Zig 0.16) rather than io.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

📥 Commits

Reviewing files that changed from the base of the PR and between 40d731b and 8666268.

📒 Files selected for processing (30)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • CONTRIBUTING.md
  • README.md
  • bench/bench.zig
  • bench/keccak_bench_cli.zig
  • bench/keccak_compare.zig
  • bench/u256_bench.zig
  • build.zig.zon
  • examples/01_derive_address.zig
  • examples/02_check_balance.zig
  • examples/03_sign_message.zig
  • examples/04_send_transaction.zig
  • examples/05_read_erc20.zig
  • examples/06_hd_wallet.zig
  • examples/07_selectors.zig
  • examples/build.zig
  • examples/build.zig.zon
  • src/flashbots.zig
  • src/http_transport.zig
  • src/keccak.zig
  • src/mnemonic.zig
  • src/retry_provider.zig
  • src/root.zig
  • src/runtime.zig
  • src/sse_transport.zig
  • src/wallet.zig
  • src/ws_client.zig
  • src/ws_transport.zig
  • tests/integration_tests.zig

Comment on lines +14 to 16
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -C2

Repository: 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 -S

Repository: 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:


Review: std.heap.DebugAllocator(.{}) usage + deinit handling in examples/02_check_balance.zig

  • std.heap.DebugAllocator(.{}) = .init matches the standard Zig pattern for instantiating DebugAllocator (and then calling gpa.allocator()).
  • defer _ = gpa.deinit(); correctly ensures cleanup runs, but it discards the deinit() leak-check result—consider using something like defer std.debug.assert(gpa.deinit() == .ok); (or otherwise handling the returned Check) so leaks don’t go unnoticed.
  • Using DebugAllocator for 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.

Comment thread src/runtime.zig
Comment on lines +3 to +18
/// 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();
}

@coderabbitai coderabbitai Bot Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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=zig

Repository: 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:


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()).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment thread src/runtime.zig
Comment on lines +20 to +27
/// 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();
}

@coderabbitai coderabbitai Bot Jun 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -C2

Repository: 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:


🌐 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:


🌐 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:


🌐 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@koko1123
koko1123 merged commit f543217 into main Jun 10, 2026
14 checks passed
@koko1123
koko1123 deleted the feat/zig-0.16 branch June 10, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zig 0.16

1 participant