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