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 let nullable_type = if base_type.ends_with('?') {
1700 base_type.clone()
1701 } else {
1702 format!("{}?", base_type)
1703 };
1704 out.push_str(&format!(
1705 " public {} {} {{ get; set; }} = null;\n",
1706 nullable_type, cs_name
1707 ));
1708 out.push('\n');
1709 continue;
1710 }
1711
1712 let default_val = match &field.typed_default {
1713 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1714 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1715 Some(DefaultValue::FloatLiteral(f)) => {
1716 let s = f.to_string();
1717 let s = if s.contains('.') { s } else { format!("{s}.0") };
1718 match &field.ty {
1719 TypeRef::Primitive(PrimitiveType::F32) => format!("{}f", s),
1720 _ => s,
1721 }
1722 }
1723 Some(DefaultValue::StringLiteral(s)) => {
1724 let escaped = s
1725 .replace('\\', "\\\\")
1726 .replace('"', "\\\"")
1727 .replace('\n', "\\n")
1728 .replace('\r', "\\r")
1729 .replace('\t', "\\t");
1730 format!("\"{}\"", escaped)
1731 }
1732 Some(DefaultValue::EnumVariant(v)) => {
1733 if base_type == "string" || base_type == "string?" {
1737 format!("\"{}\"", v.to_pascal_case())
1738 } else {
1739 format!("{}.{}", base_type, v.to_pascal_case())
1740 }
1741 }
1742 Some(DefaultValue::None) => "null".to_string(),
1743 Some(DefaultValue::Empty) | None => match &field.ty {
1744 TypeRef::Vec(_) => "[]".to_string(),
1745 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1746 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1747 TypeRef::Json => "null".to_string(),
1748 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1749 TypeRef::Primitive(p) => match p {
1750 PrimitiveType::Bool => "false".to_string(),
1751 PrimitiveType::F32 => "0.0f".to_string(),
1752 PrimitiveType::F64 => "0.0".to_string(),
1753 _ => "0".to_string(),
1754 },
1755 TypeRef::Named(name) => {
1756 let pascal = name.to_pascal_case();
1757 if complex_enums.contains(&pascal) {
1758 "null".to_string()
1760 } else if enum_names.contains(&pascal) {
1761 "null".to_string()
1764 } else {
1765 "default!".to_string()
1766 }
1767 }
1768 _ => "default!".to_string(),
1769 },
1770 };
1771
1772 let field_type = if (default_val == "null" && !base_type.ends_with('?')) || is_complex {
1774 format!("{}?", base_type)
1775 } else {
1776 base_type
1777 };
1778
1779 out.push_str(&format!(
1780 " public {} {} {{ get; set; }} = {};\n",
1781 field_type, cs_name, default_val
1782 ));
1783 } else {
1784 let field_type = if is_complex {
1788 "JsonElement".to_string()
1789 } else {
1790 csharp_type(&field.ty).to_string()
1791 };
1792 if matches!(&field.ty, TypeRef::Duration) {
1794 out.push_str(&format!(
1795 " public {} {} {{ get; set; }} = null;\n",
1796 field_type, cs_name
1797 ));
1798 } else {
1799 let default_val = match &field.ty {
1800 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1801 TypeRef::Vec(_) => "[]",
1802 TypeRef::Bytes => "Array.Empty<byte>()",
1803 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1804 TypeRef::Primitive(PrimitiveType::F32) => "0.0f",
1805 TypeRef::Primitive(PrimitiveType::F64) => "0.0",
1806 TypeRef::Primitive(_) => "0",
1807 _ => "default!",
1808 };
1809 out.push_str(&format!(
1810 " public {} {} {{ get; set; }} = {};\n",
1811 field_type, cs_name, default_val
1812 ));
1813 }
1814 }
1815
1816 out.push('\n');
1817 }
1818
1819 out.push_str("}\n");
1820
1821 out
1822}
1823
1824fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1826 match rename_all {
1827 Some("snake_case") => name.to_snake_case(),
1828 Some("camelCase") => name.to_lower_camel_case(),
1829 Some("PascalCase") => name.to_pascal_case(),
1830 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1831 Some("lowercase") => name.to_lowercase(),
1832 Some("UPPERCASE") => name.to_uppercase(),
1833 _ => name.to_lowercase(),
1834 }
1835}
1836
1837fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1838 let mut out = csharp_file_header();
1839 out.push_str("using System.Text.Json.Serialization;\n\n");
1840 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1841
1842 if enum_def.serde_tag.is_some() && has_data_variants {
1844 return gen_tagged_union(enum_def, namespace);
1845 }
1846
1847 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1855 if let Some(ref rename) = v.serde_rename {
1856 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1857 rename != &snake
1858 } else {
1859 false
1860 }
1861 });
1862
1863 let enum_pascal = enum_def.name.to_pascal_case();
1864
1865 let variants: Vec<(String, String)> = enum_def
1867 .variants
1868 .iter()
1869 .map(|v| {
1870 let json_name = v
1871 .serde_rename
1872 .clone()
1873 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1874 let pascal_name = v.name.to_pascal_case();
1875 (json_name, pascal_name)
1876 })
1877 .collect();
1878
1879 out.push_str("using System;\n");
1880 out.push_str("using System.Text.Json;\n\n");
1881
1882 out.push_str(&format!("namespace {};\n\n", namespace));
1883
1884 if !enum_def.doc.is_empty() {
1886 out.push_str("/// <summary>\n");
1887 for line in enum_def.doc.lines() {
1888 out.push_str(&format!("/// {}\n", line));
1889 }
1890 out.push_str("/// </summary>\n");
1891 }
1892
1893 if needs_custom_converter {
1894 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1895 }
1896 out.push_str(&format!("public enum {enum_pascal}\n"));
1897 out.push_str("{\n");
1898
1899 for (json_name, pascal_name) in &variants {
1900 if let Some(v) = enum_def
1902 .variants
1903 .iter()
1904 .find(|v| v.name.to_pascal_case() == *pascal_name)
1905 {
1906 if !v.doc.is_empty() {
1907 out.push_str(" /// <summary>\n");
1908 for line in v.doc.lines() {
1909 out.push_str(&format!(" /// {}\n", line));
1910 }
1911 out.push_str(" /// </summary>\n");
1912 }
1913 }
1914 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1915 out.push_str(&format!(" {pascal_name},\n"));
1916 }
1917
1918 out.push_str("}\n");
1919
1920 if needs_custom_converter {
1922 out.push('\n');
1923 out.push_str(&format!(
1924 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1925 ));
1926 out.push_str(&format!(
1927 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1928 ));
1929 out.push_str("{\n");
1930
1931 out.push_str(&format!(
1933 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1934 ));
1935 out.push_str(" {\n");
1936 out.push_str(" var value = reader.GetString();\n");
1937 out.push_str(" return value switch\n");
1938 out.push_str(" {\n");
1939 for (json_name, pascal_name) in &variants {
1940 out.push_str(&format!(
1941 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1942 ));
1943 }
1944 out.push_str(&format!(
1945 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1946 ));
1947 out.push_str(" };\n");
1948 out.push_str(" }\n\n");
1949
1950 out.push_str(&format!(
1952 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1953 ));
1954 out.push_str(" {\n");
1955 out.push_str(" var str = value switch\n");
1956 out.push_str(" {\n");
1957 for (json_name, pascal_name) in &variants {
1958 out.push_str(&format!(
1959 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1960 ));
1961 }
1962 out.push_str(&format!(
1963 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1964 ));
1965 out.push_str(" };\n");
1966 out.push_str(" writer.WriteStringValue(str);\n");
1967 out.push_str(" }\n");
1968 out.push_str("}\n");
1969 }
1970
1971 out
1972}
1973
1974fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1981 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1982 let enum_pascal = enum_def.name.to_pascal_case();
1983 let converter_name = format!("{enum_pascal}JsonConverter");
1984 let ns = namespace;
1987
1988 let mut out = csharp_file_header();
1989 out.push_str("using System;\n");
1990 out.push_str("using System.Collections.Generic;\n");
1991 out.push_str("using System.Text.Json;\n");
1992 out.push_str("using System.Text.Json.Serialization;\n\n");
1993 out.push_str(&format!("namespace {};\n\n", namespace));
1994
1995 if !enum_def.doc.is_empty() {
1997 out.push_str("/// <summary>\n");
1998 for line in enum_def.doc.lines() {
1999 out.push_str(&format!("/// {}\n", line));
2000 }
2001 out.push_str("/// </summary>\n");
2002 }
2003
2004 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
2006 out.push_str(&format!("public abstract record {enum_pascal}\n"));
2007 out.push_str("{\n");
2008
2009 let variant_names: std::collections::HashSet<String> =
2011 enum_def.variants.iter().map(|v| v.name.to_pascal_case()).collect();
2012
2013 for variant in &enum_def.variants {
2015 let pascal = variant.name.to_pascal_case();
2016
2017 if !variant.doc.is_empty() {
2018 out.push_str(" /// <summary>\n");
2019 for line in variant.doc.lines() {
2020 out.push_str(&format!(" /// {}\n", line));
2021 }
2022 out.push_str(" /// </summary>\n");
2023 }
2024
2025 if variant.fields.is_empty() {
2026 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
2028 } else {
2029 let is_copy_ctor_clash = variant.fields.len() == 1 && {
2034 let field_cs_type = csharp_type(&variant.fields[0].ty);
2035 field_cs_type.as_ref() == pascal
2036 };
2037
2038 if is_copy_ctor_clash {
2039 let cs_type = csharp_type(&variant.fields[0].ty);
2040 let qualified_cs_type = format!("global::{ns}.{cs_type}");
2044 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
2045 out.push_str(" {\n");
2046 out.push_str(&format!(
2047 " public required {qualified_cs_type} Value {{ get; init; }}\n"
2048 ));
2049 out.push_str(" }\n\n");
2050 } else {
2051 out.push_str(&format!(" public sealed record {pascal}(\n"));
2053 for (i, field) in variant.fields.iter().enumerate() {
2054 let cs_type = csharp_type(&field.ty);
2055 let cs_type = if field.optional && !cs_type.ends_with('?') {
2056 format!("{cs_type}?")
2057 } else {
2058 cs_type.to_string()
2059 };
2060 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
2061 if is_tuple_field(field) {
2062 out.push_str(&format!(" {cs_type} Value{comma}\n"));
2063 } else {
2064 let json_name = field.name.trim_start_matches('_');
2065 let cs_name = to_csharp_name(json_name);
2066 let clashes = cs_name == pascal || cs_name == cs_type || variant_names.contains(&cs_name);
2071 if clashes {
2072 out.push_str(&format!(
2074 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} Value{comma}\n"
2075 ));
2076 } else {
2077 out.push_str(&format!(
2078 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
2079 ));
2080 }
2081 }
2082 }
2083 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
2084 }
2085 }
2086 }
2087
2088 out.push_str("}\n\n");
2089
2090 out.push_str(&format!(
2092 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
2093 ));
2094 out.push_str(&format!(
2095 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
2096 ));
2097 out.push_str("{\n");
2098
2099 out.push_str(&format!(
2101 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
2102 ));
2103 out.push_str(" {\n");
2104 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
2105 out.push_str(" var root = doc.RootElement;\n");
2106 out.push_str(&format!(
2107 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
2108 ));
2109 out.push_str(&format!(
2110 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
2111 ));
2112 out.push_str(" var tag = tagEl.GetString();\n");
2113 out.push_str(" var json = root.GetRawText();\n");
2114 out.push_str(" return tag switch\n");
2115 out.push_str(" {\n");
2116
2117 for variant in &enum_def.variants {
2118 let discriminator = variant
2119 .serde_rename
2120 .clone()
2121 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2122 let pascal = variant.name.to_pascal_case();
2123 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2128 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2129 let f = &variant.fields[0];
2130 let cs_type = csharp_type(&f.ty);
2131 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2132 cs_name == pascal || cs_name == cs_type
2133 };
2134 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2135 if is_newtype {
2136 let inner_cs_type = csharp_type(&variant.fields[0].ty);
2137 if inner_cs_type == pascal {
2140 out.push_str(&format!(
2141 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2142 ));
2143 out.push_str(&format!(
2144 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
2145 ));
2146 } else {
2147 out.push_str(&format!(
2148 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
2149 ));
2150 out.push_str(&format!(
2151 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2152 ));
2153 out.push_str(&format!(
2154 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
2155 ));
2156 }
2157 } else {
2158 out.push_str(&format!(
2159 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
2160 ));
2161 out.push_str(&format!(
2162 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
2163 ));
2164 }
2165 }
2166
2167 out.push_str(&format!(
2168 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
2169 ));
2170 out.push_str(" };\n");
2171 out.push_str(" }\n\n");
2172
2173 out.push_str(&format!(
2175 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2176 ));
2177 out.push_str(" {\n");
2178
2179 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
2181 out.push_str(" switch (value)\n");
2182 out.push_str(" {\n");
2183
2184 for variant in &enum_def.variants {
2185 let discriminator = variant
2186 .serde_rename
2187 .clone()
2188 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2189 let pascal = variant.name.to_pascal_case();
2190 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2194 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2195 let f = &variant.fields[0];
2196 let cs_type = csharp_type(&f.ty);
2197 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2198 cs_name == pascal || cs_name == cs_type
2199 };
2200 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2201 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
2202 out.push_str(" {\n");
2203 if is_newtype {
2204 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
2205 } else {
2206 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
2207 }
2208 out.push_str(" writer.WriteStartObject();\n");
2209 out.push_str(&format!(
2210 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
2211 ));
2212 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2213 out.push_str(&format!(
2214 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2215 ));
2216 out.push_str(" writer.WriteEndObject();\n");
2217 out.push_str(" break;\n");
2218 out.push_str(" }\n");
2219 }
2220
2221 out.push_str(&format!(
2222 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2223 ));
2224 out.push_str(" }\n");
2225 out.push_str(" }\n");
2226 out.push_str("}\n");
2227
2228 out
2229}
2230
2231fn gen_directory_build_props() -> String {
2234 "<!-- auto-generated by alef (generate_bindings) -->\n\
2235<Project>\n \
2236<PropertyGroup>\n \
2237<Nullable>enable</Nullable>\n \
2238<LangVersion>latest</LangVersion>\n \
2239</PropertyGroup>\n\
2240</Project>\n"
2241 .to_string()
2242}
2243
2244fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
2248 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
2249
2250 for filename in stale_files {
2251 let path = base_path.join(filename);
2252 if path.exists() {
2253 std::fs::remove_file(&path)
2254 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
2255 }
2256 }
2257
2258 Ok(())
2259}