1use crate::type_map::csharp_type;
2use alef_codegen::naming::to_csharp_name;
3use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
4use alef_core::config::{AdapterPattern, AlefConfig, Language, resolve_output_dir};
5use alef_core::ir::{ApiSurface, EnumDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
6use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
7use std::collections::HashSet;
8use std::path::PathBuf;
9
10pub struct CsharpBackend;
11
12impl CsharpBackend {
13 }
15
16impl Backend for CsharpBackend {
17 fn name(&self) -> &str {
18 "csharp"
19 }
20
21 fn language(&self) -> Language {
22 Language::Csharp
23 }
24
25 fn capabilities(&self) -> Capabilities {
26 Capabilities {
27 supports_async: true,
28 supports_classes: true,
29 supports_enums: true,
30 supports_option: true,
31 supports_result: true,
32 ..Capabilities::default()
33 }
34 }
35
36 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
37 let namespace = config.csharp_namespace();
38 let prefix = config.ffi_prefix();
39 let lib_name = config.ffi_lib_name();
40
41 let bridge_param_names: HashSet<String> = config
44 .trait_bridges
45 .iter()
46 .filter_map(|b| b.param_name.clone())
47 .collect();
48 let bridge_type_aliases: HashSet<String> = config
49 .trait_bridges
50 .iter()
51 .filter_map(|b| b.type_alias.clone())
52 .collect();
53 let has_visitor_bridge = !config.trait_bridges.is_empty();
54
55 let streaming_methods: HashSet<String> = config
58 .adapters
59 .iter()
60 .filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
61 .map(|a| a.name.clone())
62 .collect();
63
64 let output_dir = resolve_output_dir(
65 config.output.csharp.as_ref(),
66 &config.crate_config.name,
67 "packages/csharp/",
68 );
69
70 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
71
72 let mut files = Vec::new();
73
74 files.push(GeneratedFile {
76 path: base_path.join("NativeMethods.cs"),
77 content: strip_trailing_whitespace(&gen_native_methods(
78 api,
79 &namespace,
80 &lib_name,
81 &prefix,
82 &bridge_param_names,
83 &bridge_type_aliases,
84 has_visitor_bridge,
85 &config.trait_bridges,
86 &streaming_methods,
87 )),
88 generated_header: true,
89 });
90
91 if !api.errors.is_empty() {
93 for error in &api.errors {
94 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
95 for (class_name, content) in error_files {
96 files.push(GeneratedFile {
97 path: base_path.join(format!("{}.cs", class_name)),
98 content: strip_trailing_whitespace(&content),
99 generated_header: false, });
101 }
102 }
103 }
104
105 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
107 if api.errors.is_empty()
108 || !api
109 .errors
110 .iter()
111 .any(|e| format!("{}Exception", e.name) == exception_class_name)
112 {
113 files.push(GeneratedFile {
114 path: base_path.join(format!("{}.cs", exception_class_name)),
115 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
116 generated_header: true,
117 });
118 }
119
120 let base_class_name = api.crate_name.to_pascal_case();
122 let wrapper_class_name = if namespace == base_class_name {
123 format!("{}Lib", base_class_name)
124 } else {
125 base_class_name
126 };
127 files.push(GeneratedFile {
128 path: base_path.join(format!("{}.cs", wrapper_class_name)),
129 content: strip_trailing_whitespace(&gen_wrapper_class(
130 api,
131 &namespace,
132 &wrapper_class_name,
133 &exception_class_name,
134 &prefix,
135 &bridge_param_names,
136 &bridge_type_aliases,
137 has_visitor_bridge,
138 &streaming_methods,
139 )),
140 generated_header: true,
141 });
142
143 if has_visitor_bridge {
145 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
146 files.push(GeneratedFile {
147 path: base_path.join(filename),
148 content: strip_trailing_whitespace(&content),
149 generated_header: true,
150 });
151 }
152 }
153
154 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
156 if typ.is_opaque {
157 let type_filename = typ.name.to_pascal_case();
158 files.push(GeneratedFile {
159 path: base_path.join(format!("{}.cs", type_filename)),
160 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
161 generated_header: true,
162 });
163 }
164 }
165
166 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
168
169 let complex_enums: HashSet<String> = api
174 .enums
175 .iter()
176 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
177 .map(|e| e.name.to_pascal_case())
178 .collect();
179
180 let custom_converter_enums: HashSet<String> = api
184 .enums
185 .iter()
186 .filter(|e| {
187 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
189 || e.variants.iter().any(|v| {
191 if let Some(ref rename) = v.serde_rename {
192 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
193 rename != &snake
194 } else {
195 false
196 }
197 })
198 })
199 .map(|e| e.name.to_pascal_case())
200 .collect();
201
202 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
204
205 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
207 if !typ.is_opaque {
208 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
211 if !typ.fields.is_empty() && !has_named_fields {
212 continue;
213 }
214 if has_visitor_bridge && (typ.name == "NodeContext" || typ.name == "VisitResult") {
216 continue;
217 }
218
219 let type_filename = typ.name.to_pascal_case();
220 files.push(GeneratedFile {
221 path: base_path.join(format!("{}.cs", type_filename)),
222 content: strip_trailing_whitespace(&gen_record_type(
223 typ,
224 &namespace,
225 &enum_names,
226 &complex_enums,
227 &custom_converter_enums,
228 &lang_rename_all,
229 )),
230 generated_header: true,
231 });
232 }
233 }
234
235 for enum_def in &api.enums {
237 if has_visitor_bridge && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
239 continue;
240 }
241 let enum_filename = enum_def.name.to_pascal_case();
242 files.push(GeneratedFile {
243 path: base_path.join(format!("{}.cs", enum_filename)),
244 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
245 generated_header: true,
246 });
247 }
248
249 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
251
252 Ok(files)
253 }
254
255 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
260 Ok(vec![])
262 }
263
264 fn build_config(&self) -> Option<BuildConfig> {
265 Some(BuildConfig {
266 tool: "dotnet",
267 crate_suffix: "",
268 depends_on_ffi: true,
269 post_build: vec![],
270 })
271 }
272}
273
274fn is_tuple_field(field: &FieldDef) -> bool {
276 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
277 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
278}
279
280fn strip_trailing_whitespace(content: &str) -> String {
282 let mut result: String = content
283 .lines()
284 .map(|line| line.trim_end())
285 .collect::<Vec<_>>()
286 .join("\n");
287 if !result.ends_with('\n') {
288 result.push('\n');
289 }
290 result
291}
292
293fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
304 match ty {
305 TypeRef::Unit => "void",
306 TypeRef::Primitive(PrimitiveType::Bool) => "int",
308 TypeRef::Primitive(PrimitiveType::U8) => "byte",
310 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
311 TypeRef::Primitive(PrimitiveType::U32) => "uint",
312 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
313 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
314 TypeRef::Primitive(PrimitiveType::I16) => "short",
315 TypeRef::Primitive(PrimitiveType::I32) => "int",
316 TypeRef::Primitive(PrimitiveType::I64) => "long",
317 TypeRef::Primitive(PrimitiveType::F32) => "float",
318 TypeRef::Primitive(PrimitiveType::F64) => "double",
319 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
320 TypeRef::Primitive(PrimitiveType::Isize) => "long",
321 TypeRef::Duration => "ulong",
323 TypeRef::String
325 | TypeRef::Char
326 | TypeRef::Bytes
327 | TypeRef::Optional(_)
328 | TypeRef::Vec(_)
329 | TypeRef::Map(_, _)
330 | TypeRef::Named(_)
331 | TypeRef::Path
332 | TypeRef::Json => "IntPtr",
333 }
334}
335
336fn returns_string(ty: &TypeRef) -> bool {
338 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
339}
340
341fn returns_bool_via_int(ty: &TypeRef) -> bool {
343 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
344}
345
346fn returns_json_object(ty: &TypeRef) -> bool {
348 matches!(
349 ty,
350 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
351 )
352}
353
354fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
365 match ty {
366 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
367 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
369 TypeRef::Unit => "void",
370 TypeRef::Primitive(PrimitiveType::Bool) => "int",
371 TypeRef::Primitive(PrimitiveType::U8) => "byte",
372 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
373 TypeRef::Primitive(PrimitiveType::U32) => "uint",
374 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
375 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
376 TypeRef::Primitive(PrimitiveType::I16) => "short",
377 TypeRef::Primitive(PrimitiveType::I32) => "int",
378 TypeRef::Primitive(PrimitiveType::I64) => "long",
379 TypeRef::Primitive(PrimitiveType::F32) => "float",
380 TypeRef::Primitive(PrimitiveType::F64) => "double",
381 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
382 TypeRef::Primitive(PrimitiveType::Isize) => "long",
383 TypeRef::Duration => "ulong",
384 }
385}
386
387fn is_bridge_param(
394 param: &alef_core::ir::ParamDef,
395 bridge_param_names: &HashSet<String>,
396 bridge_type_aliases: &HashSet<String>,
397) -> bool {
398 bridge_param_names.contains(¶m.name)
399 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
400}
401
402#[allow(clippy::too_many_arguments)]
403fn gen_native_methods(
404 api: &ApiSurface,
405 namespace: &str,
406 lib_name: &str,
407 prefix: &str,
408 bridge_param_names: &HashSet<String>,
409 bridge_type_aliases: &HashSet<String>,
410 has_visitor_bridge: bool,
411 trait_bridges: &[alef_core::config::TraitBridgeConfig],
412 streaming_methods: &HashSet<String>,
413) -> String {
414 let mut out = String::from(
415 "// This file is auto-generated by alef. DO NOT EDIT.\n\
416 using System;\n\
417 using System.Runtime.InteropServices;\n\n",
418 );
419
420 out.push_str(&format!("namespace {};\n\n", namespace));
421
422 out.push_str("internal static partial class NativeMethods\n{\n");
423 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
424
425 let mut emitted: HashSet<String> = HashSet::new();
428
429 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
432
433 let mut opaque_param_types: HashSet<String> = HashSet::new();
437 let mut opaque_return_types: HashSet<String> = HashSet::new();
438
439 for func in &api.functions {
440 for param in &func.params {
441 if let TypeRef::Named(name) = ¶m.ty {
442 if !enum_names.contains(name) {
443 opaque_param_types.insert(name.clone());
444 }
445 }
446 }
447 if let TypeRef::Named(name) = &func.return_type {
448 if !enum_names.contains(name) {
449 opaque_return_types.insert(name.clone());
450 }
451 }
452 }
453 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
454 for method in &typ.methods {
455 if streaming_methods.contains(&method.name) {
456 continue;
457 }
458 for param in &method.params {
459 if let TypeRef::Named(name) = ¶m.ty {
460 if !enum_names.contains(name) {
461 opaque_param_types.insert(name.clone());
462 }
463 }
464 }
465 if let TypeRef::Named(name) = &method.return_type {
466 if !enum_names.contains(name) {
467 opaque_return_types.insert(name.clone());
468 }
469 }
470 }
471 }
472
473 let true_opaque_types: HashSet<String> = api
475 .types
476 .iter()
477 .filter(|t| t.is_opaque)
478 .map(|t| t.name.clone())
479 .collect();
480
481 let mut sorted_param_types: Vec<&String> = opaque_param_types.iter().collect();
485 sorted_param_types.sort();
486 for type_name in sorted_param_types {
487 let snake = type_name.to_snake_case();
488 if !true_opaque_types.contains(type_name) {
489 let from_json_entry = format!("{prefix}_{snake}_from_json");
490 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
491 if emitted.insert(from_json_entry.clone()) {
492 out.push_str(&format!(
493 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
494 ));
495 out.push_str(&format!(
496 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
497 ));
498 }
499 }
500 let free_entry = format!("{prefix}_{snake}_free");
501 let free_cs = format!("{}Free", type_name.to_pascal_case());
502 if emitted.insert(free_entry.clone()) {
503 out.push_str(&format!(
504 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
505 ));
506 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
507 }
508 }
509
510 let mut sorted_return_types: Vec<&String> = opaque_return_types.iter().collect();
513 sorted_return_types.sort();
514 for type_name in sorted_return_types {
515 let snake = type_name.to_snake_case();
516 if !true_opaque_types.contains(type_name) {
517 let to_json_entry = format!("{prefix}_{snake}_to_json");
518 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
519 if emitted.insert(to_json_entry.clone()) {
520 out.push_str(&format!(
521 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
522 ));
523 out.push_str(&format!(
524 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
525 ));
526 }
527 }
528 let free_entry = format!("{prefix}_{snake}_free");
529 let free_cs = format!("{}Free", type_name.to_pascal_case());
530 if emitted.insert(free_entry.clone()) {
531 out.push_str(&format!(
532 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
533 ));
534 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
535 }
536 }
537
538 for func in &api.functions {
540 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
541 if emitted.insert(c_func_name.clone()) {
542 out.push_str(&gen_pinvoke_for_func(
543 &c_func_name,
544 func,
545 bridge_param_names,
546 bridge_type_aliases,
547 ));
548 }
549 }
550
551 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
554 let type_snake = typ.name.to_snake_case();
555 for method in &typ.methods {
556 if streaming_methods.contains(&method.name) {
557 continue;
558 }
559 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
560 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
564 if emitted.insert(c_method_name.clone()) {
565 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
566 }
567 }
568 }
569
570 out.push_str(&format!(
572 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
573 ));
574 out.push_str(" internal static extern int LastErrorCode();\n\n");
575
576 out.push_str(&format!(
577 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
578 ));
579 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
580
581 out.push_str(&format!(
582 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
583 ));
584 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
585
586 if has_visitor_bridge {
588 out.push('\n');
589 out.push_str(&crate::gen_visitor::gen_native_methods_visitor(
590 namespace, lib_name, prefix,
591 ));
592 }
593
594 if !trait_bridges.is_empty() {
596 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
598
599 let bridges: Vec<_> = trait_bridges
601 .iter()
602 .filter_map(|config| {
603 let trait_name = config.trait_name.clone();
604 trait_defs
605 .iter()
606 .find(|t| t.name == trait_name)
607 .map(|trait_def| (trait_name, config, *trait_def))
608 })
609 .collect();
610
611 if !bridges.is_empty() {
612 out.push('\n');
613 out.push_str(&crate::trait_bridge::gen_native_methods_trait_bridges(
614 namespace, prefix, &bridges,
615 ));
616 }
617 }
618
619 out.push_str("}\n");
620
621 out
622}
623
624fn gen_pinvoke_for_func(
625 c_name: &str,
626 func: &FunctionDef,
627 bridge_param_names: &HashSet<String>,
628 bridge_type_aliases: &HashSet<String>,
629) -> String {
630 let cs_name = to_csharp_name(&func.name);
631 let mut out =
632 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
633 out.push_str(" internal static extern ");
634
635 out.push_str(pinvoke_return_type(&func.return_type));
637
638 out.push_str(&format!(" {}(", cs_name));
639
640 let visible_params: Vec<_> = func
643 .params
644 .iter()
645 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
646 .collect();
647
648 if visible_params.is_empty() {
649 out.push_str(");\n\n");
650 } else {
651 out.push('\n');
652 for (i, param) in visible_params.iter().enumerate() {
653 out.push_str(" ");
654 let pinvoke_ty = pinvoke_param_type(¶m.ty);
655 if pinvoke_ty == "string" {
656 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
657 }
658 let param_name = param.name.to_lower_camel_case();
659 out.push_str(&format!("{pinvoke_ty} {param_name}"));
660
661 if i < visible_params.len() - 1 {
662 out.push(',');
663 }
664 out.push('\n');
665 }
666 out.push_str(" );\n\n");
667 }
668
669 out
670}
671
672fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
673 let mut out =
674 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
675 out.push_str(" internal static extern ");
676
677 out.push_str(pinvoke_return_type(&method.return_type));
679
680 out.push_str(&format!(" {}(", cs_name));
681
682 if method.params.is_empty() {
683 out.push_str(");\n\n");
684 } else {
685 out.push('\n');
686 for (i, param) in method.params.iter().enumerate() {
687 out.push_str(" ");
688 let pinvoke_ty = pinvoke_param_type(¶m.ty);
689 if pinvoke_ty == "string" {
690 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
691 }
692 let param_name = param.name.to_lower_camel_case();
693 out.push_str(&format!("{pinvoke_ty} {param_name}"));
694
695 if i < method.params.len() - 1 {
696 out.push(',');
697 }
698 out.push('\n');
699 }
700 out.push_str(" );\n\n");
701 }
702
703 out
704}
705
706fn gen_exception_class(namespace: &str, class_name: &str) -> String {
707 let mut out = String::from(
708 "// This file is auto-generated by alef. DO NOT EDIT.\n\
709 using System;\n\n",
710 );
711
712 out.push_str(&format!("namespace {};\n\n", namespace));
713
714 out.push_str(&format!("public class {} : Exception\n", class_name));
715 out.push_str("{\n");
716 out.push_str(" public int Code { get; }\n\n");
717 out.push_str(&format!(
718 " public {}(int code, string message) : base(message)\n",
719 class_name
720 ));
721 out.push_str(" {\n");
722 out.push_str(" Code = code;\n");
723 out.push_str(" }\n");
724 out.push_str("}\n");
725
726 out
727}
728
729#[allow(clippy::too_many_arguments)]
730fn gen_wrapper_class(
731 api: &ApiSurface,
732 namespace: &str,
733 class_name: &str,
734 exception_name: &str,
735 prefix: &str,
736 bridge_param_names: &HashSet<String>,
737 bridge_type_aliases: &HashSet<String>,
738 has_visitor_bridge: bool,
739 streaming_methods: &HashSet<String>,
740) -> String {
741 let mut out = String::from(
742 "// This file is auto-generated by alef. DO NOT EDIT.\n\
743 using System;\n\
744 using System.Collections.Generic;\n\
745 using System.Runtime.InteropServices;\n\
746 using System.Text.Json;\n\
747 using System.Text.Json.Serialization;\n\
748 using System.Threading.Tasks;\n\n",
749 );
750
751 out.push_str(&format!("namespace {};\n\n", namespace));
752
753 out.push_str(&format!("public static class {}\n", class_name));
754 out.push_str("{\n");
755 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
756 out.push_str(" {\n");
757 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
758 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
759 out.push_str(" };\n\n");
760
761 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
763
764 let true_opaque_types: HashSet<String> = api
766 .types
767 .iter()
768 .filter(|t| t.is_opaque)
769 .map(|t| t.name.clone())
770 .collect();
771
772 for func in &api.functions {
774 out.push_str(&gen_wrapper_function(
775 func,
776 exception_name,
777 prefix,
778 &enum_names,
779 &true_opaque_types,
780 bridge_param_names,
781 bridge_type_aliases,
782 ));
783 }
784
785 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
788 if typ.is_opaque {
790 continue;
791 }
792 for method in &typ.methods {
793 if streaming_methods.contains(&method.name) {
794 continue;
795 }
796 out.push_str(&gen_wrapper_method(
797 method,
798 exception_name,
799 prefix,
800 &typ.name,
801 &enum_names,
802 &true_opaque_types,
803 bridge_param_names,
804 bridge_type_aliases,
805 ));
806 }
807 }
808
809 if has_visitor_bridge {
811 out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
812 exception_name,
813 prefix,
814 ));
815 }
816
817 out.push_str(" private static ");
819 out.push_str(&format!("{} GetLastError()\n", exception_name));
820 out.push_str(" {\n");
821 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
822 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
823 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
824 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
825 out.push_str(" }\n");
826
827 out.push_str("}\n");
828
829 out
830}
831
832fn emit_named_param_setup(
849 out: &mut String,
850 params: &[alef_core::ir::ParamDef],
851 indent: &str,
852 true_opaque_types: &HashSet<String>,
853) {
854 for param in params {
855 let param_name = param.name.to_lower_camel_case();
856 let json_var = format!("{param_name}Json");
857 let handle_var = format!("{param_name}Handle");
858
859 match ¶m.ty {
860 TypeRef::Named(type_name) => {
861 if true_opaque_types.contains(type_name) {
864 continue;
865 }
866 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
867 if param.optional {
868 out.push_str(&format!(
869 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
870 ));
871 } else {
872 out.push_str(&format!(
873 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
874 ));
875 }
876 out.push_str(&format!(
877 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
878 ));
879 }
880 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
881 out.push_str(&format!(
883 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
884 ));
885 out.push_str(&format!(
886 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
887 ));
888 }
889 _ => {}
890 }
891 }
892}
893
894fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
900 match ty {
901 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
902 let bang = if optional { "!" } else { "" };
904 format!("{param_name}{bang}.Handle")
905 }
906 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
907 format!("{param_name}Handle")
908 }
909 ty => {
910 if optional {
911 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
916 if needs_value_unwrap {
917 format!("{param_name}.Value")
918 } else {
919 format!("{param_name}!")
920 }
921 } else {
922 param_name.to_string()
923 }
924 }
925 }
926}
927
928fn emit_named_param_teardown(
933 out: &mut String,
934 params: &[alef_core::ir::ParamDef],
935 true_opaque_types: &HashSet<String>,
936) {
937 for param in params {
938 let param_name = param.name.to_lower_camel_case();
939 let handle_var = format!("{param_name}Handle");
940 match ¶m.ty {
941 TypeRef::Named(type_name) => {
942 if true_opaque_types.contains(type_name) {
943 continue;
945 }
946 let free_method = format!("{}Free", type_name.to_pascal_case());
947 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
948 }
949 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
950 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
951 }
952 _ => {}
953 }
954 }
955}
956
957fn emit_named_param_teardown_indented(
959 out: &mut String,
960 params: &[alef_core::ir::ParamDef],
961 indent: &str,
962 true_opaque_types: &HashSet<String>,
963) {
964 for param in params {
965 let param_name = param.name.to_lower_camel_case();
966 let handle_var = format!("{param_name}Handle");
967 match ¶m.ty {
968 TypeRef::Named(type_name) => {
969 if true_opaque_types.contains(type_name) {
970 continue;
972 }
973 let free_method = format!("{}Free", type_name.to_pascal_case());
974 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
975 }
976 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
977 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
978 }
979 _ => {}
980 }
981 }
982}
983
984fn gen_wrapper_function(
985 func: &FunctionDef,
986 _exception_name: &str,
987 _prefix: &str,
988 enum_names: &HashSet<String>,
989 true_opaque_types: &HashSet<String>,
990 bridge_param_names: &HashSet<String>,
991 bridge_type_aliases: &HashSet<String>,
992) -> String {
993 let mut out = String::with_capacity(1024);
994
995 let visible_params: Vec<alef_core::ir::ParamDef> = func
997 .params
998 .iter()
999 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1000 .cloned()
1001 .collect();
1002
1003 if !func.doc.is_empty() {
1005 out.push_str(" /// <summary>\n");
1006 for line in func.doc.lines() {
1007 out.push_str(&format!(" /// {}\n", line));
1008 }
1009 out.push_str(" /// </summary>\n");
1010 for param in &visible_params {
1011 out.push_str(&format!(
1012 " /// <param name=\"{}\">{}</param>\n",
1013 param.name.to_lower_camel_case(),
1014 if param.optional { "Optional." } else { "" }
1015 ));
1016 }
1017 }
1018
1019 out.push_str(" public static ");
1020
1021 if func.is_async {
1023 if func.return_type == TypeRef::Unit {
1024 out.push_str("async Task");
1025 } else {
1026 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
1027 }
1028 } else if func.return_type == TypeRef::Unit {
1029 out.push_str("void");
1030 } else {
1031 out.push_str(&csharp_type(&func.return_type));
1032 }
1033
1034 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
1035 out.push('(');
1036
1037 for (i, param) in visible_params.iter().enumerate() {
1039 let param_name = param.name.to_lower_camel_case();
1040 let mapped = csharp_type(¶m.ty);
1041 if param.optional && !mapped.ends_with('?') {
1042 out.push_str(&format!("{mapped}? {param_name}"));
1043 } else {
1044 out.push_str(&format!("{mapped} {param_name}"));
1045 }
1046
1047 if i < visible_params.len() - 1 {
1048 out.push_str(", ");
1049 }
1050 }
1051
1052 out.push_str(")\n {\n");
1053
1054 for param in &visible_params {
1056 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1057 let param_name = param.name.to_lower_camel_case();
1058 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1059 }
1060 }
1061
1062 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1064
1065 let cs_native_name = to_csharp_name(&func.name);
1067
1068 if func.is_async {
1069 out.push_str(" return await Task.Run(() =>\n {\n");
1071
1072 if func.return_type != TypeRef::Unit {
1073 out.push_str(" var result = ");
1074 } else {
1075 out.push_str(" ");
1076 }
1077
1078 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1079
1080 if visible_params.is_empty() {
1081 out.push_str(");\n");
1082 } else {
1083 out.push('\n');
1084 for (i, param) in visible_params.iter().enumerate() {
1085 let param_name = param.name.to_lower_camel_case();
1086 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1087 out.push_str(&format!(" {arg}"));
1088 if i < visible_params.len() - 1 {
1089 out.push(',');
1090 }
1091 out.push('\n');
1092 }
1093 out.push_str(" );\n");
1094 }
1095
1096 if func.return_type != TypeRef::Unit {
1098 out.push_str(
1099 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
1100 );
1101 }
1102
1103 emit_return_marshalling_indented(
1104 &mut out,
1105 &func.return_type,
1106 " ",
1107 enum_names,
1108 true_opaque_types,
1109 );
1110 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1111 emit_return_statement_indented(&mut out, &func.return_type, " ");
1112 out.push_str(" });\n");
1113 } else {
1114 if func.return_type != TypeRef::Unit {
1115 out.push_str(" var result = ");
1116 } else {
1117 out.push_str(" ");
1118 }
1119
1120 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1121
1122 if visible_params.is_empty() {
1123 out.push_str(");\n");
1124 } else {
1125 out.push('\n');
1126 for (i, param) in visible_params.iter().enumerate() {
1127 let param_name = param.name.to_lower_camel_case();
1128 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1129 out.push_str(&format!(" {arg}"));
1130 if i < visible_params.len() - 1 {
1131 out.push(',');
1132 }
1133 out.push('\n');
1134 }
1135 out.push_str(" );\n");
1136 }
1137
1138 if func.return_type != TypeRef::Unit {
1140 out.push_str(
1141 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
1142 );
1143 }
1144
1145 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
1146 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1147 emit_return_statement(&mut out, &func.return_type);
1148 }
1149
1150 out.push_str(" }\n\n");
1151
1152 out
1153}
1154
1155#[allow(clippy::too_many_arguments)]
1156fn gen_wrapper_method(
1157 method: &MethodDef,
1158 _exception_name: &str,
1159 _prefix: &str,
1160 type_name: &str,
1161 enum_names: &HashSet<String>,
1162 true_opaque_types: &HashSet<String>,
1163 bridge_param_names: &HashSet<String>,
1164 bridge_type_aliases: &HashSet<String>,
1165) -> String {
1166 let mut out = String::with_capacity(1024);
1167
1168 let visible_params: Vec<alef_core::ir::ParamDef> = method
1170 .params
1171 .iter()
1172 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1173 .cloned()
1174 .collect();
1175
1176 if !method.doc.is_empty() {
1178 out.push_str(" /// <summary>\n");
1179 for line in method.doc.lines() {
1180 out.push_str(&format!(" /// {}\n", line));
1181 }
1182 out.push_str(" /// </summary>\n");
1183 for param in &visible_params {
1184 out.push_str(&format!(
1185 " /// <param name=\"{}\">{}</param>\n",
1186 param.name.to_lower_camel_case(),
1187 if param.optional { "Optional." } else { "" }
1188 ));
1189 }
1190 }
1191
1192 out.push_str(" public static ");
1194
1195 if method.is_async {
1197 if method.return_type == TypeRef::Unit {
1198 out.push_str("async Task");
1199 } else {
1200 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1201 }
1202 } else if method.return_type == TypeRef::Unit {
1203 out.push_str("void");
1204 } else {
1205 out.push_str(&csharp_type(&method.return_type));
1206 }
1207
1208 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1210 out.push_str(&format!(" {method_cs_name}"));
1211 out.push('(');
1212
1213 for (i, param) in visible_params.iter().enumerate() {
1215 let param_name = param.name.to_lower_camel_case();
1216 let mapped = csharp_type(¶m.ty);
1217 if param.optional && !mapped.ends_with('?') {
1218 out.push_str(&format!("{mapped}? {param_name}"));
1219 } else {
1220 out.push_str(&format!("{mapped} {param_name}"));
1221 }
1222
1223 if i < visible_params.len() - 1 {
1224 out.push_str(", ");
1225 }
1226 }
1227
1228 out.push_str(")\n {\n");
1229
1230 for param in &visible_params {
1232 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1233 let param_name = param.name.to_lower_camel_case();
1234 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1235 }
1236 }
1237
1238 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1240
1241 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1246
1247 if method.is_async {
1248 out.push_str(" return await Task.Run(() =>\n {\n");
1250
1251 if method.return_type != TypeRef::Unit {
1252 out.push_str(" var result = ");
1253 } else {
1254 out.push_str(" ");
1255 }
1256
1257 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1258
1259 if visible_params.is_empty() {
1260 out.push_str(");\n");
1261 } else {
1262 out.push('\n');
1263 for (i, param) in visible_params.iter().enumerate() {
1264 let param_name = param.name.to_lower_camel_case();
1265 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1266 out.push_str(&format!(" {arg}"));
1267 if i < visible_params.len() - 1 {
1268 out.push(',');
1269 }
1270 out.push('\n');
1271 }
1272 out.push_str(" );\n");
1273 }
1274
1275 emit_return_marshalling_indented(
1276 &mut out,
1277 &method.return_type,
1278 " ",
1279 enum_names,
1280 true_opaque_types,
1281 );
1282 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1283 emit_return_statement_indented(&mut out, &method.return_type, " ");
1284 out.push_str(" });\n");
1285 } else {
1286 if method.return_type != TypeRef::Unit {
1287 out.push_str(" var result = ");
1288 } else {
1289 out.push_str(" ");
1290 }
1291
1292 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1293
1294 if visible_params.is_empty() {
1295 out.push_str(");\n");
1296 } else {
1297 out.push('\n');
1298 for (i, param) in visible_params.iter().enumerate() {
1299 let param_name = param.name.to_lower_camel_case();
1300 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1301 out.push_str(&format!(" {arg}"));
1302 if i < visible_params.len() - 1 {
1303 out.push(',');
1304 }
1305 out.push('\n');
1306 }
1307 out.push_str(" );\n");
1308 }
1309
1310 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1311 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1312 emit_return_statement(&mut out, &method.return_type);
1313 }
1314
1315 out.push_str(" }\n\n");
1316
1317 out
1318}
1319
1320fn emit_return_marshalling(
1332 out: &mut String,
1333 return_type: &TypeRef,
1334 enum_names: &HashSet<String>,
1335 true_opaque_types: &HashSet<String>,
1336) {
1337 if *return_type == TypeRef::Unit {
1338 return;
1340 }
1341
1342 if returns_string(return_type) {
1343 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1345 out.push_str(" NativeMethods.FreeString(result);\n");
1346 } else if returns_bool_via_int(return_type) {
1347 out.push_str(" var returnValue = result != 0;\n");
1349 } else if let TypeRef::Named(type_name) = return_type {
1350 let pascal = type_name.to_pascal_case();
1351 if true_opaque_types.contains(type_name) {
1352 out.push_str(&format!(" var returnValue = new {pascal}(result);\n"));
1354 } else if !enum_names.contains(&pascal) {
1355 let to_json_method = format!("{pascal}ToJson");
1357 let free_method = format!("{pascal}Free");
1358 let cs_ty = csharp_type(return_type);
1359 out.push_str(&format!(
1360 " var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1361 ));
1362 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1363 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1364 out.push_str(&format!(" NativeMethods.{free_method}(result);\n"));
1365 out.push_str(&format!(
1366 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1367 cs_ty
1368 ));
1369 } else {
1370 let cs_ty = csharp_type(return_type);
1372 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1373 out.push_str(" NativeMethods.FreeString(result);\n");
1374 out.push_str(&format!(
1375 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1376 cs_ty
1377 ));
1378 }
1379 } else if returns_json_object(return_type) {
1380 let cs_ty = csharp_type(return_type);
1382 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1383 out.push_str(" NativeMethods.FreeString(result);\n");
1384 out.push_str(&format!(
1385 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1386 cs_ty
1387 ));
1388 } else {
1389 out.push_str(" var returnValue = result;\n");
1391 }
1392}
1393
1394fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1396 if *return_type != TypeRef::Unit {
1397 out.push_str(" return returnValue;\n");
1398 }
1399}
1400
1401fn emit_return_marshalling_indented(
1406 out: &mut String,
1407 return_type: &TypeRef,
1408 indent: &str,
1409 enum_names: &HashSet<String>,
1410 true_opaque_types: &HashSet<String>,
1411) {
1412 if *return_type == TypeRef::Unit {
1413 return;
1414 }
1415
1416 if returns_string(return_type) {
1417 out.push_str(&format!(
1418 "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1419 ));
1420 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1421 } else if returns_bool_via_int(return_type) {
1422 out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1423 } else if let TypeRef::Named(type_name) = return_type {
1424 let pascal = type_name.to_pascal_case();
1425 if true_opaque_types.contains(type_name) {
1426 out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1428 } else if !enum_names.contains(&pascal) {
1429 let to_json_method = format!("{pascal}ToJson");
1431 let free_method = format!("{pascal}Free");
1432 let cs_ty = csharp_type(return_type);
1433 out.push_str(&format!(
1434 "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1435 ));
1436 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1437 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1438 out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1439 out.push_str(&format!(
1440 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1441 cs_ty
1442 ));
1443 } else {
1444 let cs_ty = csharp_type(return_type);
1446 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1447 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1448 out.push_str(&format!(
1449 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1450 cs_ty
1451 ));
1452 }
1453 } else if returns_json_object(return_type) {
1454 let cs_ty = csharp_type(return_type);
1455 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1456 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1457 out.push_str(&format!(
1458 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1459 cs_ty
1460 ));
1461 } else {
1462 out.push_str(&format!("{indent}var returnValue = result;\n"));
1463 }
1464}
1465
1466fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1468 if *return_type != TypeRef::Unit {
1469 out.push_str(&format!("{indent}return returnValue;\n"));
1470 }
1471}
1472
1473fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1474 let mut out = String::from(
1475 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1476 using System;\n\n",
1477 );
1478
1479 out.push_str(&format!("namespace {};\n\n", namespace));
1480
1481 if !typ.doc.is_empty() {
1483 out.push_str("/// <summary>\n");
1484 for line in typ.doc.lines() {
1485 out.push_str(&format!("/// {}\n", line));
1486 }
1487 out.push_str("/// </summary>\n");
1488 }
1489
1490 let class_name = typ.name.to_pascal_case();
1491 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1492 out.push_str("{\n");
1493 out.push_str(" internal IntPtr Handle { get; }\n\n");
1494 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1495 out.push_str(" {\n");
1496 out.push_str(" Handle = handle;\n");
1497 out.push_str(" }\n\n");
1498 out.push_str(" public void Dispose()\n");
1499 out.push_str(" {\n");
1500 out.push_str(" // Native free will be called by the runtime\n");
1501 out.push_str(" }\n");
1502 out.push_str("}\n");
1503
1504 out
1505}
1506
1507fn gen_record_type(
1508 typ: &TypeDef,
1509 namespace: &str,
1510 enum_names: &HashSet<String>,
1511 complex_enums: &HashSet<String>,
1512 custom_converter_enums: &HashSet<String>,
1513 _lang_rename_all: &str,
1514) -> String {
1515 let mut out = String::from(
1516 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1517 using System;\n\
1518 using System.Collections.Generic;\n\
1519 using System.Text.Json;\n\
1520 using System.Text.Json.Serialization;\n\n",
1521 );
1522
1523 out.push_str(&format!("namespace {};\n\n", namespace));
1524
1525 if !typ.doc.is_empty() {
1527 out.push_str("/// <summary>\n");
1528 for line in typ.doc.lines() {
1529 out.push_str(&format!("/// {}\n", line));
1530 }
1531 out.push_str("/// </summary>\n");
1532 }
1533
1534 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1535 out.push_str("{\n");
1536
1537 for field in &typ.fields {
1538 if is_tuple_field(field) {
1540 continue;
1541 }
1542
1543 if !field.doc.is_empty() {
1545 out.push_str(" /// <summary>\n");
1546 for line in field.doc.lines() {
1547 out.push_str(&format!(" /// {}\n", line));
1548 }
1549 out.push_str(" /// </summary>\n");
1550 }
1551
1552 let field_base_type = match &field.ty {
1556 TypeRef::Named(n) => Some(n.to_pascal_case()),
1557 TypeRef::Optional(inner) => match inner.as_ref() {
1558 TypeRef::Named(n) => Some(n.to_pascal_case()),
1559 _ => None,
1560 },
1561 _ => None,
1562 };
1563 if let Some(ref base) = field_base_type {
1564 if custom_converter_enums.contains(base) {
1565 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1566 }
1567 }
1568
1569 let json_name = field.name.clone();
1573 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1574
1575 let cs_name = to_csharp_name(&field.name);
1576
1577 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1580
1581 if field.optional {
1582 let mapped = if is_complex {
1584 "JsonElement".to_string()
1585 } else {
1586 csharp_type(&field.ty).to_string()
1587 };
1588 let field_type = if mapped.ends_with('?') {
1589 mapped
1590 } else {
1591 format!("{mapped}?")
1592 };
1593 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1594 out.push_str(" = null;\n");
1595 } else if typ.has_default || field.default.is_some() {
1596 let field_type = if is_complex {
1599 "JsonElement".to_string()
1600 } else {
1601 csharp_type(&field.ty).to_string()
1602 };
1603 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1604 use alef_core::ir::DefaultValue;
1605 if matches!(&field.ty, TypeRef::Duration) {
1608 out.push_str(" = null;\n");
1609 out.push('\n');
1610 continue;
1611 }
1612 let default_val = match &field.typed_default {
1613 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1614 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1615 Some(DefaultValue::FloatLiteral(f)) => {
1616 let s = f.to_string();
1617 if s.contains('.') { s } else { format!("{s}.0") }
1618 }
1619 Some(DefaultValue::StringLiteral(s)) => format!("\"{}\"", s.replace('"', "\\\"")),
1620 Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1621 Some(DefaultValue::None) => "null".to_string(),
1622 Some(DefaultValue::Empty) | None => match &field.ty {
1623 TypeRef::Vec(_) => "[]".to_string(),
1624 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1625 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1626 TypeRef::Json => "null".to_string(),
1627 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1628 TypeRef::Primitive(p) => match p {
1629 PrimitiveType::Bool => "false".to_string(),
1630 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1631 _ => "0".to_string(),
1632 },
1633 TypeRef::Named(name) => {
1634 let pascal = name.to_pascal_case();
1635 if enum_names.contains(&pascal) {
1636 "default".to_string()
1637 } else {
1638 "default!".to_string()
1639 }
1640 }
1641 _ => "default!".to_string(),
1642 },
1643 };
1644 out.push_str(&format!(" = {};\n", default_val));
1645 } else {
1646 let field_type = if is_complex {
1650 "JsonElement".to_string()
1651 } else {
1652 csharp_type(&field.ty).to_string()
1653 };
1654 if matches!(&field.ty, TypeRef::Duration) {
1656 out.push_str(&format!(
1657 " public {} {} {{ get; set; }} = null;\n",
1658 field_type, cs_name
1659 ));
1660 } else {
1661 let default_val = match &field.ty {
1662 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1663 TypeRef::Vec(_) => "[]",
1664 TypeRef::Bytes => "Array.Empty<byte>()",
1665 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1666 TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1667 TypeRef::Primitive(_) => "0",
1668 _ => "default!",
1669 };
1670 out.push_str(&format!(
1671 " public {} {} {{ get; set; }} = {};\n",
1672 field_type, cs_name, default_val
1673 ));
1674 }
1675 }
1676
1677 out.push('\n');
1678 }
1679
1680 out.push_str("}\n");
1681
1682 out
1683}
1684
1685fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1687 match rename_all {
1688 Some("snake_case") => name.to_snake_case(),
1689 Some("camelCase") => name.to_lower_camel_case(),
1690 Some("PascalCase") => name.to_pascal_case(),
1691 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1692 Some("lowercase") => name.to_lowercase(),
1693 Some("UPPERCASE") => name.to_uppercase(),
1694 _ => name.to_lowercase(),
1695 }
1696}
1697
1698fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1699 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1700
1701 if enum_def.serde_tag.is_some() && has_data_variants {
1703 return gen_tagged_union(enum_def, namespace);
1704 }
1705
1706 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1714 if let Some(ref rename) = v.serde_rename {
1715 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1716 rename != &snake
1717 } else {
1718 false
1719 }
1720 });
1721
1722 let enum_pascal = enum_def.name.to_pascal_case();
1723
1724 let variants: Vec<(String, String)> = enum_def
1726 .variants
1727 .iter()
1728 .map(|v| {
1729 let json_name = v
1730 .serde_rename
1731 .clone()
1732 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1733 let pascal_name = v.name.to_pascal_case();
1734 (json_name, pascal_name)
1735 })
1736 .collect();
1737
1738 let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n");
1739 out.push_str("using System;\n");
1740 out.push_str("using System.Text.Json;\n");
1741 out.push_str("using System.Text.Json.Serialization;\n\n");
1742
1743 out.push_str(&format!("namespace {};\n\n", namespace));
1744
1745 if !enum_def.doc.is_empty() {
1747 out.push_str("/// <summary>\n");
1748 for line in enum_def.doc.lines() {
1749 out.push_str(&format!("/// {}\n", line));
1750 }
1751 out.push_str("/// </summary>\n");
1752 }
1753
1754 if needs_custom_converter {
1755 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1756 }
1757 out.push_str(&format!("public enum {enum_pascal}\n"));
1758 out.push_str("{\n");
1759
1760 for (json_name, pascal_name) in &variants {
1761 if let Some(v) = enum_def
1763 .variants
1764 .iter()
1765 .find(|v| v.name.to_pascal_case() == *pascal_name)
1766 {
1767 if !v.doc.is_empty() {
1768 out.push_str(" /// <summary>\n");
1769 for line in v.doc.lines() {
1770 out.push_str(&format!(" /// {}\n", line));
1771 }
1772 out.push_str(" /// </summary>\n");
1773 }
1774 }
1775 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1776 out.push_str(&format!(" {pascal_name},\n"));
1777 }
1778
1779 out.push_str("}\n");
1780
1781 if needs_custom_converter {
1783 out.push('\n');
1784 out.push_str(&format!(
1785 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1786 ));
1787 out.push_str(&format!(
1788 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1789 ));
1790 out.push_str("{\n");
1791
1792 out.push_str(&format!(
1794 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1795 ));
1796 out.push_str(" {\n");
1797 out.push_str(" var value = reader.GetString();\n");
1798 out.push_str(" return value switch\n");
1799 out.push_str(" {\n");
1800 for (json_name, pascal_name) in &variants {
1801 out.push_str(&format!(
1802 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1803 ));
1804 }
1805 out.push_str(&format!(
1806 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1807 ));
1808 out.push_str(" };\n");
1809 out.push_str(" }\n\n");
1810
1811 out.push_str(&format!(
1813 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1814 ));
1815 out.push_str(" {\n");
1816 out.push_str(" var str = value switch\n");
1817 out.push_str(" {\n");
1818 for (json_name, pascal_name) in &variants {
1819 out.push_str(&format!(
1820 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1821 ));
1822 }
1823 out.push_str(&format!(
1824 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1825 ));
1826 out.push_str(" };\n");
1827 out.push_str(" writer.WriteStringValue(str);\n");
1828 out.push_str(" }\n");
1829 out.push_str("}\n");
1830 }
1831
1832 out
1833}
1834
1835fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1842 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1843 let enum_pascal = enum_def.name.to_pascal_case();
1844 let converter_name = format!("{enum_pascal}JsonConverter");
1845 let ns = namespace;
1848
1849 let mut out = String::from(
1850 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1851 using System;\n\
1852 using System.Collections.Generic;\n\
1853 using System.Text.Json;\n\
1854 using System.Text.Json.Serialization;\n\n",
1855 );
1856 out.push_str(&format!("namespace {};\n\n", namespace));
1857
1858 if !enum_def.doc.is_empty() {
1860 out.push_str("/// <summary>\n");
1861 for line in enum_def.doc.lines() {
1862 out.push_str(&format!("/// {}\n", line));
1863 }
1864 out.push_str("/// </summary>\n");
1865 }
1866
1867 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
1869 out.push_str(&format!("public abstract record {enum_pascal}\n"));
1870 out.push_str("{\n");
1871
1872 for variant in &enum_def.variants {
1874 let pascal = variant.name.to_pascal_case();
1875
1876 if !variant.doc.is_empty() {
1877 out.push_str(" /// <summary>\n");
1878 for line in variant.doc.lines() {
1879 out.push_str(&format!(" /// {}\n", line));
1880 }
1881 out.push_str(" /// </summary>\n");
1882 }
1883
1884 if variant.fields.is_empty() {
1885 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
1887 } else {
1888 let is_copy_ctor_clash = variant.fields.len() == 1 && {
1893 let field_cs_type = csharp_type(&variant.fields[0].ty);
1894 field_cs_type.as_ref() == pascal
1895 };
1896
1897 if is_copy_ctor_clash {
1898 let cs_type = csharp_type(&variant.fields[0].ty);
1899 let qualified_cs_type = format!("global::{ns}.{cs_type}");
1903 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
1904 out.push_str(" {\n");
1905 out.push_str(&format!(
1906 " public required {qualified_cs_type} Value {{ get; init; }}\n"
1907 ));
1908 out.push_str(" }\n\n");
1909 } else {
1910 out.push_str(&format!(" public sealed record {pascal}(\n"));
1912 for (i, field) in variant.fields.iter().enumerate() {
1913 let cs_type = csharp_type(&field.ty);
1914 let cs_type = if field.optional && !cs_type.ends_with('?') {
1915 format!("{cs_type}?")
1916 } else {
1917 cs_type.to_string()
1918 };
1919 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1920 if is_tuple_field(field) {
1921 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1922 } else {
1923 let json_name = field.name.trim_start_matches('_');
1924 let cs_name = to_csharp_name(json_name);
1925 let clashes = cs_name == pascal || cs_name == cs_type;
1926 if clashes {
1927 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1928 } else {
1929 out.push_str(&format!(
1930 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1931 ));
1932 }
1933 }
1934 }
1935 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
1936 }
1937 }
1938 }
1939
1940 out.push_str("}\n\n");
1941
1942 out.push_str(&format!(
1944 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
1945 ));
1946 out.push_str(&format!(
1947 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
1948 ));
1949 out.push_str("{\n");
1950
1951 out.push_str(&format!(
1953 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1954 ));
1955 out.push_str(" {\n");
1956 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
1957 out.push_str(" var root = doc.RootElement;\n");
1958 out.push_str(&format!(
1959 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
1960 ));
1961 out.push_str(&format!(
1962 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
1963 ));
1964 out.push_str(" var tag = tagEl.GetString();\n");
1965 out.push_str(" var json = root.GetRawText();\n");
1966 out.push_str(" return tag switch\n");
1967 out.push_str(" {\n");
1968
1969 for variant in &enum_def.variants {
1970 let discriminator = variant
1971 .serde_rename
1972 .clone()
1973 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1974 let pascal = variant.name.to_pascal_case();
1975 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
1980 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
1981 let f = &variant.fields[0];
1982 let cs_type = csharp_type(&f.ty);
1983 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
1984 cs_name == pascal || cs_name == cs_type
1985 };
1986 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
1987 if is_newtype {
1988 let inner_cs_type = csharp_type(&variant.fields[0].ty);
1989 if inner_cs_type == pascal {
1992 out.push_str(&format!(
1993 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
1994 ));
1995 out.push_str(&format!(
1996 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
1997 ));
1998 } else {
1999 out.push_str(&format!(
2000 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
2001 ));
2002 out.push_str(&format!(
2003 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
2004 ));
2005 out.push_str(&format!(
2006 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
2007 ));
2008 }
2009 } else {
2010 out.push_str(&format!(
2011 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
2012 ));
2013 out.push_str(&format!(
2014 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
2015 ));
2016 }
2017 }
2018
2019 out.push_str(&format!(
2020 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
2021 ));
2022 out.push_str(" };\n");
2023 out.push_str(" }\n\n");
2024
2025 out.push_str(&format!(
2027 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2028 ));
2029 out.push_str(" {\n");
2030
2031 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
2033 out.push_str(" switch (value)\n");
2034 out.push_str(" {\n");
2035
2036 for variant in &enum_def.variants {
2037 let discriminator = variant
2038 .serde_rename
2039 .clone()
2040 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2041 let pascal = variant.name.to_pascal_case();
2042 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2046 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2047 let f = &variant.fields[0];
2048 let cs_type = csharp_type(&f.ty);
2049 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2050 cs_name == pascal || cs_name == cs_type
2051 };
2052 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2053 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
2054 out.push_str(" {\n");
2055 if is_newtype {
2056 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
2057 } else {
2058 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
2059 }
2060 out.push_str(" writer.WriteStartObject();\n");
2061 out.push_str(&format!(
2062 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
2063 ));
2064 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2065 out.push_str(&format!(
2066 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2067 ));
2068 out.push_str(" writer.WriteEndObject();\n");
2069 out.push_str(" break;\n");
2070 out.push_str(" }\n");
2071 }
2072
2073 out.push_str(&format!(
2074 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2075 ));
2076 out.push_str(" }\n");
2077 out.push_str(" }\n");
2078 out.push_str("}\n");
2079
2080 out
2081}