1use crate::type_map::csharp_type;
2use alef_codegen::naming::to_csharp_name;
3use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
4use alef_core::config::{AdapterPattern, AlefConfig, Language, resolve_output_dir};
5use alef_core::hash::{self, CommentStyle};
6use alef_core::ir::{ApiSurface, EnumDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
7use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
8use std::collections::HashSet;
9use std::path::PathBuf;
10
11pub struct CsharpBackend;
12
13impl CsharpBackend {
14 }
16
17impl Backend for CsharpBackend {
18 fn name(&self) -> &str {
19 "csharp"
20 }
21
22 fn language(&self) -> Language {
23 Language::Csharp
24 }
25
26 fn capabilities(&self) -> Capabilities {
27 Capabilities {
28 supports_async: true,
29 supports_classes: true,
30 supports_enums: true,
31 supports_option: true,
32 supports_result: true,
33 ..Capabilities::default()
34 }
35 }
36
37 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
38 let namespace = config.csharp_namespace();
39 let prefix = config.ffi_prefix();
40 let lib_name = config.ffi_lib_name();
41
42 let bridge_param_names: HashSet<String> = config
45 .trait_bridges
46 .iter()
47 .filter_map(|b| b.param_name.clone())
48 .collect();
49 let bridge_type_aliases: HashSet<String> = config
50 .trait_bridges
51 .iter()
52 .filter_map(|b| b.type_alias.clone())
53 .collect();
54 let has_visitor_bridge = !config.trait_bridges.is_empty();
55
56 let streaming_methods: HashSet<String> = config
59 .adapters
60 .iter()
61 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
62 .map(|a| a.name.clone())
63 .collect();
64
65 let output_dir = resolve_output_dir(
66 config.output.csharp.as_ref(),
67 &config.crate_config.name,
68 "packages/csharp/",
69 );
70
71 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
72
73 let mut files = Vec::new();
74
75 files.push(GeneratedFile {
77 path: base_path.join("NativeMethods.cs"),
78 content: strip_trailing_whitespace(&gen_native_methods(
79 api,
80 &namespace,
81 &lib_name,
82 &prefix,
83 &bridge_param_names,
84 &bridge_type_aliases,
85 has_visitor_bridge,
86 &config.trait_bridges,
87 &streaming_methods,
88 )),
89 generated_header: true,
90 });
91
92 if !api.errors.is_empty() {
94 for error in &api.errors {
95 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
96 for (class_name, content) in error_files {
97 files.push(GeneratedFile {
98 path: base_path.join(format!("{}.cs", class_name)),
99 content: strip_trailing_whitespace(&content),
100 generated_header: false, });
102 }
103 }
104 }
105
106 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
108 if api.errors.is_empty()
109 || !api
110 .errors
111 .iter()
112 .any(|e| format!("{}Exception", e.name) == exception_class_name)
113 {
114 files.push(GeneratedFile {
115 path: base_path.join(format!("{}.cs", exception_class_name)),
116 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
117 generated_header: true,
118 });
119 }
120
121 let base_class_name = api.crate_name.to_pascal_case();
123 let wrapper_class_name = if namespace == base_class_name {
124 format!("{}Lib", base_class_name)
125 } else {
126 base_class_name
127 };
128 files.push(GeneratedFile {
129 path: base_path.join(format!("{}.cs", wrapper_class_name)),
130 content: strip_trailing_whitespace(&gen_wrapper_class(
131 api,
132 &namespace,
133 &wrapper_class_name,
134 &exception_class_name,
135 &prefix,
136 &bridge_param_names,
137 &bridge_type_aliases,
138 has_visitor_bridge,
139 &streaming_methods,
140 )),
141 generated_header: true,
142 });
143
144 if has_visitor_bridge {
146 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
147 files.push(GeneratedFile {
148 path: base_path.join(filename),
149 content: strip_trailing_whitespace(&content),
150 generated_header: true,
151 });
152 }
153 }
154
155 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
157 if typ.is_opaque {
158 let type_filename = typ.name.to_pascal_case();
159 files.push(GeneratedFile {
160 path: base_path.join(format!("{}.cs", type_filename)),
161 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
162 generated_header: true,
163 });
164 }
165 }
166
167 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
169
170 let complex_enums: HashSet<String> = api
175 .enums
176 .iter()
177 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
178 .map(|e| e.name.to_pascal_case())
179 .collect();
180
181 let custom_converter_enums: HashSet<String> = api
185 .enums
186 .iter()
187 .filter(|e| {
188 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
190 || e.variants.iter().any(|v| {
192 if let Some(ref rename) = v.serde_rename {
193 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
194 rename != &snake
195 } else {
196 false
197 }
198 })
199 })
200 .map(|e| e.name.to_pascal_case())
201 .collect();
202
203 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
205
206 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
208 if !typ.is_opaque {
209 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
212 if !typ.fields.is_empty() && !has_named_fields {
213 continue;
214 }
215 if has_visitor_bridge && (typ.name == "NodeContext" || typ.name == "VisitResult") {
217 continue;
218 }
219
220 let type_filename = typ.name.to_pascal_case();
221 files.push(GeneratedFile {
222 path: base_path.join(format!("{}.cs", type_filename)),
223 content: strip_trailing_whitespace(&gen_record_type(
224 typ,
225 &namespace,
226 &enum_names,
227 &complex_enums,
228 &custom_converter_enums,
229 &lang_rename_all,
230 )),
231 generated_header: true,
232 });
233 }
234 }
235
236 for enum_def in &api.enums {
238 if has_visitor_bridge && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
240 continue;
241 }
242 let enum_filename = enum_def.name.to_pascal_case();
243 files.push(GeneratedFile {
244 path: base_path.join(format!("{}.cs", enum_filename)),
245 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
246 generated_header: true,
247 });
248 }
249
250 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
252
253 Ok(files)
254 }
255
256 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
261 Ok(vec![])
263 }
264
265 fn build_config(&self) -> Option<BuildConfig> {
266 Some(BuildConfig {
267 tool: "dotnet",
268 crate_suffix: "",
269 build_dep: BuildDependency::Ffi,
270 post_build: vec![],
271 })
272 }
273}
274
275fn is_tuple_field(field: &FieldDef) -> bool {
277 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
278 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
279}
280
281fn strip_trailing_whitespace(content: &str) -> String {
283 let mut result: String = content
284 .lines()
285 .map(|line| line.trim_end())
286 .collect::<Vec<_>>()
287 .join("\n");
288 if !result.ends_with('\n') {
289 result.push('\n');
290 }
291 result
292}
293
294fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
305 match ty {
306 TypeRef::Unit => "void",
307 TypeRef::Primitive(PrimitiveType::Bool) => "int",
309 TypeRef::Primitive(PrimitiveType::U8) => "byte",
311 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
312 TypeRef::Primitive(PrimitiveType::U32) => "uint",
313 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
314 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
315 TypeRef::Primitive(PrimitiveType::I16) => "short",
316 TypeRef::Primitive(PrimitiveType::I32) => "int",
317 TypeRef::Primitive(PrimitiveType::I64) => "long",
318 TypeRef::Primitive(PrimitiveType::F32) => "float",
319 TypeRef::Primitive(PrimitiveType::F64) => "double",
320 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
321 TypeRef::Primitive(PrimitiveType::Isize) => "long",
322 TypeRef::Duration => "ulong",
324 TypeRef::String
326 | TypeRef::Char
327 | TypeRef::Bytes
328 | TypeRef::Optional(_)
329 | TypeRef::Vec(_)
330 | TypeRef::Map(_, _)
331 | TypeRef::Named(_)
332 | TypeRef::Path
333 | TypeRef::Json => "IntPtr",
334 }
335}
336
337fn returns_string(ty: &TypeRef) -> bool {
339 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
340}
341
342fn returns_bool_via_int(ty: &TypeRef) -> bool {
344 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
345}
346
347fn returns_json_object(ty: &TypeRef) -> bool {
349 matches!(
350 ty,
351 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
352 )
353}
354
355fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
366 match ty {
367 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
368 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
370 TypeRef::Unit => "void",
371 TypeRef::Primitive(PrimitiveType::Bool) => "int",
372 TypeRef::Primitive(PrimitiveType::U8) => "byte",
373 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
374 TypeRef::Primitive(PrimitiveType::U32) => "uint",
375 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
376 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
377 TypeRef::Primitive(PrimitiveType::I16) => "short",
378 TypeRef::Primitive(PrimitiveType::I32) => "int",
379 TypeRef::Primitive(PrimitiveType::I64) => "long",
380 TypeRef::Primitive(PrimitiveType::F32) => "float",
381 TypeRef::Primitive(PrimitiveType::F64) => "double",
382 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
383 TypeRef::Primitive(PrimitiveType::Isize) => "long",
384 TypeRef::Duration => "ulong",
385 }
386}
387
388fn is_bridge_param(
395 param: &alef_core::ir::ParamDef,
396 bridge_param_names: &HashSet<String>,
397 bridge_type_aliases: &HashSet<String>,
398) -> bool {
399 bridge_param_names.contains(¶m.name)
400 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
401}
402
403#[allow(clippy::too_many_arguments)]
404fn gen_native_methods(
405 api: &ApiSurface,
406 namespace: &str,
407 lib_name: &str,
408 prefix: &str,
409 bridge_param_names: &HashSet<String>,
410 bridge_type_aliases: &HashSet<String>,
411 has_visitor_bridge: bool,
412 trait_bridges: &[alef_core::config::TraitBridgeConfig],
413 streaming_methods: &HashSet<String>,
414) -> String {
415 let mut out = hash::header(CommentStyle::DoubleSlash);
416 out.push_str("using System;\n");
417 out.push_str("using System.Runtime.InteropServices;\n\n");
418
419 out.push_str(&format!("namespace {};\n\n", namespace));
420
421 out.push_str("internal static partial class NativeMethods\n{\n");
422 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
423
424 let mut emitted: HashSet<String> = HashSet::new();
427
428 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
431
432 let mut opaque_param_types: HashSet<String> = HashSet::new();
436 let mut opaque_return_types: HashSet<String> = HashSet::new();
437
438 for func in &api.functions {
439 for param in &func.params {
440 if let TypeRef::Named(name) = ¶m.ty {
441 if !enum_names.contains(name) {
442 opaque_param_types.insert(name.clone());
443 }
444 }
445 }
446 if let TypeRef::Named(name) = &func.return_type {
447 if !enum_names.contains(name) {
448 opaque_return_types.insert(name.clone());
449 }
450 }
451 }
452 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
453 for method in &typ.methods {
454 if streaming_methods.contains(&method.name) {
455 continue;
456 }
457 for param in &method.params {
458 if let TypeRef::Named(name) = ¶m.ty {
459 if !enum_names.contains(name) {
460 opaque_param_types.insert(name.clone());
461 }
462 }
463 }
464 if let TypeRef::Named(name) = &method.return_type {
465 if !enum_names.contains(name) {
466 opaque_return_types.insert(name.clone());
467 }
468 }
469 }
470 }
471
472 let true_opaque_types: HashSet<String> = api
474 .types
475 .iter()
476 .filter(|t| t.is_opaque)
477 .map(|t| t.name.clone())
478 .collect();
479
480 let mut sorted_param_types: Vec<&String> = opaque_param_types.iter().collect();
484 sorted_param_types.sort();
485 for type_name in sorted_param_types {
486 let snake = type_name.to_snake_case();
487 if !true_opaque_types.contains(type_name) {
488 let from_json_entry = format!("{prefix}_{snake}_from_json");
489 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
490 if emitted.insert(from_json_entry.clone()) {
491 out.push_str(&format!(
492 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
493 ));
494 out.push_str(&format!(
495 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
496 ));
497 }
498 }
499 let free_entry = format!("{prefix}_{snake}_free");
500 let free_cs = format!("{}Free", type_name.to_pascal_case());
501 if emitted.insert(free_entry.clone()) {
502 out.push_str(&format!(
503 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
504 ));
505 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
506 }
507 }
508
509 let mut sorted_return_types: Vec<&String> = opaque_return_types.iter().collect();
512 sorted_return_types.sort();
513 for type_name in sorted_return_types {
514 let snake = type_name.to_snake_case();
515 if !true_opaque_types.contains(type_name) {
516 let to_json_entry = format!("{prefix}_{snake}_to_json");
517 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
518 if emitted.insert(to_json_entry.clone()) {
519 out.push_str(&format!(
520 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
521 ));
522 out.push_str(&format!(
523 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
524 ));
525 }
526 }
527 let free_entry = format!("{prefix}_{snake}_free");
528 let free_cs = format!("{}Free", type_name.to_pascal_case());
529 if emitted.insert(free_entry.clone()) {
530 out.push_str(&format!(
531 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
532 ));
533 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
534 }
535 }
536
537 for func in &api.functions {
539 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
540 if emitted.insert(c_func_name.clone()) {
541 out.push_str(&gen_pinvoke_for_func(
542 &c_func_name,
543 func,
544 bridge_param_names,
545 bridge_type_aliases,
546 ));
547 }
548 }
549
550 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
553 let type_snake = typ.name.to_snake_case();
554 for method in &typ.methods {
555 if streaming_methods.contains(&method.name) {
556 continue;
557 }
558 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
559 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
563 if emitted.insert(c_method_name.clone()) {
564 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
565 }
566 }
567 }
568
569 out.push_str(&format!(
571 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
572 ));
573 out.push_str(" internal static extern int LastErrorCode();\n\n");
574
575 out.push_str(&format!(
576 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
577 ));
578 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
579
580 out.push_str(&format!(
581 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
582 ));
583 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
584
585 if has_visitor_bridge {
587 out.push('\n');
588 out.push_str(&crate::gen_visitor::gen_native_methods_visitor(
589 namespace, lib_name, prefix,
590 ));
591 }
592
593 if !trait_bridges.is_empty() {
595 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
597
598 let bridges: Vec<_> = trait_bridges
600 .iter()
601 .filter_map(|config| {
602 let trait_name = config.trait_name.clone();
603 trait_defs
604 .iter()
605 .find(|t| t.name == trait_name)
606 .map(|trait_def| (trait_name, config, *trait_def))
607 })
608 .collect();
609
610 if !bridges.is_empty() {
611 out.push('\n');
612 out.push_str(&crate::trait_bridge::gen_native_methods_trait_bridges(
613 namespace, prefix, &bridges,
614 ));
615 }
616 }
617
618 out.push_str("}\n");
619
620 out
621}
622
623fn gen_pinvoke_for_func(
624 c_name: &str,
625 func: &FunctionDef,
626 bridge_param_names: &HashSet<String>,
627 bridge_type_aliases: &HashSet<String>,
628) -> String {
629 let cs_name = to_csharp_name(&func.name);
630 let mut out =
631 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
632 out.push_str(" internal static extern ");
633
634 out.push_str(pinvoke_return_type(&func.return_type));
636
637 out.push_str(&format!(" {}(", cs_name));
638
639 let visible_params: Vec<_> = func
642 .params
643 .iter()
644 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
645 .collect();
646
647 if visible_params.is_empty() {
648 out.push_str(");\n\n");
649 } else {
650 out.push('\n');
651 for (i, param) in visible_params.iter().enumerate() {
652 out.push_str(" ");
653 let pinvoke_ty = pinvoke_param_type(¶m.ty);
654 if pinvoke_ty == "string" {
655 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
656 }
657 let param_name = param.name.to_lower_camel_case();
658 out.push_str(&format!("{pinvoke_ty} {param_name}"));
659
660 if i < visible_params.len() - 1 {
661 out.push(',');
662 }
663 out.push('\n');
664 }
665 out.push_str(" );\n\n");
666 }
667
668 out
669}
670
671fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
672 let mut out =
673 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
674 out.push_str(" internal static extern ");
675
676 out.push_str(pinvoke_return_type(&method.return_type));
678
679 out.push_str(&format!(" {}(", cs_name));
680
681 if method.params.is_empty() {
682 out.push_str(");\n\n");
683 } else {
684 out.push('\n');
685 for (i, param) in method.params.iter().enumerate() {
686 out.push_str(" ");
687 let pinvoke_ty = pinvoke_param_type(¶m.ty);
688 if pinvoke_ty == "string" {
689 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
690 }
691 let param_name = param.name.to_lower_camel_case();
692 out.push_str(&format!("{pinvoke_ty} {param_name}"));
693
694 if i < method.params.len() - 1 {
695 out.push(',');
696 }
697 out.push('\n');
698 }
699 out.push_str(" );\n\n");
700 }
701
702 out
703}
704
705fn gen_exception_class(namespace: &str, class_name: &str) -> String {
706 let mut out = hash::header(CommentStyle::DoubleSlash);
707 out.push_str("using System;\n\n");
708
709 out.push_str(&format!("namespace {};\n\n", namespace));
710
711 out.push_str(&format!("public class {} : Exception\n", class_name));
712 out.push_str("{\n");
713 out.push_str(" public int Code { get; }\n\n");
714 out.push_str(&format!(
715 " public {}(int code, string message) : base(message)\n",
716 class_name
717 ));
718 out.push_str(" {\n");
719 out.push_str(" Code = code;\n");
720 out.push_str(" }\n");
721 out.push_str("}\n");
722
723 out
724}
725
726#[allow(clippy::too_many_arguments)]
727fn gen_wrapper_class(
728 api: &ApiSurface,
729 namespace: &str,
730 class_name: &str,
731 exception_name: &str,
732 prefix: &str,
733 bridge_param_names: &HashSet<String>,
734 bridge_type_aliases: &HashSet<String>,
735 has_visitor_bridge: bool,
736 streaming_methods: &HashSet<String>,
737) -> String {
738 let mut out = hash::header(CommentStyle::DoubleSlash);
739 out.push_str("using System;\n");
740 out.push_str("using System.Collections.Generic;\n");
741 out.push_str("using System.Runtime.InteropServices;\n");
742 out.push_str("using System.Text.Json;\n");
743 out.push_str("using System.Text.Json.Serialization;\n");
744 out.push_str("using System.Threading.Tasks;\n\n");
745
746 out.push_str(&format!("namespace {};\n\n", namespace));
747
748 out.push_str(&format!("public static class {}\n", class_name));
749 out.push_str("{\n");
750 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
751 out.push_str(" {\n");
752 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
753 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
754 out.push_str(" };\n\n");
755
756 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
758
759 let true_opaque_types: HashSet<String> = api
761 .types
762 .iter()
763 .filter(|t| t.is_opaque)
764 .map(|t| t.name.clone())
765 .collect();
766
767 for func in &api.functions {
769 out.push_str(&gen_wrapper_function(
770 func,
771 exception_name,
772 prefix,
773 &enum_names,
774 &true_opaque_types,
775 bridge_param_names,
776 bridge_type_aliases,
777 ));
778 }
779
780 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
783 if typ.is_opaque {
785 continue;
786 }
787 for method in &typ.methods {
788 if streaming_methods.contains(&method.name) {
789 continue;
790 }
791 out.push_str(&gen_wrapper_method(
792 method,
793 exception_name,
794 prefix,
795 &typ.name,
796 &enum_names,
797 &true_opaque_types,
798 bridge_param_names,
799 bridge_type_aliases,
800 ));
801 }
802 }
803
804 if has_visitor_bridge {
806 out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
807 exception_name,
808 prefix,
809 ));
810 }
811
812 out.push_str(" private static ");
814 out.push_str(&format!("{} GetLastError()\n", exception_name));
815 out.push_str(" {\n");
816 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
817 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
818 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
819 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
820 out.push_str(" }\n");
821
822 out.push_str("}\n");
823
824 out
825}
826
827fn emit_named_param_setup(
844 out: &mut String,
845 params: &[alef_core::ir::ParamDef],
846 indent: &str,
847 true_opaque_types: &HashSet<String>,
848) {
849 for param in params {
850 let param_name = param.name.to_lower_camel_case();
851 let json_var = format!("{param_name}Json");
852 let handle_var = format!("{param_name}Handle");
853
854 match ¶m.ty {
855 TypeRef::Named(type_name) => {
856 if true_opaque_types.contains(type_name) {
859 continue;
860 }
861 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
862 if param.optional {
863 out.push_str(&format!(
864 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
865 ));
866 } else {
867 out.push_str(&format!(
868 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
869 ));
870 }
871 out.push_str(&format!(
872 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
873 ));
874 }
875 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
876 out.push_str(&format!(
878 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
879 ));
880 out.push_str(&format!(
881 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
882 ));
883 }
884 TypeRef::Bytes => {
885 out.push_str(&format!(
887 "{indent}var {handle_var} = GCHandle.Alloc({param_name}, GCHandleType.Pinned);\n"
888 ));
889 }
890 _ => {}
891 }
892 }
893}
894
895fn returns_ptr(ty: &TypeRef) -> bool {
898 matches!(
899 ty,
900 TypeRef::String
901 | TypeRef::Char
902 | TypeRef::Path
903 | TypeRef::Json
904 | TypeRef::Named(_)
905 | TypeRef::Vec(_)
906 | TypeRef::Map(_, _)
907 | TypeRef::Bytes
908 | TypeRef::Optional(_)
909 )
910}
911
912fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
918 match ty {
919 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
920 let bang = if optional { "!" } else { "" };
922 format!("{param_name}{bang}.Handle")
923 }
924 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
925 format!("{param_name}Handle")
926 }
927 TypeRef::Bytes => {
928 format!("{param_name}Handle.AddrOfPinnedObject()")
929 }
930 ty => {
931 if optional {
932 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
937 if needs_value_unwrap {
938 format!("{param_name}.Value")
939 } else {
940 format!("{param_name}!")
941 }
942 } else {
943 param_name.to_string()
944 }
945 }
946 }
947}
948
949fn emit_named_param_teardown(
954 out: &mut String,
955 params: &[alef_core::ir::ParamDef],
956 true_opaque_types: &HashSet<String>,
957) {
958 for param in params {
959 let param_name = param.name.to_lower_camel_case();
960 let handle_var = format!("{param_name}Handle");
961 match ¶m.ty {
962 TypeRef::Named(type_name) => {
963 if true_opaque_types.contains(type_name) {
964 continue;
966 }
967 let free_method = format!("{}Free", type_name.to_pascal_case());
968 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
969 }
970 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
971 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
972 }
973 TypeRef::Bytes => {
974 out.push_str(&format!(" {handle_var}.Free();\n"));
975 }
976 _ => {}
977 }
978 }
979}
980
981fn emit_named_param_teardown_indented(
983 out: &mut String,
984 params: &[alef_core::ir::ParamDef],
985 indent: &str,
986 true_opaque_types: &HashSet<String>,
987) {
988 for param in params {
989 let param_name = param.name.to_lower_camel_case();
990 let handle_var = format!("{param_name}Handle");
991 match ¶m.ty {
992 TypeRef::Named(type_name) => {
993 if true_opaque_types.contains(type_name) {
994 continue;
996 }
997 let free_method = format!("{}Free", type_name.to_pascal_case());
998 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
999 }
1000 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1001 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
1002 }
1003 TypeRef::Bytes => {
1004 out.push_str(&format!("{indent}{handle_var}.Free();\n"));
1005 }
1006 _ => {}
1007 }
1008 }
1009}
1010
1011fn gen_wrapper_function(
1012 func: &FunctionDef,
1013 _exception_name: &str,
1014 _prefix: &str,
1015 enum_names: &HashSet<String>,
1016 true_opaque_types: &HashSet<String>,
1017 bridge_param_names: &HashSet<String>,
1018 bridge_type_aliases: &HashSet<String>,
1019) -> String {
1020 let mut out = String::with_capacity(1024);
1021
1022 let visible_params: Vec<alef_core::ir::ParamDef> = func
1024 .params
1025 .iter()
1026 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1027 .cloned()
1028 .collect();
1029
1030 if !func.doc.is_empty() {
1032 out.push_str(" /// <summary>\n");
1033 for line in func.doc.lines() {
1034 out.push_str(&format!(" /// {}\n", line));
1035 }
1036 out.push_str(" /// </summary>\n");
1037 for param in &visible_params {
1038 out.push_str(&format!(
1039 " /// <param name=\"{}\">{}</param>\n",
1040 param.name.to_lower_camel_case(),
1041 if param.optional { "Optional." } else { "" }
1042 ));
1043 }
1044 }
1045
1046 out.push_str(" public static ");
1047
1048 if func.is_async {
1050 if func.return_type == TypeRef::Unit {
1051 out.push_str("async Task");
1052 } else {
1053 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
1054 }
1055 } else if func.return_type == TypeRef::Unit {
1056 out.push_str("void");
1057 } else {
1058 out.push_str(&csharp_type(&func.return_type));
1059 }
1060
1061 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
1062 out.push('(');
1063
1064 for (i, param) in visible_params.iter().enumerate() {
1066 let param_name = param.name.to_lower_camel_case();
1067 let mapped = csharp_type(¶m.ty);
1068 if param.optional && !mapped.ends_with('?') {
1069 out.push_str(&format!("{mapped}? {param_name}"));
1070 } else {
1071 out.push_str(&format!("{mapped} {param_name}"));
1072 }
1073
1074 if i < visible_params.len() - 1 {
1075 out.push_str(", ");
1076 }
1077 }
1078
1079 out.push_str(")\n {\n");
1080
1081 for param in &visible_params {
1083 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1084 let param_name = param.name.to_lower_camel_case();
1085 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1086 }
1087 }
1088
1089 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1091
1092 let cs_native_name = to_csharp_name(&func.name);
1094
1095 if func.is_async {
1096 out.push_str(" return await Task.Run(() =>\n {\n");
1098
1099 if func.return_type != TypeRef::Unit {
1100 out.push_str(" var result = ");
1101 } else {
1102 out.push_str(" ");
1103 }
1104
1105 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1106
1107 if visible_params.is_empty() {
1108 out.push_str(");\n");
1109 } else {
1110 out.push('\n');
1111 for (i, param) in visible_params.iter().enumerate() {
1112 let param_name = param.name.to_lower_camel_case();
1113 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1114 out.push_str(&format!(" {arg}"));
1115 if i < visible_params.len() - 1 {
1116 out.push(',');
1117 }
1118 out.push('\n');
1119 }
1120 out.push_str(" );\n");
1121 }
1122
1123 if func.return_type != TypeRef::Unit {
1125 out.push_str(
1126 " if (result == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1127 );
1128 }
1129
1130 emit_return_marshalling_indented(
1131 &mut out,
1132 &func.return_type,
1133 " ",
1134 enum_names,
1135 true_opaque_types,
1136 );
1137 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1138 emit_return_statement_indented(&mut out, &func.return_type, " ");
1139 out.push_str(" });\n");
1140 } else {
1141 if func.return_type != TypeRef::Unit {
1142 out.push_str(" var result = ");
1143 } else {
1144 out.push_str(" ");
1145 }
1146
1147 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1148
1149 if visible_params.is_empty() {
1150 out.push_str(");\n");
1151 } else {
1152 out.push('\n');
1153 for (i, param) in visible_params.iter().enumerate() {
1154 let param_name = param.name.to_lower_camel_case();
1155 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1156 out.push_str(&format!(" {arg}"));
1157 if i < visible_params.len() - 1 {
1158 out.push(',');
1159 }
1160 out.push('\n');
1161 }
1162 out.push_str(" );\n");
1163 }
1164
1165 if func.return_type != TypeRef::Unit && returns_ptr(&func.return_type) {
1169 out.push_str(
1170 " if (result == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1171 );
1172 }
1173
1174 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
1175 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1176 emit_return_statement(&mut out, &func.return_type);
1177 }
1178
1179 out.push_str(" }\n\n");
1180
1181 out
1182}
1183
1184#[allow(clippy::too_many_arguments)]
1185fn gen_wrapper_method(
1186 method: &MethodDef,
1187 _exception_name: &str,
1188 _prefix: &str,
1189 type_name: &str,
1190 enum_names: &HashSet<String>,
1191 true_opaque_types: &HashSet<String>,
1192 bridge_param_names: &HashSet<String>,
1193 bridge_type_aliases: &HashSet<String>,
1194) -> String {
1195 let mut out = String::with_capacity(1024);
1196
1197 let visible_params: Vec<alef_core::ir::ParamDef> = method
1199 .params
1200 .iter()
1201 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1202 .cloned()
1203 .collect();
1204
1205 if !method.doc.is_empty() {
1207 out.push_str(" /// <summary>\n");
1208 for line in method.doc.lines() {
1209 out.push_str(&format!(" /// {}\n", line));
1210 }
1211 out.push_str(" /// </summary>\n");
1212 for param in &visible_params {
1213 out.push_str(&format!(
1214 " /// <param name=\"{}\">{}</param>\n",
1215 param.name.to_lower_camel_case(),
1216 if param.optional { "Optional." } else { "" }
1217 ));
1218 }
1219 }
1220
1221 out.push_str(" public static ");
1223
1224 if method.is_async {
1226 if method.return_type == TypeRef::Unit {
1227 out.push_str("async Task");
1228 } else {
1229 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1230 }
1231 } else if method.return_type == TypeRef::Unit {
1232 out.push_str("void");
1233 } else {
1234 out.push_str(&csharp_type(&method.return_type));
1235 }
1236
1237 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1239 out.push_str(&format!(" {method_cs_name}"));
1240 out.push('(');
1241
1242 for (i, param) in visible_params.iter().enumerate() {
1244 let param_name = param.name.to_lower_camel_case();
1245 let mapped = csharp_type(¶m.ty);
1246 if param.optional && !mapped.ends_with('?') {
1247 out.push_str(&format!("{mapped}? {param_name}"));
1248 } else {
1249 out.push_str(&format!("{mapped} {param_name}"));
1250 }
1251
1252 if i < visible_params.len() - 1 {
1253 out.push_str(", ");
1254 }
1255 }
1256
1257 out.push_str(")\n {\n");
1258
1259 for param in &visible_params {
1261 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1262 let param_name = param.name.to_lower_camel_case();
1263 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1264 }
1265 }
1266
1267 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1269
1270 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1275
1276 if method.is_async {
1277 out.push_str(" return await Task.Run(() =>\n {\n");
1279
1280 if method.return_type != TypeRef::Unit {
1281 out.push_str(" var result = ");
1282 } else {
1283 out.push_str(" ");
1284 }
1285
1286 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1287
1288 if visible_params.is_empty() {
1289 out.push_str(");\n");
1290 } else {
1291 out.push('\n');
1292 for (i, param) in visible_params.iter().enumerate() {
1293 let param_name = param.name.to_lower_camel_case();
1294 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1295 out.push_str(&format!(" {arg}"));
1296 if i < visible_params.len() - 1 {
1297 out.push(',');
1298 }
1299 out.push('\n');
1300 }
1301 out.push_str(" );\n");
1302 }
1303
1304 emit_return_marshalling_indented(
1305 &mut out,
1306 &method.return_type,
1307 " ",
1308 enum_names,
1309 true_opaque_types,
1310 );
1311 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1312 emit_return_statement_indented(&mut out, &method.return_type, " ");
1313 out.push_str(" });\n");
1314 } else {
1315 if method.return_type != TypeRef::Unit {
1316 out.push_str(" var result = ");
1317 } else {
1318 out.push_str(" ");
1319 }
1320
1321 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1322
1323 if visible_params.is_empty() {
1324 out.push_str(");\n");
1325 } else {
1326 out.push('\n');
1327 for (i, param) in visible_params.iter().enumerate() {
1328 let param_name = param.name.to_lower_camel_case();
1329 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1330 out.push_str(&format!(" {arg}"));
1331 if i < visible_params.len() - 1 {
1332 out.push(',');
1333 }
1334 out.push('\n');
1335 }
1336 out.push_str(" );\n");
1337 }
1338
1339 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1340 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1341 emit_return_statement(&mut out, &method.return_type);
1342 }
1343
1344 out.push_str(" }\n\n");
1345
1346 out
1347}
1348
1349fn emit_return_marshalling(
1361 out: &mut String,
1362 return_type: &TypeRef,
1363 enum_names: &HashSet<String>,
1364 true_opaque_types: &HashSet<String>,
1365) {
1366 if *return_type == TypeRef::Unit {
1367 return;
1369 }
1370
1371 if returns_string(return_type) {
1372 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1374 out.push_str(" NativeMethods.FreeString(result);\n");
1375 } else if returns_bool_via_int(return_type) {
1376 out.push_str(" var returnValue = result != 0;\n");
1378 } else if let TypeRef::Named(type_name) = return_type {
1379 let pascal = type_name.to_pascal_case();
1380 if true_opaque_types.contains(type_name) {
1381 out.push_str(&format!(" var returnValue = new {pascal}(result);\n"));
1383 } else if !enum_names.contains(&pascal) {
1384 let to_json_method = format!("{pascal}ToJson");
1386 let free_method = format!("{pascal}Free");
1387 let cs_ty = csharp_type(return_type);
1388 out.push_str(&format!(
1389 " var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1390 ));
1391 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1392 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1393 out.push_str(&format!(" NativeMethods.{free_method}(result);\n"));
1394 out.push_str(&format!(
1395 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1396 cs_ty
1397 ));
1398 } else {
1399 let cs_ty = csharp_type(return_type);
1401 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1402 out.push_str(" NativeMethods.FreeString(result);\n");
1403 out.push_str(&format!(
1404 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1405 cs_ty
1406 ));
1407 }
1408 } else if returns_json_object(return_type) {
1409 let cs_ty = csharp_type(return_type);
1411 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1412 out.push_str(" NativeMethods.FreeString(result);\n");
1413 out.push_str(&format!(
1414 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1415 cs_ty
1416 ));
1417 } else {
1418 out.push_str(" var returnValue = result;\n");
1420 }
1421}
1422
1423fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1425 if *return_type != TypeRef::Unit {
1426 out.push_str(" return returnValue;\n");
1427 }
1428}
1429
1430fn emit_return_marshalling_indented(
1435 out: &mut String,
1436 return_type: &TypeRef,
1437 indent: &str,
1438 enum_names: &HashSet<String>,
1439 true_opaque_types: &HashSet<String>,
1440) {
1441 if *return_type == TypeRef::Unit {
1442 return;
1443 }
1444
1445 if returns_string(return_type) {
1446 out.push_str(&format!(
1447 "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1448 ));
1449 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1450 } else if returns_bool_via_int(return_type) {
1451 out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1452 } else if let TypeRef::Named(type_name) = return_type {
1453 let pascal = type_name.to_pascal_case();
1454 if true_opaque_types.contains(type_name) {
1455 out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1457 } else if !enum_names.contains(&pascal) {
1458 let to_json_method = format!("{pascal}ToJson");
1460 let free_method = format!("{pascal}Free");
1461 let cs_ty = csharp_type(return_type);
1462 out.push_str(&format!(
1463 "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1464 ));
1465 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1466 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1467 out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1468 out.push_str(&format!(
1469 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1470 cs_ty
1471 ));
1472 } else {
1473 let cs_ty = csharp_type(return_type);
1475 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1476 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1477 out.push_str(&format!(
1478 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1479 cs_ty
1480 ));
1481 }
1482 } else if returns_json_object(return_type) {
1483 let cs_ty = csharp_type(return_type);
1484 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1485 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1486 out.push_str(&format!(
1487 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1488 cs_ty
1489 ));
1490 } else {
1491 out.push_str(&format!("{indent}var returnValue = result;\n"));
1492 }
1493}
1494
1495fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1497 if *return_type != TypeRef::Unit {
1498 out.push_str(&format!("{indent}return returnValue;\n"));
1499 }
1500}
1501
1502fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1503 let mut out = hash::header(CommentStyle::DoubleSlash);
1504 out.push_str("using System;\n\n");
1505
1506 out.push_str(&format!("namespace {};\n\n", namespace));
1507
1508 if !typ.doc.is_empty() {
1510 out.push_str("/// <summary>\n");
1511 for line in typ.doc.lines() {
1512 out.push_str(&format!("/// {}\n", line));
1513 }
1514 out.push_str("/// </summary>\n");
1515 }
1516
1517 let class_name = typ.name.to_pascal_case();
1518 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1519 out.push_str("{\n");
1520 out.push_str(" internal IntPtr Handle { get; }\n\n");
1521 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1522 out.push_str(" {\n");
1523 out.push_str(" Handle = handle;\n");
1524 out.push_str(" }\n\n");
1525 out.push_str(" public void Dispose()\n");
1526 out.push_str(" {\n");
1527 out.push_str(" // Native free will be called by the runtime\n");
1528 out.push_str(" }\n");
1529 out.push_str("}\n");
1530
1531 out
1532}
1533
1534fn gen_record_type(
1535 typ: &TypeDef,
1536 namespace: &str,
1537 enum_names: &HashSet<String>,
1538 complex_enums: &HashSet<String>,
1539 custom_converter_enums: &HashSet<String>,
1540 _lang_rename_all: &str,
1541) -> String {
1542 let mut out = hash::header(CommentStyle::DoubleSlash);
1543 out.push_str("using System;\n");
1544 out.push_str("using System.Collections.Generic;\n");
1545 out.push_str("using System.Text.Json;\n");
1546 out.push_str("using System.Text.Json.Serialization;\n\n");
1547
1548 out.push_str(&format!("namespace {};\n\n", namespace));
1549
1550 if !typ.doc.is_empty() {
1552 out.push_str("/// <summary>\n");
1553 for line in typ.doc.lines() {
1554 out.push_str(&format!("/// {}\n", line));
1555 }
1556 out.push_str("/// </summary>\n");
1557 }
1558
1559 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1560 out.push_str("{\n");
1561
1562 for field in &typ.fields {
1563 if is_tuple_field(field) {
1565 continue;
1566 }
1567
1568 if !field.doc.is_empty() {
1570 out.push_str(" /// <summary>\n");
1571 for line in field.doc.lines() {
1572 out.push_str(&format!(" /// {}\n", line));
1573 }
1574 out.push_str(" /// </summary>\n");
1575 }
1576
1577 let field_base_type = match &field.ty {
1581 TypeRef::Named(n) => Some(n.to_pascal_case()),
1582 TypeRef::Optional(inner) => match inner.as_ref() {
1583 TypeRef::Named(n) => Some(n.to_pascal_case()),
1584 _ => None,
1585 },
1586 _ => None,
1587 };
1588 if let Some(ref base) = field_base_type {
1589 if custom_converter_enums.contains(base) {
1590 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1591 }
1592 }
1593
1594 let json_name = field.name.clone();
1598 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1599
1600 let cs_name = to_csharp_name(&field.name);
1601
1602 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1605
1606 if field.optional {
1607 let mapped = if is_complex {
1609 "JsonElement".to_string()
1610 } else {
1611 csharp_type(&field.ty).to_string()
1612 };
1613 let field_type = if mapped.ends_with('?') {
1614 mapped
1615 } else {
1616 format!("{mapped}?")
1617 };
1618 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1619 out.push_str(" = null;\n");
1620 } else if typ.has_default || field.default.is_some() {
1621 let field_type = if is_complex {
1624 "JsonElement".to_string()
1625 } else {
1626 csharp_type(&field.ty).to_string()
1627 };
1628 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1629 use alef_core::ir::DefaultValue;
1630 if matches!(&field.ty, TypeRef::Duration) {
1633 out.push_str(" = null;\n");
1634 out.push('\n');
1635 continue;
1636 }
1637 let default_val = match &field.typed_default {
1638 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1639 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1640 Some(DefaultValue::FloatLiteral(f)) => {
1641 let s = f.to_string();
1642 if s.contains('.') { s } else { format!("{s}.0") }
1643 }
1644 Some(DefaultValue::StringLiteral(s)) => {
1645 let escaped = s
1646 .replace('\\', "\\\\")
1647 .replace('"', "\\\"")
1648 .replace('\n', "\\n")
1649 .replace('\r', "\\r")
1650 .replace('\t', "\\t");
1651 format!("\"{}\"", escaped)
1652 }
1653 Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1654 Some(DefaultValue::None) => "null".to_string(),
1655 Some(DefaultValue::Empty) | None => match &field.ty {
1656 TypeRef::Vec(_) => "[]".to_string(),
1657 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1658 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1659 TypeRef::Json => "null".to_string(),
1660 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1661 TypeRef::Primitive(p) => match p {
1662 PrimitiveType::Bool => "false".to_string(),
1663 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1664 _ => "0".to_string(),
1665 },
1666 TypeRef::Named(name) => {
1667 let pascal = name.to_pascal_case();
1668 if enum_names.contains(&pascal) {
1669 "default".to_string()
1670 } else {
1671 "default!".to_string()
1672 }
1673 }
1674 _ => "default!".to_string(),
1675 },
1676 };
1677 out.push_str(&format!(" = {};\n", default_val));
1678 } else {
1679 let field_type = if is_complex {
1683 "JsonElement".to_string()
1684 } else {
1685 csharp_type(&field.ty).to_string()
1686 };
1687 if matches!(&field.ty, TypeRef::Duration) {
1689 out.push_str(&format!(
1690 " public {} {} {{ get; set; }} = null;\n",
1691 field_type, cs_name
1692 ));
1693 } else {
1694 let default_val = match &field.ty {
1695 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1696 TypeRef::Vec(_) => "[]",
1697 TypeRef::Bytes => "Array.Empty<byte>()",
1698 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1699 TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1700 TypeRef::Primitive(_) => "0",
1701 _ => "default!",
1702 };
1703 out.push_str(&format!(
1704 " public {} {} {{ get; set; }} = {};\n",
1705 field_type, cs_name, default_val
1706 ));
1707 }
1708 }
1709
1710 out.push('\n');
1711 }
1712
1713 out.push_str("}\n");
1714
1715 out
1716}
1717
1718fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1720 match rename_all {
1721 Some("snake_case") => name.to_snake_case(),
1722 Some("camelCase") => name.to_lower_camel_case(),
1723 Some("PascalCase") => name.to_pascal_case(),
1724 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1725 Some("lowercase") => name.to_lowercase(),
1726 Some("UPPERCASE") => name.to_uppercase(),
1727 _ => name.to_lowercase(),
1728 }
1729}
1730
1731fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1732 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1733
1734 if enum_def.serde_tag.is_some() && has_data_variants {
1736 return gen_tagged_union(enum_def, namespace);
1737 }
1738
1739 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1747 if let Some(ref rename) = v.serde_rename {
1748 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1749 rename != &snake
1750 } else {
1751 false
1752 }
1753 });
1754
1755 let enum_pascal = enum_def.name.to_pascal_case();
1756
1757 let variants: Vec<(String, String)> = enum_def
1759 .variants
1760 .iter()
1761 .map(|v| {
1762 let json_name = v
1763 .serde_rename
1764 .clone()
1765 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1766 let pascal_name = v.name.to_pascal_case();
1767 (json_name, pascal_name)
1768 })
1769 .collect();
1770
1771 let mut out = hash::header(CommentStyle::DoubleSlash);
1772 out.push_str("using System;\n");
1773 out.push_str("using System.Text.Json;\n");
1774 out.push_str("using System.Text.Json.Serialization;\n\n");
1775
1776 out.push_str(&format!("namespace {};\n\n", namespace));
1777
1778 if !enum_def.doc.is_empty() {
1780 out.push_str("/// <summary>\n");
1781 for line in enum_def.doc.lines() {
1782 out.push_str(&format!("/// {}\n", line));
1783 }
1784 out.push_str("/// </summary>\n");
1785 }
1786
1787 if needs_custom_converter {
1788 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1789 }
1790 out.push_str(&format!("public enum {enum_pascal}\n"));
1791 out.push_str("{\n");
1792
1793 for (json_name, pascal_name) in &variants {
1794 if let Some(v) = enum_def
1796 .variants
1797 .iter()
1798 .find(|v| v.name.to_pascal_case() == *pascal_name)
1799 {
1800 if !v.doc.is_empty() {
1801 out.push_str(" /// <summary>\n");
1802 for line in v.doc.lines() {
1803 out.push_str(&format!(" /// {}\n", line));
1804 }
1805 out.push_str(" /// </summary>\n");
1806 }
1807 }
1808 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1809 out.push_str(&format!(" {pascal_name},\n"));
1810 }
1811
1812 out.push_str("}\n");
1813
1814 if needs_custom_converter {
1816 out.push('\n');
1817 out.push_str(&format!(
1818 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1819 ));
1820 out.push_str(&format!(
1821 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1822 ));
1823 out.push_str("{\n");
1824
1825 out.push_str(&format!(
1827 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1828 ));
1829 out.push_str(" {\n");
1830 out.push_str(" var value = reader.GetString();\n");
1831 out.push_str(" return value switch\n");
1832 out.push_str(" {\n");
1833 for (json_name, pascal_name) in &variants {
1834 out.push_str(&format!(
1835 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1836 ));
1837 }
1838 out.push_str(&format!(
1839 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1840 ));
1841 out.push_str(" };\n");
1842 out.push_str(" }\n\n");
1843
1844 out.push_str(&format!(
1846 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1847 ));
1848 out.push_str(" {\n");
1849 out.push_str(" var str = value switch\n");
1850 out.push_str(" {\n");
1851 for (json_name, pascal_name) in &variants {
1852 out.push_str(&format!(
1853 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1854 ));
1855 }
1856 out.push_str(&format!(
1857 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1858 ));
1859 out.push_str(" };\n");
1860 out.push_str(" writer.WriteStringValue(str);\n");
1861 out.push_str(" }\n");
1862 out.push_str("}\n");
1863 }
1864
1865 out
1866}
1867
1868fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1875 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1876 let enum_pascal = enum_def.name.to_pascal_case();
1877 let converter_name = format!("{enum_pascal}JsonConverter");
1878 let ns = namespace;
1881
1882 let mut out = hash::header(CommentStyle::DoubleSlash);
1883 out.push_str("using System;\n");
1884 out.push_str("using System.Collections.Generic;\n");
1885 out.push_str("using System.Text.Json;\n");
1886 out.push_str("using System.Text.Json.Serialization;\n\n");
1887 out.push_str(&format!("namespace {};\n\n", namespace));
1888
1889 if !enum_def.doc.is_empty() {
1891 out.push_str("/// <summary>\n");
1892 for line in enum_def.doc.lines() {
1893 out.push_str(&format!("/// {}\n", line));
1894 }
1895 out.push_str("/// </summary>\n");
1896 }
1897
1898 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
1900 out.push_str(&format!("public abstract record {enum_pascal}\n"));
1901 out.push_str("{\n");
1902
1903 for variant in &enum_def.variants {
1905 let pascal = variant.name.to_pascal_case();
1906
1907 if !variant.doc.is_empty() {
1908 out.push_str(" /// <summary>\n");
1909 for line in variant.doc.lines() {
1910 out.push_str(&format!(" /// {}\n", line));
1911 }
1912 out.push_str(" /// </summary>\n");
1913 }
1914
1915 if variant.fields.is_empty() {
1916 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
1918 } else {
1919 let is_copy_ctor_clash = variant.fields.len() == 1 && {
1924 let field_cs_type = csharp_type(&variant.fields[0].ty);
1925 field_cs_type.as_ref() == pascal
1926 };
1927
1928 if is_copy_ctor_clash {
1929 let cs_type = csharp_type(&variant.fields[0].ty);
1930 let qualified_cs_type = format!("global::{ns}.{cs_type}");
1934 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
1935 out.push_str(" {\n");
1936 out.push_str(&format!(
1937 " public required {qualified_cs_type} Value {{ get; init; }}\n"
1938 ));
1939 out.push_str(" }\n\n");
1940 } else {
1941 out.push_str(&format!(" public sealed record {pascal}(\n"));
1943 for (i, field) in variant.fields.iter().enumerate() {
1944 let cs_type = csharp_type(&field.ty);
1945 let cs_type = if field.optional && !cs_type.ends_with('?') {
1946 format!("{cs_type}?")
1947 } else {
1948 cs_type.to_string()
1949 };
1950 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1951 if is_tuple_field(field) {
1952 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1953 } else {
1954 let json_name = field.name.trim_start_matches('_');
1955 let cs_name = to_csharp_name(json_name);
1956 let clashes = cs_name == pascal || cs_name == cs_type;
1957 if clashes {
1958 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1959 } else {
1960 out.push_str(&format!(
1961 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1962 ));
1963 }
1964 }
1965 }
1966 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
1967 }
1968 }
1969 }
1970
1971 out.push_str("}\n\n");
1972
1973 out.push_str(&format!(
1975 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
1976 ));
1977 out.push_str(&format!(
1978 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
1979 ));
1980 out.push_str("{\n");
1981
1982 out.push_str(&format!(
1984 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1985 ));
1986 out.push_str(" {\n");
1987 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
1988 out.push_str(" var root = doc.RootElement;\n");
1989 out.push_str(&format!(
1990 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
1991 ));
1992 out.push_str(&format!(
1993 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
1994 ));
1995 out.push_str(" var tag = tagEl.GetString();\n");
1996 out.push_str(" var json = root.GetRawText();\n");
1997 out.push_str(" return tag switch\n");
1998 out.push_str(" {\n");
1999
2000 for variant in &enum_def.variants {
2001 let discriminator = variant
2002 .serde_rename
2003 .clone()
2004 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2005 let pascal = variant.name.to_pascal_case();
2006 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2011 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2012 let f = &variant.fields[0];
2013 let cs_type = csharp_type(&f.ty);
2014 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2015 cs_name == pascal || cs_name == cs_type
2016 };
2017 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2018 if is_newtype {
2019 let inner_cs_type = csharp_type(&variant.fields[0].ty);
2020 if inner_cs_type == pascal {
2023 out.push_str(&format!(
2024 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2025 ));
2026 out.push_str(&format!(
2027 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
2028 ));
2029 } else {
2030 out.push_str(&format!(
2031 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
2032 ));
2033 out.push_str(&format!(
2034 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2035 ));
2036 out.push_str(&format!(
2037 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
2038 ));
2039 }
2040 } else {
2041 out.push_str(&format!(
2042 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
2043 ));
2044 out.push_str(&format!(
2045 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
2046 ));
2047 }
2048 }
2049
2050 out.push_str(&format!(
2051 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
2052 ));
2053 out.push_str(" };\n");
2054 out.push_str(" }\n\n");
2055
2056 out.push_str(&format!(
2058 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2059 ));
2060 out.push_str(" {\n");
2061
2062 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
2064 out.push_str(" switch (value)\n");
2065 out.push_str(" {\n");
2066
2067 for variant in &enum_def.variants {
2068 let discriminator = variant
2069 .serde_rename
2070 .clone()
2071 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2072 let pascal = variant.name.to_pascal_case();
2073 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2077 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2078 let f = &variant.fields[0];
2079 let cs_type = csharp_type(&f.ty);
2080 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2081 cs_name == pascal || cs_name == cs_type
2082 };
2083 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2084 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
2085 out.push_str(" {\n");
2086 if is_newtype {
2087 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
2088 } else {
2089 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
2090 }
2091 out.push_str(" writer.WriteStartObject();\n");
2092 out.push_str(&format!(
2093 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
2094 ));
2095 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2096 out.push_str(&format!(
2097 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2098 ));
2099 out.push_str(" writer.WriteEndObject();\n");
2100 out.push_str(" break;\n");
2101 out.push_str(" }\n");
2102 }
2103
2104 out.push_str(&format!(
2105 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2106 ));
2107 out.push_str(" }\n");
2108 out.push_str(" }\n");
2109 out.push_str("}\n");
2110
2111 out
2112}