Skip to main content

hyperlight_component_csharp/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 The Hyperlight Authors.
3
4//! C# bindings for the host side of a WIT world.
5//!
6//! The generator walks the elaborated component type from
7//! [`hyperlight_component_util`], which the Rust bindings also use. The
8//! generated code encodes values in the format described in
9//! `docs/component-wire-format.md` and calls guest functions through the
10//! interfaces in [`RUNTIME_SOURCE`]. It covers every WIT value type and
11//! resources the host defines. Resources the guest defines, async
12//! functions, futures, streams, maps and flags with more than 32 members
13//! are rejected.
14
15mod codec;
16mod host;
17mod names;
18mod types;
19
20use std::collections::{HashMap, HashSet, VecDeque};
21use std::fmt;
22use std::path::Path;
23
24use hyperlight_component_util::emit::{State, split_wit_name};
25use hyperlight_component_util::etypes::{Component, ExternDesc, Value};
26use hyperlight_component_util::util::{WitSource, read_wit_type};
27use names::{Src, pascal};
28
29/// C# source for the `Hyperlight.Component` namespace that generated
30/// bindings depend on.
31pub const RUNTIME_SOURCE: &str = include_str!("Runtime.cs");
32
33/// Namespace of [`RUNTIME_SOURCE`].
34const RT: &str = "global::Hyperlight.Component";
35
36/// A WIT input the C# bindings cannot be generated for.
37#[derive(Debug)]
38pub struct Error(String);
39
40impl fmt::Display for Error {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.write_str(&self.0)
43    }
44}
45
46impl std::error::Error for Error {}
47
48fn error(msg: impl Into<String>) -> Error {
49    Error(msg.into())
50}
51
52/// Generate C# bindings for a world in the `.wit` file or directory at
53/// `path`. A relative `path` is resolved against `CARGO_MANIFEST_DIR` when
54/// set, and the working directory otherwise. See [`generate`] for `world`
55/// and `namespace`.
56pub fn generate_from_path(
57    path: &Path,
58    world: Option<&str>,
59    namespace: &str,
60) -> Result<String, Error> {
61    generate(WitSource::Wit(path.to_path_buf()), world, namespace)
62}
63
64/// Generate C# bindings for the world named `world` in `source`, or its
65/// last world. All generated types are placed under `namespace`.
66///
67/// # Panics
68///
69/// If `source` cannot be read or elaborated, as the bindgen macros do.
70pub fn generate(source: WitSource, world: Option<&str>, namespace: &str) -> Result<String, Error> {
71    if !namespace.split('.').all(names::is_identifier) {
72        return Err(error(format!("invalid C# namespace '{namespace}'")));
73    }
74    let world = world.map(str::to_string);
75    read_wit_type(source, world, |name, ct| emit(&name, ct, namespace))
76}
77
78type S<'a, 'b> = State<'a, 'b, Root<'b>>;
79
80fn emit(name: &str, ct: &Component<'_>, namespace: &str) -> Result<String, Error> {
81    let mut root = Root::new(namespace, name, ct);
82    // Export types refer to the existential variables, then the
83    // universal ones. Import types refer to the universal ones.
84    let mut bound_vars = ct
85        .instance
86        .evars
87        .iter()
88        .rev()
89        .chain(ct.uvars.iter().rev())
90        .cloned()
91        .collect::<VecDeque<_>>();
92    let mut vars_needs_vars = VecDeque::new();
93    let mut s = State::new(
94        &mut root,
95        &mut bound_vars,
96        &mut vars_needs_vars,
97        false,
98        false,
99    );
100    types::emit_toplevel(&mut s, ct)?;
101    host::emit_toplevel(&mut s, ct)?;
102    Ok(root.render())
103}
104
105/// A record, variant, enum or flags type declared in C#.
106struct Decl<'b> {
107    /// Absolute index of the type variable that defines it
108    var: usize,
109    /// Other type variables for the same WIT type, from a second copy of
110    /// its interface
111    aliases: Vec<usize>,
112    /// Definition, relative to the variables after `var`
113    value: Value<'b>,
114    /// Fully qualified C# namespace
115    namespace: String,
116    name: String,
117    /// Suffix of its codec methods
118    mangled: String,
119    /// Whether the world declares it outside any interface
120    world_level: bool,
121    /// Instance whose type it is, which identifies the WIT type with
122    /// its name
123    instance: Option<String>,
124}
125
126/// A resource type the host defines. The host implements its C#
127/// interface, and the guest holds its values by handle.
128struct ResourceDecl {
129    /// Absolute index of the type variable that introduces it
130    var: usize,
131    /// WIT name of the resource
132    wit_name: String,
133    /// Instance that defines it, or `None` for the world itself
134    instance: Option<String>,
135    /// Fully qualified C# namespace
136    namespace: String,
137    /// C# interface name, e.g. `IFields`
138    name: String,
139    /// Name of its handle table in the resources class
140    table: String,
141}
142
143/// The C# source for one world, grouped by namespace, and the names of
144/// its declared types.
145pub(crate) struct Root<'b> {
146    /// Root C# namespace
147    ns: String,
148    /// WIT name of the world
149    world: String,
150    /// C# name of the world, e.g. `Wire` for `test:wire/wire@0.1.0`
151    world_type: String,
152    /// Fully qualified name of the static class holding codec methods
153    codec: String,
154    /// Fully qualified name of the class holding resource tables
155    resources_class: String,
156    decls: Vec<Decl<'b>>,
157    resources: Vec<ResourceDecl>,
158    /// `namespace:package` names the world uses at several versions
159    versioned: HashSet<String>,
160    /// Package namespaces and version of the world, which also hold
161    /// its inline interfaces
162    world_package: (Vec<String>, String),
163    /// Counter for unique local variable names
164    temps: usize,
165    /// Source of each namespace, in the order first written
166    namespaces: Vec<(String, Src)>,
167    /// Methods of the codec class
168    codec_methods: Src,
169}
170
171impl<'b> Root<'b> {
172    fn new(ns: &str, world: &str, ct: &Component<'_>) -> Self {
173        let wn = split_wit_name(world);
174        let world_type = pascal(wn.name);
175        let world_package = (
176            wn.namespaces
177                .iter()
178                .map(|n| n.to_string())
179                .collect::<Vec<_>>(),
180            wn._version.join("@"),
181        );
182        let mut versions = HashMap::<String, HashSet<String>>::new();
183        for ed in ct.imports.iter().chain(&ct.instance.unqualified.exports) {
184            if let ExternDesc::Instance(_) = ed.desc {
185                let (namespaces, version) = package(&world_package, ed.kebab_name);
186                if !namespaces.is_empty() {
187                    versions
188                        .entry(namespaces.join(":"))
189                        .or_default()
190                        .insert(version);
191                }
192            }
193        }
194        Root {
195            ns: ns.to_string(),
196            world: world.to_string(),
197            codec: format!("global::{ns}.{world_type}Codec"),
198            resources_class: format!("global::{ns}.{world_type}Resources"),
199            world_type,
200            decls: Vec::new(),
201            resources: Vec::new(),
202            versioned: versions
203                .into_iter()
204                .filter(|(_, v)| v.len() > 1)
205                .map(|(k, _)| k)
206                .collect(),
207            world_package,
208            temps: 0,
209            namespaces: Vec::new(),
210            codec_methods: Src::default(),
211        }
212    }
213
214    /// Append a declaration to the namespace `ns`.
215    fn add(&mut self, ns: String, src: Src) {
216        match self.namespaces.iter_mut().find(|(n, _)| *n == ns) {
217            Some((_, existing)) => {
218                existing.line("");
219                existing.append(src);
220            }
221            None => self.namespaces.push((ns, src)),
222        }
223    }
224
225    fn add_codec_methods(&mut self, src: Src) {
226        if !self.codec_methods.is_empty() {
227            self.codec_methods.line("");
228        }
229        self.codec_methods.append(src);
230    }
231
232    fn render(mut self) -> String {
233        let name = self.codec.rsplit('.').next().unwrap().to_string();
234        let mut codec = Src::default();
235        codec.line(format!("internal static class {name}"));
236        codec.open();
237        codec.append(std::mem::take(&mut self.codec_methods));
238        codec.close();
239        self.add(self.ns.clone(), codec);
240
241        let mut out = Src::default();
242        out.line("// <auto-generated>");
243        out.line(format!(
244            "// Generated by hyperlight-component-csharp from WIT world {}.",
245            self.world
246        ));
247        out.line("// </auto-generated>");
248        out.line("");
249        out.line("#nullable enable");
250        for (ns, src) in self.namespaces {
251            out.line("");
252            out.line(format!("namespace {ns}"));
253            out.open();
254            out.append(src);
255            out.close();
256        }
257        out.into_string()
258    }
259
260    /// Namespace of an instance's package relative to the root namespace,
261    /// e.g. `Wasi.Http`. The version is added, e.g. `Wasi.Http.V0_2_0`,
262    /// when the world uses the package at several versions.
263    fn package_namespace(&self, instance: &str) -> Option<String> {
264        let (namespaces, version) = package(&self.world_package, instance);
265        if namespaces.is_empty() {
266            return None;
267        }
268        let mut ns = namespaces
269            .iter()
270            .map(|n| pascal(n))
271            .collect::<Vec<_>>()
272            .join(".");
273        if self.versioned.contains(&namespaces.join(":")) {
274            let version = version
275                .chars()
276                .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
277                .collect::<String>();
278            ns.push_str(&format!(".V{version}"));
279        }
280        Some(ns)
281    }
282
283    /// Namespace of an instance's types relative to the root namespace,
284    /// e.g. `Wasi.Http.Types`.
285    fn interface_namespace(&self, instance: &str) -> String {
286        let name = pascal(split_wit_name(instance).name);
287        match self.package_namespace(instance) {
288            Some(pkg) => format!("{pkg}.{name}"),
289            None => name,
290        }
291    }
292
293    fn qualify(&self, relative: Option<String>) -> String {
294        match relative {
295            Some(ns) => format!("{}.{ns}", self.ns),
296            None => self.ns.clone(),
297        }
298    }
299
300    fn temp(&mut self) -> String {
301        self.temps += 1;
302        format!("__t{}", self.temps - 1)
303    }
304
305    /// Index in `decls` of the type declared for the variable `var`.
306    fn decl_index(&self, var: usize) -> Option<usize> {
307        self.decls
308            .iter()
309            .position(|d| d.var == var || d.aliases.contains(&var))
310    }
311
312    /// Fully qualified C# name of a declared type.
313    fn type_name(&self, d: &Decl) -> String {
314        format!("global::{}.{}", d.namespace, d.name)
315    }
316
317    /// Index in `resources` of the resource introduced by the variable `var`.
318    fn resource_index(&self, var: usize) -> Option<usize> {
319        self.resources.iter().position(|r| r.var == var)
320    }
321
322    /// Expression for the table of resource `i`, given an expression for
323    /// the resources object.
324    fn resource_table(&self, i: usize, resources: &str) -> String {
325        format!(
326            "(({}){resources}!).{}",
327            self.resources_class, self.resources[i].table
328        )
329    }
330}
331
332/// Package namespaces and version of an instance. Inline interfaces
333/// belong to the world's package.
334fn package(world_package: &(Vec<String>, String), instance: &str) -> (Vec<String>, String) {
335    let wn = split_wit_name(instance);
336    if wn.namespaces.is_empty() {
337        return world_package.clone();
338    }
339    (
340        wn.namespaces.iter().map(|n| n.to_string()).collect(),
341        wn._version.join("@"),
342    )
343}
344
345#[cfg(test)]
346mod tests;