Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
519b858
feat(components): introduce Component class for component model loading
kitsunoff May 2, 2026
512e4d1
feat(components): add ComponentLinker and instantiation support
kitsunoff May 2, 2026
d0e529b
feat(components): add primitive ComponentValue, ComponentFunction, fu…
kitsunoff May 2, 2026
ee2b929
test(components): cover ComponentValue layout and basic Component/Lin…
kitsunoff May 2, 2026
b243806
test(components): end-to-end add(u32, u32) -> u32 component invocation
kitsunoff May 2, 2026
22fc4a8
feat(components): add String marshalling and full composite-type unio…
kitsunoff May 2, 2026
2b09853
feat(components): add Enum and Flags marshalling
kitsunoff May 2, 2026
300451a
feat(components): add List and Tuple marshalling
kitsunoff May 2, 2026
d55adc0
feat(components): add Record marshalling
kitsunoff May 2, 2026
cb670c9
feat(components): add Variant, Option, and Result marshalling
kitsunoff May 2, 2026
cf74ac0
fix(components): correct WasmtimeComponentFunc layout to 24 bytes
kitsunoff May 2, 2026
ed2c0f2
test(components): cargo-component fixture exercising every WASI 0.2 c…
kitsunoff May 2, 2026
e6a4d3a
feat(components): host-defined imports via ComponentLinkerInstance.De…
kitsunoff May 2, 2026
77aed29
feat(components): Roslyn source generator skeleton + [ComponentBindin…
kitsunoff May 2, 2026
430db53
feat(components): WIT parsing via wasm-tools JSON IR
kitsunoff May 2, 2026
fd8902a
feat(components): emit C# types from WIT IR (record / enum / flags / …
kitsunoff May 2, 2026
bafab6e
feat(components): emit C# call wrappers for primitive-signature exports
kitsunoff May 2, 2026
b7bc83e
feat(components): generate lift/lower for every composite signature
kitsunoff May 2, 2026
21abc8e
test(components): record round-trip through generated bindings
kitsunoff May 2, 2026
26fc6bd
feat(components): rebuild fixture as a .NET component via componentiz…
kitsunoff May 2, 2026
0869856
docs(components): README section, docs/component-model.md, regenerate.sh
kitsunoff May 2, 2026
652307a
fix(components): emit Option<T> for nested option<option<T>>
kitsunoff May 3, 2026
c842cd4
docs(components): track open branch-review follow-ups
kitsunoff May 3, 2026
76bc458
fix(components): zero ownsHeap-byte in wasmtime-written ComponentValu…
kitsunoff May 3, 2026
579d7ac
refactor(components): extract ClearManagedOwnership helper
kitsunoff May 3, 2026
5c791a6
fix(components): attach WASI 0.2 context to store via WasiP2Configura…
kitsunoff May 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,46 @@ $ dotnet run

This should print `Hello from C#!`.

## Component Model

WASI 0.2 components are supported in the `Wasmtime.Components` namespace. A
component is loaded with `Component.FromBytes`/`FromFile`, instantiated through
`ComponentLinker`, and called via `ComponentInstance.GetFunction` +
`ComponentValue` marshalling. A Roslyn source generator
(`Wasmtime.Component.SourceGenerators`) turns `.wit` files into idiomatic C#
bindings — types, export call wrappers, and an `IImports` interface for
host-supplied functions.

```csharp
using Wasmtime;
using Wasmtime.Components;

[ComponentBindings("greeter.wit", world: "host")]
public partial class GreeterBindings { }

class HostImports : GreeterBindings.IImports
{
public void Log(string message) => Console.WriteLine(message);
}

using var engine = new Engine();
using var component = Component.FromFile(engine, "greeter.wasm");
using var linker = new ComponentLinker(engine);
using var store = new Store(engine);
store.SetWasiConfiguration(new WasiConfiguration());
linker.AddWasiPreview2();

GreeterBindings.RegisterImports(linker, new HostImports());
var instance = linker.Instantiate(store, component);
var bindings = new GreeterBindings(instance);

string result = bindings.Greet(new GreeterBindings.Person("Alice", 30));
```

See [`docs/component-model.md`](docs/component-model.md) for the full type
mapping, build pipeline, and current limitations (notably WIT `resource` types,
which require a wasmtime C API upgrade — tracked as a follow-up).

## Contributing

### Building
Expand Down
39 changes: 39 additions & 0 deletions Wasmtime.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,63 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasmtime", "src\Wasmtime.cs
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasmtime.Tests", "tests\Wasmtime.Tests.csproj", "{8A200114-1D0B-4F90-9F82-1FFE47C207DD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wasmtime.Component.SourceGenerators", "src\Wasmtime.Component.SourceGenerators\Wasmtime.Component.SourceGenerators.csproj", "{87F136FC-1D1C-4268-9C65-D0C3C193DB09}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Debug|x64.ActiveCfg = Debug|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Debug|x64.Build.0 = Debug|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Debug|x86.ActiveCfg = Debug|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Debug|x86.Build.0 = Debug|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Release|Any CPU.Build.0 = Release|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Release|x64.ActiveCfg = Release|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Release|x64.Build.0 = Release|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Release|x86.ActiveCfg = Release|Any CPU
{5EB63C51-5286-4DDF-BF7F-4110CC6D80B8}.Release|x86.Build.0 = Release|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Debug|x64.Build.0 = Debug|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Debug|x86.Build.0 = Debug|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Release|Any CPU.Build.0 = Release|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Release|x64.ActiveCfg = Release|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Release|x64.Build.0 = Release|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Release|x86.ActiveCfg = Release|Any CPU
{8A200114-1D0B-4F90-9F82-1FFE47C207DD}.Release|x86.Build.0 = Release|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Debug|x64.ActiveCfg = Debug|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Debug|x64.Build.0 = Debug|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Debug|x86.ActiveCfg = Debug|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Debug|x86.Build.0 = Debug|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Release|Any CPU.Build.0 = Release|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Release|x64.ActiveCfg = Release|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Release|x64.Build.0 = Release|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Release|x86.ActiveCfg = Release|Any CPU
{87F136FC-1D1C-4268-9C65-D0C3C193DB09}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{87F136FC-1D1C-4268-9C65-D0C3C193DB09} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F5AC35E5-1373-49E6-97DC-68CB5E0369E0}
EndGlobalSection
Expand Down
153 changes: 153 additions & 0 deletions docs/component-model-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Component Model — pending follow-ups from branch review

A `/branch-review` pass on this branch surfaced eleven items. Five were addressed
in the work that already landed on `component-model` (notably the
`option<option<T>>` → `Option<T>` fix in `652307a`). The rest are tracked here
because they need either a wasmtime upgrade, a deeper refactor than fits this
branch, or just dedicated test coverage. None of them is fully closed — every
item below must be paired with a regression test before merge.

## Blocking

### 1. `ComponentValue.ownsHeap` squats on Rust's enum padding

`src/Components/ComponentValue.cs` carries a managed-only `ownsHeap` byte at
offset 1 of a struct that mirrors `wasmtime_component_val_t`. The Rust side is
`#[repr(C, u8)]`, which leaves bytes 1–7 as alignment padding and explicitly
does not guarantee they're zero. Today the test suite happens to land zeroes
there, so `Free()` short-circuits on wasmtime-filled results and the program
limps along — but on any future allocator pattern the byte can be non-zero,
the `Free` switch will fire on Rust-allocated pointers, and the process will
crash with a heap corruption.

The fix needs ownership to live outside the C ABI footprint. Two viable
shapes:

- A managed-only sidecar (`ConditionalWeakTable<ComponentValue, ...>` keyed by
pointer, or a `Dictionary<UIntPtr, OwnerInfo>`) that the factories populate
and `FreeManaged` consults.
- A scope wrapper (`ComponentValueScope : IDisposable`) that owns the array of
managed-side allocations and disposes them en masse; the raw `ComponentValue`
array stays internal.

Either way `Free()` splits into:

- `FreeManaged()` — for values built by `From*` factories. Releases via
`Marshal.FreeHGlobal`.
- `ReleaseRustOwned(ref ComponentValue)` — for values that wasmtime wrote.
Wraps `wasmtime_component_val_delete` (`drop_in_place`) so Rust frees its
own `Vec`/`String`/`Box`.

### 2. Composite return values from exports leak Rust-allocated memory

Every export that returns `string`, `list`, `record`, `tuple`, `variant`,
`flags`, `option<composite>`, or `result<composite, ...>` currently leaks the
`Vec`/`String`/`Box` allocations wasmtime put into the result slot.
`wasmtime_component_func_post_return` only releases guest-side `cabi_realloc`
buffers; the Rust-allocated host-side copy needs `wasmtime_component_val_delete`
(or per-vec `_delete` siblings).

Fix is paired with #1 — once `ReleaseRustOwned` is wired up, the generator's
`finally` block calls it for every `rets[i]`. Repro: call any composite-result
export 10 000 times and watch RSS.

### 3. `Call` runs `post_return` before the caller has read the result

`ComponentFunction.Call` invokes `post_return` immediately after the function
call, before the user lifts `results[]`. Today wasmtime clones the Rust
`Val` out of guest memory before returning, so the lifted view is stable —
but that's an implementation detail of the current C API, not a contract.
The header is explicit ("after the embedder has finished processing the return
value then this function must be invoked").

Attempted fix in this branch: split `Call` into call + `PostReturn()` and let
the generator emit `try { call → lift } finally { PostReturn → free }`.
Triggers a `panic!("None")` in `crates/c-api/src/store.rs:116:30` on certain
test paths even though wasmtime's Rust API documents a no-op for functions
without a post-return option. Needs a smaller repro to file upstream before
re-attempting.

### 4. `option<tuple<...>>` does not compile

`FunctionEmitter.IsValueType` only treats primitives, enums, and flags as
value types. Tuples and anonymous result/option types are also value types in
the emitted C# (`ValueTuple<...>`, `Wasmtime.Components.Result<T,E>`,
`Wasmtime.Components.Option<T>`), so `LowerOption` falls into the
reference-type branch and emits `var!.ItemN`, which is invalid against
`Nullable<ValueTuple<...>>`.

One-line fix: extend `IsValueType` with `or WitTupleKind or WitResultKind or
WitOptionKind`. Test by adding `export maybe-pair: func(present: bool) ->
option<tuple<u32, string>>;` to the fixture and asserting the round-trip.
(Attempted in this branch but rolled back together with #1/#2/#3 because the
combined diff couldn't keep the test suite green.)

### 5. Type aliases (`type my-list = list<u32>`) generate broken code

`EmitContext.ResolveIndex` returns `MyList` for any named type definition,
but `TypeEmitter.EmitNamedTypes` only emits declarations for `record`,
`enum`, `flags`, and `variant`. Aliases to `list`/`option`/`result`/`tuple`
or another named type produce a reference to a type that's never declared
(`CS0246`).

Two paths: emit the alias as a `using` (`using MyList = ...;` at the top of
the generated file) so the rest of the bindings keep referring to the alias
name; or fall through to structural rendering and ignore the alias name.
The second is a one-liner in `ResolveIndex` (only emit `def.Name` for the
four nominal kinds; otherwise drop into the structural switch).

### 6. Duplicate `EmbeddedResource` for `fixtures.wasm`

`tests/Wasmtime.Tests.csproj` had both an `Update` and an `Include` for the
same file. The `Update` has nothing to update (no glob picks `*.wasm`), so
it's dead code. Drop one of them.

## Should be addressed

### 7. README example references a non-existent `GreeterBindings` fixture

The "Component Model" section in `README.md` was added in `0869856`. It
shows `[ComponentBindings("greeter.wit", world: "host")]` plus a
`HostImports` implementation, but there's no greeter fixture committed.
Either ship a minimal greeter alongside (`tests/Components/greeter-src/`)
or rewrite the example against the existing `FixtureBindings`.

### 8. `AsList` / `AsRecord` shallow-copies retain owner bits

`DecodeValueArray` does `result[i] = array[i]` — a struct copy. With #1
fixed, the copy must scrub whatever ownership marker the new design uses so
that an accidental `Free` on a returned element is a safe no-op rather than
a double-free.

### 10. `RegisterImports` partial-failure recovery

If `DefineFunc` fails for the third out of five imports, the first two
trampolines stay registered on the linker. Document the resulting "linker
must be discarded" contract on `RegisterImports` xmldoc, or track the
registered names and unbind them on failure (the C API may not support the
latter, in which case documenting is the only path).

### 11. WIT case name `none` collides with `Wasmtime.Components.Option<T>.None`

Already mostly under control because every variant case is nested inside the
generated variant type (`Greeting.None`, not bare `None`), but anyone bringing
both into scope via `using static` will hit the ambiguity. Add a short note
in `docs/component-model.md`'s limitations section.

## Recommended

- Diagnostic for `WitUnknownKind` rather than silently emitting `object`.
- `using System.Linq;` and `using System.Collections.Generic;` directives at
the top of the generated file so emitted code reads more naturally.
- `Debug.Assert` on `Marshal.SizeOf<WasmtimeComponentFunc>()` and on
`Marshal.SizeOf<WasmtimeComponentValUnion>()` (mirror the Rust-side
`const _: ()` size assertions).
- Include `ex.GetType().FullName` plus a stack frame in the host-trampoline's
`wasmtime_error_new` message.

## Process

Each item above must land with a test that fails without the fix and passes
with it. The `/branch-review` rule is "no pre-existing", and these are now
explicitly tracked work — so they belong to this PR thread, not someone
else's.
Loading
Loading