hyperlight_component_csharp/
lib.rs1mod 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
29pub const RUNTIME_SOURCE: &str = include_str!("Runtime.cs");
32
33const RT: &str = "global::Hyperlight.Component";
35
36#[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
52pub 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
64pub 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 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
105struct Decl<'b> {
107 var: usize,
109 aliases: Vec<usize>,
112 value: Value<'b>,
114 namespace: String,
116 name: String,
117 mangled: String,
119 world_level: bool,
121 instance: Option<String>,
124}
125
126struct ResourceDecl {
129 var: usize,
131 wit_name: String,
133 instance: Option<String>,
135 namespace: String,
137 name: String,
139 table: String,
141}
142
143pub(crate) struct Root<'b> {
146 ns: String,
148 world: String,
150 world_type: String,
152 codec: String,
154 resources_class: String,
156 decls: Vec<Decl<'b>>,
157 resources: Vec<ResourceDecl>,
158 versioned: HashSet<String>,
160 world_package: (Vec<String>, String),
163 temps: usize,
165 namespaces: Vec<(String, Src)>,
167 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 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 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 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 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 fn type_name(&self, d: &Decl) -> String {
314 format!("global::{}.{}", d.namespace, d.name)
315 }
316
317 fn resource_index(&self, var: usize) -> Option<usize> {
319 self.resources.iter().position(|r| r.var == var)
320 }
321
322 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
332fn 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;