Skip to content

feat(examples): add hello-odin, an Odin ABI v2 guest - #158

Merged
jonasz-lasut merged 4 commits into
mainfrom
feat/example-odin
Oct 7, 2026
Merged

jonasz-lasut merged 4 commits into
mainfrom
feat/example-odin

Conversation

@jonasz-lasut

Copy link
Copy Markdown
Owner

Promotes the Odin spike (#155) to a real example: examples/hello-odin, the hello guest in Odin as an ABI v2 component on the c flavour's plumbing, with a render (odin) e2e job and the docs. Per the 2026-10-07 decision, an example only: no scaffold, guestfn build builds it through the c path (its build.zig).

Part of #155.

What it is

  • wit/, src/gen/, src/fnv1/, proto/ are the c flavour's byte for byte, and examples_share_the_scaffold_plumbing now holds them identical (hello-odin is a seventh entry in the list, under LANG_C, sharing proto/, wit/, src/fnv1/, src/gen/; its glue and build.zig are its own). The root Makefile's vendor-proto-codecs and gen-bindings regenerate the example's copies too.
  • src/fnv1.odin declares nanopb's generated structs again in Odin (request 152 B, response 144 B, Value 16 B with a #raw_union, the streams 16 and 20 B) and holds every size and offset with #asserts against what zig cc computes for wasm32; the native layout is held by a unit test that sends a request through nanopb's decode, run and encode. src/wasmfn.odin defines the bindings' exports_function_run, the typed log, http_get_text over the c glue's wasi:http@0.2 client (src/wasmfn.c, kept as it is minus the export, codec and log) and an Odin allocator over wasi-libc's heap. src/main.odin is the function.
  • build.zig: odin build src -target:freestanding_wasm32 -build-mode:obj -no-entry-point (the Odin runtime then imports nothing), the C half through zig cc, the core module linked by zig cc (a wasip1 reactor with no wasi_snapshot_preview1 imports, so no adapter), zig build test (odin test natively over the C half compiled for the host, as objects: zig's archiver writes mach-o members Apple's ld rejects) and zig build lint (odin check -vet -strict-style). Makefile: build, test, lint, gen-proto, gen-bindings, render, render-check, clean. example/ is the c example's (both xprin cases).

Measurements

  • Component 70.9 KB (72,601 B; the core module 68 KB) against hello-c's 62.5 KB and hello-zig's 61.2 KB. core:fmt is left out on purpose: its reflection tables make the module 198 KB.
  • odin build of the guest 0.2 s (a 24 KB object). Cold guestfn build (odin, eight zig cc compiles, the link, the wrap; nanopb already fetched) 6.9 s, warm 0.2 s.
  • Engine compile, release build: 10 to 11 ms (hello-c 10.5 to 12 ms, hello-zig 12 ms).

FFI and allocator notes

  • On a wasm target a foreign import x "name.o" yields plain link-time symbols (no wasm-import-module), and Odin never opens the file in an object build; a bare name or system: makes the procedures wasm imports of that module (the smoke module showed function.function_log never binding to the C definition). A foreign variable (the message descriptors) must sit in a library-less foreign {} block. Natively the same imports name the objects zig build test compiles.
  • @(require, linkage = "strong") on the export: nothing in Odin references it, and @(export) would add a second wasm export beside the world's run.
  • Odin lowers a by-value struct return in proc "c" as sret on wasm32, matching clang, so pb_istream_from_buffer is called as declared.
  • The canonical ABI frees with libc's free (cabi_realloc is realloc, cabi_post_run is free), so the context's allocator wraps malloc/realloc/free/aligned_alloc: one heap, anything Odin allocates may be handed over. A proc "c" sets its own context; the freestanding temp allocator is the nil allocator, aliased to the libc one. A panic traps without a message; no _startup_runtime runs.

The bzero workaround and its upstream fix

Odin's runtime (base/runtime/procs.odin) defines memcpy, memset, memmove and bzero strongly, with require, on every wasm target; bzero returns rawptr where libc's returns void. Zig's libc copies are weak and lose, but wasm-ld reports the bzero pair as a function signature mismatch, which zig build-exe turns into a build failure ("unexpected LLD stderr"). build.zig therefore links through a zig cc system command, which passes the warning through (exit 0; zig echoes it once on a cold build under a misleading "failed command:" label). The self-hosted zig linker is no alternative (it cannot read function_component_type.o and refuses Odin's object for bulk-memory). The upstream fix is a void return on Odin's bzero, after which build.zig can use the normal addExecutable path.

Docs checklist

  1. README.md: the "C# is an example only" sentence now names Odin too (examples/hello-odin, ~71 KB, on the c flavour's bindings and codec); the flavour table lists scaffolds only and did not change.
  2. docs/guests/other-languages.md: "Tested on ABI v2" gains Odin, the "Possible, untested" Odin bullet is gone, and a paragraph beside the C# one describes the example (toolchain, size, build and compile times, the guestfn build path, core:fmt, the bzero and silent-panic notes, native tests).
  3. docs/guests/write-a-module.md, docs/introduction.md, docs/use-cases.md: nothing needed (none lists or counts the hello examples; use-cases lists the use-case examples only).
  4. AGENTS.md: the examples tree entry, (cd examples/hello-odin && zig build test) in Testing, make lint in Linting, the Testing paragraph's toolchains and suite exemption (Odin beside C#), the plumbing sentence (hello-odin rides the c template's four directories), the proto copy count (seventeen: five templates, five goldens, seven examples), the WASI WIT and gen-bindings lists, the render command list. Key Reference Documents: nothing relevant.
  5. Root Makefile: vendor-proto-codecs runs gen-proto in hello-odin, gen-bindings runs it there too (the c template is still mirrored from hello-c; the plumbing test holds hello-odin to it); the header comments' counts and lists updated.
  6. docs/one-pager-language-support.md: one sentence in the 2026-10-07 paragraph, revision 0.5 to 0.6. docs/one-pager-use-case-examples.md: nothing needed (it names no hello example).
  7. .github/renovate.json5: no rule change. The proto and WASI WIT trackers match the example's copies by file pattern; its build.zig.zon (nanopb by URL and hash, the same pin as hello-c's) is read by no manager, so it moves by hand at release time like hello-c's and hello-zig's; the three comments listing the examples name it. The monthly examples batch (cargo, npm, gomod, nuget) has nothing of its to pick up.
  8. A grep for hello-zig as the model: every site checked (renovate comments, e2e.yml, AGENTS.md, Makefile, README, scaffold.rs, other-languages.md, the language one-pager).

Verified locally

  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test -p guestfn (the plumbing test with hello-odin in it) pass.
  • make -C examples/hello-odin lint test build render-check with the release runtime and guestfn: lint clean, 5 native tests pass, the component builds, both xprin cases pass (hi example-xr, howdy example-xr through the host's egress).
  • mdbook build clean; actionlint reports nothing new (the local one predates the ubuntu-26.04 labels, and the SC2046 is the go job's).
  • The diff against main carries no em dash.

The render (odin) job runs on the next /e2e after the merge: the comment-triggered workflow runs from main's file. Odin dev-2026-10 for linux amd64 is pinned by sha256 c3c8b095621fd0c75f7f73e3a0829f1b4d45324225f20ba11ed8dc4da310a8ab; the release binary is statically linked and its base/ and core/ collections sit beside it, so the job needs no LLVM.

The hello guest in Odin, an example only (#155): the c flavour's
plumbing byte for byte (wit/, src/gen/, src/fnv1/, proto/, which the
examples-share-the-scaffold-plumbing test now holds identical, and
which the root Makefile's vendor-proto and gen-bindings regenerate
there too), the generated nanopb structs declared again in Odin with
their wasm32 layout held by #asserts, the codec and the c glue's
wasi:http client called over Odin's foreign interface, and the
bindings' exports_function_run defined in Odin. odin builds a
freestanding wasm32 object, build.zig compiles the C half and links
the core module through zig cc (not build-exe: Odin's runtime defines
bzero with a return value, and wasm-ld's signature-mismatch warning
fails build-exe), and guestfn build takes the project for a c one and
wraps it with no adapter: a 71 KB component, compiled by the runtime
in about 10 ms. Unit tests run natively with odin test over the C half
compiled for the host; the xprin suite is the c example's.
A render (odin) job modelled on render (c): Odin from its GitHub
release for linux amd64, pinned by version and sha256 (the binary is
statically linked, its collections sit beside it), Zig as the c job
installs it, the c job's codec and bindings drift checks over the
example's copies, lint and the native tests, then make render-check
with the prebuilt runtime, guestfn and xprin. The report job counts it.
Odin moves from the possible-untested list to the tested set on the
other-languages page, with a paragraph on the example beside the C#
one; the README names it as an example only; AGENTS.md gains the
examples tree entry, the per-example test, lint and render commands,
and the seventeen-copy count of the vendored proto; the language
one-pager's 2026-10-07 paragraph records it (revision 0.6); the
Renovate comments listing the examples name it.
@jonasz-lasut
jonasz-lasut merged commit 3c07470 into main Oct 7, 2026
5 checks passed
@jonasz-lasut
jonasz-lasut deleted the feat/example-odin branch October 7, 2026 14:18
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.

1 participant