Skip to content

Commit 62933b1

Browse files
committed
Add hyperlight-component-csharp
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 4f881b7 commit 62933b1

16 files changed

Lines changed: 2546 additions & 6 deletions

File tree

‎.github/workflows/CargoPublish.yml‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
VERSION="${VERSION#refs/heads/release/v}"
5353
echo "VERSION=$VERSION" >> $GITHUB_ENV
5454
fi
55-
./dev/verify-version.sh "$VERSION" hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-guest-tracing
55+
./dev/verify-version.sh "$VERSION" hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-component-csharp hyperlight-guest-tracing
5656
5757
- name: Determine which crates need publishing
5858
run: |
@@ -64,6 +64,7 @@ jobs:
6464
hyperlight-guest-bin \
6565
hyperlight-component-util \
6666
hyperlight-component-macro \
67+
hyperlight-component-csharp \
6768
hyperlight-host \
6869
hyperlight-guest-tracing >> "$GITHUB_ENV"
6970
@@ -134,6 +135,13 @@ jobs:
134135
CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }}
135136
if: env.PUBLISH_HYPERLIGHT_HOST != 'false'
136137

138+
- name: Publish hyperlight-component-csharp
139+
continue-on-error: ${{ inputs.dry_run }}
140+
run: cargo publish --manifest-path ./src/hyperlight_component_csharp/Cargo.toml ${{ inputs.dry_run && '--dry-run' || '' }}
141+
env:
142+
CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }}
143+
if: env.PUBLISH_HYPERLIGHT_COMPONENT_CSHARP != 'false'
144+
137145

138146
# TODO: Do we want to publish hyperlight-guest-capi to crates.io given that it's not for Rust consumption?
139147
# - name: Publish hyperlight-guest-capi

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55
## [Prerelease] - Unreleased
66

77
### Added
8+
* `hyperlight-component-csharp` crate for generating C# host bindings from WIT worlds by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1946
89

910
### Changed
1011
* **Breaking:** `hyperlight-component-macro`: `host_bindgen!` and `guest_bindgen!` encode values little-endian and qualify function names with their interface, such as `wasi:http/types@0.2.0#[method]fields.get` by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1946

‎Cargo.lock‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ members = [
1919
"src/hyperlight_libc",
2020
"src/hyperlight_component_util",
2121
"src/hyperlight_component_macro",
22+
"src/hyperlight_component_csharp",
2223
"src/trace_dump",
2324
]
2425
# Guests have custom linker flags and live in their own nested workspace,
@@ -47,6 +48,7 @@ hyperlight-guest-tracing = { path = "src/hyperlight_guest_tracing", version = "0
4748
hyperlight-libc = { path = "src/hyperlight_libc", version = "0.18.0", default-features = false }
4849
hyperlight-component-util = { path = "src/hyperlight_component_util", version = "0.18.0", default-features = false }
4950
hyperlight-component-macro = { path = "src/hyperlight_component_macro", version = "0.18.0", default-features = false }
51+
hyperlight-component-csharp = { path = "src/hyperlight_component_csharp", version = "0.18.0", default-features = false }
5052

5153
[workspace.lints.rust]
5254
unsafe_op_in_unsafe_fn = "deny"

‎Justfile‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ code-checks-like-ci config=default-target hypervisor="kvm":
151151
{{ if os() == "linux" { "just clippy-exhaustive " + config } else { "" } }}
152152

153153
@# Verify MSRV
154-
./dev/verify-msrv.sh hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-guest-tracing
154+
./dev/verify-msrv.sh hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-component-csharp hyperlight-guest-tracing
155155

156156
@# Check cargo features compile
157157
just check
@@ -287,6 +287,7 @@ test-integration target=default-target features="":
287287

288288
@# run component-util unit tests and the integration tests that exercise WIT input codegen
289289
{{ cargo-cmd }} test -p hyperlight-component-util --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib --test wasmtime_guest_codegen
290+
{{ cargo-cmd }} test -p hyperlight-component-csharp --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib
290291

291292
@# run the rest of the integration tests. `snapshot_goldens` is in
292293
@# the glob but no-ops without a mode token, so it needs no golden
@@ -414,6 +415,7 @@ clippy-exhaustive target=default-target:
414415
./hack/clippy-package-features.sh hyperlight-testing {{ target }} {{ target-triple }}
415416
./hack/clippy-package-features.sh hyperlight-component-macro {{ target }} {{ target-triple }}
416417
./hack/clippy-package-features.sh hyperlight-component-util {{ target }} {{ target-triple }}
418+
./hack/clippy-package-features.sh hyperlight-component-csharp {{ target }} {{ target-triple }}
417419
{{ if hyperlight-target-arch == "x86_64" { "./hack/clippy-package-features.sh hyperlight-guest-tracing " + target } else { "" } }}
418420
just clippy-guests {{ target }}
419421

@@ -423,7 +425,7 @@ clippy-package package target=default-target:
423425

424426
# Verify Minimum Supported Rust Version
425427
verify-msrv:
426-
./dev/verify-msrv.sh hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-guest-tracing
428+
./dev/verify-msrv.sh hyperlight-common hyperlight-guest hyperlight-guest-bin hyperlight-host hyperlight-component-util hyperlight-component-macro hyperlight-component-csharp hyperlight-guest-tracing
427429

428430
#####################
429431
### RUST EXAMPLES ###
@@ -548,7 +550,7 @@ ensure-cargo-llvm-cov:
548550

549551
# host-side packages to collect coverage for (guest/no_std crates are excluded because they
550552
# define #[panic_handler] and cannot be compiled for the host target under coverage instrumentation)
551-
coverage-packages := "-p hyperlight-common -p hyperlight-host -p hyperlight-testing -p hyperlight-component-util -p hyperlight-component-macro"
553+
coverage-packages := "-p hyperlight-common -p hyperlight-host -p hyperlight-testing -p hyperlight-component-util -p hyperlight-component-macro -p hyperlight-component-csharp"
552554

553555
# run all tests and examples with coverage instrumentation, collecting profdata without
554556
# generating a report. Mirrors test-like-ci + run-examples-like-ci to exercise all code paths

‎docs/component-wire-format.md‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Component wire format
22

33
Bindings generated from WIT exchange values in this format. It is used by
4-
`host_bindgen!`, `guest_bindgen!` and the hyperlight-wasm guest runtime. The
5-
host and guest must be built from the same Hyperlight version.
4+
`host_bindgen!`, `guest_bindgen!`, the hyperlight-wasm guest runtime, and the
5+
C# generator in `hyperlight-component-csharp`. The host and guest must be built
6+
from the same Hyperlight version.
67

78
## Calls
89

@@ -49,3 +50,47 @@ their parts with no alignment.
4950
lists encodings of the types in
5051
[`wire.wit`](../src/tests/component_wire/wire.wit). The Rust host bindings are
5152
tested against it.
53+
54+
## C# host bindings
55+
56+
`hyperlight_component_csharp::generate_from_path` generates C# for the host
57+
side of a world. The generated code depends on the `Hyperlight.Component`
58+
namespace in `hyperlight_component_csharp::RUNTIME_SOURCE`. It targets .NET 8
59+
or later.
60+
61+
For a world `w`, the generator emits:
62+
63+
* `WComponent`, constructed from an `IGuestCaller`. It has a property for each
64+
exported interface and a method for each function the world exports.
65+
* `WComponent.RegisterImports`, which registers host functions on an
66+
`IHostFunctionRegistry` from an implementation of each imported interface.
67+
* A C# interface `IName` for each imported interface `name`, and `IWImports`
68+
for functions the world imports.
69+
* Types for every record, variant, enum and flags in the world's interfaces.
70+
71+
| WIT type | C# type |
72+
|---|---|
73+
| `bool`, integers, floats | `bool`, `sbyte` to `ulong`, `float`, `double` |
74+
| `char` | `System.Text.Rune` |
75+
| `string` | `string` |
76+
| `list<u8>`, `list<u8, N>` | `byte[]` |
77+
| `list<T>`, `list<T, N>` | `IReadOnlyList<T>` |
78+
| `tuple<...>` | Value tuple |
79+
| `option<T>` | `T?`, or `Option<T>` when `T` is an `option` |
80+
| `result<T, E>` | `Result<T, E>`, with `Unit` for a missing type |
81+
| `record` | Sealed record with `required init` properties |
82+
| `variant` | Abstract record with a nested sealed record per case |
83+
| `enum` | `enum` with `uint` values |
84+
| `flags` | `[Flags]` enum with `uint` values |
85+
86+
Resources, async functions, futures, streams, maps and flags with more than 32
87+
members are rejected.
88+
89+
Records, variant cases, `Option` and `Result` compare lists by element.
90+
Variants and `Result` have `Match` and `Switch` methods that take one function
91+
per case. These give a compile error when a case is missing.
92+
93+
For a root namespace `Root`, types from `ns:pkg/iface` are in
94+
`Root.Ns.Pkg.Iface`. The C# interface or exports class for `iface` is in
95+
`Root.Ns.Pkg`. A world that uses two versions of a package gets the version
96+
after `Pkg`, such as `Root.Ns.Pkg.V1_0_0`.

‎docs/how-to-make-releases.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ When this job is done, a new [GitHub release](https://github.com/hyperlight-dev/
6363
- `hyperlight-guest-bin`
6464
- `hyperlight-component-util`
6565
- `hyperlight-component-macro`
66+
- `hyperlight-component-csharp`
6667
- `hyperlight-host`
6768
- `hyperlight-guest-tracing`
6869

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "hyperlight-component-csharp"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
license.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
readme = "README.md"
10+
description = """
11+
Generates C# host bindings for Hyperlight components from WIT worlds
12+
"""
13+
14+
[lib]
15+
name = "hyperlight_component_csharp"
16+
17+
[dependencies]
18+
hyperlight-component-util = { workspace = true }
19+
20+
[dev-dependencies]
21+
proc-macro2 = { version = "1.0.106" }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# hyperlight-component-csharp
2+
3+
Generates C# host bindings for a WIT world. A .NET host uses them to call a
4+
Hyperlight component guest and to serve its imports.
5+
6+
hyperlight-wasm's .NET packages use this crate to generate bindings at build
7+
time. Hyperlight itself has no .NET host.
8+
9+
## Usage
10+
11+
```rust
12+
let cs = hyperlight_component_csharp::generate_from_path(
13+
Path::new("wit/world.wit"),
14+
None, // world name, or None for the last world
15+
"MyApp.Guest", // root C# namespace
16+
)?;
17+
std::fs::write("Bindings.g.cs", cs)?;
18+
```
19+
20+
`generate` takes a `hyperlight_component_util::util::WitSource`, the input
21+
type of the bindgen macros. It reads the world through the same component type
22+
elaboration as the macros.
23+
24+
For a world `w`, the output has:
25+
26+
* `WComponent`, which calls the guest's exports.
27+
* `WComponent.RegisterImports`, which registers host implementations of the
28+
guest's imports.
29+
* A C# type for each record, variant, enum and flags type.
30+
31+
See [docs/component-wire-format.md](../../docs/component-wire-format.md) for
32+
the type mapping and the wire format.
33+
34+
## Runtime.cs
35+
36+
The generated code needs the C# types in [`src/Runtime.cs`](src/Runtime.cs).
37+
The Rust constant `hyperlight_component_csharp::RUNTIME_SOURCE` holds the text
38+
of this file, so tools can write it out next to the generated code.
39+
40+
A .NET host library, such as hyperlight-wasm's, includes this file in its build.
41+
It implements the file's `IGuestCaller` and `IHostFunctionRegistry` interfaces
42+
with its sandbox.

0 commit comments

Comments
 (0)