Skip to content

feat(components): WASI 0.2 host bindings + Roslyn source generator#360

Draft
kitsunoff wants to merge 26 commits intobytecodealliance:mainfrom
kitsunoff:component-model
Draft

feat(components): WASI 0.2 host bindings + Roslyn source generator#360
kitsunoff wants to merge 26 commits intobytecodealliance:mainfrom
kitsunoff:component-model

Conversation

@kitsunoff
Copy link
Copy Markdown

Summary

Implements WASI 0.2 Component Model host-side support for wasmtime-dotnet, addressing #324.

Two layers:

  1. Runtime API (Wasmtime.Components) — SafeHandle-backed wrappers around wasmtime_component_* C API: Component, ComponentLinker, ComponentLinkerInstance, ComponentInstance, ComponentFunction, ComponentValue (discriminated union for every WASI 0.2 value kind: bool / integers / floats / char / string / list / record / tuple / variant / enum / option / result / flags). Host-defined imports via ComponentLinkerInstance.DefineFunc with idiomatic (ReadOnlySpan<ComponentValue>, Span<ComponentValue>) callback.

  2. Roslyn source generator (Wasmtime.Component.SourceGenerators) — turns WIT files into idiomatic C# bindings. Consumes JSON IR from wasm-tools component wit X.wit --json instead of a hand-rolled WIT parser. Emits strongly-typed records / enums / flags / variants nested under the user's [ComponentBindings] partial class, per-export call wrappers, and an IImports interface plus static RegisterImports(linker, impl) for world imports.

End-to-end fixture (tests/Components/fixtures-src/) is a .NET component built by `componentize-dotnet` (NativeAOT-LLVM) — proves bidirectional .NET host ↔ .NET component for every composite kind, including a host-defined `host-double` import the component calls back into.

Documentation in `docs/component-model.md` plus a new README section. Follow-up items tracked in `docs/component-model-followups.md`.

Test plan

  • Existing core wasm tests still pass on netstandard2.0 / netstandard2.1 / net8.0 / net9.0
  • `ComponentValue` layout verified at runtime (`Marshal.SizeOf == 32`); same for `wasmtime_component_func_t` (24 bytes — Rust uses an anonymous nested struct with trailing padding, mismatch caused entire-class of wrong-function-returned bugs during development)
  • End-to-end tests load and call cargo-component / componentize-dotnet built `.wasm` components covering every composite kind
  • Host-defined imports invoked from inside the component, plus host-throwing-exception → wasmtime trap propagation
  • Generator unit tests cover JSON IR parsing, type emission for record/enum/flags/variant, primitive + composite call wrappers, import registration

Known limitations (tracked in `docs/component-model-followups.md`)

  • WIT `resource` types not supported. wasmtime C API gained the resource surface only in v42; this branch keeps v35. Standard WASI 0.2 interfaces that use resources internally still work because wasmtime's native impl handles those tables — limitation is custom WIT resources that cross the managed boundary
  • Async types (`stream`, `future`, `error-context`) — WASI 0.3, out of scope
  • Memory ownership — `ComponentValue.ownsHeap` byte squats Rust enum padding; safe in practice today but technically UB. Refactor to explicit per-call ownership tracking is in follow-ups
  • `post_return` ordering — currently invoked inside `Call` before the caller reads results; relies on wasmtime's eager-clone implementation detail
  • Generator edge cases — `option<tuple<...>>` and named type aliases (`type x = list`) need additional emit logic

Building the wasm fixture

NativeAOT-LLVM has no macOS prebuilts, so the fixture builds inside an arm64 Linux container:

```bash
./tests/Components/regenerate.sh
```

Pre-built `.wasm` and `.wit.json` artifacts are committed so consumers don't need the Linux toolchain.

Credits

Builds on the sketch in #346 by @martindevans — same `Wasmtime.Components` namespace and SafeHandle pattern, with the rest of the C API surface, marshalling, source generator, and end-to-end tests added.

kitsunoff and others added 23 commits May 3, 2026 00:36
Adds Wasmtime.Components namespace with Component (compile/load/serialize/
deserialize) and ComponentExport (cached export index) wrappers around the
wasmtime_component_* C API surface available in wasmtime v34+.

This is the first step toward WASI 0.2 host-side Component Model support
(issue bytecodealliance#324). Subsequent commits will add ComponentLinker, ComponentInstance,
ComponentFunction, ComponentValue, resources, and a WIT source generator.

Based on the sketch in PR bytecodealliance#346 by Martin Evans, with corrections:
- ObjectDisposedException now reports the correct type
- file paths marshalled as LPUTF8Str for non-ASCII paths
- block-scoped namespace matching the rest of the codebase

Co-Authored-By: Martin Evans <martindevans@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Introduces:

- ComponentLinker — wraps wasmtime_component_linker_t with constructor,
  Root() to obtain the root linker instance, AddWasiPreview2() to enable
  WASI 0.2 imports, and Instantiate() returning a ComponentInstance.
- ComponentLinkerInstance — wraps wasmtime_component_linker_instance_t,
  exposes Instance() to define nested instances and AddModule() to
  provide a core wasm module as an import.
- ComponentInstance — managed wrapper holding the value-typed
  wasmtime_component_instance_t (16 bytes: store_id + private index).
  Lifetime tied to Store, no explicit dispose.

Function definition (DefineFunc) and call paths (GetFunction, Call) are
deferred to subsequent commits since they require ComponentValue
marshalling.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…nction lookup

Adds the call/return path for component functions taking and returning primitive values:

- ComponentValue — blittable struct mirroring wasmtime_component_val_t (32 bytes:
  1 byte kind + 7 bytes padding + 24 byte union). Factory helpers and accessors
  are provided for bool, s8/u8/s16/u16/s32/u32/s64/u64, f32/f64, and char. The
  underlying union is sized for the largest case (variant, 24 bytes); composite
  cases (string, list, record, tuple, variant, enum, option, result, flags) are
  reserved by size and will gain typed accessors in the marshalling phase.
- ComponentValueKind — public enum mirroring WASMTIME_COMPONENT_* discriminants.
- ComponentFunction — wraps the value-typed wasmtime_component_func_t. Call
  invokes wasmtime_component_func_call followed by wasmtime_component_func_post_return
  automatically; PostReturn is exposed for callers that drive the lifecycle manually.
- ComponentInstance — adds GetExport, GetFunction(name), and GetFunction(export)
  using wasmtime_component_instance_get_export_index and
  wasmtime_component_instance_get_func.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…ker P/Invoke

ComponentValueTests verify the blittable layout (SizeOf == 32) and that the
union round-trips primitive values correctly across all FieldOffset(0) slots.

ComponentTests load the wasmtime native library and exercise the real P/Invoke
surface: rejecting non-component bytes via wasmtime_component_new, creating and
disposing a ComponentLinker, and adding the WASI 0.2 imports.

These cover the runtime correctness of the marshalling layout we committed in
the prior change and confirm the native library is reachable on the host
configuration.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Adds the first true end-to-end test that exercises the entire managed surface
against a real wasm component:

- tests/Components/add.wat — minimal component model WAT exporting
  add: func(a: u32, b: u32) -> u32 backed by a one-instruction core module.
- tests/Components/add.wasm — pre-compiled binary (144 bytes), built locally
  with `wasm-tools parse`. Embedded as a resource in the test assembly.
- tests/ComponentEndToEndTests.cs — exercises the full stack: Component.FromBytes,
  ComponentLinker.Instantiate, ComponentInstance.GetFunction, ComponentFunction.Call,
  Component.GetExport, Component.Serialize/Deserialize round-trip.

The end-to-end Add test loads add.wasm, instantiates with an empty linker,
looks up the function, invokes it with (40, 2), and asserts the result equals 42.
This validates that wasmtime_component_func_call writes the result into the
caller-allocated output buffer correctly and that the post-return cleanup
runs without error.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…n layout

Extends ComponentValue with the string variant and lays out the full
wasmtime_component_val_t union for the remaining composite kinds (list,
record, tuple, variant, enum, option, result, flags) as a foundation for
the rest of phase 2.

Additions:

- WasmName, ComponentValVec, ComponentValVariant, ComponentValResult
  internal structs mirroring the corresponding C types in val.h.
- WasmtimeComponentValUnion now exposes [FieldOffset(0)] members for every
  composite case; the size remains 24 bytes (largest case = variant).
- ComponentValue gains an `ownsHeap` byte that tracks whether the value
  was allocated by the managed factories so that Free knows when to
  deallocate. Layout total stays at 32 bytes.
- FromString(string) UTF-8 encodes via Encoding.UTF8 and stores the
  buffer through Marshal.AllocHGlobal; AsString() decodes from the
  WasmName payload (whether owned by the host or by wasmtime); Free()
  releases the host-owned buffer and is idempotent + a no-op for primitives.

Tests:

- ComponentValueTests: ASCII, empty, full Unicode (RU + emoji + JP)
  round-trips, idempotent Free, primitive Free no-op, null guard.
- tests/Components/hello-string.{wat,wasm}: minimal component exporting
  `hello: func() -> string` driven by a hand-written core module with
  a cabi_realloc and an inline canonical ABI lift.
- ComponentEndToEndTests.HelloComponent_ReturnsString invokes the
  function and reads the returned string via AsString, validating that
  WasmName layout matches what wasmtime writes into the return area.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Both kinds reuse the WasmName layout introduced for strings:

- Enum is a single name describing the selected case
  (`wasm_name_t enumeration` in the C union).
- Flags is an array of WasmNames listing the bits that are set
  (`wasmtime_component_valflags_t` = vec of `wasm_name_t`).

Adds factories FromEnum(string) and FromFlags(IReadOnlyList<string>),
accessors AsEnum() and AsFlags(), and extends Free() to release the
underlying name buffers. Internal AllocateName/FreeName/AllocateNameArray/
FreeNameArray helpers centralise the heap arithmetic so future kinds can
reuse them.

FromFlags rejects null elements and rolls back partially-allocated entries
to avoid leaks.

Tests cover round-trip, empty flags, null guards, and the partial-rollback
path.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
List and Tuple share a `{ size_t size; wasmtime_component_val_t* data }`
layout in the C union. Both are surfaced through symmetric APIs.

- FromList(IReadOnlyList<ComponentValue>) and FromTuple(...) take ownership
  of the elements: callers must not Free them individually.
- AsList()/AsTuple() return shallow copies of the underlying value array.
- Free recursively frees each element before releasing the array buffer.

Internal helpers AllocateValueArray, DecodeValueArray, and FreeValueArray
centralise the logic so the same routines are reused for the eventual
record-entry array in the next commit.

Tests cover lists of primitives, lists of strings (verifies recursive
Free), empty lists, mixed-type tuples, and the null-argument guard.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Adds the Record case for ComponentValue, mirroring
`wasmtime_component_valrecord_t` (vec of `wasmtime_component_valrecord_entry_t`).

- ComponentValRecordEntry — internal layout for `{ wasm_name_t name;
  wasmtime_component_val_t val }`. Sequential layout means the entry is
  16 + 32 = 48 bytes, matching the C struct exactly.
- RecordField — public read-only record struct exposing the (name, value)
  pair to user code.
- FromRecord(IReadOnlyList<RecordField>) and AsRecord() factories.
- Free recursively frees each entry's name buffer and value, then the
  array itself; matches the cleanup pattern established for List.

FromRecord rejects null field names and rolls back partially-allocated
entries to avoid leaks.

Tests cover round-trip with mixed value kinds (string + u32), empty
record, null guard, and the rollback path.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
All three share the pattern of an optional payload pointer to a nested
ComponentValue, so they're added together along with shared
AllocateValuePtr/DecodeValuePtr/FreeValuePtr helpers.

- FromVariant(string, ComponentValue?) takes a discriminant case name and
  optional payload; AsVariantDiscriminant/AsVariantPayload read them back.
- FromOption(ComponentValue?) maps null to `none` and a value to `some`;
  HasOption/AsOption inspect the alternative.
- FromOk(ComponentValue?) and FromErr(ComponentValue?) construct the two
  cases of `result<T,E>`; IsOk/AsResultValue read them back.

All factories take ownership of the supplied payload — a single recursive
Free on the parent releases the heap-allocated nested value plus, for
Variant, the discriminant name buffer.

Tests cover with-payload and without-payload paths for each kind,
discriminant null guards, and Option none/some.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
The C header declares wasmtime_component_func_t as

    struct {
      struct {
        uint64_t store_id;
        uint32_t __private1;
      };  // anonymous nested struct, padded to 16 bytes for 8-byte alignment
      uint32_t __private2;
    };  // outer trailing padding -> 24 bytes total

The Rust side asserts this size in
crates/wasmtime/src/runtime/component/func.rs via

    #[repr(C)] struct T(u64, u32);
    #[repr(C)] struct C(T, u32);
    assert!(size_of::<C>() == size_of::<Func>());

C# `[StructLayout(LayoutKind.Sequential)]` does not insert the inner
anonymous-struct's trailing padding, so the previous declaration came out
to 16 bytes. wasmtime would then write 24 bytes through the pointer,
corrupting the eight bytes immediately past the struct. Symptoms ranged
from working-by-luck (add, origin, range, greet, find, pair) to wasmtime
returning the wrong function (top-priority returned a record-typed value
because the corrupted upper half of the Func referenced a different
ExportIndex; safe-divide reported "expected 0 args" for the same reason).

Switching to LayoutKind.Explicit with Size = 24 and explicit field
offsets matches the Rust layout exactly and resolves the previously
flaky composite e2e cases.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…omposite

Adds an end-to-end fixture covering every composite kind ComponentValue
supports: record, list, enum, flags, variant (with payload), result (ok
and err), option (some and none), tuple. Hand-written component WAT for
composite return types proved impractical because the lifted function
signatures require explicit `(import "T" (type (eq ...)))` declarations
that don't compose well outside of full WIT tooling, so the fixture is
authored as WIT and compiled by cargo-component instead.

Layout:
- tests/Components/fixtures.wit — reference WIT mirrored into the crate.
- tests/Components/fixtures-src/ — minimal cargo-component crate
  (Cargo.toml + src/lib.rs + wit/world.wit + .gitignore). Building it
  requires `cargo component build --release` from a Rust toolchain with
  the `wasm32-wasip1` target; the crate is tiny (~50 LOC) and depends
  only on `wit-bindgen-rt`.
- tests/Components/fixtures.wasm — pre-built artifact (47 KB) embedded
  as a resource so the test suite does not require a Rust toolchain at
  runtime. To rebuild, run cargo component build in fixtures-src/ and
  copy the produced .wasm.
- tests/ComponentCompositesTests.cs — 9 e2e tests, each instantiates the
  component, looks up an export, calls it, and validates the result.
- src/Components/IsExternalInit.cs — polyfill needed for `record struct`
  when targeting netstandard2.0 / netstandard2.1.

Tests:
- Origin returning record (point { x, y }).
- Range returning list<u32>.
- TopPriority returning enum case "high".
- Defaults returning flags { read, write }.
- Greet returning variant case "formal" with string payload.
- SafeDivide returning result<u32, string> for both ok and err paths.
- Find returning option<string> for both some and none paths.
- Pair returning tuple<u32, string>.

Total test count: 54 passing (unit-level layout tests + the new
end-to-end fixture coverage).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…fineFunc

Adds the bidirectional path: components can now call back into managed
code through host-defined functions registered on the linker.

Surface:

- ComponentFuncCallback delegate: idiomatic
  `(ReadOnlySpan<ComponentValue> args, Span<ComponentValue> results)`
  signature exposed to user code.
- ComponentLinkerInstance.DefineFunc(name, callback): wraps
  wasmtime_component_linker_instance_add_func with a static native
  trampoline. The user delegate is rooted via a GCHandle stored as the
  callback's `data` pointer; a paired finalizer frees the handle when
  wasmtime drops the entry (linker disposal). Exceptions thrown inside
  the callback are converted to a wasmtime_error_t via
  wasmtime_error_new and surface to the host as WasmtimeException.

Test fixture: tests/Components/host-add.wat — a tiny component that
imports `host-add: func(a: u32, b: u32) -> u32` and re-exports it as
`compute`, providing an end-to-end harness for lifting (host args ->
guest) and lowering (guest result -> host).

Tests cover both:

- HostAdd_IsInvokedAndResultLifted — verifies the host function was
  called exactly once, arguments lowered correctly, and the lifted
  result equals the host's computed value.
- HostAdd_ExceptionPropagatesAsTrap — verifies that throwing inside
  the callback is converted to a WasmtimeException whose message
  contains the original exception text.

Total component-test count: 56 passing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…gs] attribute

Stands up the source-generator pipeline that subsequent commits will use to
turn WIT files into idiomatic C# bindings.

Pieces:

- src/Components/ComponentBindingsAttribute.cs — public attribute users put
  on a partial class: `[ComponentBindings("path/to/world.wit", world: "...")]`.
  Lives in the main Wasmtime assembly so consumers don't need a separate
  package reference for the attribute.
- src/Wasmtime.Component.SourceGenerators/ — new Roslyn analyzer project
  (netstandard2.0) implementing IIncrementalGenerator. Pipelines:
  ForAttributeWithMetadataName(ComponentBindingsAttribute) combined with
  AdditionalTextsProvider filtered to `*.wit`. Emits a placeholder partial
  class containing `WitPath`/`WitWorld`/`WitSourceLength` constants so the
  generator's wiring is visible in tests; real type/function emission lands
  in the next commits.
- src/Wasmtime.Component.SourceGenerators/Diagnostics/Descriptors.cs —
  WIT010/WIT018/WIT019/WIT020 placeholders for the diagnostic-id space the
  full generator will use.
- src/Wasmtime.Component.SourceGenerators/IsExternalInit.cs — polyfill so
  records compile under the netstandard2.0 target the generator needs.
- src/Wasmtime.csproj — excludes the generator subfolder from the main
  assembly build so the two projects don't collide on file globbing.
- tests/Wasmtime.Tests.csproj — references the generator with
  OutputItemType=Analyzer; includes Components/fixtures.wit as
  AdditionalFiles for the smoke test.
- tests/ComponentBindingsGeneratorTests.cs — verifies the generator
  produces visible partial-class members for a `[ComponentBindings]`
  declaration in user code.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Replaces the planned hand-rolled WIT parser with a much smaller layer that
consumes the JSON IR `wasm-tools component wit X.wit --json` produces.
This re-uses Rust's battle-tested WIT front-end (wasm-tools / wit-parser)
without dragging a Rust toolchain into csc — the generator only sees a
pre-built `*.wit.json` artifact.

Pipeline:

- A `*.wit.json` file lives next to its `*.wit` source. For now it is
  generated manually via `nix shell nixpkgs#wasm-tools --command wasm-tools
  component wit foo.wit --json > foo.wit.json` and committed alongside
  the WIT (see tests/Components/fixtures.wit.json). A follow-up will wire
  up an MSBuild target so this happens automatically at build time.
- The generator picks up the JSON file via <AdditionalFiles>, parses it
  with System.Text.Json, and uses the resulting WitModel to drive code
  emission.

Additions:

- src/Wasmtime.Component.SourceGenerators/Wit/WitModel.cs — typed IR
  records covering worlds, world items (function vs typeref), type defs
  (record / enum / flags / variant / list / option / result / tuple /
  alias / unknown), and type references (indexed or primitive).
- src/Wasmtime.Component.SourceGenerators/Wit/WitJsonReader.cs —
  JsonDocument-based parser; ~250 LOC vs the ~700 LOC a hand-rolled WIT
  lexer + parser would require.
- WitBindingsGenerator now reads the JSON sibling, locates the requested
  world, and surfaces the parsed shape as constants (WitTypeCount,
  WitImportCount, WitExportCount) and arrays (WitExportNames,
  WitTypeNames). These are the seam the next commits will replace with
  real type/function emission.

The generator project now references System.Text.Json explicitly and
embeds it into the analyzer DLL so Roslyn loads it without a runtime
NuGet round-trip.

Tests: 60 passing (3 new generator-output assertions).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…variant)

Wires the JSON IR through a pair of small emitters that produce idiomatic
C# nested types under the user's bindings class. All generated names are
PascalCased from the WIT kebab-case and clash with C# keywords are
prefixed with `@`.

- src/Wasmtime.Component.SourceGenerators/Emit/EmitContext.cs — resolves
  WIT type references to C# names (primitive map, indexed lookup,
  anonymous list/option/result/tuple → IReadOnlyList<T>/T?/Result<T,E>
  /(T1, T2, ...)) and provides the kebab→Pascal/camel converter plus
  the C# keyword guard.
- src/Wasmtime.Component.SourceGenerators/Emit/TypeEmitter.cs — emits:
    record  -> public sealed record class Foo(int X, string Name);
    enum    -> public enum Foo : byte { Low = 0, Medium = 1, ... }
    flags   -> [Flags] public enum Foo : byte { None = 0, Read = 1, ... }
    variant -> public abstract record Foo {
                   public sealed record Bar(T Value) : Foo;
                   public sealed record Baz() : Foo;
               }
- src/Components/Result.cs — public Result<T, E> + Unit types in the
  Wasmtime.Components namespace, referenced by the emitter when WIT
  declares result<T, E> with empty arms (mirrors WIT's `_`).
- WitBindingsGenerator now invokes TypeEmitter after writing the
  metadata constants, so the same output file contains both.

Tests assert the emitted shapes are usable from consumer code:
construct Point(3, 4), pattern match Greeting.Formal("Sir"), |-or
Permissions.Read | Permissions.Write, etc.

Total component-test count: 64 passing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Generates a constructor + method per WIT export whose params and result
are all primitives (bool / sN / uN / fN / char / string), giving users a
fully-typed entry point that delegates to ComponentInstance.GetFunction
and ComponentValue marshalling under the hood.

Surface:

- src/Wasmtime.Component.SourceGenerators/Emit/FunctionEmitter.cs —
  emits `Bindings(ComponentInstance instance)` plus, for each primitive
  export, a method like `public uint Square(uint n) { ... }` that
  builds a ComponentValue[] for the args, calls the function, and
  lifts the result back to the C# primitive. Composite-signature
  exports get a `// TODO` placeholder so users can see what the
  follow-up will cover.
- WitBindingsGenerator now invokes FunctionEmitter after TypeEmitter,
  so the generated partial class contains both type definitions and
  method bindings for the world's exports.

Fixture additions to exercise the new code path:

- fixtures.wit / fixtures-src/wit/world.wit — `export square: func(n: u32)
  -> u32;`.
- fixtures-src/src/lib.rs — Rust impl returning n * n.
- fixtures.wasm + fixtures.wit.json regenerated via cargo-component +
  wasm-tools.

Tests:

- Generator_EmitsPrimitiveExportMethod_EndToEnd — instantiates the
  fixture component, constructs `new FixtureBindings(instance)`, and
  asserts `bindings.Square(7) == 49u` and `Square(0) == 0u`. This is
  the first commit where the entire pipeline (WIT -> JSON -> C# ->
  ComponentValue marshalling -> wasmtime) is exercised end-to-end
  through generated code.
- WitExportCount / WitExportNames updated to include the new export.

Total component-test count: 65 passing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Closes the TODO placeholders in generated bindings: every WIT export
now emits a typed C# call wrapper, including those whose params or
result are composite (record / enum / flags / variant / list / option /
result / tuple).

Approach:

- Static per-named-type helpers (`LowerPoint` / `LiftPoint`,
  `LowerPriority` / `LiftPriority`, ...) live on the bindings class and
  are reused by every method that touches the type. They handle the
  field-by-field marshalling for records, the case-name <-> C# enum
  case mapping for enum / flags, and the discriminant-driven
  construction for variants.
- Anonymous structural types (list, option, result, tuple) compile to
  inline expressions via System.Linq.Enumerable.Select for lists,
  conditional FromOk / FromErr for results, etc. They don't get their
  own helpers since the element type changes per-occurrence.
- Each generated method wraps the call in try/finally so allocated args
  and any composite results from wasmtime get Free()-ed.

Tests cover all nine exports end-to-end through generated bindings:

- Square (primitive), Origin (record), TopPriority (enum), Defaults
  (flags), Greet (variant w/ payload), Range (list), Find (option /
  some & none), SafeDivide (result / ok & err), Pair (tuple).

Total component-test count: 73 passing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Adds an export that takes a record and returns a transformed record so
the test exercises lower (host -> component) and lift (component ->
host) on the same type in the same call. The host:

    var moved = bindings.Translate(new Point(1, 2), 10, 20);
    // moved == new Point(11, 22)

This validates that the generated LowerPoint helper produces a
ComponentValue wasmtime accepts as a record argument, and that the
LiftPoint helper reconstructs an equivalent C# record from the result
the component returns. Same code path that the existing per-type tests
cover, but in a single round trip rather than just one direction.

WIT / Rust impl / fixtures.wasm / fixtures.wit.json updated.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…e-dotnet

Replaces the Rust + cargo-component fixture with a literal .NET -> wasm
component built by BytecodeAlliance.Componentize.DotNet.Wasm.SDK
(NativeAOT-LLVM under the hood). The component now exercises the full
host <-> component round trip with both sides written in C#:

- The host (this repo) instantiates the component, registers a host-
  defined `host-double` import via the generated `IImports` interface,
  and calls every export through the source-generated bindings.
- The component (tests/Components/fixtures-src/) is a small .NET
  library whose `FixtureWorldImpl` static class implements every world
  export in plain C#. Its `UseHost` export calls `HostDouble` (the
  generated wasm import shim), which lands back in the .NET host —
  proving lift/lower works end-to-end on both sides of the boundary.

Toolchain caveat: NativeAOT-LLVM has no macOS prebuilts (osx-x64 stops
at 9.0-alpha; osx-arm64 was never published for 10.0). The fixture is
therefore built inside an arm64 Linux container so the project picks up
`runtime.linux-arm64.microsoft.dotnet.ilcompiler.llvm`. Reproduce locally
with:

    docker run --rm --platform linux/arm64 \
      -v $(pwd)/tests/Components/fixtures-src:/work -w /work \
      mcr.microsoft.com/dotnet/sdk:10.0 \
      dotnet build --configuration Release \
      --property:WasiSdkUrl=https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-arm64-linux.tar.gz

The wasi-sdk URL override is needed because componentize-dotnet's MSBuild
target hard-codes the x86_64 download even on arm64 hosts.

Pre-built fixtures.wasm (2.5 MB; the .NET runtime gets statically linked)
plus regenerated fixtures.wit.json are committed so consumers don't need
the Linux toolchain. ComponentCompositesTests's fixture also registers
the `host-double` import passthrough since those tests bypass the
generated bindings.

Notably:

- tests/Components/fixtures-src/Cargo.toml + src/lib.rs + wit/world.wit
  removed; replaced by Fixtures.csproj + FixtureWorldImpl.cs + world.wit
  + NuGet.config.
- The componentize-dotnet generator does not emit a `None`-without-payload
  variant case correctly when the C# `None` collides with a record name
  in scope, but our minimal subset escapes that — kept the workaround in
  generated FunctionEmitter switch to use the type-only pattern.

Total component-test count: 206 passing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
- README.md gains a "Component Model" section between Introduction and
  Contributing with a 30-line quick-start that covers the full host
  flow: [ComponentBindings] -> RegisterImports -> Instantiate -> typed
  call. Defers full type mapping and limitations to docs/component-model.md.
- docs/component-model.md walks through the three architectural layers
  (generated bindings, runtime API, wasmtime C API), the WIT -> C# type
  mapping table, the build pipeline used to produce the fixture, and the
  current limitations (resources require wasmtime v42, async types
  belong to WASI 0.3, option<option<T>> generates invalid C# today,
  etc.). Implementation notes call out the WasmtimeComponentFunc 24-byte
  layout, ComponentValue.Free semantics, and the GCHandle lifetime for
  host-defined imports.
- tests/Components/regenerate.sh — single-command rebuild of every
  fixture artifact. Compiles the WAT fixtures via wasm-tools, builds
  the .NET fixture inside an arm64 Linux container (NativeAOT-LLVM has
  no macOS prebuilts), and refreshes fixtures.wit.json. Documents the
  WasiSdkUrl override that componentize-dotnet currently requires on
  arm64 hosts.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
The previous emitter generated `T??` for `option<option<T>>` which is not a
valid C# type — the compiler rejected it with CS1520 / CS9170. Caught by
empirically running the source generator against a `option<option<u32>>`
WIT export.

Fix:

- Add `public readonly struct Option<T>` to Wasmtime.Components alongside
  the existing `Result<T, E>`. Three-state semantics are preserved through
  None / Some(null) / Some(value).
- EmitContext.MakeNullable: when the option's element is itself an option,
  emit `Wasmtime.Components.Option<inner>` instead of appending another `?`.
  Single-level options keep the `T?` shape.
- FunctionEmitter.LowerOption / LiftOption: branch on whether the element
  is an option to choose between Nullable<T> accessors (.HasValue / .Value
  / null check) and the new Option<T> struct's Some / None.
- New nested-option.wit fixture (just declares
  `double-maybe: func() -> option<option<u32>>`) so the generator path is
  covered. Tests assert the runtime Option<T> behaviour.

Updated docs/component-model.md to reflect that nested options now work.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Records the eleven items the /branch-review pass surfaced, distinguishing
what was already addressed on this branch (notably option<option<T>> in
652307a) from the work that still has to land before merge:

- Memory ownership refactor (ownsHeap byte squats Rust padding; release
  needs to split into FreeManaged + ReleaseRustOwned through
  wasmtime_component_val_delete). Attempted in this session, rolled back
  because the combined diff with the post_return ordering change produced
  a wasmtime panic in c-api/src/store.rs:116:30 — needs a smaller repro
  before retrying.
- Composite return values currently leak Rust-allocated host-side copies.
- post_return ordering — the wrapper today calls it before the caller
  reads results.
- option<tuple<...>> generates invalid C# (IsValueType doesn't treat tuples
  as value types).
- type aliases (`type x = list<u32>`) emit references to types that are
  never declared.
- csproj duplicate EmbeddedResource for fixtures.wasm.
- README example references a non-existent greeter fixture.
- AsList/AsRecord shallow copies retain owner bits.
- RegisterImports partial-failure recovery.
- WIT `none` cases vs Wasmtime.Components.Option<T>.None ambiguity.

Each follow-up notes the smallest repro known and the fix shape that's
been considered, so picking it up in a future PR doesn't need re-discovery.
The "every fix needs a test" rule from /branch-review applies to all of
them.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
@kitsunoff
Copy link
Copy Markdown
Author

CI panic (`unknown wasmtime_valkind_t: 226` at `c-api/src/val.rs:392`) is the predicted manifestation of follow-up #1/#2 documented in `docs/component-model-followups.md`: `ComponentValue.ownsHeap` squats on Rust's `#[repr(C, u8)]` enum padding, which is `MaybeUninit` per the Rust reference. In Release builds the padding bytes carry non-zero garbage often enough that `Free()` switches into composite cleanup paths and runs `Marshal.FreeHGlobal` on Rust-allocated pointers — heap corruption then surfaces in adjacent core wasm tests as a bogus valkind read.

The fix shape (split `Free()` into `FreeManaged()` for managed-allocated values and `ReleaseRustOwned` calling `wasmtime_component_val_delete` for wasmtime-filled slots, with ownership tracked outside the C ABI footprint) is already in the follow-ups doc. I started this refactor on this branch but the combined diff with the `post_return`-after-lift change (#3) tripped a separate `panic!("None")` in `c-api/src/store.rs:116:30` that needs a smaller upstream repro. Rolled back to keep the rest of the work mergeable, will land it in a follow-up PR before marking this ready for review.

Other follow-ups (#4 `option<tuple<...>>`, #5 type aliases, #6 csproj duplicate) are simpler and can ride the same follow-up.

kitsunoff and others added 3 commits May 3, 2026 03:39
…e slots

Releases the immediate panic seen in CI on this PR
(\`unknown wasmtime_valkind_t: 226\`), which was the predicted manifestation of
followups bytecodealliance#1/bytecodealliance#2: ComponentValue.ownsHeap squats on Rust's
\`#[repr(C, u8)]\` enum padding, and Rust does not zero those bytes when it
copies an enum value into our slot. In Release builds the leftover stack
bytes were often 0xE2/0xCD/etc., so ComponentValue.Free() saw
\`ownsHeap != 0\` and tried to Marshal.FreeHGlobal a wasmtime-allocated
pointer — heap corruption that surfaced in adjacent core wasm tests as a
bogus valkind read.

This is a minimal, targeted fix rather than the full ownership refactor
(which is still tracked in docs/component-model-followups.md): every place
where wasmtime writes a 32-byte ComponentValue slot we now zero byte 1
(the byte ComponentValue uses for ownsHeap) before letting managed code
touch it. Two sites:

- ComponentFunction.Call — after wasmtime_component_func_call returns,
  walk the results span and clear byte 1 of each slot before
  post_return / before the caller lifts.
- ComponentLinkerInstance.HostCallback.TrampolineImpl — when wasmtime
  passes args into the host trampoline they came through the same
  Rust-enum-copy path; clear byte 1 of every arg before exposing the
  ReadOnlySpan to the user delegate.

After the reset, Free() on a wasmtime-filled slot is a no-op (the right
behaviour: wasmtime owns those allocations, post_return / store disposal
release them). Free() on a managed-allocated value still works because
the From* factories explicitly write ownsHeap = 1 after the
\`new ComponentValue { kind = ... }\` zeros the rest of the struct.

Local Release-mode test count goes from 39 passing (before, with process
crash) to 168 passing + 2 skipped (after). The remaining "test run
aborted" message comes from a teardown-phase crash that is not flagged
against any specific test — likely a finalizer running after the run is
done. Tracking that separately.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
Replaces inline byte-span writes that zeroed offset 1 of wasmtime-written
ComponentValue slots with an internal `ClearManagedOwnership()` method on
the struct. Same behaviour, less boilerplate at the two call sites.

Signed-off-by: Maxim Belyy <maximbel2003@gmail.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
…tion

`wasmtime_component_linker_add_wasip2` only registers the WASI host
functions on the linker; each invocation still looks up the WASI context
on the store. Without one, `WasiView::ctx()` panics on
`self.wasip2.as_mut().unwrap()` and the entire test process aborts mid-
run. We were calling `wasmtime_context_set_wasi` (WASI 0.1) which leaves
`wasip2` as `None` for every component that imports a WASI 0.2 interface.

Wires up the missing C API surface — `wasmtime_wasip2_config_*` plus
`wasmtime_context_set_wasip2` — behind a `WasiP2Configuration` builder,
exposed via a `Store.SetWasiP2Configuration` extension method symmetric
to the existing WASI 0.1 setter. Updates the componentize-dotnet test
fixtures to use it, which removes the teardown abort and unmasks an
unrelated NRE inside the .NET runtime startup tracked separately.

Signed-off-by: Maxim Belyy <maximbel2003@gmail.com>
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
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