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, BridgeBinding, 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
12#[derive(Clone, Debug)]
17struct OptionsFieldBridgeInfo {
18 options_type: String,
20 field_name: String,
22 bridge_cs_type: String,
24 ffi_prefix: String,
26}
27
28impl OptionsFieldBridgeInfo {
29 fn cs_setter_name(&self) -> String {
34 let opts_pascal = self.options_type.to_pascal_case();
35 let field_pascal = self.field_name.to_pascal_case();
36 format!("{}Set{}", opts_pascal, field_pascal)
37 }
38
39 fn ffi_symbol(&self) -> String {
44 let field_snake = self.field_name.to_snake_case();
45 format!("{}_options_set_{}", self.ffi_prefix, field_snake)
46 }
47}
48
49fn collect_options_field_bridges(config: &AlefConfig, api: &ApiSurface) -> Vec<OptionsFieldBridgeInfo> {
52 let prefix = config.ffi_prefix();
53 let mut result = Vec::new();
54 for bridge_cfg in &config.trait_bridges {
55 if bridge_cfg.bind_via != BridgeBinding::OptionsField {
56 continue;
57 }
58 if bridge_cfg.exclude_languages.contains(&"csharp".to_string()) {
59 continue;
60 }
61 let options_type = match bridge_cfg.options_type.as_deref() {
62 Some(t) => t.to_string(),
63 None => continue,
64 };
65 let field_name = match bridge_cfg.resolved_options_field() {
66 Some(f) => f.to_string(),
67 None => continue,
68 };
69 let bridge_cs_type = bridge_cfg.trait_name.clone();
72
73 if !api.types.iter().any(|t| t.name == options_type) {
75 continue;
76 }
77 result.push(OptionsFieldBridgeInfo {
78 options_type,
79 field_name,
80 bridge_cs_type,
81 ffi_prefix: prefix.clone(),
82 });
83 }
84 result
85}
86
87pub struct CsharpBackend;
88
89impl CsharpBackend {
90 }
92
93impl Backend for CsharpBackend {
94 fn name(&self) -> &str {
95 "csharp"
96 }
97
98 fn language(&self) -> Language {
99 Language::Csharp
100 }
101
102 fn capabilities(&self) -> Capabilities {
103 Capabilities {
104 supports_async: true,
105 supports_classes: true,
106 supports_enums: true,
107 supports_option: true,
108 supports_result: true,
109 ..Capabilities::default()
110 }
111 }
112
113 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
114 let namespace = config.csharp_namespace();
115 let prefix = config.ffi_prefix();
116 let lib_name = config.ffi_lib_name();
117
118 let bridge_param_names: HashSet<String> = config
121 .trait_bridges
122 .iter()
123 .filter_map(|b| b.param_name.clone())
124 .collect();
125 let bridge_type_aliases: HashSet<String> = config
126 .trait_bridges
127 .iter()
128 .filter_map(|b| b.type_alias.clone())
129 .collect();
130 let has_visitor_callbacks = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
132
133 let options_field_bridges = collect_options_field_bridges(config, api);
135
136 let streaming_methods: HashSet<String> = config
139 .adapters
140 .iter()
141 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
142 .map(|a| a.name.clone())
143 .collect();
144
145 let mut exclude_functions: HashSet<String> = config
147 .csharp
148 .as_ref()
149 .map(|c| c.exclude_functions.iter().cloned().collect())
150 .unwrap_or_default();
151
152 let trait_bridge_reg_fns =
155 alef_codegen::generators::trait_bridge::collect_trait_bridge_registration_fn_names(&config.trait_bridges);
156 exclude_functions.extend(trait_bridge_reg_fns);
157
158 let output_dir = resolve_output_dir(
159 config.output.csharp.as_ref(),
160 &config.crate_config.name,
161 "packages/csharp/",
162 );
163
164 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
165
166 let mut files = Vec::new();
167
168 files.push(GeneratedFile {
170 path: base_path.join("NativeMethods.cs"),
171 content: strip_trailing_whitespace(&gen_native_methods(
172 api,
173 &namespace,
174 &lib_name,
175 &prefix,
176 &bridge_param_names,
177 &bridge_type_aliases,
178 has_visitor_callbacks,
179 &config.trait_bridges,
180 &streaming_methods,
181 &exclude_functions,
182 &options_field_bridges,
183 )),
184 generated_header: true,
185 });
186
187 if !api.errors.is_empty() {
189 for error in &api.errors {
190 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
191 for (class_name, content) in error_files {
192 files.push(GeneratedFile {
193 path: base_path.join(format!("{}.cs", class_name)),
194 content: strip_trailing_whitespace(&content),
195 generated_header: false, });
197 }
198 }
199 }
200
201 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
203 if api.errors.is_empty()
204 || !api
205 .errors
206 .iter()
207 .any(|e| format!("{}Exception", e.name) == exception_class_name)
208 {
209 files.push(GeneratedFile {
210 path: base_path.join(format!("{}.cs", exception_class_name)),
211 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
212 generated_header: true,
213 });
214 }
215
216 let base_class_name = api.crate_name.to_pascal_case();
218 let wrapper_class_name = if namespace == base_class_name {
219 format!("{}Lib", base_class_name)
220 } else {
221 base_class_name
222 };
223 files.push(GeneratedFile {
224 path: base_path.join(format!("{}.cs", wrapper_class_name)),
225 content: strip_trailing_whitespace(&gen_wrapper_class(
226 api,
227 &namespace,
228 &wrapper_class_name,
229 &exception_class_name,
230 &prefix,
231 &bridge_param_names,
232 &bridge_type_aliases,
233 has_visitor_callbacks,
234 &streaming_methods,
235 &exclude_functions,
236 &options_field_bridges,
237 )),
238 generated_header: true,
239 });
240
241 if has_visitor_callbacks {
243 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
244 files.push(GeneratedFile {
245 path: base_path.join(filename),
246 content: strip_trailing_whitespace(&content),
247 generated_header: true,
248 });
249 }
250 } else {
251 delete_stale_visitor_files(&base_path)?;
254 }
255
256 if !config.trait_bridges.is_empty() {
258 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
259 let bridges: Vec<_> = config
260 .trait_bridges
261 .iter()
262 .filter_map(|cfg| {
263 let trait_name = cfg.trait_name.clone();
264 trait_defs
265 .iter()
266 .find(|t| t.name == trait_name)
267 .map(|trait_def| (trait_name, cfg, *trait_def))
268 })
269 .collect();
270
271 if !bridges.is_empty() {
272 let (filename, content) = crate::trait_bridge::gen_trait_bridges_file(&namespace, &prefix, &bridges);
273 files.push(GeneratedFile {
274 path: base_path.join(filename),
275 content: strip_trailing_whitespace(&content),
276 generated_header: true,
277 });
278 }
279 }
280
281 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
283 if typ.is_opaque {
284 let type_filename = typ.name.to_pascal_case();
285 files.push(GeneratedFile {
286 path: base_path.join(format!("{}.cs", type_filename)),
287 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
288 generated_header: true,
289 });
290 }
291 }
292
293 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
295
296 let complex_enums: HashSet<String> = api
301 .enums
302 .iter()
303 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
304 .map(|e| e.name.to_pascal_case())
305 .collect();
306
307 let custom_converter_enums: HashSet<String> = api
311 .enums
312 .iter()
313 .filter(|e| {
314 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
316 || e.variants.iter().any(|v| {
318 if let Some(ref rename) = v.serde_rename {
319 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
320 rename != &snake
321 } else {
322 false
323 }
324 })
325 })
326 .map(|e| e.name.to_pascal_case())
327 .collect();
328
329 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
331
332 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
334 if !typ.is_opaque {
335 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
338 if !typ.fields.is_empty() && !has_named_fields {
339 continue;
340 }
341 if has_visitor_callbacks && (typ.name == "NodeContext" || typ.name == "VisitResult") {
343 continue;
344 }
345
346 let type_filename = typ.name.to_pascal_case();
347 files.push(GeneratedFile {
348 path: base_path.join(format!("{}.cs", type_filename)),
349 content: strip_trailing_whitespace(&gen_record_type(
350 typ,
351 &namespace,
352 &enum_names,
353 &complex_enums,
354 &custom_converter_enums,
355 &lang_rename_all,
356 &options_field_bridges,
357 )),
358 generated_header: true,
359 });
360 }
361 }
362
363 for enum_def in &api.enums {
365 if has_visitor_callbacks && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
367 continue;
368 }
369 let enum_filename = enum_def.name.to_pascal_case();
370 files.push(GeneratedFile {
371 path: base_path.join(format!("{}.cs", enum_filename)),
372 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
373 generated_header: true,
374 });
375 }
376
377 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
379
380 files.push(GeneratedFile {
384 path: PathBuf::from("packages/csharp/Directory.Build.props"),
385 content: gen_directory_build_props(),
386 generated_header: true,
387 });
388
389 Ok(files)
390 }
391
392 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
397 Ok(vec![])
399 }
400
401 fn build_config(&self) -> Option<BuildConfig> {
402 Some(BuildConfig {
403 tool: "dotnet",
404 crate_suffix: "",
405 build_dep: BuildDependency::Ffi,
406 post_build: vec![],
407 })
408 }
409}
410
411fn is_tuple_field(field: &FieldDef) -> bool {
413 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
414 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
415}
416
417fn strip_trailing_whitespace(content: &str) -> String {
419 let mut result: String = content
420 .lines()
421 .map(|line| line.trim_end())
422 .collect::<Vec<_>>()
423 .join("\n");
424 if !result.ends_with('\n') {
425 result.push('\n');
426 }
427 result
428}
429
430fn csharp_file_header() -> String {
432 let mut out = hash::header(CommentStyle::DoubleSlash);
433 out.push_str("#nullable enable\n\n");
434 out
435}
436
437fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
448 match ty {
449 TypeRef::Unit => "void",
450 TypeRef::Primitive(PrimitiveType::Bool) => "int",
452 TypeRef::Primitive(PrimitiveType::U8) => "byte",
454 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
455 TypeRef::Primitive(PrimitiveType::U32) => "uint",
456 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
457 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
458 TypeRef::Primitive(PrimitiveType::I16) => "short",
459 TypeRef::Primitive(PrimitiveType::I32) => "int",
460 TypeRef::Primitive(PrimitiveType::I64) => "long",
461 TypeRef::Primitive(PrimitiveType::F32) => "float",
462 TypeRef::Primitive(PrimitiveType::F64) => "double",
463 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
464 TypeRef::Primitive(PrimitiveType::Isize) => "long",
465 TypeRef::Duration => "ulong",
467 TypeRef::String
469 | TypeRef::Char
470 | TypeRef::Bytes
471 | TypeRef::Optional(_)
472 | TypeRef::Vec(_)
473 | TypeRef::Map(_, _)
474 | TypeRef::Named(_)
475 | TypeRef::Path
476 | TypeRef::Json => "IntPtr",
477 }
478}
479
480fn returns_string(ty: &TypeRef) -> bool {
482 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
483}
484
485fn returns_bool_via_int(ty: &TypeRef) -> bool {
487 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
488}
489
490fn returns_json_object(ty: &TypeRef) -> bool {
492 matches!(
493 ty,
494 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
495 )
496}
497
498fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
509 match ty {
510 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
511 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
513 TypeRef::Unit => "void",
514 TypeRef::Primitive(PrimitiveType::Bool) => "int",
515 TypeRef::Primitive(PrimitiveType::U8) => "byte",
516 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
517 TypeRef::Primitive(PrimitiveType::U32) => "uint",
518 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
519 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
520 TypeRef::Primitive(PrimitiveType::I16) => "short",
521 TypeRef::Primitive(PrimitiveType::I32) => "int",
522 TypeRef::Primitive(PrimitiveType::I64) => "long",
523 TypeRef::Primitive(PrimitiveType::F32) => "float",
524 TypeRef::Primitive(PrimitiveType::F64) => "double",
525 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
526 TypeRef::Primitive(PrimitiveType::Isize) => "long",
527 TypeRef::Duration => "ulong",
528 }
529}
530
531fn is_bridge_param(
538 param: &alef_core::ir::ParamDef,
539 bridge_param_names: &HashSet<String>,
540 bridge_type_aliases: &HashSet<String>,
541) -> bool {
542 bridge_param_names.contains(¶m.name)
543 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
544}
545
546#[allow(clippy::too_many_arguments)]
547fn gen_native_methods(
548 api: &ApiSurface,
549 namespace: &str,
550 lib_name: &str,
551 prefix: &str,
552 bridge_param_names: &HashSet<String>,
553 bridge_type_aliases: &HashSet<String>,
554 has_visitor_callbacks: bool,
555 trait_bridges: &[alef_core::config::TraitBridgeConfig],
556 streaming_methods: &HashSet<String>,
557 exclude_functions: &HashSet<String>,
558 options_field_bridges: &[OptionsFieldBridgeInfo],
559) -> String {
560 let mut out = csharp_file_header();
561 out.push_str("using System;\n");
562 out.push_str("using System.Runtime.InteropServices;\n\n");
563
564 out.push_str(&format!("namespace {};\n\n", namespace));
565
566 out.push_str("internal static partial class NativeMethods\n{\n");
567 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
568
569 let mut emitted: HashSet<String> = HashSet::new();
572
573 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
576
577 let mut opaque_param_types: HashSet<String> = HashSet::new();
581 let mut opaque_return_types: HashSet<String> = HashSet::new();
582
583 for func in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
587 for param in &func.params {
588 if let TypeRef::Named(name) = ¶m.ty {
589 opaque_param_types.insert(name.clone());
590 }
591 }
592 if let TypeRef::Named(name) = &func.return_type {
593 if !enum_names.contains(name) {
594 opaque_return_types.insert(name.clone());
595 }
596 }
597 }
598 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
599 for method in &typ.methods {
600 if streaming_methods.contains(&method.name) {
601 continue;
602 }
603 for param in &method.params {
604 if let TypeRef::Named(name) = ¶m.ty {
605 opaque_param_types.insert(name.clone());
606 }
607 }
608 if let TypeRef::Named(name) = &method.return_type {
609 if !enum_names.contains(name) {
610 opaque_return_types.insert(name.clone());
611 }
612 }
613 }
614 }
615
616 let true_opaque_types: HashSet<String> = api
618 .types
619 .iter()
620 .filter(|t| t.is_opaque)
621 .map(|t| t.name.clone())
622 .collect();
623
624 let options_field_bridge_options_types: HashSet<&str> =
629 options_field_bridges.iter().map(|b| b.options_type.as_str()).collect();
630
631 let mut sorted_param_types: Vec<&String> = opaque_param_types.iter().collect();
636 sorted_param_types.sort();
637 for type_name in sorted_param_types {
638 let snake = type_name.to_snake_case();
639 let is_options_field_type = options_field_bridge_options_types.contains(type_name.as_str());
640 if !true_opaque_types.contains(type_name) && !is_options_field_type {
641 let from_json_entry = format!("{prefix}_{snake}_from_json");
642 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
643 if emitted.insert(from_json_entry.clone()) {
644 out.push_str(&format!(
645 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
646 ));
647 out.push_str(&format!(
648 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
649 ));
650 }
651 }
652 let free_entry = format!("{prefix}_{snake}_free");
653 let free_cs = format!("{}Free", type_name.to_pascal_case());
654 if emitted.insert(free_entry.clone()) {
655 out.push_str(&format!(
656 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
657 ));
658 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
659 }
660 }
661
662 let mut sorted_return_types: Vec<&String> = opaque_return_types.iter().collect();
665 sorted_return_types.sort();
666 for type_name in sorted_return_types {
667 let snake = type_name.to_snake_case();
668 if !true_opaque_types.contains(type_name) {
669 let to_json_entry = format!("{prefix}_{snake}_to_json");
670 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
671 if emitted.insert(to_json_entry.clone()) {
672 out.push_str(&format!(
673 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
674 ));
675 out.push_str(&format!(
676 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
677 ));
678 }
679 }
680 let free_entry = format!("{prefix}_{snake}_free");
681 let free_cs = format!("{}Free", type_name.to_pascal_case());
682 if emitted.insert(free_entry.clone()) {
683 out.push_str(&format!(
684 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
685 ));
686 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
687 }
688 }
689
690 for func in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
692 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
693 if emitted.insert(c_func_name.clone()) {
694 out.push_str(&gen_pinvoke_for_func(
695 &c_func_name,
696 func,
697 bridge_param_names,
698 bridge_type_aliases,
699 ));
700 }
701 }
702
703 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
706 let type_snake = typ.name.to_snake_case();
707 for method in &typ.methods {
708 if streaming_methods.contains(&method.name) {
709 continue;
710 }
711 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
712 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
716 if emitted.insert(c_method_name.clone()) {
717 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
718 }
719 }
720 }
721
722 out.push_str(&format!(
724 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
725 ));
726 out.push_str(" internal static extern int LastErrorCode();\n\n");
727
728 out.push_str(&format!(
729 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
730 ));
731 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
732
733 out.push_str(&format!(
734 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
735 ));
736 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
737
738 if has_visitor_callbacks {
742 let has_options_field_bridge = !options_field_bridges.is_empty();
743 let visitor_decls =
744 crate::gen_visitor::gen_native_methods_visitor(namespace, lib_name, prefix, has_options_field_bridge);
745 if !visitor_decls.is_empty() {
746 out.push('\n');
747 out.push_str(&visitor_decls);
748 }
749 }
750
751 if !trait_bridges.is_empty() {
753 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
755
756 let bridges: Vec<_> = trait_bridges
758 .iter()
759 .filter_map(|config| {
760 let trait_name = config.trait_name.clone();
761 trait_defs
762 .iter()
763 .find(|t| t.name == trait_name)
764 .map(|trait_def| (trait_name, config, *trait_def))
765 })
766 .collect();
767
768 if !bridges.is_empty() {
769 out.push('\n');
770 out.push_str(&crate::trait_bridge::gen_native_methods_trait_bridges(
771 namespace, prefix, &bridges,
772 ));
773 }
774 }
775
776 for bridge in options_field_bridges {
779 let entry_point = bridge.ffi_symbol();
780 let cs_name = bridge.cs_setter_name();
781 out.push_str("\n // Options-field bridge setter\n");
782 out.push_str(&format!(
783 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{entry_point}\")]\n"
784 ));
785 out.push_str(&format!(
786 " internal static extern void {cs_name}(IntPtr options, IntPtr vtable);\n"
787 ));
788 }
789
790 out.push_str("}\n");
791
792 out
793}
794
795fn gen_pinvoke_for_func(
796 c_name: &str,
797 func: &FunctionDef,
798 bridge_param_names: &HashSet<String>,
799 bridge_type_aliases: &HashSet<String>,
800) -> String {
801 let cs_name = to_csharp_name(&func.name);
802 let mut out =
803 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
804 out.push_str(" internal static extern ");
805
806 out.push_str(pinvoke_return_type(&func.return_type));
808
809 out.push_str(&format!(" {}(", cs_name));
810
811 let visible_params: Vec<_> = func
814 .params
815 .iter()
816 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
817 .collect();
818
819 if visible_params.is_empty() {
820 out.push_str(");\n\n");
821 } else {
822 out.push('\n');
823 for (i, param) in visible_params.iter().enumerate() {
824 out.push_str(" ");
825 let pinvoke_ty = pinvoke_param_type(¶m.ty);
826 if pinvoke_ty == "string" {
827 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
828 }
829 let param_name = param.name.to_lower_camel_case();
830 out.push_str(&format!("{pinvoke_ty} {param_name}"));
831
832 if i < visible_params.len() - 1 {
833 out.push(',');
834 }
835 out.push('\n');
836 }
837 out.push_str(" );\n\n");
838 }
839
840 out
841}
842
843fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
844 let mut out =
845 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
846 out.push_str(" internal static extern ");
847
848 out.push_str(pinvoke_return_type(&method.return_type));
850
851 out.push_str(&format!(" {}(", cs_name));
852
853 let has_receiver = !method.is_static && method.receiver.is_some();
859
860 if !has_receiver && method.params.is_empty() {
861 out.push_str(");\n\n");
862 } else {
863 out.push('\n');
864 let total = if has_receiver {
865 method.params.len() + 1
866 } else {
867 method.params.len()
868 };
869 let mut idx = 0usize;
870 if has_receiver {
871 out.push_str(" IntPtr handle");
872 if total > 1 {
873 out.push(',');
874 }
875 out.push('\n');
876 idx += 1;
877 }
878 for param in method.params.iter() {
879 out.push_str(" ");
880 let pinvoke_ty = pinvoke_param_type(¶m.ty);
881 if pinvoke_ty == "string" {
882 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
883 }
884 let param_name = param.name.to_lower_camel_case();
885 out.push_str(&format!("{pinvoke_ty} {param_name}"));
886
887 if idx < total - 1 {
888 out.push(',');
889 }
890 out.push('\n');
891 idx += 1;
892 }
893 out.push_str(" );\n\n");
894 }
895
896 out
897}
898
899fn gen_exception_class(namespace: &str, class_name: &str) -> String {
900 let mut out = csharp_file_header();
901 out.push_str("using System;\n\n");
902
903 out.push_str(&format!("namespace {};\n\n", namespace));
904
905 out.push_str(&format!("public class {} : Exception\n", class_name));
906 out.push_str("{\n");
907 out.push_str(" public int Code { get; }\n\n");
908 out.push_str(&format!(
909 " public {}(int code, string message) : base(message)\n",
910 class_name
911 ));
912 out.push_str(" {\n");
913 out.push_str(" Code = code;\n");
914 out.push_str(" }\n");
915 out.push_str("}\n");
916
917 out
918}
919
920#[allow(clippy::too_many_arguments)]
921fn gen_wrapper_class(
922 api: &ApiSurface,
923 namespace: &str,
924 class_name: &str,
925 exception_name: &str,
926 prefix: &str,
927 bridge_param_names: &HashSet<String>,
928 bridge_type_aliases: &HashSet<String>,
929 has_visitor_callbacks: bool,
930 streaming_methods: &HashSet<String>,
931 exclude_functions: &HashSet<String>,
932 options_field_bridges: &[OptionsFieldBridgeInfo],
933) -> String {
934 let mut out = csharp_file_header();
935 out.push_str("using System;\n");
936 out.push_str("using System.Collections.Generic;\n");
937 out.push_str("using System.Runtime.InteropServices;\n");
938 out.push_str("using System.Text.Json;\n");
939 out.push_str("using System.Text.Json.Serialization;\n");
940 out.push_str("using System.Threading.Tasks;\n\n");
941
942 out.push_str(&format!("namespace {};\n\n", namespace));
943
944 out.push_str(&format!("public static class {}\n", class_name));
945 out.push_str("{\n");
946 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
947 out.push_str(" {\n");
948 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
949 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
950 out.push_str(" };\n\n");
951
952 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
954
955 let true_opaque_types: HashSet<String> = api
957 .types
958 .iter()
959 .filter(|t| t.is_opaque)
960 .map(|t| t.name.clone())
961 .collect();
962
963 for func in api.functions.iter().filter(|f| !exclude_functions.contains(&f.name)) {
965 let bridge_info = options_field_bridges.iter().find(|b| {
967 func.params.iter().any(|p| {
968 let type_name = match &p.ty {
969 TypeRef::Named(n) => Some(n.as_str()),
970 TypeRef::Optional(inner) => {
971 if let TypeRef::Named(n) = inner.as_ref() {
972 Some(n.as_str())
973 } else {
974 None
975 }
976 }
977 _ => None,
978 };
979 type_name == Some(b.options_type.as_str())
980 })
981 });
982 out.push_str(&gen_wrapper_function(
983 func,
984 exception_name,
985 prefix,
986 &enum_names,
987 &true_opaque_types,
988 bridge_param_names,
989 bridge_type_aliases,
990 bridge_info,
991 ));
992 }
993
994 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
997 if typ.is_opaque {
999 continue;
1000 }
1001 for method in &typ.methods {
1002 if streaming_methods.contains(&method.name) {
1003 continue;
1004 }
1005 out.push_str(&gen_wrapper_method(
1006 method,
1007 exception_name,
1008 prefix,
1009 &typ.name,
1010 &enum_names,
1011 &true_opaque_types,
1012 bridge_param_names,
1013 bridge_type_aliases,
1014 ));
1015 }
1016 }
1017
1018 let has_options_field_bridge = !options_field_bridges.is_empty();
1023 if has_visitor_callbacks && !has_options_field_bridge {
1024 out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
1025 exception_name,
1026 prefix,
1027 ));
1028 }
1029
1030 out.push_str(" private static ");
1032 out.push_str(&format!("{} GetLastError()\n", exception_name));
1033 out.push_str(" {\n");
1034 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
1035 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
1036 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
1037 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
1038 out.push_str(" }\n");
1039
1040 out.push_str("}\n");
1041
1042 out
1043}
1044
1045fn emit_named_param_setup(
1062 out: &mut String,
1063 params: &[alef_core::ir::ParamDef],
1064 indent: &str,
1065 true_opaque_types: &HashSet<String>,
1066) {
1067 for param in params {
1068 let param_name = param.name.to_lower_camel_case();
1069 let json_var = format!("{param_name}Json");
1070 let handle_var = format!("{param_name}Handle");
1071
1072 match ¶m.ty {
1073 TypeRef::Named(type_name) => {
1074 if true_opaque_types.contains(type_name) {
1077 continue;
1078 }
1079 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
1080 if param.optional {
1081 out.push_str(&format!(
1082 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
1083 ));
1084 } else {
1085 out.push_str(&format!(
1086 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
1087 ));
1088 }
1089 out.push_str(&format!(
1090 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
1091 ));
1092 }
1093 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1094 out.push_str(&format!(
1096 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
1097 ));
1098 out.push_str(&format!(
1099 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
1100 ));
1101 }
1102 TypeRef::Bytes => {
1103 out.push_str(&format!(
1105 "{indent}var {handle_var} = GCHandle.Alloc({param_name}, GCHandleType.Pinned);\n"
1106 ));
1107 }
1108 _ => {}
1109 }
1110 }
1111}
1112
1113fn emit_named_param_setup_excluding(
1118 out: &mut String,
1119 params: &[alef_core::ir::ParamDef],
1120 indent: &str,
1121 true_opaque_types: &HashSet<String>,
1122 exclude: &HashSet<String>,
1123) {
1124 let filtered: Vec<alef_core::ir::ParamDef> = params
1125 .iter()
1126 .filter(|p| !exclude.contains(&p.name.to_lower_camel_case()))
1127 .cloned()
1128 .collect();
1129 emit_named_param_setup(out, &filtered, indent, true_opaque_types);
1130}
1131
1132fn returns_ptr(ty: &TypeRef) -> bool {
1135 matches!(
1136 ty,
1137 TypeRef::String
1138 | TypeRef::Char
1139 | TypeRef::Path
1140 | TypeRef::Json
1141 | TypeRef::Named(_)
1142 | TypeRef::Vec(_)
1143 | TypeRef::Map(_, _)
1144 | TypeRef::Bytes
1145 | TypeRef::Optional(_)
1146 )
1147}
1148
1149fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
1155 match ty {
1156 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
1157 let bang = if optional { "!" } else { "" };
1159 format!("{param_name}{bang}.Handle")
1160 }
1161 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1162 format!("{param_name}Handle")
1163 }
1164 TypeRef::Bytes => {
1165 format!("{param_name}Handle.AddrOfPinnedObject()")
1166 }
1167 TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool) => {
1168 if optional {
1170 format!("({param_name}?.Value ? 1 : 0)")
1171 } else {
1172 format!("({param_name} ? 1 : 0)")
1173 }
1174 }
1175 ty => {
1176 if optional {
1177 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
1181 if needs_value_unwrap {
1182 format!("{param_name}.GetValueOrDefault()")
1183 } else {
1184 format!("{param_name}!")
1185 }
1186 } else {
1187 param_name.to_string()
1188 }
1189 }
1190 }
1191}
1192
1193fn emit_named_param_teardown(
1198 out: &mut String,
1199 params: &[alef_core::ir::ParamDef],
1200 true_opaque_types: &HashSet<String>,
1201) {
1202 for param in params {
1203 let param_name = param.name.to_lower_camel_case();
1204 let handle_var = format!("{param_name}Handle");
1205 match ¶m.ty {
1206 TypeRef::Named(type_name) => {
1207 if true_opaque_types.contains(type_name) {
1208 continue;
1210 }
1211 let free_method = format!("{}Free", type_name.to_pascal_case());
1212 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
1213 }
1214 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1215 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
1216 }
1217 TypeRef::Bytes => {
1218 out.push_str(&format!(" {handle_var}.Free();\n"));
1219 }
1220 _ => {}
1221 }
1222 }
1223}
1224
1225fn emit_named_param_teardown_indented(
1227 out: &mut String,
1228 params: &[alef_core::ir::ParamDef],
1229 indent: &str,
1230 true_opaque_types: &HashSet<String>,
1231) {
1232 for param in params {
1233 let param_name = param.name.to_lower_camel_case();
1234 let handle_var = format!("{param_name}Handle");
1235 match ¶m.ty {
1236 TypeRef::Named(type_name) => {
1237 if true_opaque_types.contains(type_name) {
1238 continue;
1240 }
1241 let free_method = format!("{}Free", type_name.to_pascal_case());
1242 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
1243 }
1244 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1245 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
1246 }
1247 TypeRef::Bytes => {
1248 out.push_str(&format!("{indent}{handle_var}.Free();\n"));
1249 }
1250 _ => {}
1251 }
1252 }
1253}
1254
1255#[allow(clippy::too_many_arguments)]
1256fn gen_wrapper_function(
1257 func: &FunctionDef,
1258 _exception_name: &str,
1259 _prefix: &str,
1260 enum_names: &HashSet<String>,
1261 true_opaque_types: &HashSet<String>,
1262 bridge_param_names: &HashSet<String>,
1263 bridge_type_aliases: &HashSet<String>,
1264 options_field_bridge: Option<&OptionsFieldBridgeInfo>,
1265) -> String {
1266 let mut out = String::with_capacity(1024);
1267
1268 let visible_params: Vec<alef_core::ir::ParamDef> = func
1270 .params
1271 .iter()
1272 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1273 .cloned()
1274 .collect();
1275
1276 doc_emission::emit_csharp_doc(&mut out, &func.doc, " ");
1278 for param in &visible_params {
1279 if !func.doc.is_empty() {
1280 out.push_str(&format!(
1281 " /// <param name=\"{}\">{}</param>\n",
1282 param.name.to_lower_camel_case(),
1283 if param.optional { "Optional." } else { "" }
1284 ));
1285 }
1286 }
1287
1288 out.push_str(" public static ");
1289
1290 if func.is_async {
1292 if func.return_type == TypeRef::Unit {
1293 out.push_str("async Task");
1294 } else {
1295 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
1296 }
1297 } else if func.return_type == TypeRef::Unit {
1298 out.push_str("void");
1299 } else {
1300 out.push_str(&csharp_type(&func.return_type));
1301 }
1302
1303 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
1304 out.push('(');
1305
1306 for (i, param) in visible_params.iter().enumerate() {
1308 let param_name = param.name.to_lower_camel_case();
1309 let mapped = csharp_type(¶m.ty);
1310 if param.optional && !mapped.ends_with('?') {
1311 out.push_str(&format!("{mapped}? {param_name}"));
1312 } else {
1313 out.push_str(&format!("{mapped} {param_name}"));
1314 }
1315
1316 if i < visible_params.len() - 1 {
1317 out.push_str(", ");
1318 }
1319 }
1320
1321 out.push_str(")\n {\n");
1322
1323 for param in &visible_params {
1325 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1326 let param_name = param.name.to_lower_camel_case();
1327 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1328 }
1329 }
1330
1331 let bridge_options_param: Option<(&alef_core::ir::ParamDef, &OptionsFieldBridgeInfo)> =
1335 if let Some(bridge) = options_field_bridge {
1336 visible_params
1337 .iter()
1338 .find(|p| {
1339 let type_name = match &p.ty {
1340 TypeRef::Named(n) => Some(n.as_str()),
1341 TypeRef::Optional(inner) => {
1342 if let TypeRef::Named(n) = inner.as_ref() {
1343 Some(n.as_str())
1344 } else {
1345 None
1346 }
1347 }
1348 _ => None,
1349 };
1350 type_name == Some(bridge.options_type.as_str())
1351 })
1352 .map(|p| (p, bridge))
1353 } else {
1354 None
1355 };
1356
1357 let skip_param_names: HashSet<String> = bridge_options_param
1359 .iter()
1360 .map(|(p, _)| p.name.to_lower_camel_case())
1361 .collect();
1362
1363 emit_named_param_setup_excluding(
1366 &mut out,
1367 &visible_params,
1368 " ",
1369 true_opaque_types,
1370 &skip_param_names,
1371 );
1372
1373 if let Some((opts_param, bridge)) = bridge_options_param {
1378 let opts_name = opts_param.name.to_lower_camel_case();
1379 let opts_handle = format!("{opts_name}Handle");
1380 let opts_pascal = bridge.options_type.to_pascal_case();
1381 let update_from_json_method = format!("{}UpdateFromJson", opts_pascal);
1382 let from_update_method = format!("{}FromUpdate", opts_pascal);
1383 let update_free_method = format!("{}UpdateFree", opts_pascal);
1384 let default_method = format!("{}Default", opts_pascal);
1385 let field_pascal = bridge.field_name.to_pascal_case();
1386 let _bridge_type = &bridge.bridge_cs_type;
1387 let setter = bridge.cs_setter_name();
1388
1389 if opts_param.optional {
1391 out.push_str(" // options-field bridge: build options handle via update pattern\n");
1392 out.push_str(&format!(" var {opts_handle} = IntPtr.Zero;\n"));
1393 out.push_str(&format!(" if ({opts_name} != null)\n {{\n"));
1394 out.push_str(&format!(
1395 " var {opts_name}Json = JsonSerializer.Serialize({opts_name}, JsonOptions);\n"
1396 ));
1397 out.push_str(&format!(
1398 " var {opts_name}UpdateHandle = NativeMethods.{update_from_json_method}({opts_name}Json);\n"
1399 ));
1400 out.push_str(&format!(
1401 " {opts_handle} = NativeMethods.{from_update_method}({opts_name}UpdateHandle);\n"
1402 ));
1403 out.push_str(&format!(
1404 " NativeMethods.{update_free_method}({opts_name}UpdateHandle);\n"
1405 ));
1406 out.push_str(" }\n");
1407 out.push_str(" else\n {\n");
1408 out.push_str(&format!(
1409 " {opts_handle} = NativeMethods.{default_method}();\n"
1410 ));
1411 out.push_str(" }\n");
1412 } else {
1413 out.push_str(" // options-field bridge: build options handle via update pattern\n");
1414 out.push_str(&format!(
1415 " var {opts_name}Json = JsonSerializer.Serialize({opts_name}, JsonOptions);\n"
1416 ));
1417 out.push_str(&format!(
1418 " var {opts_name}UpdateHandle = NativeMethods.{update_from_json_method}({opts_name}Json);\n"
1419 ));
1420 out.push_str(&format!(
1421 " var {opts_handle} = NativeMethods.{from_update_method}({opts_name}UpdateHandle);\n"
1422 ));
1423 out.push_str(&format!(
1424 " NativeMethods.{update_free_method}({opts_name}UpdateHandle);\n"
1425 ));
1426 }
1427
1428 if opts_param.optional {
1432 out.push_str(" // options-field bridge: attach visitor when present\n");
1433 out.push_str(&format!(
1434 " if ({opts_name} != null && {opts_name}.{field_pascal} != null)\n {{\n"
1435 ));
1436 out.push_str(&format!(
1437 " NativeMethods.{setter}({opts_handle}, {opts_name}.{field_pascal}._vtable);\n"
1438 ));
1439 out.push_str(" }\n");
1440 } else {
1441 out.push_str(" // options-field bridge: attach visitor when present\n");
1442 out.push_str(&format!(
1443 " if ({opts_name}.{field_pascal} != null)\n {{\n"
1444 ));
1445 out.push_str(&format!(
1446 " NativeMethods.{setter}({opts_handle}, {opts_name}.{field_pascal}._vtable);\n"
1447 ));
1448 out.push_str(" }\n");
1449 }
1450
1451 let _ = default_method; }
1455
1456 let cs_native_name = to_csharp_name(&func.name);
1458
1459 if func.is_async {
1460 if func.return_type == TypeRef::Unit {
1464 out.push_str(" await Task.Run(() =>\n {\n");
1465 } else {
1466 out.push_str(" return await Task.Run(() =>\n {\n");
1467 }
1468
1469 if func.return_type != TypeRef::Unit {
1470 out.push_str(" var nativeResult = ");
1471 } else {
1472 out.push_str(" ");
1473 }
1474
1475 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1476
1477 if visible_params.is_empty() {
1478 out.push_str(");\n");
1479 } else {
1480 out.push('\n');
1481 for (i, param) in visible_params.iter().enumerate() {
1482 let param_name = param.name.to_lower_camel_case();
1483 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1484 out.push_str(&format!(" {arg}"));
1485 if i < visible_params.len() - 1 {
1486 out.push(',');
1487 }
1488 out.push('\n');
1489 }
1490 out.push_str(" );\n");
1491 }
1492
1493 if func.return_type != TypeRef::Unit {
1495 out.push_str(
1496 " if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1497 );
1498 }
1499
1500 emit_return_marshalling_indented(
1501 &mut out,
1502 &func.return_type,
1503 " ",
1504 enum_names,
1505 true_opaque_types,
1506 );
1507 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1508 emit_return_statement_indented(&mut out, &func.return_type, " ");
1509 out.push_str(" });\n");
1510 } else {
1511 if func.return_type != TypeRef::Unit {
1512 out.push_str(" var nativeResult = ");
1513 } else {
1514 out.push_str(" ");
1515 }
1516
1517 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1518
1519 if visible_params.is_empty() {
1520 out.push_str(");\n");
1521 } else {
1522 out.push('\n');
1523 for (i, param) in visible_params.iter().enumerate() {
1524 let param_name = param.name.to_lower_camel_case();
1525 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1526 out.push_str(&format!(" {arg}"));
1527 if i < visible_params.len() - 1 {
1528 out.push(',');
1529 }
1530 out.push('\n');
1531 }
1532 out.push_str(" );\n");
1533 }
1534
1535 if func.return_type != TypeRef::Unit && returns_ptr(&func.return_type) {
1539 out.push_str(
1540 " if (nativeResult == IntPtr.Zero)\n {\n var err = GetLastError();\n if (err.Code != 0)\n {\n throw err;\n }\n }\n",
1541 );
1542 }
1543
1544 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
1545 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1546 emit_return_statement(&mut out, &func.return_type);
1547 }
1548
1549 out.push_str(" }\n\n");
1550
1551 out
1552}
1553
1554#[allow(clippy::too_many_arguments)]
1555fn gen_wrapper_method(
1556 method: &MethodDef,
1557 _exception_name: &str,
1558 _prefix: &str,
1559 type_name: &str,
1560 enum_names: &HashSet<String>,
1561 true_opaque_types: &HashSet<String>,
1562 bridge_param_names: &HashSet<String>,
1563 bridge_type_aliases: &HashSet<String>,
1564) -> String {
1565 let mut out = String::with_capacity(1024);
1566
1567 let visible_params: Vec<alef_core::ir::ParamDef> = method
1569 .params
1570 .iter()
1571 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1572 .cloned()
1573 .collect();
1574
1575 doc_emission::emit_csharp_doc(&mut out, &method.doc, " ");
1577 for param in &visible_params {
1578 if !method.doc.is_empty() {
1579 out.push_str(&format!(
1580 " /// <param name=\"{}\">{}</param>\n",
1581 param.name.to_lower_camel_case(),
1582 if param.optional { "Optional." } else { "" }
1583 ));
1584 }
1585 }
1586
1587 out.push_str(" public static ");
1589
1590 if method.is_async {
1592 if method.return_type == TypeRef::Unit {
1593 out.push_str("async Task");
1594 } else {
1595 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1596 }
1597 } else if method.return_type == TypeRef::Unit {
1598 out.push_str("void");
1599 } else {
1600 out.push_str(&csharp_type(&method.return_type));
1601 }
1602
1603 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1605 out.push_str(&format!(" {method_cs_name}"));
1606 out.push('(');
1607
1608 let has_receiver = !method.is_static && method.receiver.is_some();
1612 if has_receiver {
1613 out.push_str("IntPtr handle");
1614 if !visible_params.is_empty() {
1615 out.push_str(", ");
1616 }
1617 }
1618
1619 for (i, param) in visible_params.iter().enumerate() {
1621 let param_name = param.name.to_lower_camel_case();
1622 let mapped = csharp_type(¶m.ty);
1623 if param.optional && !mapped.ends_with('?') {
1624 out.push_str(&format!("{mapped}? {param_name}"));
1625 } else {
1626 out.push_str(&format!("{mapped} {param_name}"));
1627 }
1628
1629 if i < visible_params.len() - 1 {
1630 out.push_str(", ");
1631 }
1632 }
1633
1634 out.push_str(")\n {\n");
1635
1636 for param in &visible_params {
1638 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1639 let param_name = param.name.to_lower_camel_case();
1640 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1641 }
1642 }
1643
1644 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1646
1647 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1652
1653 if method.is_async {
1654 if method.return_type == TypeRef::Unit {
1657 out.push_str(" await Task.Run(() =>\n {\n");
1658 } else {
1659 out.push_str(" return await Task.Run(() =>\n {\n");
1660 }
1661
1662 if method.return_type != TypeRef::Unit {
1663 out.push_str(" var nativeResult = ");
1664 } else {
1665 out.push_str(" ");
1666 }
1667
1668 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1669
1670 if !has_receiver && visible_params.is_empty() {
1671 out.push_str(");\n");
1672 } else {
1673 out.push('\n');
1674 let total = if has_receiver {
1675 visible_params.len() + 1
1676 } else {
1677 visible_params.len()
1678 };
1679 let mut idx = 0usize;
1680 if has_receiver {
1681 out.push_str(" handle");
1682 if total > 1 {
1683 out.push(',');
1684 }
1685 out.push('\n');
1686 idx += 1;
1687 }
1688 for param in visible_params.iter() {
1689 let param_name = param.name.to_lower_camel_case();
1690 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1691 out.push_str(&format!(" {arg}"));
1692 if idx < total - 1 {
1693 out.push(',');
1694 }
1695 out.push('\n');
1696 idx += 1;
1697 }
1698 out.push_str(" );\n");
1699 }
1700
1701 emit_return_marshalling_indented(
1702 &mut out,
1703 &method.return_type,
1704 " ",
1705 enum_names,
1706 true_opaque_types,
1707 );
1708 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1709 emit_return_statement_indented(&mut out, &method.return_type, " ");
1710 out.push_str(" });\n");
1711 } else {
1712 if method.return_type != TypeRef::Unit {
1713 out.push_str(" var nativeResult = ");
1714 } else {
1715 out.push_str(" ");
1716 }
1717
1718 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1719
1720 if !has_receiver && visible_params.is_empty() {
1721 out.push_str(");\n");
1722 } else {
1723 out.push('\n');
1724 let total = if has_receiver {
1725 visible_params.len() + 1
1726 } else {
1727 visible_params.len()
1728 };
1729 let mut idx = 0usize;
1730 if has_receiver {
1731 out.push_str(" handle");
1732 if total > 1 {
1733 out.push(',');
1734 }
1735 out.push('\n');
1736 idx += 1;
1737 }
1738 for param in visible_params.iter() {
1739 let param_name = param.name.to_lower_camel_case();
1740 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1741 out.push_str(&format!(" {arg}"));
1742 if idx < total - 1 {
1743 out.push(',');
1744 }
1745 out.push('\n');
1746 idx += 1;
1747 }
1748 out.push_str(" );\n");
1749 }
1750
1751 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1752 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1753 emit_return_statement(&mut out, &method.return_type);
1754 }
1755
1756 out.push_str(" }\n\n");
1757
1758 out
1759}
1760
1761fn emit_return_marshalling(
1773 out: &mut String,
1774 return_type: &TypeRef,
1775 enum_names: &HashSet<String>,
1776 true_opaque_types: &HashSet<String>,
1777) {
1778 if *return_type == TypeRef::Unit {
1779 return;
1781 }
1782
1783 if returns_string(return_type) {
1784 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n");
1786 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1787 } else if returns_bool_via_int(return_type) {
1788 out.push_str(" var returnValue = nativeResult != 0;\n");
1790 } else if let TypeRef::Named(type_name) = return_type {
1791 let pascal = type_name.to_pascal_case();
1792 if true_opaque_types.contains(type_name) {
1793 out.push_str(&format!(" var returnValue = new {pascal}(nativeResult);\n"));
1795 } else if !enum_names.contains(&pascal) {
1796 let to_json_method = format!("{pascal}ToJson");
1798 let free_method = format!("{pascal}Free");
1799 let cs_ty = csharp_type(return_type);
1800 out.push_str(&format!(
1801 " var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
1802 ));
1803 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1804 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1805 out.push_str(&format!(" NativeMethods.{free_method}(nativeResult);\n"));
1806 out.push_str(&format!(
1807 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1808 cs_ty
1809 ));
1810 } else {
1811 let cs_ty = csharp_type(return_type);
1813 out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
1814 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1815 out.push_str(&format!(
1816 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1817 cs_ty
1818 ));
1819 }
1820 } else if returns_json_object(return_type) {
1821 let cs_ty = csharp_type(return_type);
1823 out.push_str(" var json = Marshal.PtrToStringUTF8(nativeResult);\n");
1824 out.push_str(" NativeMethods.FreeString(nativeResult);\n");
1825 out.push_str(&format!(
1826 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1827 cs_ty
1828 ));
1829 } else {
1830 out.push_str(" var returnValue = nativeResult;\n");
1832 }
1833}
1834
1835fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1837 if *return_type != TypeRef::Unit {
1838 out.push_str(" return returnValue;\n");
1839 }
1840}
1841
1842fn emit_return_marshalling_indented(
1847 out: &mut String,
1848 return_type: &TypeRef,
1849 indent: &str,
1850 enum_names: &HashSet<String>,
1851 true_opaque_types: &HashSet<String>,
1852) {
1853 if *return_type == TypeRef::Unit {
1854 return;
1855 }
1856
1857 if returns_string(return_type) {
1858 out.push_str(&format!(
1859 "{indent}var returnValue = Marshal.PtrToStringUTF8(nativeResult) ?? string.Empty;\n"
1860 ));
1861 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1862 } else if returns_bool_via_int(return_type) {
1863 out.push_str(&format!("{indent}var returnValue = nativeResult != 0;\n"));
1864 } else if let TypeRef::Named(type_name) = return_type {
1865 let pascal = type_name.to_pascal_case();
1866 if true_opaque_types.contains(type_name) {
1867 out.push_str(&format!("{indent}var returnValue = new {pascal}(nativeResult);\n"));
1869 } else if !enum_names.contains(&pascal) {
1870 let to_json_method = format!("{pascal}ToJson");
1872 let free_method = format!("{pascal}Free");
1873 let cs_ty = csharp_type(return_type);
1874 out.push_str(&format!(
1875 "{indent}var jsonPtr = NativeMethods.{to_json_method}(nativeResult);\n"
1876 ));
1877 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1878 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1879 out.push_str(&format!("{indent}NativeMethods.{free_method}(nativeResult);\n"));
1880 out.push_str(&format!(
1881 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1882 cs_ty
1883 ));
1884 } else {
1885 let cs_ty = csharp_type(return_type);
1887 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
1888 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1889 out.push_str(&format!(
1890 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1891 cs_ty
1892 ));
1893 }
1894 } else if returns_json_object(return_type) {
1895 let cs_ty = csharp_type(return_type);
1896 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(nativeResult);\n"));
1897 out.push_str(&format!("{indent}NativeMethods.FreeString(nativeResult);\n"));
1898 out.push_str(&format!(
1899 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1900 cs_ty
1901 ));
1902 } else {
1903 out.push_str(&format!("{indent}var returnValue = nativeResult;\n"));
1904 }
1905}
1906
1907fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1909 if *return_type != TypeRef::Unit {
1910 out.push_str(&format!("{indent}return returnValue;\n"));
1911 }
1912}
1913
1914fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1915 let mut out = csharp_file_header();
1916 out.push_str("using System;\n\n");
1917
1918 out.push_str(&format!("namespace {};\n\n", namespace));
1919
1920 if !typ.doc.is_empty() {
1922 out.push_str("/// <summary>\n");
1923 for line in typ.doc.lines() {
1924 out.push_str(&format!("/// {}\n", line));
1925 }
1926 out.push_str("/// </summary>\n");
1927 }
1928
1929 let class_name = typ.name.to_pascal_case();
1930 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1931 out.push_str("{\n");
1932 out.push_str(" internal IntPtr Handle { get; }\n\n");
1933 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1934 out.push_str(" {\n");
1935 out.push_str(" Handle = handle;\n");
1936 out.push_str(" }\n\n");
1937 out.push_str(" public void Dispose()\n");
1938 out.push_str(" {\n");
1939 out.push_str(" // Native free will be called by the runtime\n");
1940 out.push_str(" }\n");
1941 out.push_str("}\n");
1942
1943 out
1944}
1945
1946fn gen_record_type(
1947 typ: &TypeDef,
1948 namespace: &str,
1949 enum_names: &HashSet<String>,
1950 complex_enums: &HashSet<String>,
1951 custom_converter_enums: &HashSet<String>,
1952 _lang_rename_all: &str,
1953 options_field_bridges: &[OptionsFieldBridgeInfo],
1954) -> String {
1955 let bridge_for_type = options_field_bridges.iter().find(|b| b.options_type == typ.name);
1957
1958 let mut out = csharp_file_header();
1959 out.push_str("using System;\n");
1960 out.push_str("using System.Collections.Generic;\n");
1961 out.push_str("using System.Text.Json;\n");
1962 out.push_str("using System.Text.Json.Serialization;\n\n");
1963
1964 out.push_str(&format!("namespace {};\n\n", namespace));
1965
1966 if !typ.doc.is_empty() {
1968 out.push_str("/// <summary>\n");
1969 for line in typ.doc.lines() {
1970 out.push_str(&format!("/// {}\n", line));
1971 }
1972 out.push_str("/// </summary>\n");
1973 }
1974
1975 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1976 out.push_str("{\n");
1977
1978 for field in &typ.fields {
1979 if is_tuple_field(field) {
1981 continue;
1982 }
1983
1984 if let Some(b) = bridge_for_type {
1987 if field.name == b.field_name {
1988 continue;
1989 }
1990 }
1991
1992 if !field.doc.is_empty() {
1994 out.push_str(" /// <summary>\n");
1995 for line in field.doc.lines() {
1996 out.push_str(&format!(" /// {}\n", line));
1997 }
1998 out.push_str(" /// </summary>\n");
1999 }
2000
2001 let field_base_type = match &field.ty {
2005 TypeRef::Named(n) => Some(n.to_pascal_case()),
2006 TypeRef::Optional(inner) => match inner.as_ref() {
2007 TypeRef::Named(n) => Some(n.to_pascal_case()),
2008 _ => None,
2009 },
2010 _ => None,
2011 };
2012 if let Some(ref base) = field_base_type {
2013 if custom_converter_enums.contains(base) {
2014 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
2015 }
2016 }
2017
2018 let json_name = field.name.clone();
2022 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
2023
2024 let cs_name = to_csharp_name(&field.name);
2025
2026 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
2029
2030 if field.optional {
2031 let mapped = if is_complex {
2033 "JsonElement".to_string()
2034 } else {
2035 csharp_type(&field.ty).to_string()
2036 };
2037 let field_type = if mapped.ends_with('?') {
2038 mapped
2039 } else {
2040 format!("{mapped}?")
2041 };
2042 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
2043 out.push_str(" = null;\n");
2044 } else if typ.has_default || field.default.is_some() {
2045 use alef_core::ir::DefaultValue;
2048
2049 let base_type = if is_complex {
2051 "JsonElement".to_string()
2052 } else {
2053 csharp_type(&field.ty).to_string()
2054 };
2055
2056 if matches!(&field.ty, TypeRef::Duration) {
2059 let nullable_type = if base_type.ends_with('?') {
2061 base_type.clone()
2062 } else {
2063 format!("{}?", base_type)
2064 };
2065 out.push_str(&format!(
2066 " public {} {} {{ get; set; }} = null;\n",
2067 nullable_type, cs_name
2068 ));
2069 out.push('\n');
2070 continue;
2071 }
2072
2073 let default_val = match &field.typed_default {
2074 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
2075 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
2076 Some(DefaultValue::FloatLiteral(f)) => {
2077 let s = f.to_string();
2078 let s = if s.contains('.') { s } else { format!("{s}.0") };
2079 match &field.ty {
2080 TypeRef::Primitive(PrimitiveType::F32) => format!("{}f", s),
2081 _ => s,
2082 }
2083 }
2084 Some(DefaultValue::StringLiteral(s)) => {
2085 let escaped = s
2086 .replace('\\', "\\\\")
2087 .replace('"', "\\\"")
2088 .replace('\n', "\\n")
2089 .replace('\r', "\\r")
2090 .replace('\t', "\\t");
2091 format!("\"{}\"", escaped)
2092 }
2093 Some(DefaultValue::EnumVariant(v)) => {
2094 if base_type == "string" || base_type == "string?" {
2098 format!("\"{}\"", v.to_pascal_case())
2099 } else {
2100 format!("{}.{}", base_type, v.to_pascal_case())
2101 }
2102 }
2103 Some(DefaultValue::None) => "null".to_string(),
2104 Some(DefaultValue::Empty) | None => match &field.ty {
2105 TypeRef::Vec(_) => "[]".to_string(),
2106 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
2107 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
2108 TypeRef::Json => "null".to_string(),
2109 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
2110 TypeRef::Primitive(p) => match p {
2111 PrimitiveType::Bool => "false".to_string(),
2112 PrimitiveType::F32 => "0.0f".to_string(),
2113 PrimitiveType::F64 => "0.0".to_string(),
2114 _ => "0".to_string(),
2115 },
2116 TypeRef::Named(name) => {
2117 let pascal = name.to_pascal_case();
2118 if complex_enums.contains(&pascal) {
2119 "null".to_string()
2121 } else if enum_names.contains(&pascal) {
2122 "null".to_string()
2125 } else {
2126 "default!".to_string()
2127 }
2128 }
2129 _ => "default!".to_string(),
2130 },
2131 };
2132
2133 let field_type = if (default_val == "null" && !base_type.ends_with('?')) || is_complex {
2135 format!("{}?", base_type)
2136 } else {
2137 base_type
2138 };
2139
2140 out.push_str(&format!(
2141 " public {} {} {{ get; set; }} = {};\n",
2142 field_type, cs_name, default_val
2143 ));
2144 } else {
2145 let field_type = if is_complex {
2149 "JsonElement".to_string()
2150 } else {
2151 csharp_type(&field.ty).to_string()
2152 };
2153 if matches!(&field.ty, TypeRef::Duration) {
2155 out.push_str(&format!(
2156 " public {} {} {{ get; set; }} = null;\n",
2157 field_type, cs_name
2158 ));
2159 } else {
2160 let default_val = match &field.ty {
2161 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
2162 TypeRef::Vec(_) => "[]",
2163 TypeRef::Bytes => "Array.Empty<byte>()",
2164 TypeRef::Primitive(PrimitiveType::Bool) => "false",
2165 TypeRef::Primitive(PrimitiveType::F32) => "0.0f",
2166 TypeRef::Primitive(PrimitiveType::F64) => "0.0",
2167 TypeRef::Primitive(_) => "0",
2168 _ => "default!",
2169 };
2170 out.push_str(&format!(
2171 " public {} {} {{ get; set; }} = {};\n",
2172 field_type, cs_name, default_val
2173 ));
2174 }
2175 }
2176
2177 out.push('\n');
2178 }
2179
2180 if let Some(b) = bridge_for_type {
2185 let prop_name = b.field_name.to_pascal_case();
2186 let bridge_type = &b.bridge_cs_type;
2187 out.push_str(" /// <summary>\n");
2188 out.push_str(&format!(
2189 " /// Optional {bridge_type} bridge. When set, the native converter will call back\n"
2190 ));
2191 out.push_str(" /// into the managed implementation for each visited node.\n");
2192 out.push_str(" /// Not serialized to JSON — attached via the FFI setter before conversion.\n");
2193 out.push_str(" /// </summary>\n");
2194 out.push_str(" [JsonIgnore]\n");
2195 out.push_str(&format!(
2196 " public {bridge_type}Bridge? {prop_name} {{ get; set; }} = null;\n\n"
2197 ));
2198 }
2199
2200 out.push_str("}\n");
2201
2202 out
2203}
2204
2205fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
2207 match rename_all {
2208 Some("snake_case") => name.to_snake_case(),
2209 Some("camelCase") => name.to_lower_camel_case(),
2210 Some("PascalCase") => name.to_pascal_case(),
2211 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
2212 Some("lowercase") => name.to_lowercase(),
2213 Some("UPPERCASE") => name.to_uppercase(),
2214 _ => name.to_lowercase(),
2215 }
2216}
2217
2218fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
2219 let mut out = csharp_file_header();
2220 out.push_str("using System.Text.Json.Serialization;\n\n");
2221 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
2222
2223 if enum_def.serde_tag.is_some() && has_data_variants {
2225 return gen_tagged_union(enum_def, namespace);
2226 }
2227
2228 let needs_custom_converter = enum_def.variants.iter().any(|v| {
2236 if let Some(ref rename) = v.serde_rename {
2237 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
2238 rename != &snake
2239 } else {
2240 false
2241 }
2242 });
2243
2244 let enum_pascal = enum_def.name.to_pascal_case();
2245
2246 let variants: Vec<(String, String)> = enum_def
2248 .variants
2249 .iter()
2250 .map(|v| {
2251 let json_name = v
2252 .serde_rename
2253 .clone()
2254 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
2255 let pascal_name = v.name.to_pascal_case();
2256 (json_name, pascal_name)
2257 })
2258 .collect();
2259
2260 out.push_str("using System;\n");
2261 out.push_str("using System.Text.Json;\n\n");
2262
2263 out.push_str(&format!("namespace {};\n\n", namespace));
2264
2265 if !enum_def.doc.is_empty() {
2267 out.push_str("/// <summary>\n");
2268 for line in enum_def.doc.lines() {
2269 out.push_str(&format!("/// {}\n", line));
2270 }
2271 out.push_str("/// </summary>\n");
2272 }
2273
2274 if needs_custom_converter {
2275 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
2276 }
2277 out.push_str(&format!("public enum {enum_pascal}\n"));
2278 out.push_str("{\n");
2279
2280 for (json_name, pascal_name) in &variants {
2281 if let Some(v) = enum_def
2283 .variants
2284 .iter()
2285 .find(|v| v.name.to_pascal_case() == *pascal_name)
2286 {
2287 if !v.doc.is_empty() {
2288 out.push_str(" /// <summary>\n");
2289 for line in v.doc.lines() {
2290 out.push_str(&format!(" /// {}\n", line));
2291 }
2292 out.push_str(" /// </summary>\n");
2293 }
2294 }
2295 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
2296 out.push_str(&format!(" {pascal_name},\n"));
2297 }
2298
2299 out.push_str("}\n");
2300
2301 if needs_custom_converter {
2303 out.push('\n');
2304 out.push_str(&format!(
2305 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
2306 ));
2307 out.push_str(&format!(
2308 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
2309 ));
2310 out.push_str("{\n");
2311
2312 out.push_str(&format!(
2314 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
2315 ));
2316 out.push_str(" {\n");
2317 out.push_str(" var value = reader.GetString();\n");
2318 out.push_str(" return value switch\n");
2319 out.push_str(" {\n");
2320 for (json_name, pascal_name) in &variants {
2321 out.push_str(&format!(
2322 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
2323 ));
2324 }
2325 out.push_str(&format!(
2326 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
2327 ));
2328 out.push_str(" };\n");
2329 out.push_str(" }\n\n");
2330
2331 out.push_str(&format!(
2333 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2334 ));
2335 out.push_str(" {\n");
2336 out.push_str(" var str = value switch\n");
2337 out.push_str(" {\n");
2338 for (json_name, pascal_name) in &variants {
2339 out.push_str(&format!(
2340 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
2341 ));
2342 }
2343 out.push_str(&format!(
2344 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
2345 ));
2346 out.push_str(" };\n");
2347 out.push_str(" writer.WriteStringValue(str);\n");
2348 out.push_str(" }\n");
2349 out.push_str("}\n");
2350 }
2351
2352 out
2353}
2354
2355fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
2362 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
2363 let enum_pascal = enum_def.name.to_pascal_case();
2364 let converter_name = format!("{enum_pascal}JsonConverter");
2365 let ns = namespace;
2368
2369 let mut out = csharp_file_header();
2370 out.push_str("using System;\n");
2371 out.push_str("using System.Collections.Generic;\n");
2372 out.push_str("using System.Text.Json;\n");
2373 out.push_str("using System.Text.Json.Serialization;\n\n");
2374 out.push_str(&format!("namespace {};\n\n", namespace));
2375
2376 if !enum_def.doc.is_empty() {
2378 out.push_str("/// <summary>\n");
2379 for line in enum_def.doc.lines() {
2380 out.push_str(&format!("/// {}\n", line));
2381 }
2382 out.push_str("/// </summary>\n");
2383 }
2384
2385 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
2387 out.push_str(&format!("public abstract record {enum_pascal}\n"));
2388 out.push_str("{\n");
2389
2390 let variant_names: std::collections::HashSet<String> =
2392 enum_def.variants.iter().map(|v| v.name.to_pascal_case()).collect();
2393
2394 for variant in &enum_def.variants {
2396 let pascal = variant.name.to_pascal_case();
2397
2398 if !variant.doc.is_empty() {
2399 out.push_str(" /// <summary>\n");
2400 for line in variant.doc.lines() {
2401 out.push_str(&format!(" /// {}\n", line));
2402 }
2403 out.push_str(" /// </summary>\n");
2404 }
2405
2406 if variant.fields.is_empty() {
2407 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
2409 } else {
2410 let is_copy_ctor_clash = variant.fields.len() == 1 && {
2415 let field_cs_type = csharp_type(&variant.fields[0].ty);
2416 field_cs_type.as_ref() == pascal
2417 };
2418
2419 if is_copy_ctor_clash {
2420 let cs_type = csharp_type(&variant.fields[0].ty);
2421 let qualified_cs_type = format!("global::{ns}.{cs_type}");
2425 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
2426 out.push_str(" {\n");
2427 out.push_str(&format!(
2428 " public required {qualified_cs_type} Value {{ get; init; }}\n"
2429 ));
2430 out.push_str(" }\n\n");
2431 } else {
2432 out.push_str(&format!(" public sealed record {pascal}(\n"));
2434 for (i, field) in variant.fields.iter().enumerate() {
2435 let cs_type = csharp_type(&field.ty);
2436 let cs_type = if field.optional && !cs_type.ends_with('?') {
2437 format!("{cs_type}?")
2438 } else {
2439 cs_type.to_string()
2440 };
2441 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
2442 if is_tuple_field(field) {
2443 out.push_str(&format!(" {cs_type} Value{comma}\n"));
2444 } else {
2445 let json_name = field.name.trim_start_matches('_');
2446 let cs_name = to_csharp_name(json_name);
2447 let clashes = cs_name == pascal || cs_name == cs_type || variant_names.contains(&cs_name);
2452 if clashes {
2453 out.push_str(&format!(
2455 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} Value{comma}\n"
2456 ));
2457 } else {
2458 out.push_str(&format!(
2459 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
2460 ));
2461 }
2462 }
2463 }
2464 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
2465 }
2466 }
2467 }
2468
2469 out.push_str("}\n\n");
2470
2471 out.push_str(&format!(
2473 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
2474 ));
2475 out.push_str(&format!(
2476 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
2477 ));
2478 out.push_str("{\n");
2479
2480 out.push_str(&format!(
2482 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
2483 ));
2484 out.push_str(" {\n");
2485 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
2486 out.push_str(" var root = doc.RootElement;\n");
2487 out.push_str(&format!(
2488 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
2489 ));
2490 out.push_str(&format!(
2491 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
2492 ));
2493 out.push_str(" var tag = tagEl.GetString();\n");
2494 out.push_str(" var json = root.GetRawText();\n");
2495 out.push_str(" return tag switch\n");
2496 out.push_str(" {\n");
2497
2498 for variant in &enum_def.variants {
2499 let discriminator = variant
2500 .serde_rename
2501 .clone()
2502 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2503 let pascal = variant.name.to_pascal_case();
2504 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2509 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2510 let f = &variant.fields[0];
2511 let cs_type = csharp_type(&f.ty);
2512 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2513 cs_name == pascal || cs_name == cs_type
2514 };
2515 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2516 if is_newtype {
2517 let inner_cs_type = csharp_type(&variant.fields[0].ty);
2518 if inner_cs_type == pascal {
2521 out.push_str(&format!(
2522 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2523 ));
2524 out.push_str(&format!(
2525 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
2526 ));
2527 } else {
2528 out.push_str(&format!(
2529 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
2530 ));
2531 out.push_str(&format!(
2532 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2533 ));
2534 out.push_str(&format!(
2535 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
2536 ));
2537 }
2538 } else {
2539 out.push_str(&format!(
2540 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
2541 ));
2542 out.push_str(&format!(
2543 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
2544 ));
2545 }
2546 }
2547
2548 out.push_str(&format!(
2549 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
2550 ));
2551 out.push_str(" };\n");
2552 out.push_str(" }\n\n");
2553
2554 out.push_str(&format!(
2556 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2557 ));
2558 out.push_str(" {\n");
2559
2560 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
2562 out.push_str(" switch (value)\n");
2563 out.push_str(" {\n");
2564
2565 for variant in &enum_def.variants {
2566 let discriminator = variant
2567 .serde_rename
2568 .clone()
2569 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2570 let pascal = variant.name.to_pascal_case();
2571 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2575 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2576 let f = &variant.fields[0];
2577 let cs_type = csharp_type(&f.ty);
2578 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2579 cs_name == pascal || cs_name == cs_type
2580 };
2581 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2582 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
2586 out.push_str(" {\n");
2587 if is_newtype {
2588 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
2589 } else {
2590 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
2591 }
2592 out.push_str(" writer.WriteStartObject();\n");
2593 out.push_str(&format!(
2594 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
2595 ));
2596 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2597 out.push_str(&format!(
2598 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2599 ));
2600 out.push_str(" writer.WriteEndObject();\n");
2601 out.push_str(" break;\n");
2602 out.push_str(" }\n");
2603 }
2604
2605 out.push_str(&format!(
2606 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2607 ));
2608 out.push_str(" }\n");
2609 out.push_str(" }\n");
2610 out.push_str("}\n");
2611
2612 out
2613}
2614
2615fn gen_directory_build_props() -> String {
2618 "<!-- auto-generated by alef (generate_bindings) -->\n\
2619<Project>\n \
2620<PropertyGroup>\n \
2621<Nullable>enable</Nullable>\n \
2622<LangVersion>latest</LangVersion>\n \
2623</PropertyGroup>\n\
2624</Project>\n"
2625 .to_string()
2626}
2627
2628fn delete_stale_visitor_files(base_path: &std::path::Path) -> anyhow::Result<()> {
2632 let stale_files = vec!["IVisitor.cs", "VisitorCallbacks.cs", "NodeContext.cs", "VisitResult.cs"];
2633
2634 for filename in stale_files {
2635 let path = base_path.join(filename);
2636 if path.exists() {
2637 std::fs::remove_file(&path)
2638 .map_err(|e| anyhow::anyhow!("Failed to delete stale visitor file {}: {}", path.display(), e))?;
2639 }
2640 }
2641
2642 Ok(())
2643}