1use crate::type_map::csharp_type;
2use alef_codegen::doc_emission;
3use alef_codegen::naming::to_csharp_name;
4use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
5use alef_core::config::{AdapterPattern, AlefConfig, Language, resolve_output_dir};
6use alef_core::hash::{self, CommentStyle};
7use alef_core::ir::{ApiSurface, EnumDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
8use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
9use std::collections::HashSet;
10use std::path::PathBuf;
11
12pub struct CsharpBackend;
13
14impl CsharpBackend {
15 }
17
18impl Backend for CsharpBackend {
19 fn name(&self) -> &str {
20 "csharp"
21 }
22
23 fn language(&self) -> Language {
24 Language::Csharp
25 }
26
27 fn capabilities(&self) -> Capabilities {
28 Capabilities {
29 supports_async: true,
30 supports_classes: true,
31 supports_enums: true,
32 supports_option: true,
33 supports_result: true,
34 ..Capabilities::default()
35 }
36 }
37
38 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
39 let namespace = config.csharp_namespace();
40 let prefix = config.ffi_prefix();
41 let lib_name = config.ffi_lib_name();
42
43 let bridge_param_names: HashSet<String> = config
46 .trait_bridges
47 .iter()
48 .filter_map(|b| b.param_name.clone())
49 .collect();
50 let bridge_type_aliases: HashSet<String> = config
51 .trait_bridges
52 .iter()
53 .filter_map(|b| b.type_alias.clone())
54 .collect();
55 let has_visitor_callbacks = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
57
58 let streaming_methods: HashSet<String> = config
61 .adapters
62 .iter()
63 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
64 .map(|a| a.name.clone())
65 .collect();
66
67 let exclude_functions: HashSet<String> = config
69 .csharp
70 .as_ref()
71 .map(|c| c.exclude_functions.iter().cloned().collect())
72 .unwrap_or_default();
73
74 let output_dir = resolve_output_dir(
75 config.output.csharp.as_ref(),
76 &config.crate_config.name,
77 "packages/csharp/",
78 );
79
80 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
81
82 let mut files = Vec::new();
83
84 files.push(GeneratedFile {
86 path: base_path.join("NativeMethods.cs"),
87 content: strip_trailing_whitespace(&gen_native_methods(
88 api,
89 &namespace,
90 &lib_name,
91 &prefix,
92 &bridge_param_names,
93 &bridge_type_aliases,
94 has_visitor_callbacks,
95 &config.trait_bridges,
96 &streaming_methods,
97 &exclude_functions,
98 )),
99 generated_header: true,
100 });
101
102 if !api.errors.is_empty() {
104 for error in &api.errors {
105 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
106 for (class_name, content) in error_files {
107 files.push(GeneratedFile {
108 path: base_path.join(format!("{}.cs", class_name)),
109 content: strip_trailing_whitespace(&content),
110 generated_header: false, });
112 }
113 }
114 }
115
116 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
118 if api.errors.is_empty()
119 || !api
120 .errors
121 .iter()
122 .any(|e| format!("{}Exception", e.name) == exception_class_name)
123 {
124 files.push(GeneratedFile {
125 path: base_path.join(format!("{}.cs", exception_class_name)),
126 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
127 generated_header: true,
128 });
129 }
130
131 let base_class_name = api.crate_name.to_pascal_case();
133 let wrapper_class_name = if namespace == base_class_name {
134 format!("{}Lib", base_class_name)
135 } else {
136 base_class_name
137 };
138 files.push(GeneratedFile {
139 path: base_path.join(format!("{}.cs", wrapper_class_name)),
140 content: strip_trailing_whitespace(&gen_wrapper_class(
141 api,
142 &namespace,
143 &wrapper_class_name,
144 &exception_class_name,
145 &prefix,
146 &bridge_param_names,
147 &bridge_type_aliases,
148 has_visitor_callbacks,
149 &streaming_methods,
150 &exclude_functions,
151 )),
152 generated_header: true,
153 });
154
155 if has_visitor_callbacks {
157 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
158 files.push(GeneratedFile {
159 path: base_path.join(filename),
160 content: strip_trailing_whitespace(&content),
161 generated_header: true,
162 });
163 }
164 } else {
165 delete_stale_visitor_files(&base_path)?;
168 }
169
170 if !config.trait_bridges.is_empty() {
172 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
173 let bridges: Vec<_> = config
174 .trait_bridges
175 .iter()
176 .filter_map(|cfg| {
177 let trait_name = cfg.trait_name.clone();
178 trait_defs
179 .iter()
180 .find(|t| t.name == trait_name)
181 .map(|trait_def| (trait_name, cfg, *trait_def))
182 })
183 .collect();
184
185 if !bridges.is_empty() {
186 let (filename, content) = crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges);
187 files.push(GeneratedFile {
188 path: base_path.join(filename),
189 content: strip_trailing_whitespace(&content),
190 generated_header: true,
191 });
192 }
193 }
194
195 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
197 if typ.is_opaque {
198 let type_filename = typ.name.to_pascal_case();
199 files.push(GeneratedFile {
200 path: base_path.join(format!("{}.cs", type_filename)),
201 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
202 generated_header: true,
203 });
204 }
205 }
206
207 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
209
210 let complex_enums: HashSet<String> = api
215 .enums
216 .iter()
217 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
218 .map(|e| e.name.to_pascal_case())
219 .collect();
220
221 let custom_converter_enums: HashSet<String> = api
225 .enums
226 .iter()
227 .filter(|e| {
228 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
230 || e.variants.iter().any(|v| {
232 if let Some(ref rename) = v.serde_rename {
233 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
234 rename != &snake
235 } else {
236 false
237 }
238 })
239 })
240 .map(|e| e.name.to_pascal_case())
241 .collect();
242
243 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
245
246 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
248 if !typ.is_opaque {
249 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
252 if !typ.fields.is_empty() && !has_named_fields {
253 continue;
254 }
255 if has_visitor_callbacks && (typ.name == "NodeContext" || typ.name == "VisitResult") {
257 continue;
258 }
259
260 let type_filename = typ.name.to_pascal_case();
261 files.push(GeneratedFile {
262 path: base_path.join(format!("{}.cs", type_filename)),
263 content: strip_trailing_whitespace(&gen_record_type(
264 typ,
265 &namespace,
266 &enum_names,
267 &complex_enums,
268 &custom_converter_enums,
269 &lang_rename_all,
270 )),
271 generated_header: true,
272 });
273 }
274 }
275
276 for enum_def in &api.enums {
278 if has_visitor_callbacks && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
280 continue;
281 }
282 let enum_filename = enum_def.name.to_pascal_case();
283 files.push(GeneratedFile {
284 path: base_path.join(format!("{}.cs", enum_filename)),
285 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
286 generated_header: true,
287 });
288 }
289
290 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
292
293 files.push(GeneratedFile {
297 path: PathBuf::from("packages/csharp/Directory.Build.props"),
298 content: gen_directory_build_props(),
299 generated_header: true,
300 });
301
302 Ok(files)
303 }
304
305 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
310 Ok(vec![])
312 }
313
314 fn build_config(&self) -> Option<BuildConfig> {
315 Some(BuildConfig {
316 tool: "dotnet",
317 crate_suffix: "",
318 build_dep: BuildDependency::Ffi,
319 post_build: vec![],
320 })
321 }
322}
323
324fn is_tuple_field(field: &FieldDef) -> bool {
326 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
327 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
328}
329
330fn strip_trailing_whitespace(content: &str) -> String {
332 let mut result: String = content
333 .lines()
334 .map(|line| line.trim_end())
335 .collect::<Vec<_>>()
336 .join("\n");
337 if !result.ends_with('\n') {
338 result.push('\n');
339 }
340 result
341}
342
343fn csharp_file_header() -> String {
345 let mut out = hash::header(CommentStyle::DoubleSlash);
346 out.push_str("#nullable enable\n\n");
347 out
348}
349
350fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
361 match ty {
362 TypeRef::Unit => "void",
363 TypeRef::Primitive(PrimitiveType::Bool) => "int",
365 TypeRef::Primitive(PrimitiveType::U8) => "byte",
367 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
368 TypeRef::Primitive(PrimitiveType::U32) => "uint",
369 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
370 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
371 TypeRef::Primitive(PrimitiveType::I16) => "short",
372 TypeRef::Primitive(PrimitiveType::I32) => "int",
373 TypeRef::Primitive(PrimitiveType::I64) => "long",
374 TypeRef::Primitive(PrimitiveType::F32) => "float",
375 TypeRef::Primitive(PrimitiveType::F64) => "double",
376 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
377 TypeRef::Primitive(PrimitiveType::Isize) => "long",
378 TypeRef::Duration => "ulong",
380 TypeRef::String
382 | TypeRef::Char
383 | TypeRef::Bytes
384 | TypeRef::Optional(_)
385 | TypeRef::Vec(_)
386 | TypeRef::Map(_, _)
387 | TypeRef::Named(_)
388 | TypeRef::Path
389 | TypeRef::Json => "IntPtr",
390 }
391}
392
393fn returns_string(ty: &TypeRef) -> bool {
395 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
396}
397
398fn returns_bool_via_int(ty: &TypeRef) -> bool {
400 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
401}
402
403fn returns_json_object(ty: &TypeRef) -> bool {
405 matches!(
406 ty,
407 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
408 )
409}
410
411fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
422 match ty {
423 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
424 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
426 TypeRef::Unit => "void",
427 TypeRef::Primitive(PrimitiveType::Bool) => "int",
428 TypeRef::Primitive(PrimitiveType::U8) => "byte",
429 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
430 TypeRef::Primitive(PrimitiveType::U32) => "uint",
431 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
432 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
433 TypeRef::Primitive(PrimitiveType::I16) => "short",
434 TypeRef::Primitive(PrimitiveType::I32) => "int",
435 TypeRef::Primitive(PrimitiveType::I64) => "long",
436 TypeRef::Primitive(PrimitiveType::F32) => "float",
437 TypeRef::Primitive(PrimitiveType::F64) => "double",
438 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
439 TypeRef::Primitive(PrimitiveType::Isize) => "long",
440 TypeRef::Duration => "ulong",
441 }
442}
443
444fn is_bridge_param(
451 param: &alef_core::ir::ParamDef,
452 bridge_param_names: &HashSet<String>,
453 bridge_type_aliases: &HashSet<String>,
454) -> bool {
455 bridge_param_names.contains(¶m.name)
456 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
457}
458
459#[allow(clippy::too_many_arguments)]
460fn gen_native_methods(
461 api: &ApiSurface,
462 namespace: &str,
463 lib_name: &str,
464 prefix: &str,
465 bridge_param_names: &HashSet<String>,
466 bridge_type_aliases: &HashSet<String>,
467 has_visitor_callbacks: bool,
468 trait_bridges: &[alef_core::config::TraitBridgeConfig],
469 streaming_methods: &HashSet<String>,
470 exclude_functions: &HashSet<String>,
471) -> String {
472 let mut out = csharp_file_header();
473 out.push_str("using System;\n");
474 out.push_str("using System.Runtime.InteropServices;\n\n");
475
476 out.push_str(&format!("namespace {};\n\n", namespace));
477
478 out.push_str("internal static partial class NativeMethods\n{\n");
479 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
480
481 let mut emitted: HashSet<String> = HashSet::new();
484
485 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
488
489 let mut opaque_param_types: HashSet<String> = HashSet::new();
493 let mut opaque_return_types: HashSet<String> = HashSet::new();
494
495 for func in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
499 for param in &func.params {
500 if let TypeRef::Named(name) = ¶m.ty {
501 opaque_param_types.insert(name.clone());
502 }
503 }
504 if let TypeRef::Named(name) = &func.return_type {
505 if !enum_names.contains(name) {
506 opaque_return_types.insert(name.clone());
507 }
508 }
509 }
510 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
511 for method in &typ.methods {
512 if streaming_methods.contains(&method.name) {
513 continue;
514 }
515 for param in &method.params {
516 if let TypeRef::Named(name) = ¶m.ty {
517 opaque_param_types.insert(name.clone());
518 }
519 }
520 if let TypeRef::Named(name) = &method.return_type {
521 if !enum_names.contains(name) {
522 opaque_return_types.insert(name.clone());
523 }
524 }
525 }
526 }
527
528 let true_opaque_types: HashSet<String> = api
530 .types
531 .iter()
532 .filter(|t| t.is_opaque)
533 .map(|t| t.name.clone())
534 .collect();
535
536 let mut sorted_param_types: Vec<&String> = opaque_param_types.iter().collect();
540 sorted_param_types.sort();
541 for type_name in sorted_param_types {
542 let snake = type_name.to_snake_case();
543 if !true_opaque_types.contains(type_name) {
544 let from_json_entry = format!("{prefix}_{snake}_from_json");
545 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
546 if emitted.insert(from_json_entry.clone()) {
547 out.push_str(&format!(
548 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
549 ));
550 out.push_str(&format!(
551 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
552 ));
553 }
554 }
555 let free_entry = format!("{prefix}_{snake}_free");
556 let free_cs = format!("{}Free", type_name.to_pascal_case());
557 if emitted.insert(free_entry.clone()) {
558 out.push_str(&format!(
559 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
560 ));
561 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
562 }
563 }
564
565 let mut sorted_return_types: Vec<&String> = opaque_return_types.iter().collect();
568 sorted_return_types.sort();
569 for type_name in sorted_return_types {
570 let snake = type_name.to_snake_case();
571 if !true_opaque_types.contains(type_name) {
572 let to_json_entry = format!("{prefix}_{snake}_to_json");
573 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
574 if emitted.insert(to_json_entry.clone()) {
575 out.push_str(&format!(
576 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
577 ));
578 out.push_str(&format!(
579 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
580 ));
581 }
582 }
583 let free_entry = format!("{prefix}_{snake}_free");
584 let free_cs = format!("{}Free", type_name.to_pascal_case());
585 if emitted.insert(free_entry.clone()) {
586 out.push_str(&format!(
587 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
588 ));
589 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
590 }
591 }
592
593 for func in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
595 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
596 if emitted.insert(c_func_name.clone()) {
597 out.push_str(&gen_pinvoke_for_func(
598 &c_func_name,
599 func,
600 bridge_param_names,
601 bridge_type_aliases,
602 ));
603 }
604 }
605
606 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
609 let type_snake = typ.name.to_snake_case();
610 for method in &typ.methods {
611 if streaming_methods.contains(&method.name) {
612 continue;
613 }
614 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
615 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
619 if emitted.insert(c_method_name.clone()) {
620 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
621 }
622 }
623 }
624
625 out.push_str(&format!(
627 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
628 ));
629 out.push_str(" internal static extern int LastErrorCode();\n\n");
630
631 out.push_str(&format!(
632 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
633 ));
634 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
635
636 out.push_str(&format!(
637 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
638 ));
639 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
640
641 if has_visitor_callbacks {
643 out.push('\n');
644 out.push_str(&crate::gen_visitor::gen_native_methods_visitor(
645 namespace, lib_name, prefix,
646 ));
647 }
648
649 if !trait_bridges.is_empty() {
651 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
653
654 let bridges: Vec<_> = trait_bridges
656 .iter()
657 .filter_map(|config| {
658 let trait_name = config.trait_name.clone();
659 trait_defs
660 .iter()
661 .find(|t| t.name == trait_name)
662 .map(|trait_def| (trait_name, config, *trait_def))
663 })
664 .collect();
665
666 if !bridges.is_empty() {
667 out.push('\n');
668 out.push_str(&crate::trait_bridge::gen_native_methods_trait_bridges(
669 namespace, prefix, &bridges,
670 ));
671 }
672 }
673
674 out.push_str("}\n");
675
676 out
677}
678
679fn gen_pinvoke_for_func(
680 c_name: &str,
681 func: &FunctionDef,
682 bridge_param_names: &HashSet<String>,
683 bridge_type_aliases: &HashSet<String>,
684) -> String {
685 let cs_name = to_csharp_name(&func.name);
686 let mut out =
687 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
688 out.push_str(" internal static extern ");
689
690 out.push_str(pinvoke_return_type(&func.return_type));
692
693 out.push_str(&format!(" {}(", cs_name));
694
695 let visible_params: Vec<_> = func
698 .params
699 .iter()
700 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
701 .collect();
702
703 if visible_params.is_empty() {
704 out.push_str(");\n\n");
705 } else {
706 out.push('\n');
707 for (i, param) in visible_params.iter().enumerate() {
708 out.push_str(" ");
709 let pinvoke_ty = pinvoke_param_type(¶m.ty);
710 if pinvoke_ty == "string" {
711 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
712 }
713 let param_name = param.name.to_lower_camel_case();
714 out.push_str(&format!("{pinvoke_ty} {param_name}"));
715
716 if i < visible_params.len() - 1 {
717 out.push(',');
718 }
719 out.push('\n');
720 }
721 out.push_str(" );\n\n");
722 }
723
724 out
725}
726
727fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
728 let mut out =
729 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
730 out.push_str(" internal static extern ");
731
732 out.push_str(pinvoke_return_type(&method.return_type));
734
735 out.push_str(&format!(" {}(", cs_name));
736
737 let has_receiver = !method.is_static && method.receiver.is_some();
743
744 if !has_receiver && method.params.is_empty() {
745 out.push_str(");\n\n");
746 } else {
747 out.push('\n');
748 let total = if has_receiver {
749 method.params.len() + 1
750 } else {
751 method.params.len()
752 };
753 let mut idx = 0usize;
754 if has_receiver {
755 out.push_str(" IntPtr handle");
756 if total > 1 {
757 out.push(',');
758 }
759 out.push('\n');
760 idx += 1;
761 }
762 for param in method.params.iter() {
763 out.push_str(" ");
764 let pinvoke_ty = pinvoke_param_type(¶m.ty);
765 if pinvoke_ty == "string" {
766 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
767 }
768 let param_name = param.name.to_lower_camel_case();
769 out.push_str(&format!("{pinvoke_ty} {param_name}"));
770
771 if idx < total - 1 {
772 out.push(',');
773 }
774 out.push('\n');
775 idx += 1;
776 }
777 out.push_str(" );\n\n");
778 }
779
780 out
781}
782
783fn gen_exception_class(namespace: &str, class_name: &str) -> String {
784 let mut out = csharp_file_header();
785 out.push_str("using System;\n\n");
786
787 out.push_str(&format!("namespace {};\n\n", namespace));
788
789 out.push_str(&format!("public class {} : Exception\n", class_name));
790 out.push_str("{\n");
791 out.push_str(" public int Code { get; }\n\n");
792 out.push_str(&format!(
793 " public {}(int code, string message) : base(message)\n",
794 class_name
795 ));
796 out.push_str(" {\n");
797 out.push_str(" Code = code;\n");
798 out.push_str(" }\n");
799 out.push_str("}\n");
800
801 out
802}
803
804#[allow(clippy::too_many_arguments)]
805fn gen_wrapper_class(
806 api: &ApiSurface,
807 namespace: &str,
808 class_name: &str,
809 exception_name: &str,
810 prefix: &str,
811 bridge_param_names: &HashSet<String>,
812 bridge_type_aliases: &HashSet<String>,
813 has_visitor_callbacks: bool,
814 streaming_methods: &HashSet<String>,
815 exclude_functions: &HashSet<String>,
816) -> String {
817 let mut out = csharp_file_header();
818 out.push_str("using System;\n");
819 out.push_str("using System.Collections.Generic;\n");
820 out.push_str("using System.Runtime.InteropServices;\n");
821 out.push_str("using System.Text.Json;\n");
822 out.push_str("using System.Text.Json.Serialization;\n");
823 out.push_str("using System.Threading.Tasks;\n\n");
824
825 out.push_str(&format!("namespace {};\n\n", namespace));
826
827 out.push_str(&format!("public static class {}\n", class_name));
828 out.push_str("{\n");
829 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
830 out.push_str(" {\n");
831 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
832 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
833 out.push_str(" };\n\n");
834
835 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
837
838 let true_opaque_types: HashSet<String> = api
840 .types
841 .iter()
842 .filter(|t| t.is_opaque)
843 .map(|t| t.name.clone())
844 .collect();
845
846 for func in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
848 out.push_str(&gen_wrapper_function(
849 func,
850 exception_name,
851 prefix,
852 &enum_names,
853 &true_opaque_types,
854 bridge_param_names,
855 bridge_type_aliases,
856 ));
857 }
858
859 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
862 if typ.is_opaque {
864 continue;
865 }
866 for method in &typ.methods {
867 if streaming_methods.contains(&method.name) {
868 continue;
869 }
870 out.push_str(&gen_wrapper_method(
871 method,
872 exception_name,
873 prefix,
874 &typ.name,
875 &enum_names,
876 &true_opaque_types,
877 bridge_param_names,
878 bridge_type_aliases,
879 ));
880 }
881 }
882
883 if has_visitor_callbacks {
885 out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
886 exception_name,
887 prefix,
888 ));
889 }
890
891 out.push_str(" private static ");
893 out.push_str(&format!("{} GetLastError()\n", exception_name));
894 out.push_str(" {\n");
895 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
896 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
897 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
898 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
899 out.push_str(" }\n");
900
901 out.push_str("}\n");
902
903 out
904}
905
906fn emit_named_param_setup(
923 out: &mut String,
924 params: &[alef_core::ir::ParamDef],
925 indent: &str,
926 true_opaque_types: &HashSet<String>,
927) {
928 for param in params {
929 let param_name = param.name.to_lower_camel_case();
930 let json_var = format!("{param_name}Json");
931 let handle_var = format!("{param_name}Handle");
932
933 match ¶m.ty {
934 TypeRef::Named(type_name) => {
935 if true_opaque_types.contains(type_name) {
938 continue;
939 }
940 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
941 if param.optional {
942 out.push_str(&format!(
943 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
944 ));
945 } else {
946 out.push_str(&format!(
947 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
948 ));
949 }
950 out.push_str(&format!(
951 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
952 ));
953 }
954 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
955 out.push_str(&format!(
957 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
958 ));
959 out.push_str(&format!(
960 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
961 ));
962 }
963 TypeRef::Bytes => {
964 out.push_str(&format!(
966 "{indent}var {handle_var} = GCHandle.Alloc({param_name}, GCHandleType.Pinned);\n"
967 ));
968 }
969 _ => {}
970 }
971 }
972}
973
974fn returns_ptr(ty: &TypeRef) -> bool {
977 matches!(
978 ty,
979 TypeRef::String
980 | TypeRef::Char
981 | TypeRef::Path
982 | TypeRef::Json
983 | TypeRef::Named(_)
984 | TypeRef::Vec(_)
985 | TypeRef::Map(_, _)
986 | TypeRef::Bytes
987 | TypeRef::Optional(_)
988 )
989}
990
991fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
997 match ty {
998 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
999 let bang = if optional { "!" } else { "" };
1001 format!("{param_name}{bang}.Handle")
1002 }
1003 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1004 format!("{param_name}Handle")
1005 }
1006 TypeRef::Bytes => {
1007 format!("{param_name}Handle.AddrOfPinnedObject()")
1008 }
1009 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
1010 if optional {
1012 format!("({param_name}?.Value ? 1 : 0)")
1013 } else {
1014 format!("({param_name} ? 1 : 0)")
1015 }
1016 }
1017 ty => {
1018 if optional {
1019 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
1023 if needs_value_unwrap {
1024 format!("{param_name}.GetValueOrDefault()")
1025 } else {
1026 format!("{param_name}!")
1027 }
1028 } else {
1029 param_name.to_string()
1030 }
1031 }
1032 }
1033}
1034
1035fn emit_named_param_teardown(
1040 out: &mut String,
1041 params: &[alef_core::ir::ParamDef],
1042 true_opaque_types: &HashSet<String>,
1043) {
1044 for param in params {
1045 let param_name = param.name.to_lower_camel_case();
1046 let handle_var = format!("{param_name}Handle");
1047 match ¶m.ty {
1048 TypeRef::Named(type_name) => {
1049 if true_opaque_types.contains(type_name) {
1050 continue;
1052 }
1053 let free_method = format!("{}Free", type_name.to_pascal_case());
1054 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
1055 }
1056 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1057 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
1058 }
1059 TypeRef::Bytes => {
1060 out.push_str(&format!(" {handle_var}.Free();\n"));
1061 }
1062 _ => {}
1063 }
1064 }
1065}
1066
1067fn emit_named_param_teardown_indented(
1069 out: &mut String,
1070 params: &[alef_core::ir::ParamDef],
1071 indent: &str,
1072 true_opaque_types: &HashSet<String>,
1073) {
1074 for param in params {
1075 let param_name = param.name.to_lower_camel_case();
1076 let handle_var = format!("{param_name}Handle");
1077 match ¶m.ty {
1078 TypeRef::Named(type_name) => {
1079 if true_opaque_types.contains(type_name) {
1080 continue;
1082 }
1083 let free_method = format!("{}Free", type_name.to_pascal_case());
1084 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
1085 }
1086 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1087 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
1088 }
1089 TypeRef::Bytes => {
1090 out.push_str(&format!("{indent}{handle_var}.Free();\n"));
1091 }
1092 _ => {}
1093 }
1094 }
1095}
1096
1097fn gen_wrapper_function(
1098 func: &FunctionDef,
1099 _exception_name: &str,
1100 _prefix: &str,
1101 enum_names: &HashSet<String>,
1102 true_opaque_types: &HashSet<String>,
1103 bridge_param_names: &HashSet<String>,
1104 bridge_type_aliases: &HashSet<String>,
1105) -> String {
1106 let mut out = String::with_capacity(1024);
1107
1108 let visible_params: Vec<alef_core::ir::ParamDef> = func
1110 .params
1111 .iter()
1112 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1113 .cloned()
1114 .collect();
1115
1116 doc_emission::emit_csharp_doc(&mut out, &func.doc, " ");
1118 for param in &visible_params {
1119 if !func.doc.is_empty() {
1120 out.push_str(&format!(
1121 " /// <param name=\"{}\">{}</param>\n",
1122 param.name.to_lower_camel_case(),
1123 if param.optional { "Optional." } else { "" }
1124 ));
1125 }
1126 }
1127
1128 out.push_str(" public static ");
1129
1130 if func.is_async {
1132 if func.return_type == TypeRef::Unit {
1133 out.push_str("async Task");
1134 } else {
1135 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
1136 }
1137 } else if func.return_type == TypeRef::Unit {
1138 out.push_str("void");
1139 } else {
1140 out.push_str(&csharp_type(&func.return_type));
1141 }
1142
1143 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
1144 out.push('(');
1145
1146 for (i, param) in visible_params.iter().enumerate() {
1148 let param_name = param.name.to_lower_camel_case();
1149 let mapped = csharp_type(¶m.ty);
1150 if param.optional && !mapped.ends_with('?') {
1151 out.push_str(&format!("{mapped}? {param_name}"));
1152 } else {
1153 out.push_str(&format!("{mapped} {param_name}"));
1154 }
1155
1156 if i < visible_params.len() - 1 {
1157 out.push_str(", ");
1158 }
1159 }
1160
1161 out.push_str(")\n {\n");
1162
1163 for param in &visible_params {
1165 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1166 let param_name = param.name.to_lower_camel_case();
1167 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1168 }
1169 }
1170
1171 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1173
1174 let cs_native_name = to_csharp_name(&func.name);
1176
1177 if func.is_async {
1178 if func.return_type == TypeRef::Unit {
1182 out.push_str(" await Task.Run(() =>\n {\n");
1183 } else {
1184 out.push_str(" return await Task.Run(() =>\n {\n");
1185 }
1186
1187 if func.return_type != TypeRef::Unit {
1188 out.push_str(" var nativeResult = ");
1189 } else {
1190 out.push_str(" ");
1191 }
1192
1193 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1194
1195 if visible_params.is_empty() {
1196 out.push_str(");\n");
1197 } else {
1198 out.push('\n');
1199 for (i, param) in visible_params.iter().enumerate() {
1200 let param_name = param.name.to_lower_camel_case();
1201 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1202 out.push_str(&format!(" {arg}"));
1203 if i < visible_params.len() - 1 {
1204 out.push(',');
1205 }
1206 out.push('\n');
1207 }
1208 out.push_str(" );\n");
1209 }
1210
1211 if func.return_type != TypeRef::Unit {
1213 out.push_str(
1214 " if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1215 );
1216 }
1217
1218 emit_return_marshalling_indented(
1219 &mut out,
1220 &func.return_type,
1221 " ",
1222 enum_names,
1223 true_opaque_types,
1224 );
1225 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1226 emit_return_statement_indented(&mut out, &func.return_type, " ");
1227 out.push_str(" });\n");
1228 } else {
1229 if func.return_type != TypeRef::Unit {
1230 out.push_str(" var nativeResult = ");
1231 } else {
1232 out.push_str(" ");
1233 }
1234
1235 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1236
1237 if visible_params.is_empty() {
1238 out.push_str(");\n");
1239 } else {
1240 out.push('\n');
1241 for (i, param) in visible_params.iter().enumerate() {
1242 let param_name = param.name.to_lower_camel_case();
1243 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1244 out.push_str(&format!(" {arg}"));
1245 if i < visible_params.len() - 1 {
1246 out.push(',');
1247 }
1248 out.push('\n');
1249 }
1250 out.push_str(" );\n");
1251 }
1252
1253 if func.return_type != TypeRef::Unit && returns_ptr(&func.return_type) {
1257 out.push_str(
1258 " if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1259 );
1260 }
1261
1262 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
1263 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1264 emit_return_statement(&mut out, &func.return_type);
1265 }
1266
1267 out.push_str(" }\n\n");
1268
1269 out
1270}
1271
1272#[allow(clippy::too_many_arguments)]
1273fn gen_wrapper_method(
1274 method: &MethodDef,
1275 _exception_name: &str,
1276 _prefix: &str,
1277 type_name: &str,
1278 enum_names: &HashSet<String>,
1279 true_opaque_types: &HashSet<String>,
1280 bridge_param_names: &HashSet<String>,
1281 bridge_type_aliases: &HashSet<String>,
1282) -> String {
1283 let mut out = String::with_capacity(1024);
1284
1285 let visible_params: Vec<alef_core::ir::ParamDef> = method
1287 .params
1288 .iter()
1289 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1290 .cloned()
1291 .collect();
1292
1293 doc_emission::emit_csharp_doc(&mut out, &method.doc, " ");
1295 for param in &visible_params {
1296 if !method.doc.is_empty() {
1297 out.push_str(&format!(
1298 " /// <param name=\"{}\">{}</param>\n",
1299 param.name.to_lower_camel_case(),
1300 if param.optional { "Optional." } else { "" }
1301 ));
1302 }
1303 }
1304
1305 out.push_str(" public static ");
1307
1308 if method.is_async {
1310 if method.return_type == TypeRef::Unit {
1311 out.push_str("async Task");
1312 } else {
1313 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1314 }
1315 } else if method.return_type == TypeRef::Unit {
1316 out.push_str("void");
1317 } else {
1318 out.push_str(&csharp_type(&method.return_type));
1319 }
1320
1321 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1323 out.push_str(&format!(" {method_cs_name}"));
1324 out.push('(');
1325
1326 let has_receiver = !method.is_static && method.receiver.is_some();
1330 if has_receiver {
1331 out.push_str("IntPtr handle");
1332 if !visible_params.is_empty() {
1333 out.push_str(", ");
1334 }
1335 }
1336
1337 for (i, param) in visible_params.iter().enumerate() {
1339 let param_name = param.name.to_lower_camel_case();
1340 let mapped = csharp_type(¶m.ty);
1341 if param.optional && !mapped.ends_with('?') {
1342 out.push_str(&format!("{mapped}? {param_name}"));
1343 } else {
1344 out.push_str(&format!("{mapped} {param_name}"));
1345 }
1346
1347 if i < visible_params.len() - 1 {
1348 out.push_str(", ");
1349 }
1350 }
1351
1352 out.push_str(")\n {\n");
1353
1354 for param in &visible_params {
1356 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1357 let param_name = param.name.to_lower_camel_case();
1358 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1359 }
1360 }
1361
1362 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1364
1365 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1370
1371 if method.is_async {
1372 if method.return_type == TypeRef::Unit {
1375 out.push_str(" await Task.Run(() =>\n {\n");
1376 } else {
1377 out.push_str(" return await Task.Run(() =>\n {\n");
1378 }
1379
1380 if method.return_type != TypeRef::Unit {
1381 out.push_str(" var nativeResult = ");
1382 } else {
1383 out.push_str(" ");
1384 }
1385
1386 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1387
1388 if !has_receiver && visible_params.is_empty() {
1389 out.push_str(");\n");
1390 } else {
1391 out.push('\n');
1392 let total = if has_receiver {
1393 visible_params.len() + 1
1394 } else {
1395 visible_params.len()
1396 };
1397 let mut idx = 0usize;
1398 if has_receiver {
1399 out.push_str(" handle");
1400 if total > 1 {
1401 out.push(',');
1402 }
1403 out.push('\n');
1404 idx += 1;
1405 }
1406 for param in visible_params.iter() {
1407 let param_name = param.name.to_lower_camel_case();
1408 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1409 out.push_str(&format!(" {arg}"));
1410 if idx < total - 1 {
1411 out.push(',');
1412 }
1413 out.push('\n');
1414 idx += 1;
1415 }
1416 out.push_str(" );\n");
1417 }
1418
1419 emit_return_marshalling_indented(
1420 &mut out,
1421 &method.return_type,
1422 " ",
1423 enum_names,
1424 true_opaque_types,
1425 );
1426 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1427 emit_return_statement_indented(&mut out, &method.return_type, " ");
1428 out.push_str(" });\n");
1429 } else {
1430 if method.return_type != TypeRef::Unit {
1431 out.push_str(" var nativeResult = ");
1432 } else {
1433 out.push_str(" ");
1434 }
1435
1436 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1437
1438 if !has_receiver && visible_params.is_empty() {
1439 out.push_str(");\n");
1440 } else {
1441 out.push('\n');
1442 let total = if has_receiver {
1443 visible_params.len() + 1
1444 } else {
1445 visible_params.len()
1446 };
1447 let mut idx = 0usize;
1448 if has_receiver {
1449 out.push_str(" handle");
1450 if total > 1 {
1451 out.push(',');
1452 }
1453 out.push('\n');
1454 idx += 1;
1455 }
1456 for param in visible_params.iter() {
1457 let param_name = param.name.to_lower_camel_case();
1458 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1459 out.push_str(&format!(" {arg}"));
1460 if idx < total - 1 {
1461 out.push(',');
1462 }
1463 out.push('\n');
1464 idx += 1;
1465 }
1466 out.push_str(" );\n");
1467 }
1468
1469 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1470 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1471 emit_return_statement(&mut out, &method.return_type);
1472 }
1473
1474 out.push_str(" }\n\n");
1475
1476 out
1477}
1478
1479fn emit_return_marshalling(
1491 out: &mut String,
1492 return_type: &TypeRef,
1493 enum_names: &HashSet<String>,
1494 true_opaque_types: &HashSet<String>,
1495) {
1496 if *return_type == TypeRef::Unit {
1497 return;
1499 }
1500
1501 if returns_string(return_type) {
1502 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n");
1504 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1505 } else if returns_bool_via_int(return_type) {
1506 out.push_str(" var returnValue = nativeResult != 0;\n");
1508 } else if let TypeRef::Named(type_name) = return_type {
1509 let pascal = type_name.to_pascal_case();
1510 if true_opaque_types.contains(type_name) {
1511 out.push_str(&format!(" var returnValue = new {pascal}(nativeResult);\n"));
1513 } else if !enum_names.contains(&pascal) {
1514 let to_json_method = format!("{pascal}ToJson");
1516 let free_method = format!("{pascal}Free");
1517 let cs_ty = csharp_type(return_type);
1518 out.push_str(&format!(
1519 " var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
1520 ));
1521 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1522 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1523 out.push_str(&format!(" NativeMethods.{free_method}(nativeResult);\n"));
1524 out.push_str(&format!(
1525 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1526 cs_ty
1527 ));
1528 } else {
1529 let cs_ty = csharp_type(return_type);
1531 out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
1532 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1533 out.push_str(&format!(
1534 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1535 cs_ty
1536 ));
1537 }
1538 } else if returns_json_object(return_type) {
1539 let cs_ty = csharp_type(return_type);
1541 out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
1542 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1543 out.push_str(&format!(
1544 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1545 cs_ty
1546 ));
1547 } else {
1548 out.push_str(" var returnValue = nativeResult;\n");
1550 }
1551}
1552
1553fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1555 if *return_type != TypeRef::Unit {
1556 out.push_str(" return returnValue;\n");
1557 }
1558}
1559
1560fn emit_return_marshalling_indented(
1565 out: &mut String,
1566 return_type: &TypeRef,
1567 indent: &str,
1568 enum_names: &HashSet<String>,
1569 true_opaque_types: &HashSet<String>,
1570) {
1571 if *return_type == TypeRef::Unit {
1572 return;
1573 }
1574
1575 if returns_string(return_type) {
1576 out.push_str(&format!(
1577 "{indent}var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n"
1578 ));
1579 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1580 } else if returns_bool_via_int(return_type) {
1581 out.push_str(&format!("{indent}var returnValue = nativeResult != 0;\n"));
1582 } else if let TypeRef::Named(type_name) = return_type {
1583 let pascal = type_name.to_pascal_case();
1584 if true_opaque_types.contains(type_name) {
1585 out.push_str(&format!("{indent}var returnValue = new {pascal}(nativeResult);\n"));
1587 } else if !enum_names.contains(&pascal) {
1588 let to_json_method = format!("{pascal}ToJson");
1590 let free_method = format!("{pascal}Free");
1591 let cs_ty = csharp_type(return_type);
1592 out.push_str(&format!(
1593 "{indent}var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
1594 ));
1595 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1596 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1597 out.push_str(&format!("{indent}NativeMethods.{free_method}(nativeResult);\n"));
1598 out.push_str(&format!(
1599 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1600 cs_ty
1601 ));
1602 } else {
1603 let cs_ty = csharp_type(return_type);
1605 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
1606 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1607 out.push_str(&format!(
1608 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1609 cs_ty
1610 ));
1611 }
1612 } else if returns_json_object(return_type) {
1613 let cs_ty = csharp_type(return_type);
1614 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
1615 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1616 out.push_str(&format!(
1617 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1618 cs_ty
1619 ));
1620 } else {
1621 out.push_str(&format!("{indent}var returnValue = nativeResult;\n"));
1622 }
1623}
1624
1625fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1627 if *return_type != TypeRef::Unit {
1628 out.push_str(&format!("{indent}return returnValue;\n"));
1629 }
1630}
1631
1632fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1633 let mut out = csharp_file_header();
1634 out.push_str("using System;\n\n");
1635
1636 out.push_str(&format!("namespace {};\n\n", namespace));
1637
1638 if !typ.doc.is_empty() {
1640 out.push_str("/// <summary>\n");
1641 for line in typ.doc.lines() {
1642 out.push_str(&format!("/// {}\n", line));
1643 }
1644 out.push_str("/// </summary>\n");
1645 }
1646
1647 let class_name = typ.name.to_pascal_case();
1648 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1649 out.push_str("{\n");
1650 out.push_str(" internal IntPtr Handle { get; }\n\n");
1651 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1652 out.push_str(" {\n");
1653 out.push_str(" Handle = handle;\n");
1654 out.push_str(" }\n\n");
1655 out.push_str(" public void Dispose()\n");
1656 out.push_str(" {\n");
1657 out.push_str(" // Native free will be called by the runtime\n");
1658 out.push_str(" }\n");
1659 out.push_str("}\n");
1660
1661 out
1662}
1663
1664fn gen_record_type(
1665 typ: &TypeDef,
1666 namespace: &str,
1667 enum_names: &HashSet<String>,
1668 complex_enums: &HashSet<String>,
1669 custom_converter_enums: &HashSet<String>,
1670 _lang_rename_all: &str,
1671) -> String {
1672 let mut out = csharp_file_header();
1673 out.push_str("using System;\n");
1674 out.push_str("using System.Collections.Generic;\n");
1675 out.push_str("using System.Text.Json;\n");
1676 out.push_str("using System.Text.Json.Serialization;\n\n");
1677
1678 out.push_str(&format!("namespace {};\n\n", namespace));
1679
1680 if !typ.doc.is_empty() {
1682 out.push_str("/// <summary>\n");
1683 for line in typ.doc.lines() {
1684 out.push_str(&format!("/// {}\n", line));
1685 }
1686 out.push_str("/// </summary>\n");
1687 }
1688
1689 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1690 out.push_str("{\n");
1691
1692 for field in &typ.fields {
1693 if is_tuple_field(field) {
1695 continue;
1696 }
1697
1698 if !field.doc.is_empty() {
1700 out.push_str(" /// <summary>\n");
1701 for line in field.doc.lines() {
1702 out.push_str(&format!(" /// {}\n", line));
1703 }
1704 out.push_str(" /// </summary>\n");
1705 }
1706
1707 let field_base_type = match &field.ty {
1711 TypeRef::Named(n) => Some(n.to_pascal_case()),
1712 TypeRef::Optional(inner) => match inner.as_ref() {
1713 TypeRef::Named(n) => Some(n.to_pascal_case()),
1714 _ => None,
1715 },
1716 _ => None,
1717 };
1718 if let Some(ref base) = field_base_type {
1719 if custom_converter_enums.contains(base) {
1720 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1721 }
1722 }
1723
1724 let json_name = field.name.clone();
1728 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1729
1730 let cs_name = to_csharp_name(&field.name);
1731
1732 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1735
1736 if field.optional {
1737 let mapped = if is_complex {
1739 "JsonElement".to_string()
1740 } else {
1741 csharp_type(&field.ty).to_string()
1742 };
1743 let field_type = if mapped.ends_with('?') {
1744 mapped
1745 } else {
1746 format!("{mapped}?")
1747 };
1748 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1749 out.push_str(" = null;\n");
1750 } else if typ.has_default || field.default.is_some() {
1751 use alef_core::ir::DefaultValue;
1754
1755 let base_type = if is_complex {
1757 "JsonElement".to_string()
1758 } else {
1759 csharp_type(&field.ty).to_string()
1760 };
1761
1762 if matches!(&field.ty, TypeRef::Duration) {
1765 let nullable_type = if base_type.ends_with('?') {
1767 base_type.clone()
1768 } else {
1769 format!("{}?", base_type)
1770 };
1771 out.push_str(&format!(
1772 " public {} {} {{ get; set; }} = null;\n",
1773 nullable_type, cs_name
1774 ));
1775 out.push('\n');
1776 continue;
1777 }
1778
1779 let default_val = match &field.typed_default {
1780 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1781 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1782 Some(DefaultValue::FloatLiteral(f)) => {
1783 let s = f.to_string();
1784 let s = if s.contains('.') { s } else { format!("{s}.0") };
1785 match &field.ty {
1786 TypeRef::Primitive(PrimitiveType::F32) => format!("{}f", s),
1787 _ => s,
1788 }
1789 }
1790 Some(DefaultValue::StringLiteral(s)) => {
1791 let escaped = s
1792 .replace('\\', "\\\\")
1793 .replace('"', "\\\"")
1794 .replace('\n', "\\n")
1795 .replace('\r', "\\r")
1796 .replace('\t', "\\t");
1797 format!("\"{}\"", escaped)
1798 }
1799 Some(DefaultValue::EnumVariant(v)) => {
1800 if base_type == "string" || base_type == "string?" {
1804 format!("\"{}\"", v.to_pascal_case())
1805 } else {
1806 format!("{}.{}", base_type, v.to_pascal_case())
1807 }
1808 }
1809 Some(DefaultValue::None) => "null".to_string(),
1810 Some(DefaultValue::Empty) | None => match &field.ty {
1811 TypeRef::Vec(_) => "[]".to_string(),
1812 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1813 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1814 TypeRef::Json => "null".to_string(),
1815 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1816 TypeRef::Primitive(p) => match p {
1817 PrimitiveType::Bool => "false".to_string(),
1818 PrimitiveType::F32 => "0.0f".to_string(),
1819 PrimitiveType::F64 => "0.0".to_string(),
1820 _ => "0".to_string(),
1821 },
1822 TypeRef::Named(name) => {
1823 let pascal = name.to_pascal_case();
1824 if complex_enums.contains(&pascal) {
1825 "null".to_string()
1827 } else if enum_names.contains(&pascal) {
1828 "null".to_string()
1831 } else {
1832 "default!".to_string()
1833 }
1834 }
1835 _ => "default!".to_string(),
1836 },
1837 };
1838
1839 let field_type = if (default_val == "null" && !base_type.ends_with('?')) || is_complex {
1841 format!("{}?", base_type)
1842 } else {
1843 base_type
1844 };
1845
1846 out.push_str(&format!(
1847 " public {} {} {{ get; set; }} = {};\n",
1848 field_type, cs_name, default_val
1849 ));
1850 } else {
1851 let field_type = if is_complex {
1855 "JsonElement".to_string()
1856 } else {
1857 csharp_type(&field.ty).to_string()
1858 };
1859 if matches!(&field.ty, TypeRef::Duration) {
1861 out.push_str(&format!(
1862 " public {} {} {{ get; set; }} = null;\n",
1863 field_type, cs_name
1864 ));
1865 } else {
1866 let default_val = match &field.ty {
1867 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1868 TypeRef::Vec(_) => "[]",
1869 TypeRef::Bytes => "Array.Empty<byte>()",
1870 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1871 TypeRef::Primitive(PrimitiveType::F32) => "0.0f",
1872 TypeRef::Primitive(PrimitiveType::F64) => "0.0",
1873 TypeRef::Primitive(_) => "0",
1874 _ => "default!",
1875 };
1876 out.push_str(&format!(
1877 " public {} {} {{ get; set; }} = {};\n",
1878 field_type, cs_name, default_val
1879 ));
1880 }
1881 }
1882
1883 out.push('\n');
1884 }
1885
1886 out.push_str("}\n");
1887
1888 out
1889}
1890
1891fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1893 match rename_all {
1894 Some("snake_case") => name.to_snake_case(),
1895 Some("camelCase") => name.to_lower_camel_case(),
1896 Some("PascalCase") => name.to_pascal_case(),
1897 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1898 Some("lowercase") => name.to_lowercase(),
1899 Some("UPPERCASE") => name.to_uppercase(),
1900 _ => name.to_lowercase(),
1901 }
1902}
1903
1904fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1905 let mut out = csharp_file_header();
1906 out.push_str("using System.Text.Json.Serialization;\n\n");
1907 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1908
1909 if enum_def.serde_tag.is_some() && has_data_variants {
1911 return gen_tagged_union(enum_def, namespace);
1912 }
1913
1914 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1922 if let Some(ref rename) = v.serde_rename {
1923 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1924 rename != &snake
1925 } else {
1926 false
1927 }
1928 });
1929
1930 let enum_pascal = enum_def.name.to_pascal_case();
1931
1932 let variants: Vec<(String, String)> = enum_def
1934 .variants
1935 .iter()
1936 .map(|v| {
1937 let json_name = v
1938 .serde_rename
1939 .clone()
1940 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1941 let pascal_name = v.name.to_pascal_case();
1942 (json_name, pascal_name)
1943 })
1944 .collect();
1945
1946 out.push_str("using System;\n");
1947 out.push_str("using System.Text.Json;\n\n");
1948
1949 out.push_str(&format!("namespace {};\n\n", namespace));
1950
1951 if !enum_def.doc.is_empty() {
1953 out.push_str("/// <summary>\n");
1954 for line in enum_def.doc.lines() {
1955 out.push_str(&format!("/// {}\n", line));
1956 }
1957 out.push_str("/// </summary>\n");
1958 }
1959
1960 if needs_custom_converter {
1961 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1962 }
1963 out.push_str(&format!("public enum {enum_pascal}\n"));
1964 out.push_str("{\n");
1965
1966 for (json_name, pascal_name) in &variants {
1967 if let Some(v) = enum_def
1969 .variants
1970 .iter()
1971 .find(|v| v.name.to_pascal_case() == *pascal_name)
1972 {
1973 if !v.doc.is_empty() {
1974 out.push_str(" /// <summary>\n");
1975 for line in v.doc.lines() {
1976 out.push_str(&format!(" /// {}\n", line));
1977 }
1978 out.push_str(" /// </summary>\n");
1979 }
1980 }
1981 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1982 out.push_str(&format!(" {pascal_name},\n"));
1983 }
1984
1985 out.push_str("}\n");
1986
1987 if needs_custom_converter {
1989 out.push('\n');
1990 out.push_str(&format!(
1991 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1992 ));
1993 out.push_str(&format!(
1994 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1995 ));
1996 out.push_str("{\n");
1997
1998 out.push_str(&format!(
2000 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
2001 ));
2002 out.push_str(" {\n");
2003 out.push_str(" var value = reader.GetString();\n");
2004 out.push_str(" return value switch\n");
2005 out.push_str(" {\n");
2006 for (json_name, pascal_name) in &variants {
2007 out.push_str(&format!(
2008 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
2009 ));
2010 }
2011 out.push_str(&format!(
2012 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
2013 ));
2014 out.push_str(" };\n");
2015 out.push_str(" }\n\n");
2016
2017 out.push_str(&format!(
2019 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2020 ));
2021 out.push_str(" {\n");
2022 out.push_str(" var str = value switch\n");
2023 out.push_str(" {\n");
2024 for (json_name, pascal_name) in &variants {
2025 out.push_str(&format!(
2026 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
2027 ));
2028 }
2029 out.push_str(&format!(
2030 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
2031 ));
2032 out.push_str(" };\n");
2033 out.push_str(" writer.WriteStringValue(str);\n");
2034 out.push_str(" }\n");
2035 out.push_str("}\n");
2036 }
2037
2038 out
2039}
2040
2041fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
2048 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
2049 let enum_pascal = enum_def.name.to_pascal_case();
2050 let converter_name = format!("{enum_pascal}JsonConverter");
2051 let ns = namespace;
2054
2055 let mut out = csharp_file_header();
2056 out.push_str("using System;\n");
2057 out.push_str("using System.Collections.Generic;\n");
2058 out.push_str("using System.Text.Json;\n");
2059 out.push_str("using System.Text.Json.Serialization;\n\n");
2060 out.push_str(&format!("namespace {};\n\n", namespace));
2061
2062 if !enum_def.doc.is_empty() {
2064 out.push_str("/// <summary>\n");
2065 for line in enum_def.doc.lines() {
2066 out.push_str(&format!("/// {}\n", line));
2067 }
2068 out.push_str("/// </summary>\n");
2069 }
2070
2071 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
2073 out.push_str(&format!("public abstract record {enum_pascal}\n"));
2074 out.push_str("{\n");
2075
2076 let variant_names: std::collections::HashSet<String> =
2078 enum_def.variants.iter().map(|v| v.name.to_pascal_case()).collect();
2079
2080 for variant in &enum_def.variants {
2082 let pascal = variant.name.to_pascal_case();
2083
2084 if !variant.doc.is_empty() {
2085 out.push_str(" /// <summary>\n");
2086 for line in variant.doc.lines() {
2087 out.push_str(&format!(" /// {}\n", line));
2088 }
2089 out.push_str(" /// </summary>\n");
2090 }
2091
2092 if variant.fields.is_empty() {
2093 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
2095 } else {
2096 let is_copy_ctor_clash = variant.fields.len() == 1 && {
2101 let field_cs_type = csharp_type(&variant.fields[0].ty);
2102 field_cs_type.as_ref() == pascal
2103 };
2104
2105 if is_copy_ctor_clash {
2106 let cs_type = csharp_type(&variant.fields[0].ty);
2107 let qualified_cs_type = format!("global::{ns}.{cs_type}");
2111 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
2112 out.push_str(" {\n");
2113 out.push_str(&format!(
2114 " public required {qualified_cs_type} Value {{ get; init; }}\n"
2115 ));
2116 out.push_str(" }\n\n");
2117 } else {
2118 out.push_str(&format!(" public sealed record {pascal}(\n"));
2120 for (i, field) in variant.fields.iter().enumerate() {
2121 let cs_type = csharp_type(&field.ty);
2122 let cs_type = if field.optional && !cs_type.ends_with('?') {
2123 format!("{cs_type}?")
2124 } else {
2125 cs_type.to_string()
2126 };
2127 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
2128 if is_tuple_field(field) {
2129 out.push_str(&format!(" {cs_type} Value{comma}\n"));
2130 } else {
2131 let json_name = field.name.trim_start_matches('_');
2132 let cs_name = to_csharp_name(json_name);
2133 let clashes = cs_name == pascal || cs_name == cs_type || variant_names.contains(&cs_name);
2138 if clashes {
2139 out.push_str(&format!(
2141 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} Value{comma}\n"
2142 ));
2143 } else {
2144 out.push_str(&format!(
2145 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
2146 ));
2147 }
2148 }
2149 }
2150 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
2151 }
2152 }
2153 }
2154
2155 out.push_str("}\n\n");
2156
2157 out.push_str(&format!(
2159 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
2160 ));
2161 out.push_str(&format!(
2162 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
2163 ));
2164 out.push_str("{\n");
2165
2166 out.push_str(&format!(
2168 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
2169 ));
2170 out.push_str(" {\n");
2171 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
2172 out.push_str(" var root = doc.RootElement;\n");
2173 out.push_str(&format!(
2174 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
2175 ));
2176 out.push_str(&format!(
2177 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
2178 ));
2179 out.push_str(" var tag = tagEl.GetString();\n");
2180 out.push_str(" var json = root.GetRawText();\n");
2181 out.push_str(" return tag switch\n");
2182 out.push_str(" {\n");
2183
2184 for variant in &enum_def.variants {
2185 let discriminator = variant
2186 .serde_rename
2187 .clone()
2188 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2189 let pascal = variant.name.to_pascal_case();
2190 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2195 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2196 let f = &variant.fields[0];
2197 let cs_type = csharp_type(&f.ty);
2198 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2199 cs_name == pascal || cs_name == cs_type
2200 };
2201 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2202 if is_newtype {
2203 let inner_cs_type = csharp_type(&variant.fields[0].ty);
2204 if inner_cs_type == pascal {
2207 out.push_str(&format!(
2208 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2209 ));
2210 out.push_str(&format!(
2211 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
2212 ));
2213 } else {
2214 out.push_str(&format!(
2215 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
2216 ));
2217 out.push_str(&format!(
2218 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2219 ));
2220 out.push_str(&format!(
2221 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
2222 ));
2223 }
2224 } else {
2225 out.push_str(&format!(
2226 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
2227 ));
2228 out.push_str(&format!(
2229 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
2230 ));
2231 }
2232 }
2233
2234 out.push_str(&format!(
2235 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
2236 ));
2237 out.push_str(" };\n");
2238 out.push_str(" }\n\n");
2239
2240 out.push_str(&format!(
2242 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2243 ));
2244 out.push_str(" {\n");
2245
2246 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
2248 out.push_str(" switch (value)\n");
2249 out.push_str(" {\n");
2250
2251 for variant in &enum_def.variants {
2252 let discriminator = variant
2253 .serde_rename
2254 .clone()
2255 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2256 let pascal = variant.name.to_pascal_case();
2257 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2261 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2262 let f = &variant.fields[0];
2263 let cs_type = csharp_type(&f.ty);
2264 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2265 cs_name == pascal || cs_name == cs_type
2266 };
2267 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2268 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
2272 out.push_str(" {\n");
2273 if is_newtype {
2274 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
2275 } else {
2276 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
2277 }
2278 out.push_str(" writer.WriteStartObject();\n");
2279 out.push_str(&format!(
2280 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
2281 ));
2282 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2283 out.push_str(&format!(
2284 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2285 ));
2286 out.push_str(" writer.WriteEndObject();\n");
2287 out.push_str(" break;\n");
2288 out.push_str(" }\n");
2289 }
2290
2291 out.push_str(&format!(
2292 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2293 ));
2294 out.push_str(" }\n");
2295 out.push_str(" }\n");
2296 out.push_str("}\n");
2297
2298 out
2299}
2300
2301fn gen_directory_build_props() -> String {
2304 "<!-- auto-generated by alef (generate_bindings) -->\n\
2305<Project>\n \
2306<PropertyGroup>\n \
2307<Nullable>enable</Nullable>\n \
2308<LangVersion>latest</LangVersion>\n \
2309</PropertyGroup>\n\
2310</Project>\n"
2311 .to_string()
2312}
2313
2314fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
2318 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
2319
2320 for filename in stale_files {
2321 let path = base_path.join(filename);
2322 if path.exists() {
2323 std::fs::remove_file(&path)
2324 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
2325 }
2326 }
2327
2328 Ok(())
2329}