feat(components): WASI 0.2 host bindings + Roslyn source generator#360
feat(components): WASI 0.2 host bindings + Roslyn source generator#360kitsunoff wants to merge 26 commits intobytecodealliance:mainfrom
Conversation
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>
|
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. |
…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>
Summary
Implements WASI 0.2 Component Model host-side support for wasmtime-dotnet, addressing #324.
Two layers:
Runtime API (
Wasmtime.Components) —SafeHandle-backed wrappers aroundwasmtime_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 viaComponentLinkerInstance.DefineFuncwith idiomatic(ReadOnlySpan<ComponentValue>, Span<ComponentValue>)callback.Roslyn source generator (
Wasmtime.Component.SourceGenerators) — turns WIT files into idiomatic C# bindings. Consumes JSON IR fromwasm-tools component wit X.wit --jsoninstead 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 anIImportsinterface plus staticRegisterImports(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
Known limitations (tracked in `docs/component-model-followups.md`)
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.