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 output_dir = resolve_output_dir(
68 config.output.csharp.as_ref(),
69 &config.crate_config.name,
70 "packages/csharp/",
71 );
72
73 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
74
75 let mut files = Vec::new();
76
77 files.push(GeneratedFile {
79 path: base_path.join("NativeMethods.cs"),
80 content: strip_trailing_whitespace(&gen_native_methods(
81 api,
82 &namespace,
83 &lib_name,
84 &prefix,
85 &bridge_param_names,
86 &bridge_type_aliases,
87 has_visitor_callbacks,
88 &config.trait_bridges,
89 &streaming_methods,
90 )),
91 generated_header: true,
92 });
93
94 if !api.errors.is_empty() {
96 for error in &api.errors {
97 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
98 for (class_name, content) in error_files {
99 files.push(GeneratedFile {
100 path: base_path.join(format!("{}.cs", class_name)),
101 content: strip_trailing_whitespace(&content),
102 generated_header: false, });
104 }
105 }
106 }
107
108 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
110 if api.errors.is_empty()
111 || !api
112 .errors
113 .iter()
114 .any(|e| format!("{}Exception", e.name) == exception_class_name)
115 {
116 files.push(GeneratedFile {
117 path: base_path.join(format!("{}.cs", exception_class_name)),
118 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
119 generated_header: true,
120 });
121 }
122
123 let base_class_name = api.crate_name.to_pascal_case();
125 let wrapper_class_name = if namespace == base_class_name {
126 format!("{}Lib", base_class_name)
127 } else {
128 base_class_name
129 };
130 files.push(GeneratedFile {
131 path: base_path.join(format!("{}.cs", wrapper_class_name)),
132 content: strip_trailing_whitespace(&gen_wrapper_class(
133 api,
134 &namespace,
135 &wrapper_class_name,
136 &exception_class_name,
137 &prefix,
138 &bridge_param_names,
139 &bridge_type_aliases,
140 has_visitor_callbacks,
141 &streaming_methods,
142 )),
143 generated_header: true,
144 });
145
146 if has_visitor_callbacks {
148 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
149 files.push(GeneratedFile {
150 path: base_path.join(filename),
151 content: strip_trailing_whitespace(&content),
152 generated_header: true,
153 });
154 }
155 } else {
156 delete_stale_visitor_files(&base_path)?;
159 }
160
161 if !config.trait_bridges.is_empty() {
163 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
164 let bridges: Vec<_> = config
165 .trait_bridges
166 .iter()
167 .filter_map(|cfg| {
168 let trait_name = cfg.trait_name.clone();
169 trait_defs
170 .iter()
171 .find(|t| t.name == trait_name)
172 .map(|trait_def| (trait_name, cfg, *trait_def))
173 })
174 .collect();
175
176 if !bridges.is_empty() {
177 let (filename, content) = crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges);
178 files.push(GeneratedFile {
179 path: base_path.join(filename),
180 content: strip_trailing_whitespace(&content),
181 generated_header: true,
182 });
183 }
184 }
185
186 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
188 if typ.is_opaque {
189 let type_filename = typ.name.to_pascal_case();
190 files.push(GeneratedFile {
191 path: base_path.join(format!("{}.cs", type_filename)),
192 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
193 generated_header: true,
194 });
195 }
196 }
197
198 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
200
201 let complex_enums: HashSet<String> = api
206 .enums
207 .iter()
208 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
209 .map(|e| e.name.to_pascal_case())
210 .collect();
211
212 let custom_converter_enums: HashSet<String> = api
216 .enums
217 .iter()
218 .filter(|e| {
219 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
221 || e.variants.iter().any(|v| {
223 if let Some(ref rename) = v.serde_rename {
224 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
225 rename != &snake
226 } else {
227 false
228 }
229 })
230 })
231 .map(|e| e.name.to_pascal_case())
232 .collect();
233
234 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
236
237 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
239 if !typ.is_opaque {
240 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
243 if !typ.fields.is_empty() && !has_named_fields {
244 continue;
245 }
246 if has_visitor_callbacks && (typ.name == "NodeContext" || typ.name == "VisitResult") {
248 continue;
249 }
250
251 let type_filename = typ.name.to_pascal_case();
252 files.push(GeneratedFile {
253 path: base_path.join(format!("{}.cs", type_filename)),
254 content: strip_trailing_whitespace(&gen_record_type(
255 typ,
256 &namespace,
257 &enum_names,
258 &complex_enums,
259 &custom_converter_enums,
260 &lang_rename_all,
261 )),
262 generated_header: true,
263 });
264 }
265 }
266
267 for enum_def in &api.enums {
269 if has_visitor_callbacks && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
271 continue;
272 }
273 let enum_filename = enum_def.name.to_pascal_case();
274 files.push(GeneratedFile {
275 path: base_path.join(format!("{}.cs", enum_filename)),
276 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
277 generated_header: true,
278 });
279 }
280
281 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
283
284 files.push(GeneratedFile {
288 path: PathBuf::from("packages/csharp/Directory.Build.props"),
289 content: gen_directory_build_props(),
290 generated_header: true,
291 });
292
293 Ok(files)
294 }
295
296 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
301 Ok(vec![])
303 }
304
305 fn build_config(&self) -> Option<BuildConfig> {
306 Some(BuildConfig {
307 tool: "dotnet",
308 crate_suffix: "",
309 build_dep: BuildDependency::Ffi,
310 post_build: vec![],
311 })
312 }
313}
314
315fn is_tuple_field(field: &FieldDef) -> bool {
317 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
318 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
319}
320
321fn strip_trailing_whitespace(content: &str) -> String {
323 let mut result: String = content
324 .lines()
325 .map(|line| line.trim_end())
326 .collect::<Vec<_>>()
327 .join("\n");
328 if !result.ends_with('\n') {
329 result.push('\n');
330 }
331 result
332}
333
334fn csharp_file_header() -> String {
336 let mut out = hash::header(CommentStyle::DoubleSlash);
337 out.push_str("#nullable enable\n\n");
338 out
339}
340
341fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
352 match ty {
353 TypeRef::Unit => "void",
354 TypeRef::Primitive(PrimitiveType::Bool) => "int",
356 TypeRef::Primitive(PrimitiveType::U8) => "byte",
358 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
359 TypeRef::Primitive(PrimitiveType::U32) => "uint",
360 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
361 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
362 TypeRef::Primitive(PrimitiveType::I16) => "short",
363 TypeRef::Primitive(PrimitiveType::I32) => "int",
364 TypeRef::Primitive(PrimitiveType::I64) => "long",
365 TypeRef::Primitive(PrimitiveType::F32) => "float",
366 TypeRef::Primitive(PrimitiveType::F64) => "double",
367 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
368 TypeRef::Primitive(PrimitiveType::Isize) => "long",
369 TypeRef::Duration => "ulong",
371 TypeRef::String
373 | TypeRef::Char
374 | TypeRef::Bytes
375 | TypeRef::Optional(_)
376 | TypeRef::Vec(_)
377 | TypeRef::Map(_, _)
378 | TypeRef::Named(_)
379 | TypeRef::Path
380 | TypeRef::Json => "IntPtr",
381 }
382}
383
384fn returns_string(ty: &TypeRef) -> bool {
386 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
387}
388
389fn returns_bool_via_int(ty: &TypeRef) -> bool {
391 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
392}
393
394fn returns_json_object(ty: &TypeRef) -> bool {
396 matches!(
397 ty,
398 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
399 )
400}
401
402fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
413 match ty {
414 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
415 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
417 TypeRef::Unit => "void",
418 TypeRef::Primitive(PrimitiveType::Bool) => "int",
419 TypeRef::Primitive(PrimitiveType::U8) => "byte",
420 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
421 TypeRef::Primitive(PrimitiveType::U32) => "uint",
422 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
423 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
424 TypeRef::Primitive(PrimitiveType::I16) => "short",
425 TypeRef::Primitive(PrimitiveType::I32) => "int",
426 TypeRef::Primitive(PrimitiveType::I64) => "long",
427 TypeRef::Primitive(PrimitiveType::F32) => "float",
428 TypeRef::Primitive(PrimitiveType::F64) => "double",
429 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
430 TypeRef::Primitive(PrimitiveType::Isize) => "long",
431 TypeRef::Duration => "ulong",
432 }
433}
434
435fn is_bridge_param(
442 param: &alef_core::ir::ParamDef,
443 bridge_param_names: &HashSet<String>,
444 bridge_type_aliases: &HashSet<String>,
445) -> bool {
446 bridge_param_names.contains(¶m.name)
447 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
448}
449
450#[allow(clippy::too_many_arguments)]
451fn gen_native_methods(
452 api: &ApiSurface,
453 namespace: &str,
454 lib_name: &str,
455 prefix: &str,
456 bridge_param_names: &HashSet<String>,
457 bridge_type_aliases: &HashSet<String>,
458 has_visitor_callbacks: bool,
459 trait_bridges: &[alef_core::config::TraitBridgeConfig],
460 streaming_methods: &HashSet<String>,
461) -> String {
462 let mut out = csharp_file_header();
463 out.push_str("using System;\n");
464 out.push_str("using System.Runtime.InteropServices;\n\n");
465
466 out.push_str(&format!("namespace {};\n\n", namespace));
467
468 out.push_str("internal static partial class NativeMethods\n{\n");
469 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
470
471 let mut emitted: HashSet<String> = HashSet::new();
474
475 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
478
479 let mut opaque_param_types: HashSet<String> = HashSet::new();
483 let mut opaque_return_types: HashSet<String> = HashSet::new();
484
485 for func in &api.functions {
489 for param in &func.params {
490 if let TypeRef::Named(name) = ¶m.ty {
491 opaque_param_types.insert(name.clone());
492 }
493 }
494 if let TypeRef::Named(name) = &func.return_type {
495 if !enum_names.contains(name) {
496 opaque_return_types.insert(name.clone());
497 }
498 }
499 }
500 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
501 for method in &typ.methods {
502 if streaming_methods.contains(&method.name) {
503 continue;
504 }
505 for param in &method.params {
506 if let TypeRef::Named(name) = ¶m.ty {
507 opaque_param_types.insert(name.clone());
508 }
509 }
510 if let TypeRef::Named(name) = &method.return_type {
511 if !enum_names.contains(name) {
512 opaque_return_types.insert(name.clone());
513 }
514 }
515 }
516 }
517
518 let true_opaque_types: HashSet<String> = api
520 .types
521 .iter()
522 .filter(|t| t.is_opaque)
523 .map(|t| t.name.clone())
524 .collect();
525
526 let mut sorted_param_types: Vec<&String> = opaque_param_types.iter().collect();
530 sorted_param_types.sort();
531 for type_name in sorted_param_types {
532 let snake = type_name.to_snake_case();
533 if !true_opaque_types.contains(type_name) {
534 let from_json_entry = format!("{prefix}_{snake}_from_json");
535 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
536 if emitted.insert(from_json_entry.clone()) {
537 out.push_str(&format!(
538 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
539 ));
540 out.push_str(&format!(
541 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
542 ));
543 }
544 }
545 let free_entry = format!("{prefix}_{snake}_free");
546 let free_cs = format!("{}Free", type_name.to_pascal_case());
547 if emitted.insert(free_entry.clone()) {
548 out.push_str(&format!(
549 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
550 ));
551 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
552 }
553 }
554
555 let mut sorted_return_types: Vec<&String> = opaque_return_types.iter().collect();
558 sorted_return_types.sort();
559 for type_name in sorted_return_types {
560 let snake = type_name.to_snake_case();
561 if !true_opaque_types.contains(type_name) {
562 let to_json_entry = format!("{prefix}_{snake}_to_json");
563 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
564 if emitted.insert(to_json_entry.clone()) {
565 out.push_str(&format!(
566 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
567 ));
568 out.push_str(&format!(
569 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
570 ));
571 }
572 }
573 let free_entry = format!("{prefix}_{snake}_free");
574 let free_cs = format!("{}Free", type_name.to_pascal_case());
575 if emitted.insert(free_entry.clone()) {
576 out.push_str(&format!(
577 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
578 ));
579 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
580 }
581 }
582
583 for func in &api.functions {
585 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
586 if emitted.insert(c_func_name.clone()) {
587 out.push_str(&gen_pinvoke_for_func(
588 &c_func_name,
589 func,
590 bridge_param_names,
591 bridge_type_aliases,
592 ));
593 }
594 }
595
596 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
599 let type_snake = typ.name.to_snake_case();
600 for method in &typ.methods {
601 if streaming_methods.contains(&method.name) {
602 continue;
603 }
604 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
605 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
609 if emitted.insert(c_method_name.clone()) {
610 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
611 }
612 }
613 }
614
615 out.push_str(&format!(
617 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
618 ));
619 out.push_str(" internal static extern int LastErrorCode();\n\n");
620
621 out.push_str(&format!(
622 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
623 ));
624 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
625
626 out.push_str(&format!(
627 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
628 ));
629 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
630
631 if has_visitor_callbacks {
633 out.push('\n');
634 out.push_str(&crate::gen_visitor::gen_native_methods_visitor(
635 namespace, lib_name, prefix,
636 ));
637 }
638
639 if !trait_bridges.is_empty() {
641 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
643
644 let bridges: Vec<_> = trait_bridges
646 .iter()
647 .filter_map(|config| {
648 let trait_name = config.trait_name.clone();
649 trait_defs
650 .iter()
651 .find(|t| t.name == trait_name)
652 .map(|trait_def| (trait_name, config, *trait_def))
653 })
654 .collect();
655
656 if !bridges.is_empty() {
657 out.push('\n');
658 out.push_str(&crate::trait_bridge::gen_native_methods_trait_bridges(
659 namespace, prefix, &bridges,
660 ));
661 }
662 }
663
664 out.push_str("}\n");
665
666 out
667}
668
669fn gen_pinvoke_for_func(
670 c_name: &str,
671 func: &FunctionDef,
672 bridge_param_names: &HashSet<String>,
673 bridge_type_aliases: &HashSet<String>,
674) -> String {
675 let cs_name = to_csharp_name(&func.name);
676 let mut out =
677 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
678 out.push_str(" internal static extern ");
679
680 out.push_str(pinvoke_return_type(&func.return_type));
682
683 out.push_str(&format!(" {}(", cs_name));
684
685 let visible_params: Vec<_> = func
688 .params
689 .iter()
690 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
691 .collect();
692
693 if visible_params.is_empty() {
694 out.push_str(");\n\n");
695 } else {
696 out.push('\n');
697 for (i, param) in visible_params.iter().enumerate() {
698 out.push_str(" ");
699 let pinvoke_ty = pinvoke_param_type(¶m.ty);
700 if pinvoke_ty == "string" {
701 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
702 }
703 let param_name = param.name.to_lower_camel_case();
704 out.push_str(&format!("{pinvoke_ty} {param_name}"));
705
706 if i < visible_params.len() - 1 {
707 out.push(',');
708 }
709 out.push('\n');
710 }
711 out.push_str(" );\n\n");
712 }
713
714 out
715}
716
717fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
718 let mut out =
719 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
720 out.push_str(" internal static extern ");
721
722 out.push_str(pinvoke_return_type(&method.return_type));
724
725 out.push_str(&format!(" {}(", cs_name));
726
727 if method.params.is_empty() {
728 out.push_str(");\n\n");
729 } else {
730 out.push('\n');
731 for (i, param) in method.params.iter().enumerate() {
732 out.push_str(" ");
733 let pinvoke_ty = pinvoke_param_type(¶m.ty);
734 if pinvoke_ty == "string" {
735 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
736 }
737 let param_name = param.name.to_lower_camel_case();
738 out.push_str(&format!("{pinvoke_ty} {param_name}"));
739
740 if i < method.params.len() - 1 {
741 out.push(',');
742 }
743 out.push('\n');
744 }
745 out.push_str(" );\n\n");
746 }
747
748 out
749}
750
751fn gen_exception_class(namespace: &str, class_name: &str) -> String {
752 let mut out = csharp_file_header();
753 out.push_str("using System;\n\n");
754
755 out.push_str(&format!("namespace {};\n\n", namespace));
756
757 out.push_str(&format!("public class {} : Exception\n", class_name));
758 out.push_str("{\n");
759 out.push_str(" public int Code { get; }\n\n");
760 out.push_str(&format!(
761 " public {}(int code, string message) : base(message)\n",
762 class_name
763 ));
764 out.push_str(" {\n");
765 out.push_str(" Code = code;\n");
766 out.push_str(" }\n");
767 out.push_str("}\n");
768
769 out
770}
771
772#[allow(clippy::too_many_arguments)]
773fn gen_wrapper_class(
774 api: &ApiSurface,
775 namespace: &str,
776 class_name: &str,
777 exception_name: &str,
778 prefix: &str,
779 bridge_param_names: &HashSet<String>,
780 bridge_type_aliases: &HashSet<String>,
781 has_visitor_callbacks: bool,
782 streaming_methods: &HashSet<String>,
783) -> String {
784 let mut out = csharp_file_header();
785 out.push_str("using System;\n");
786 out.push_str("using System.Collections.Generic;\n");
787 out.push_str("using System.Runtime.InteropServices;\n");
788 out.push_str("using System.Text.Json;\n");
789 out.push_str("using System.Text.Json.Serialization;\n");
790 out.push_str("using System.Threading.Tasks;\n\n");
791
792 out.push_str(&format!("namespace {};\n\n", namespace));
793
794 out.push_str(&format!("public static class {}\n", class_name));
795 out.push_str("{\n");
796 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
797 out.push_str(" {\n");
798 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
799 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
800 out.push_str(" };\n\n");
801
802 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
804
805 let true_opaque_types: HashSet<String> = api
807 .types
808 .iter()
809 .filter(|t| t.is_opaque)
810 .map(|t| t.name.clone())
811 .collect();
812
813 for func in &api.functions {
815 out.push_str(&gen_wrapper_function(
816 func,
817 exception_name,
818 prefix,
819 &enum_names,
820 &true_opaque_types,
821 bridge_param_names,
822 bridge_type_aliases,
823 ));
824 }
825
826 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
829 if typ.is_opaque {
831 continue;
832 }
833 for method in &typ.methods {
834 if streaming_methods.contains(&method.name) {
835 continue;
836 }
837 out.push_str(&gen_wrapper_method(
838 method,
839 exception_name,
840 prefix,
841 &typ.name,
842 &enum_names,
843 &true_opaque_types,
844 bridge_param_names,
845 bridge_type_aliases,
846 ));
847 }
848 }
849
850 if has_visitor_callbacks {
852 out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
853 exception_name,
854 prefix,
855 ));
856 }
857
858 out.push_str(" private static ");
860 out.push_str(&format!("{} GetLastError()\n", exception_name));
861 out.push_str(" {\n");
862 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
863 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
864 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
865 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
866 out.push_str(" }\n");
867
868 out.push_str("}\n");
869
870 out
871}
872
873fn emit_named_param_setup(
890 out: &mut String,
891 params: &[alef_core::ir::ParamDef],
892 indent: &str,
893 true_opaque_types: &HashSet<String>,
894) {
895 for param in params {
896 let param_name = param.name.to_lower_camel_case();
897 let json_var = format!("{param_name}Json");
898 let handle_var = format!("{param_name}Handle");
899
900 match ¶m.ty {
901 TypeRef::Named(type_name) => {
902 if true_opaque_types.contains(type_name) {
905 continue;
906 }
907 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
908 if param.optional {
909 out.push_str(&format!(
910 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
911 ));
912 } else {
913 out.push_str(&format!(
914 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
915 ));
916 }
917 out.push_str(&format!(
918 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
919 ));
920 }
921 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
922 out.push_str(&format!(
924 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
925 ));
926 out.push_str(&format!(
927 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
928 ));
929 }
930 TypeRef::Bytes => {
931 out.push_str(&format!(
933 "{indent}var {handle_var} = GCHandle.Alloc({param_name}, GCHandleType.Pinned);\n"
934 ));
935 }
936 _ => {}
937 }
938 }
939}
940
941fn returns_ptr(ty: &TypeRef) -> bool {
944 matches!(
945 ty,
946 TypeRef::String
947 | TypeRef::Char
948 | TypeRef::Path
949 | TypeRef::Json
950 | TypeRef::Named(_)
951 | TypeRef::Vec(_)
952 | TypeRef::Map(_, _)
953 | TypeRef::Bytes
954 | TypeRef::Optional(_)
955 )
956}
957
958fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
964 match ty {
965 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
966 let bang = if optional { "!" } else { "" };
968 format!("{param_name}{bang}.Handle")
969 }
970 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
971 format!("{param_name}Handle")
972 }
973 TypeRef::Bytes => {
974 format!("{param_name}Handle.AddrOfPinnedObject()")
975 }
976 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
977 if optional {
979 format!("({param_name}?.Value ? 1 : 0)")
980 } else {
981 format!("({param_name} ? 1 : 0)")
982 }
983 }
984 ty => {
985 if optional {
986 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
990 if needs_value_unwrap {
991 format!("{param_name}.GetValueOrDefault()")
992 } else {
993 format!("{param_name}!")
994 }
995 } else {
996 param_name.to_string()
997 }
998 }
999 }
1000}
1001
1002fn emit_named_param_teardown(
1007 out: &mut String,
1008 params: &[alef_core::ir::ParamDef],
1009 true_opaque_types: &HashSet<String>,
1010) {
1011 for param in params {
1012 let param_name = param.name.to_lower_camel_case();
1013 let handle_var = format!("{param_name}Handle");
1014 match ¶m.ty {
1015 TypeRef::Named(type_name) => {
1016 if true_opaque_types.contains(type_name) {
1017 continue;
1019 }
1020 let free_method = format!("{}Free", type_name.to_pascal_case());
1021 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
1022 }
1023 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1024 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
1025 }
1026 TypeRef::Bytes => {
1027 out.push_str(&format!(" {handle_var}.Free();\n"));
1028 }
1029 _ => {}
1030 }
1031 }
1032}
1033
1034fn emit_named_param_teardown_indented(
1036 out: &mut String,
1037 params: &[alef_core::ir::ParamDef],
1038 indent: &str,
1039 true_opaque_types: &HashSet<String>,
1040) {
1041 for param in params {
1042 let param_name = param.name.to_lower_camel_case();
1043 let handle_var = format!("{param_name}Handle");
1044 match ¶m.ty {
1045 TypeRef::Named(type_name) => {
1046 if true_opaque_types.contains(type_name) {
1047 continue;
1049 }
1050 let free_method = format!("{}Free", type_name.to_pascal_case());
1051 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
1052 }
1053 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1054 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
1055 }
1056 TypeRef::Bytes => {
1057 out.push_str(&format!("{indent}{handle_var}.Free();\n"));
1058 }
1059 _ => {}
1060 }
1061 }
1062}
1063
1064fn gen_wrapper_function(
1065 func: &FunctionDef,
1066 _exception_name: &str,
1067 _prefix: &str,
1068 enum_names: &HashSet<String>,
1069 true_opaque_types: &HashSet<String>,
1070 bridge_param_names: &HashSet<String>,
1071 bridge_type_aliases: &HashSet<String>,
1072) -> String {
1073 let mut out = String::with_capacity(1024);
1074
1075 let visible_params: Vec<alef_core::ir::ParamDef> = func
1077 .params
1078 .iter()
1079 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1080 .cloned()
1081 .collect();
1082
1083 doc_emission::emit_csharp_doc(&mut out, &func.doc, " ");
1085 for param in &visible_params {
1086 if !func.doc.is_empty() {
1087 out.push_str(&format!(
1088 " /// <param name=\"{}\">{}</param>\n",
1089 param.name.to_lower_camel_case(),
1090 if param.optional { "Optional." } else { "" }
1091 ));
1092 }
1093 }
1094
1095 out.push_str(" public static ");
1096
1097 if func.is_async {
1099 if func.return_type == TypeRef::Unit {
1100 out.push_str("async Task");
1101 } else {
1102 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
1103 }
1104 } else if func.return_type == TypeRef::Unit {
1105 out.push_str("void");
1106 } else {
1107 out.push_str(&csharp_type(&func.return_type));
1108 }
1109
1110 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
1111 out.push('(');
1112
1113 for (i, param) in visible_params.iter().enumerate() {
1115 let param_name = param.name.to_lower_camel_case();
1116 let mapped = csharp_type(¶m.ty);
1117 if param.optional && !mapped.ends_with('?') {
1118 out.push_str(&format!("{mapped}? {param_name}"));
1119 } else {
1120 out.push_str(&format!("{mapped} {param_name}"));
1121 }
1122
1123 if i < visible_params.len() - 1 {
1124 out.push_str(", ");
1125 }
1126 }
1127
1128 out.push_str(")\n {\n");
1129
1130 for param in &visible_params {
1132 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1133 let param_name = param.name.to_lower_camel_case();
1134 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1135 }
1136 }
1137
1138 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1140
1141 let cs_native_name = to_csharp_name(&func.name);
1143
1144 if func.is_async {
1145 if func.return_type == TypeRef::Unit {
1149 out.push_str(" await Task.Run(() =>\n {\n");
1150 } else {
1151 out.push_str(" return await Task.Run(() =>\n {\n");
1152 }
1153
1154 if func.return_type != TypeRef::Unit {
1155 out.push_str(" var nativeResult = ");
1156 } else {
1157 out.push_str(" ");
1158 }
1159
1160 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1161
1162 if visible_params.is_empty() {
1163 out.push_str(");\n");
1164 } else {
1165 out.push('\n');
1166 for (i, param) in visible_params.iter().enumerate() {
1167 let param_name = param.name.to_lower_camel_case();
1168 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1169 out.push_str(&format!(" {arg}"));
1170 if i < visible_params.len() - 1 {
1171 out.push(',');
1172 }
1173 out.push('\n');
1174 }
1175 out.push_str(" );\n");
1176 }
1177
1178 if func.return_type != TypeRef::Unit {
1180 out.push_str(
1181 " if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1182 );
1183 }
1184
1185 emit_return_marshalling_indented(
1186 &mut out,
1187 &func.return_type,
1188 " ",
1189 enum_names,
1190 true_opaque_types,
1191 );
1192 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1193 emit_return_statement_indented(&mut out, &func.return_type, " ");
1194 out.push_str(" });\n");
1195 } else {
1196 if func.return_type != TypeRef::Unit {
1197 out.push_str(" var nativeResult = ");
1198 } else {
1199 out.push_str(" ");
1200 }
1201
1202 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1203
1204 if visible_params.is_empty() {
1205 out.push_str(");\n");
1206 } else {
1207 out.push('\n');
1208 for (i, param) in visible_params.iter().enumerate() {
1209 let param_name = param.name.to_lower_camel_case();
1210 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1211 out.push_str(&format!(" {arg}"));
1212 if i < visible_params.len() - 1 {
1213 out.push(',');
1214 }
1215 out.push('\n');
1216 }
1217 out.push_str(" );\n");
1218 }
1219
1220 if func.return_type != TypeRef::Unit && returns_ptr(&func.return_type) {
1224 out.push_str(
1225 " if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1226 );
1227 }
1228
1229 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
1230 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1231 emit_return_statement(&mut out, &func.return_type);
1232 }
1233
1234 out.push_str(" }\n\n");
1235
1236 out
1237}
1238
1239#[allow(clippy::too_many_arguments)]
1240fn gen_wrapper_method(
1241 method: &MethodDef,
1242 _exception_name: &str,
1243 _prefix: &str,
1244 type_name: &str,
1245 enum_names: &HashSet<String>,
1246 true_opaque_types: &HashSet<String>,
1247 bridge_param_names: &HashSet<String>,
1248 bridge_type_aliases: &HashSet<String>,
1249) -> String {
1250 let mut out = String::with_capacity(1024);
1251
1252 let visible_params: Vec<alef_core::ir::ParamDef> = method
1254 .params
1255 .iter()
1256 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1257 .cloned()
1258 .collect();
1259
1260 doc_emission::emit_csharp_doc(&mut out, &method.doc, " ");
1262 for param in &visible_params {
1263 if !method.doc.is_empty() {
1264 out.push_str(&format!(
1265 " /// <param name=\"{}\">{}</param>\n",
1266 param.name.to_lower_camel_case(),
1267 if param.optional { "Optional." } else { "" }
1268 ));
1269 }
1270 }
1271
1272 out.push_str(" public static ");
1274
1275 if method.is_async {
1277 if method.return_type == TypeRef::Unit {
1278 out.push_str("async Task");
1279 } else {
1280 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1281 }
1282 } else if method.return_type == TypeRef::Unit {
1283 out.push_str("void");
1284 } else {
1285 out.push_str(&csharp_type(&method.return_type));
1286 }
1287
1288 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1290 out.push_str(&format!(" {method_cs_name}"));
1291 out.push('(');
1292
1293 for (i, param) in visible_params.iter().enumerate() {
1295 let param_name = param.name.to_lower_camel_case();
1296 let mapped = csharp_type(¶m.ty);
1297 if param.optional && !mapped.ends_with('?') {
1298 out.push_str(&format!("{mapped}? {param_name}"));
1299 } else {
1300 out.push_str(&format!("{mapped} {param_name}"));
1301 }
1302
1303 if i < visible_params.len() - 1 {
1304 out.push_str(", ");
1305 }
1306 }
1307
1308 out.push_str(")\n {\n");
1309
1310 for param in &visible_params {
1312 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1313 let param_name = param.name.to_lower_camel_case();
1314 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1315 }
1316 }
1317
1318 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1320
1321 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1326
1327 if method.is_async {
1328 if method.return_type == TypeRef::Unit {
1331 out.push_str(" await Task.Run(() =>\n {\n");
1332 } else {
1333 out.push_str(" return await Task.Run(() =>\n {\n");
1334 }
1335
1336 if method.return_type != TypeRef::Unit {
1337 out.push_str(" var nativeResult = ");
1338 } else {
1339 out.push_str(" ");
1340 }
1341
1342 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1343
1344 if visible_params.is_empty() {
1345 out.push_str(");\n");
1346 } else {
1347 out.push('\n');
1348 for (i, param) in visible_params.iter().enumerate() {
1349 let param_name = param.name.to_lower_camel_case();
1350 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1351 out.push_str(&format!(" {arg}"));
1352 if i < visible_params.len() - 1 {
1353 out.push(',');
1354 }
1355 out.push('\n');
1356 }
1357 out.push_str(" );\n");
1358 }
1359
1360 emit_return_marshalling_indented(
1361 &mut out,
1362 &method.return_type,
1363 " ",
1364 enum_names,
1365 true_opaque_types,
1366 );
1367 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1368 emit_return_statement_indented(&mut out, &method.return_type, " ");
1369 out.push_str(" });\n");
1370 } else {
1371 if method.return_type != TypeRef::Unit {
1372 out.push_str(" var nativeResult = ");
1373 } else {
1374 out.push_str(" ");
1375 }
1376
1377 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1378
1379 if visible_params.is_empty() {
1380 out.push_str(");\n");
1381 } else {
1382 out.push('\n');
1383 for (i, param) in visible_params.iter().enumerate() {
1384 let param_name = param.name.to_lower_camel_case();
1385 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1386 out.push_str(&format!(" {arg}"));
1387 if i < visible_params.len() - 1 {
1388 out.push(',');
1389 }
1390 out.push('\n');
1391 }
1392 out.push_str(" );\n");
1393 }
1394
1395 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1396 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1397 emit_return_statement(&mut out, &method.return_type);
1398 }
1399
1400 out.push_str(" }\n\n");
1401
1402 out
1403}
1404
1405fn emit_return_marshalling(
1417 out: &mut String,
1418 return_type: &TypeRef,
1419 enum_names: &HashSet<String>,
1420 true_opaque_types: &HashSet<String>,
1421) {
1422 if *return_type == TypeRef::Unit {
1423 return;
1425 }
1426
1427 if returns_string(return_type) {
1428 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n");
1430 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1431 } else if returns_bool_via_int(return_type) {
1432 out.push_str(" var returnValue = nativeResult != 0;\n");
1434 } else if let TypeRef::Named(type_name) = return_type {
1435 let pascal = type_name.to_pascal_case();
1436 if true_opaque_types.contains(type_name) {
1437 out.push_str(&format!(" var returnValue = new {pascal}(nativeResult);\n"));
1439 } else if !enum_names.contains(&pascal) {
1440 let to_json_method = format!("{pascal}ToJson");
1442 let free_method = format!("{pascal}Free");
1443 let cs_ty = csharp_type(return_type);
1444 out.push_str(&format!(
1445 " var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
1446 ));
1447 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1448 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1449 out.push_str(&format!(" NativeMethods.{free_method}(nativeResult);\n"));
1450 out.push_str(&format!(
1451 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1452 cs_ty
1453 ));
1454 } else {
1455 let cs_ty = csharp_type(return_type);
1457 out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
1458 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1459 out.push_str(&format!(
1460 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1461 cs_ty
1462 ));
1463 }
1464 } else if returns_json_object(return_type) {
1465 let cs_ty = csharp_type(return_type);
1467 out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
1468 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1469 out.push_str(&format!(
1470 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1471 cs_ty
1472 ));
1473 } else {
1474 out.push_str(" var returnValue = nativeResult;\n");
1476 }
1477}
1478
1479fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1481 if *return_type != TypeRef::Unit {
1482 out.push_str(" return returnValue;\n");
1483 }
1484}
1485
1486fn emit_return_marshalling_indented(
1491 out: &mut String,
1492 return_type: &TypeRef,
1493 indent: &str,
1494 enum_names: &HashSet<String>,
1495 true_opaque_types: &HashSet<String>,
1496) {
1497 if *return_type == TypeRef::Unit {
1498 return;
1499 }
1500
1501 if returns_string(return_type) {
1502 out.push_str(&format!(
1503 "{indent}var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n"
1504 ));
1505 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1506 } else if returns_bool_via_int(return_type) {
1507 out.push_str(&format!("{indent}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!("{indent}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 "{indent}var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
1520 ));
1521 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1522 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1523 out.push_str(&format!("{indent}NativeMethods.{free_method}(nativeResult);\n"));
1524 out.push_str(&format!(
1525 "{indent}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(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
1532 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1533 out.push_str(&format!(
1534 "{indent}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);
1540 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
1541 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1542 out.push_str(&format!(
1543 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1544 cs_ty
1545 ));
1546 } else {
1547 out.push_str(&format!("{indent}var returnValue = nativeResult;\n"));
1548 }
1549}
1550
1551fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1553 if *return_type != TypeRef::Unit {
1554 out.push_str(&format!("{indent}return returnValue;\n"));
1555 }
1556}
1557
1558fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1559 let mut out = csharp_file_header();
1560 out.push_str("using System;\n\n");
1561
1562 out.push_str(&format!("namespace {};\n\n", namespace));
1563
1564 if !typ.doc.is_empty() {
1566 out.push_str("/// <summary>\n");
1567 for line in typ.doc.lines() {
1568 out.push_str(&format!("/// {}\n", line));
1569 }
1570 out.push_str("/// </summary>\n");
1571 }
1572
1573 let class_name = typ.name.to_pascal_case();
1574 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1575 out.push_str("{\n");
1576 out.push_str(" internal IntPtr Handle { get; }\n\n");
1577 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1578 out.push_str(" {\n");
1579 out.push_str(" Handle = handle;\n");
1580 out.push_str(" }\n\n");
1581 out.push_str(" public void Dispose()\n");
1582 out.push_str(" {\n");
1583 out.push_str(" // Native free will be called by the runtime\n");
1584 out.push_str(" }\n");
1585 out.push_str("}\n");
1586
1587 out
1588}
1589
1590fn gen_record_type(
1591 typ: &TypeDef,
1592 namespace: &str,
1593 enum_names: &HashSet<String>,
1594 complex_enums: &HashSet<String>,
1595 custom_converter_enums: &HashSet<String>,
1596 _lang_rename_all: &str,
1597) -> String {
1598 let mut out = csharp_file_header();
1599 out.push_str("using System;\n");
1600 out.push_str("using System.Collections.Generic;\n");
1601 out.push_str("using System.Text.Json;\n");
1602 out.push_str("using System.Text.Json.Serialization;\n\n");
1603
1604 out.push_str(&format!("namespace {};\n\n", namespace));
1605
1606 if !typ.doc.is_empty() {
1608 out.push_str("/// <summary>\n");
1609 for line in typ.doc.lines() {
1610 out.push_str(&format!("/// {}\n", line));
1611 }
1612 out.push_str("/// </summary>\n");
1613 }
1614
1615 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1616 out.push_str("{\n");
1617
1618 for field in &typ.fields {
1619 if is_tuple_field(field) {
1621 continue;
1622 }
1623
1624 if !field.doc.is_empty() {
1626 out.push_str(" /// <summary>\n");
1627 for line in field.doc.lines() {
1628 out.push_str(&format!(" /// {}\n", line));
1629 }
1630 out.push_str(" /// </summary>\n");
1631 }
1632
1633 let field_base_type = match &field.ty {
1637 TypeRef::Named(n) => Some(n.to_pascal_case()),
1638 TypeRef::Optional(inner) => match inner.as_ref() {
1639 TypeRef::Named(n) => Some(n.to_pascal_case()),
1640 _ => None,
1641 },
1642 _ => None,
1643 };
1644 if let Some(ref base) = field_base_type {
1645 if custom_converter_enums.contains(base) {
1646 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1647 }
1648 }
1649
1650 let json_name = field.name.clone();
1654 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1655
1656 let cs_name = to_csharp_name(&field.name);
1657
1658 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1661
1662 if field.optional {
1663 let mapped = if is_complex {
1665 "JsonElement".to_string()
1666 } else {
1667 csharp_type(&field.ty).to_string()
1668 };
1669 let field_type = if mapped.ends_with('?') {
1670 mapped
1671 } else {
1672 format!("{mapped}?")
1673 };
1674 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1675 out.push_str(" = null;\n");
1676 } else if typ.has_default || field.default.is_some() {
1677 use alef_core::ir::DefaultValue;
1680
1681 let base_type = if is_complex {
1683 "JsonElement".to_string()
1684 } else {
1685 csharp_type(&field.ty).to_string()
1686 };
1687
1688 if matches!(&field.ty, TypeRef::Duration) {
1691 let nullable_type = if base_type.ends_with('?') {
1693 base_type.clone()
1694 } else {
1695 format!("{}?", base_type)
1696 };
1697 out.push_str(&format!(
1698 " public {} {} {{ get; set; }} = null;\n",
1699 nullable_type, cs_name
1700 ));
1701 out.push('\n');
1702 continue;
1703 }
1704
1705 let default_val = match &field.typed_default {
1706 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1707 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1708 Some(DefaultValue::FloatLiteral(f)) => {
1709 let s = f.to_string();
1710 let s = if s.contains('.') { s } else { format!("{s}.0") };
1711 match &field.ty {
1712 TypeRef::Primitive(PrimitiveType::F32) => format!("{}f", s),
1713 _ => s,
1714 }
1715 }
1716 Some(DefaultValue::StringLiteral(s)) => {
1717 let escaped = s
1718 .replace('\\', "\\\\")
1719 .replace('"', "\\\"")
1720 .replace('\n', "\\n")
1721 .replace('\r', "\\r")
1722 .replace('\t', "\\t");
1723 format!("\"{}\"", escaped)
1724 }
1725 Some(DefaultValue::EnumVariant(v)) => {
1726 if base_type == "string" || base_type == "string?" {
1730 format!("\"{}\"", v.to_pascal_case())
1731 } else {
1732 format!("{}.{}", base_type, v.to_pascal_case())
1733 }
1734 }
1735 Some(DefaultValue::None) => "null".to_string(),
1736 Some(DefaultValue::Empty) | None => match &field.ty {
1737 TypeRef::Vec(_) => "[]".to_string(),
1738 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1739 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1740 TypeRef::Json => "null".to_string(),
1741 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1742 TypeRef::Primitive(p) => match p {
1743 PrimitiveType::Bool => "false".to_string(),
1744 PrimitiveType::F32 => "0.0f".to_string(),
1745 PrimitiveType::F64 => "0.0".to_string(),
1746 _ => "0".to_string(),
1747 },
1748 TypeRef::Named(name) => {
1749 let pascal = name.to_pascal_case();
1750 if complex_enums.contains(&pascal) {
1751 "null".to_string()
1753 } else if enum_names.contains(&pascal) {
1754 "null".to_string()
1757 } else {
1758 "default!".to_string()
1759 }
1760 }
1761 _ => "default!".to_string(),
1762 },
1763 };
1764
1765 let field_type = if (default_val == "null" && !base_type.ends_with('?')) || is_complex {
1767 format!("{}?", base_type)
1768 } else {
1769 base_type
1770 };
1771
1772 out.push_str(&format!(
1773 " public {} {} {{ get; set; }} = {};\n",
1774 field_type, cs_name, default_val
1775 ));
1776 } else {
1777 let field_type = if is_complex {
1781 "JsonElement".to_string()
1782 } else {
1783 csharp_type(&field.ty).to_string()
1784 };
1785 if matches!(&field.ty, TypeRef::Duration) {
1787 out.push_str(&format!(
1788 " public {} {} {{ get; set; }} = null;\n",
1789 field_type, cs_name
1790 ));
1791 } else {
1792 let default_val = match &field.ty {
1793 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1794 TypeRef::Vec(_) => "[]",
1795 TypeRef::Bytes => "Array.Empty<byte>()",
1796 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1797 TypeRef::Primitive(PrimitiveType::F32) => "0.0f",
1798 TypeRef::Primitive(PrimitiveType::F64) => "0.0",
1799 TypeRef::Primitive(_) => "0",
1800 _ => "default!",
1801 };
1802 out.push_str(&format!(
1803 " public {} {} {{ get; set; }} = {};\n",
1804 field_type, cs_name, default_val
1805 ));
1806 }
1807 }
1808
1809 out.push('\n');
1810 }
1811
1812 out.push_str("}\n");
1813
1814 out
1815}
1816
1817fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1819 match rename_all {
1820 Some("snake_case") => name.to_snake_case(),
1821 Some("camelCase") => name.to_lower_camel_case(),
1822 Some("PascalCase") => name.to_pascal_case(),
1823 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1824 Some("lowercase") => name.to_lowercase(),
1825 Some("UPPERCASE") => name.to_uppercase(),
1826 _ => name.to_lowercase(),
1827 }
1828}
1829
1830fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1831 let mut out = csharp_file_header();
1832 out.push_str("using System.Text.Json.Serialization;\n\n");
1833 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1834
1835 if enum_def.serde_tag.is_some() && has_data_variants {
1837 return gen_tagged_union(enum_def, namespace);
1838 }
1839
1840 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1848 if let Some(ref rename) = v.serde_rename {
1849 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1850 rename != &snake
1851 } else {
1852 false
1853 }
1854 });
1855
1856 let enum_pascal = enum_def.name.to_pascal_case();
1857
1858 let variants: Vec<(String, String)> = enum_def
1860 .variants
1861 .iter()
1862 .map(|v| {
1863 let json_name = v
1864 .serde_rename
1865 .clone()
1866 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1867 let pascal_name = v.name.to_pascal_case();
1868 (json_name, pascal_name)
1869 })
1870 .collect();
1871
1872 out.push_str("using System;\n");
1873 out.push_str("using System.Text.Json;\n\n");
1874
1875 out.push_str(&format!("namespace {};\n\n", namespace));
1876
1877 if !enum_def.doc.is_empty() {
1879 out.push_str("/// <summary>\n");
1880 for line in enum_def.doc.lines() {
1881 out.push_str(&format!("/// {}\n", line));
1882 }
1883 out.push_str("/// </summary>\n");
1884 }
1885
1886 if needs_custom_converter {
1887 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1888 }
1889 out.push_str(&format!("public enum {enum_pascal}\n"));
1890 out.push_str("{\n");
1891
1892 for (json_name, pascal_name) in &variants {
1893 if let Some(v) = enum_def
1895 .variants
1896 .iter()
1897 .find(|v| v.name.to_pascal_case() == *pascal_name)
1898 {
1899 if !v.doc.is_empty() {
1900 out.push_str(" /// <summary>\n");
1901 for line in v.doc.lines() {
1902 out.push_str(&format!(" /// {}\n", line));
1903 }
1904 out.push_str(" /// </summary>\n");
1905 }
1906 }
1907 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1908 out.push_str(&format!(" {pascal_name},\n"));
1909 }
1910
1911 out.push_str("}\n");
1912
1913 if needs_custom_converter {
1915 out.push('\n');
1916 out.push_str(&format!(
1917 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1918 ));
1919 out.push_str(&format!(
1920 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1921 ));
1922 out.push_str("{\n");
1923
1924 out.push_str(&format!(
1926 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1927 ));
1928 out.push_str(" {\n");
1929 out.push_str(" var value = reader.GetString();\n");
1930 out.push_str(" return value switch\n");
1931 out.push_str(" {\n");
1932 for (json_name, pascal_name) in &variants {
1933 out.push_str(&format!(
1934 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1935 ));
1936 }
1937 out.push_str(&format!(
1938 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1939 ));
1940 out.push_str(" };\n");
1941 out.push_str(" }\n\n");
1942
1943 out.push_str(&format!(
1945 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1946 ));
1947 out.push_str(" {\n");
1948 out.push_str(" var str = value switch\n");
1949 out.push_str(" {\n");
1950 for (json_name, pascal_name) in &variants {
1951 out.push_str(&format!(
1952 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1953 ));
1954 }
1955 out.push_str(&format!(
1956 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1957 ));
1958 out.push_str(" };\n");
1959 out.push_str(" writer.WriteStringValue(str);\n");
1960 out.push_str(" }\n");
1961 out.push_str("}\n");
1962 }
1963
1964 out
1965}
1966
1967fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1974 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1975 let enum_pascal = enum_def.name.to_pascal_case();
1976 let converter_name = format!("{enum_pascal}JsonConverter");
1977 let ns = namespace;
1980
1981 let mut out = csharp_file_header();
1982 out.push_str("using System;\n");
1983 out.push_str("using System.Collections.Generic;\n");
1984 out.push_str("using System.Text.Json;\n");
1985 out.push_str("using System.Text.Json.Serialization;\n\n");
1986 out.push_str(&format!("namespace {};\n\n", namespace));
1987
1988 if !enum_def.doc.is_empty() {
1990 out.push_str("/// <summary>\n");
1991 for line in enum_def.doc.lines() {
1992 out.push_str(&format!("/// {}\n", line));
1993 }
1994 out.push_str("/// </summary>\n");
1995 }
1996
1997 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
1999 out.push_str(&format!("public abstract record {enum_pascal}\n"));
2000 out.push_str("{\n");
2001
2002 let variant_names: std::collections::HashSet<String> =
2004 enum_def.variants.iter().map(|v| v.name.to_pascal_case()).collect();
2005
2006 for variant in &enum_def.variants {
2008 let pascal = variant.name.to_pascal_case();
2009
2010 if !variant.doc.is_empty() {
2011 out.push_str(" /// <summary>\n");
2012 for line in variant.doc.lines() {
2013 out.push_str(&format!(" /// {}\n", line));
2014 }
2015 out.push_str(" /// </summary>\n");
2016 }
2017
2018 if variant.fields.is_empty() {
2019 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
2021 } else {
2022 let is_copy_ctor_clash = variant.fields.len() == 1 && {
2027 let field_cs_type = csharp_type(&variant.fields[0].ty);
2028 field_cs_type.as_ref() == pascal
2029 };
2030
2031 if is_copy_ctor_clash {
2032 let cs_type = csharp_type(&variant.fields[0].ty);
2033 let qualified_cs_type = format!("global::{ns}.{cs_type}");
2037 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
2038 out.push_str(" {\n");
2039 out.push_str(&format!(
2040 " public required {qualified_cs_type} Value {{ get; init; }}\n"
2041 ));
2042 out.push_str(" }\n\n");
2043 } else {
2044 out.push_str(&format!(" public sealed record {pascal}(\n"));
2046 for (i, field) in variant.fields.iter().enumerate() {
2047 let cs_type = csharp_type(&field.ty);
2048 let cs_type = if field.optional && !cs_type.ends_with('?') {
2049 format!("{cs_type}?")
2050 } else {
2051 cs_type.to_string()
2052 };
2053 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
2054 if is_tuple_field(field) {
2055 out.push_str(&format!(" {cs_type} Value{comma}\n"));
2056 } else {
2057 let json_name = field.name.trim_start_matches('_');
2058 let cs_name = to_csharp_name(json_name);
2059 let clashes = cs_name == pascal || cs_name == cs_type || variant_names.contains(&cs_name);
2064 if clashes {
2065 out.push_str(&format!(
2067 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} Value{comma}\n"
2068 ));
2069 } else {
2070 out.push_str(&format!(
2071 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
2072 ));
2073 }
2074 }
2075 }
2076 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
2077 }
2078 }
2079 }
2080
2081 out.push_str("}\n\n");
2082
2083 out.push_str(&format!(
2085 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
2086 ));
2087 out.push_str(&format!(
2088 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
2089 ));
2090 out.push_str("{\n");
2091
2092 out.push_str(&format!(
2094 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
2095 ));
2096 out.push_str(" {\n");
2097 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
2098 out.push_str(" var root = doc.RootElement;\n");
2099 out.push_str(&format!(
2100 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
2101 ));
2102 out.push_str(&format!(
2103 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
2104 ));
2105 out.push_str(" var tag = tagEl.GetString();\n");
2106 out.push_str(" var json = root.GetRawText();\n");
2107 out.push_str(" return tag switch\n");
2108 out.push_str(" {\n");
2109
2110 for variant in &enum_def.variants {
2111 let discriminator = variant
2112 .serde_rename
2113 .clone()
2114 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2115 let pascal = variant.name.to_pascal_case();
2116 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2121 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2122 let f = &variant.fields[0];
2123 let cs_type = csharp_type(&f.ty);
2124 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2125 cs_name == pascal || cs_name == cs_type
2126 };
2127 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2128 if is_newtype {
2129 let inner_cs_type = csharp_type(&variant.fields[0].ty);
2130 if inner_cs_type == pascal {
2133 out.push_str(&format!(
2134 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2135 ));
2136 out.push_str(&format!(
2137 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
2138 ));
2139 } else {
2140 out.push_str(&format!(
2141 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
2142 ));
2143 out.push_str(&format!(
2144 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2145 ));
2146 out.push_str(&format!(
2147 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
2148 ));
2149 }
2150 } else {
2151 out.push_str(&format!(
2152 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
2153 ));
2154 out.push_str(&format!(
2155 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
2156 ));
2157 }
2158 }
2159
2160 out.push_str(&format!(
2161 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
2162 ));
2163 out.push_str(" };\n");
2164 out.push_str(" }\n\n");
2165
2166 out.push_str(&format!(
2168 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2169 ));
2170 out.push_str(" {\n");
2171
2172 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
2174 out.push_str(" switch (value)\n");
2175 out.push_str(" {\n");
2176
2177 for variant in &enum_def.variants {
2178 let discriminator = variant
2179 .serde_rename
2180 .clone()
2181 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2182 let pascal = variant.name.to_pascal_case();
2183 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2187 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2188 let f = &variant.fields[0];
2189 let cs_type = csharp_type(&f.ty);
2190 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2191 cs_name == pascal || cs_name == cs_type
2192 };
2193 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2194 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
2195 out.push_str(" {\n");
2196 if is_newtype {
2197 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
2198 } else {
2199 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
2200 }
2201 out.push_str(" writer.WriteStartObject();\n");
2202 out.push_str(&format!(
2203 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
2204 ));
2205 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2206 out.push_str(&format!(
2207 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2208 ));
2209 out.push_str(" writer.WriteEndObject();\n");
2210 out.push_str(" break;\n");
2211 out.push_str(" }\n");
2212 }
2213
2214 out.push_str(&format!(
2215 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2216 ));
2217 out.push_str(" }\n");
2218 out.push_str(" }\n");
2219 out.push_str("}\n");
2220
2221 out
2222}
2223
2224fn gen_directory_build_props() -> String {
2227 "<!-- auto-generated by alef (generate_bindings) -->\n\
2228<Project>\n \
2229<PropertyGroup>\n \
2230<Nullable>enable</Nullable>\n \
2231<LangVersion>latest</LangVersion>\n \
2232</PropertyGroup>\n\
2233</Project>\n"
2234 .to_string()
2235}
2236
2237fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
2241 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
2242
2243 for filename in stale_files {
2244 let path = base_path.join(filename);
2245 if path.exists() {
2246 std::fs::remove_file(&path)
2247 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
2248 }
2249 }
2250
2251 Ok(())
2252}