Skip to content

Add crate for host-side .net component bindings - #1946

Open
ludfjig wants to merge 7 commits into
mainfrom
component-wire-format
Open

ludfjig wants to merge 7 commits into
mainfrom
component-wire-format

Conversation

@ludfjig

@ludfjig ludfjig commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Adds new crate hyperlight-component-csharp which generates c# host bindings from wit. The intended usage is a .NET hyperlight-wasm library, so .net hosts don't need to write rust.

The crate is in rust and not .net because it reuses the component type elaboration from hyperlight-component-util, the same one the rust bindings use. Both generators resolve types and name wire functions the same way. In the future the remaining core (encoding) could be shared too, so they cannot drift.

breakign changes:

  • host_bindgen! and guest_bindgen! encode values little-endian (instead of native endian)
  • Functions are named <instance>#<function>, such as wasi:http/types@0.2.0#[method]fields.get, rather than r#wasi::r#http::[method]fields.get. The old name dropped the interface and version, so functions in different interfaces or versions of one package collided. For example, wasi:clocks/monotonic-clock and wasi:clocks/wall-clock both registered now as r#wasi::r#clocks::now, and the second silently replaced the first.

The C# bindings support resources the host defines, like the Rust host bindings.

Testing: a test-only native library (src/tests/csharp_host/native) runs witguest in a sandbox, and the C# tests call it through the generated bindings. guest.wit got world-level functions, an inline interface and more composite types, so the Rust and C# hosts are tested against the same guest.

@ludfjig ludfjig added the kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. label Oct 16, 2026
@hyperlight-gh-bot

This comment has been minimized.

@syntactically

Copy link
Copy Markdown
Member

I didn't look at this in detail yet, but I did notice it is not using the same component type elaboration infrastructure from the Rust macros. I think this is probably a mistake as it does not seem to me that most of the more complex cases are possible to get right without the explicit representation of type variables. I think pretty much the only thing in State that is specific to rust is the root_mod field which uses the Mod type that represents the submodule/subtrait structure that is used for Rust. Maybe you can make State parametrised on the type of the root module like that and have a different alternative for holding the information you need for C# syntax trees? Then I think you can follow the same pattern as hyperlight_component_macro to drive the host/guest emit using read_wit_type and then having your own equivalents of rtype::emit_toplevel and host/guest::emit_toplevel that follow the structure of the fully elaborated component inductively. (Probably some of the BindgenInputParams/source stuff can be moved from hyperlight_component_macro to hyperlight_component_util also to reduce duplication).

@ludfjig
ludfjig force-pushed the component-wire-format branch from 9ab4272 to fcd4c0f Compare October 20, 2026 16:36
@ludfjig

ludfjig commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

I didn't look at this in detail yet, but I did notice it is not using the same component type elaboration infrastructure from the Rust macros. I think this is probably a mistake as it does not seem to me that most of the more complex cases are possible to get right without the explicit representation of type variables. I think pretty much the only thing in State that is specific to rust is the root_mod field which uses the Mod type that represents the submodule/subtrait structure that is used for Rust. Maybe you can make State parametrised on the type of the root module like that and have a different alternative for holding the information you need for C# syntax trees? Then I think you can follow the same pattern as hyperlight_component_macro to drive the host/guest emit using read_wit_type and then having your own equivalents of rtype::emit_toplevel and host/guest::emit_toplevel that follow the structure of the fully elaborated component inductively. (Probably some of the BindgenInputParams/source stuff can be moved from hyperlight_component_macro to hyperlight_component_util also to reduce duplication).

Thanks, reworked it this way. The C# generator now drives off read_wit_type and the elaborated component, with its own equivalents of rtypes/host emit_toplevel in types.rs and host.rs. State is parametrised on the root module (defaulting to Mod), and C# uses resolve_bound_var and emit_fn_hl_name from it. I left BindgenInputParams in the macro crate, since C# takes a WitSource and calls read_wit_type, so the source handling is already shared and the parser's only user is the macro syntax.

@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig marked this pull request as ready for review October 20, 2026 17:32
Copilot AI balanced review requested due to automatic review settings October 20, 2026 17:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Supported inputs can produce uncompilable bindings, and decoding and structural hashing defects remain.

11 open issues
What changed in this PR

Adds C# host bindings for Hyperlight components, sharing WIT type resolution and wire naming with the Rust generators.

Changes:

  • C# generation and runtime support for component values and calls.
  • Little-endian encoding and interface-qualified wire names.
  • Cross-language tests, documentation, CI and publishing integration.
File Description
src/​tests/​component_wire/​wire.wit Shared wire-format fixture.
src/​tests/​component_wire/​vectors.txt Cross-language encoding vectors.
src/​tests/​component_wire/​csharp/​WireTests.cs Encoding and decoding checks.
src/​tests/​component_wire/​csharp/​RuntimeTests.cs Runtime validation checks.
src/​tests/​component_wire/​csharp/​Program.cs Test runner and assertions.
src/​tests/​component_wire/​csharp/​Fakes.cs Fake guest and registry.
src/​tests/​component_wire/​csharp/​EdgeTests.cs Naming and type checks.
src/​tests/​component_wire/​csharp/​edge.wit Edge-case WIT fixture.
src/​tests/​component_wire/​csharp/​ComponentTests.csproj Generates and compiles test bindings.
src/​hyperlight_host/​tests/​component_wire_test.rs Rust wire-vector checks.
src/​hyperlight_component_util/​src/​util.rs WIT path resolution fallback.
src/​hyperlight_component_util/​src/​host.rs Host instance naming context.
src/​hyperlight_component_util/​src/​hl.rs Wire names and little-endian encoding.
src/​hyperlight_component_util/​src/​guest.rs Guest instance naming context.
src/​hyperlight_component_util/​src/​emit.rs Shared generation state.
src/​hyperlight_component_csharp/​src/​types.rs Type discovery and validation.
src/​hyperlight_component_csharp/​src/​tests.rs Generator tests.
src/​hyperlight_component_csharp/​src/​Runtime.cs C# codecs and support types.
src/​hyperlight_component_csharp/​src/​names.rs C# identifier helpers.
src/​hyperlight_component_csharp/​src/​lib.rs Generator API and namespaces.
src/​hyperlight_component_csharp/​src/​host.rs Call wrappers and import registration.
src/​hyperlight_component_csharp/​src/​codec.rs Type and codec generation.
src/​hyperlight_component_csharp/​README.md Generator usage documentation.
src/​hyperlight_component_csharp/​examples/​csharp_bindgen.rs Command-line generation example.
src/​hyperlight_component_csharp/​Cargo.toml Crate definition.
Justfile Build and test integration.
docs/​README.md Wire-format documentation link.
docs/​how-to-make-releases.md Published crate list.
docs/​component-wire-format.md Wire contract and C# mappings.
CHANGELOG.md Features and breaking changes.
Cargo.toml Workspace registration.
Cargo.lock Crate dependency entry.
.github/​workflows/​dep_code_checks.yml C# test execution.
.github/​workflows/​CargoPublish.yml Crate publishing integration.

Review effort: Balanced

@ludfjig
ludfjig force-pushed the component-wire-format branch from fcd4c0f to 92cc474 Compare October 20, 2026 17:58
@hyperlight-gh-bot

This comment has been minimized.

andreiltd
andreiltd previously approved these changes Oct 22, 2026

@andreiltd andreiltd left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey! This looks very clean! I only reviewed rust part of the patch as I don't have prior experience with c#. That said, the PR will likely need another pair of eyes. 👀

@simongdavies simongdavies left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't know if this should be in hyperlight rather than hyperlight-wasm, I appreciate we have the rust example but we also have a Rust hosting API so we can show how it is used completely. It feels like half a solution here, that said if we moved it to hyperlight-wasm that wouldn't be solved, we still need a dotnet hosting API.

I think we should consider this in our investigation into the feasibility of having a single host API instead of hyperlight-js/hypelright-wasm etc.

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason not to use a dotnet test framework like xunit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wanted to keep it as light as possible and have no requirements to download anything

@ludfjig

ludfjig commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

I don't know if this should be in hyperlight rather than hyperlight-wasm, I appreciate we have the rust example but we also have a Rust hosting API so we can show how it is used completely. It feels like half a solution here, that said if we moved it to hyperlight-wasm that wouldn't be solved, we still need a dotnet hosting API.

I believe this should definitely be here in hyperlight, it's the same as host_bindgen! but for c#, and is unrelated to wasm.

I think we should consider this in our investigation into the feasibility of having a single host API instead of hyperlight-js/hypelright-wasm etc.

I agree we should consider this, but since there's no concrete plan for it yet, I think it's better we get this in. And this will still be needed even if we manage to make a single host api if we want c# host bindigs (at least some form of this)

@syntactically

Copy link
Copy Markdown
Member

I don't know if this should be in hyperlight rather than hyperlight-wasm, I appreciate we have the rust example but we also have a Rust hosting API so we can show how it is used completely. It feels like half a solution here, that said if we moved it to hyperlight-wasm that wouldn't be solved, we still need a dotnet hosting API.

I think we should consider this in our investigation into the feasibility of having a single host API instead of hyperlight-js/hypelright-wasm etc.

I definitely think it should stay in hl core. The wit macros are usable entirely without hyperlight-wasm and I think that is one promising route to having a more uniform approach to host/guest API. Especially since there is room to improve the performance of guest function dispatch by eliminating names when the wit macros are in use.

Comment thread src/hyperlight_component_util/src/hl.rs Resolved
Comment thread CHANGELOG.md Resolved

@jsturtevant jsturtevant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

overall LGTM, a few comments and really cool way to enable .net developers to write hyperlight guests and I love all the tests!

We don't really have a way to mark a package as experimental. Maybe this is something we want to think about in 1.0? I am sure we will have more of these in the future.

@ludfjig
ludfjig force-pushed the component-wire-format branch 3 times, most recently from c907c97 to ad25c60 Compare October 30, 2026 18:11
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig force-pushed the component-wire-format branch from ad25c60 to 2e27c96 Compare October 30, 2026 19:56
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig force-pushed the component-wire-format branch from 2e27c96 to 616d788 Compare October 30, 2026 22:25
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig ludfjig added this to the v0.19.0 milestone Nov 2, 2026
Comment thread src/tests/csharp_host/Sandbox.cs Outdated

public void Dispose()
{
hltest_sandbox_free(_sandbox);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium: A managed host callback can call Dispose() while the outer hltest_call still holds an exclusive reference to this native sandbox. This frees the sandbox during its use. A nested or concurrent Call() also violates the native exclusive-access contract. Could we guard native operations against reentry and concurrent use, reject disposal during an active call, and add a callback regression test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed with a SafeHandle: P/Invoke keeps the sandbox alive during each call, so disposing from a host callback frees it when the call returns, and later calls throw ObjectDisposedException. There's a regression test for that. The wrapper is documented as not thread-safe and host functions must not call into it, like other native wrappers, rather than adding locks to test code.

jsturtevant
jsturtevant previously approved these changes Nov 3, 2026
@jsturtevant

Copy link
Copy Markdown
Contributor

The addition of the test for real wit was really helpful. Thank you!

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Wire names were the module path plus the function, such as
r#wasi::r#clocks::now. They dropped the interface and version, so
functions in different interfaces collided. They are now
<instance>#<function>, such as wasi:clocks/monotonic-clock@0.2.0#now.

Values are encoded little-endian. Every supported target is
little-endian, so the bytes are unchanged.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Add world-level functions, an inline export interface, and nested and
composite types to guest.wit. Other host bindings can then be tested
against witguest.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
State takes its output type as a parameter, which defaults to Mod.
new, clone and resolve_bound_var work for any output, and so does
emit_fn_hl_name. read_wit_type also works outside a cargo build.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
A test-only native library runs witguest in a sandbox, and the C# tests
call it through the generated bindings. They run in the X64 build and
test jobs, which have the guests and the .NET SDK.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
@hyperlight-gh-bot

Copy link
Copy Markdown

Benchmark Results

Measured commit: ca2b8f78bd53
Baseline commit: 3294ee330d6c

kvm / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
379.82 µs (➖ 1.03x faster)
byte_chunks 769.77 ns (➖ 1.01x slower)
vec_bytes 623.57 ns (➖ 1.06x slower)

payload_allocation

slot_pool_segmented
65536 142.49 ns (➖ 1.01x slower)
262144 523.56 ns (➖ 1.00x slower)

sandboxes

create_initialized_and_drop
medium 13.16 ms (➖ 1.00x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
7.73 ns (➖ 1.23x faster) 7.74 ns (➖ 1.04x faster) 7.72 ns (➖ 1.00x slower)

snapshot_files

load_snapshot_unverified
small 100.63 µs (➖ 1.02x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 1.93 µs (➖ 1.09x faster) 2.15 µs (➖ 1.11x slower)
262144 7.05 µs (➖ 1.05x faster) 7.31 µs (➖ 1.02x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.04 µs (➖ 1.00x faster) 1.06 µs (➖ 1.04x slower)
65536 6.12 µs (➖ 1.02x faster) 6.19 µs (➖ 1.08x faster)
262144 28.05 µs (➖ 1.01x slower)
kvm / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
659.19 µs (➖ 1.01x faster)
byte_chunks 691.41 ns (➖ 1.02x slower)
vec_bytes 504.72 ns (➖ 1.01x faster)

payload_allocation

slot_pool_segmented
65536 156.25 ns (➖ 1.01x slower)
262144 494.62 ns (➖ 1.00x faster)

sandboxes

create_initialized_and_drop
medium 15.24 ms (➖ 1.05x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
6.78 ns (➖ 1.00x faster) 6.77 ns (➖ 1.00x faster) 6.81 ns (➖ 1.01x slower)

snapshot_files

load_snapshot_unverified
small 49.20 µs (➖ 1.02x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.11 µs (➖ 1.00x slower) 2.11 µs (➖ 1.01x faster)
262144 7.52 µs (➖ 1.00x slower) 7.49 µs (➖ 1.00x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 706.47 ns (➖ 1.04x faster) 731.67 ns (➖ 1.15x faster)
65536 7.01 µs (➖ 1.02x faster) 7.02 µs (➖ 1.05x faster)
262144 29.01 µs (➖ 1.04x faster)
mshv3 / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
298.54 µs (➖ 1.10x slower)
byte_chunks 945.62 ns (➖ 1.01x faster)
vec_bytes 719.13 ns (➖ 1.03x slower)

payload_allocation

slot_pool_segmented
65536 195.48 ns (➖ 1.01x slower)
262144 732.70 ns (➖ 1.01x slower)

sandboxes

create_initialized_and_drop
medium 57.58 ms (➖ 1.10x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.56 ns (➖ 1.01x faster) 9.60 ns (➖ 1.00x faster) 9.60 ns (➖ 1.00x faster)

snapshot_files

load_snapshot_unverified
small 108.70 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.24 µs (➖ 1.01x slower) 2.39 µs (➖ 1.02x faster)
262144 10.22 µs (➖ 1.06x slower) 9.91 µs (➖ 1.18x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.26 µs (➖ 1.02x slower) 1.25 µs (➖ 1.08x faster)
65536 8.21 µs (➖ 1.17x faster) 8.24 µs (➖ 1.17x faster)
262144 35.52 µs (➖ 1.06x faster)
mshv3 / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
658.46 µs (➖ 1.15x faster)
byte_chunks 956.21 ns (➖ 1.05x slower)
vec_bytes 666.60 ns (➖ 1.01x slower)

payload_allocation

slot_pool_segmented
65536 265.53 ns (➖ 1.13x slower)
262144 661.71 ns (➖ 1.01x slower)

sandboxes

create_initialized_and_drop
medium 54.48 ms (➖ 1.32x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.40 ns (➖ 1.08x slower) 8.70 ns (➖ 1.00x faster) 8.69 ns (➖ 1.00x slower)

snapshot_files

load_snapshot_unverified
small 53.88 µs (➖ 1.00x faster)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.28 µs (➖ 1.03x faster) 2.27 µs (➖ 1.01x faster)
262144 7.99 µs (➖ 1.00x slower) 8.02 µs (➖ 1.01x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 893.64 ns (➖ 1.02x faster) 883.51 ns (➖ 1.05x faster)
65536 7.44 µs (➖ 1.01x faster) 7.45 µs (➖ 1.02x faster)
262144 39.01 µs (➖ 1.01x faster)
hyperv-ws2025 / amd (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
1.86 ms (➖ 1.01x faster)
byte_chunks 1.16 µs (➖ 1.01x faster)
vec_bytes 789.15 ns (➖ 1.01x faster)

payload_allocation

slot_pool_segmented
65536 319.32 ns (➖ 1.38x slower)
262144 785.74 ns (➖ 1.00x faster)

sandboxes

create_initialized_and_drop
medium 10.21 ms (➖ 1.04x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.06 ns (➖ 1.03x faster) 10.07 ns (➖ 1.06x faster) 9.99 ns (➖ 1.02x faster)

snapshot_files

load_snapshot_unverified
small 1.07 ms (➖ 1.09x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.35 µs (➖ 1.07x faster) 2.31 µs (➖ 1.02x faster)
262144 9.03 µs (➖ 1.03x faster) 9.01 µs (➖ 1.04x faster)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.27 µs (➖ 1.11x faster) 1.30 µs (➖ 1.03x faster)
65536 10.02 µs (➖ 1.02x slower) 8.92 µs (➖ 1.00x faster)
262144 37.83 µs (➖ 1.16x faster)
hyperv-ws2025 / intel (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

decode_vec_bytes_copy encode_control
2.92 ms (➖ 1.01x faster)
byte_chunks 1.21 µs (➖ 1.02x slower)
vec_bytes 760.97 ns (➖ 1.00x faster)

payload_allocation

slot_pool_segmented
65536 207.88 ns (➖ 1.02x faster)
262144 755.29 ns (➖ 1.01x slower)

sandboxes

create_initialized_and_drop
medium 14.32 ms (➖ 1.20x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.23 ns (➖ 1.00x slower) 9.70 ns (➖ 1.00x faster) 9.59 ns (➖ 1.01x faster)

snapshot_files

load_snapshot_unverified
small 714.33 µs (➖ 1.08x slower)

virtq_readonly

slot_pool_segmented slot_pool_segmented_fragmented
65536 2.43 µs (➖ 1.04x slower) 2.28 µs (➖ 1.00x slower)
262144 7.46 µs (➖ 1.02x faster) 7.61 µs (➖ 1.01x slower)

virtq_readwrite

slot_pool_segmented slot_pool_segmented_fragmented
8192 1.06 µs (➖ 1.01x slower) 1.17 µs (➖ 1.12x slower)
65536 8.44 µs (➖ 1.02x faster) 8.23 µs (➖ 1.01x faster)
262144 40.39 µs (➖ 1.34x faster)

Reported by cargo ci bench-report --candidate run:41753179193 --config-file bench_report.toml.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement For PRs adding features, improving functionality, docs, tests, etc.

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

6 participants