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