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::{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 output_dir = resolve_output_dir(
56 config.output.csharp.as_ref(),
57 &config.crate_config.name,
58 "packages/csharp/",
59 );
60
61 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
62
63 let mut files = Vec::new();
64
65 files.push(GeneratedFile {
67 path: base_path.join("NativeMethods.cs"),
68 content: strip_trailing_whitespace(&gen_native_methods(
69 api,
70 &namespace,
71 &lib_name,
72 &prefix,
73 &bridge_param_names,
74 &bridge_type_aliases,
75 has_visitor_bridge,
76 &config.trait_bridges,
77 )),
78 generated_header: true,
79 });
80
81 if !api.errors.is_empty() {
83 for error in &api.errors {
84 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
85 for (class_name, content) in error_files {
86 files.push(GeneratedFile {
87 path: base_path.join(format!("{}.cs", class_name)),
88 content: strip_trailing_whitespace(&content),
89 generated_header: false, });
91 }
92 }
93 }
94
95 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
97 if api.errors.is_empty()
98 || !api
99 .errors
100 .iter()
101 .any(|e| format!("{}Exception", e.name) == exception_class_name)
102 {
103 files.push(GeneratedFile {
104 path: base_path.join(format!("{}.cs", exception_class_name)),
105 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
106 generated_header: true,
107 });
108 }
109
110 let base_class_name = api.crate_name.to_pascal_case();
112 let wrapper_class_name = if namespace == base_class_name {
113 format!("{}Lib", base_class_name)
114 } else {
115 base_class_name
116 };
117 files.push(GeneratedFile {
118 path: base_path.join(format!("{}.cs", wrapper_class_name)),
119 content: strip_trailing_whitespace(&gen_wrapper_class(
120 api,
121 &namespace,
122 &wrapper_class_name,
123 &exception_class_name,
124 &prefix,
125 &bridge_param_names,
126 &bridge_type_aliases,
127 has_visitor_bridge,
128 )),
129 generated_header: true,
130 });
131
132 if has_visitor_bridge {
134 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
135 files.push(GeneratedFile {
136 path: base_path.join(filename),
137 content: strip_trailing_whitespace(&content),
138 generated_header: true,
139 });
140 }
141 }
142
143 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
145 if typ.is_opaque {
146 let type_filename = typ.name.to_pascal_case();
147 files.push(GeneratedFile {
148 path: base_path.join(format!("{}.cs", type_filename)),
149 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
150 generated_header: true,
151 });
152 }
153 }
154
155 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
157
158 let complex_enums: HashSet<String> = api
163 .enums
164 .iter()
165 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
166 .map(|e| e.name.to_pascal_case())
167 .collect();
168
169 let custom_converter_enums: HashSet<String> = api
173 .enums
174 .iter()
175 .filter(|e| {
176 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
178 || e.variants.iter().any(|v| {
180 if let Some(ref rename) = v.serde_rename {
181 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
182 rename != &snake
183 } else {
184 false
185 }
186 })
187 })
188 .map(|e| e.name.to_pascal_case())
189 .collect();
190
191 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
193
194 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
196 if !typ.is_opaque {
197 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
200 if !typ.fields.is_empty() && !has_named_fields {
201 continue;
202 }
203 if has_visitor_bridge && (typ.name == "NodeContext" || typ.name == "VisitResult") {
205 continue;
206 }
207
208 let type_filename = typ.name.to_pascal_case();
209 files.push(GeneratedFile {
210 path: base_path.join(format!("{}.cs", type_filename)),
211 content: strip_trailing_whitespace(&gen_record_type(
212 typ,
213 &namespace,
214 &enum_names,
215 &complex_enums,
216 &custom_converter_enums,
217 &lang_rename_all,
218 )),
219 generated_header: true,
220 });
221 }
222 }
223
224 for enum_def in &api.enums {
226 if has_visitor_bridge && (enum_def.name == "VisitResult" || enum_def.name == "NodeContext") {
228 continue;
229 }
230 let enum_filename = enum_def.name.to_pascal_case();
231 files.push(GeneratedFile {
232 path: base_path.join(format!("{}.cs", enum_filename)),
233 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
234 generated_header: true,
235 });
236 }
237
238 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
240
241 Ok(files)
242 }
243
244 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
249 Ok(vec![])
251 }
252
253 fn build_config(&self) -> Option<BuildConfig> {
254 Some(BuildConfig {
255 tool: "dotnet",
256 crate_suffix: "",
257 depends_on_ffi: true,
258 post_build: vec![],
259 })
260 }
261}
262
263fn is_tuple_field(field: &FieldDef) -> bool {
265 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
266 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
267}
268
269fn strip_trailing_whitespace(content: &str) -> String {
271 let mut result: String = content
272 .lines()
273 .map(|line| line.trim_end())
274 .collect::<Vec<_>>()
275 .join("\n");
276 if !result.ends_with('\n') {
277 result.push('\n');
278 }
279 result
280}
281
282fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
293 match ty {
294 TypeRef::Unit => "void",
295 TypeRef::Primitive(PrimitiveType::Bool) => "int",
297 TypeRef::Primitive(PrimitiveType::U8) => "byte",
299 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
300 TypeRef::Primitive(PrimitiveType::U32) => "uint",
301 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
302 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
303 TypeRef::Primitive(PrimitiveType::I16) => "short",
304 TypeRef::Primitive(PrimitiveType::I32) => "int",
305 TypeRef::Primitive(PrimitiveType::I64) => "long",
306 TypeRef::Primitive(PrimitiveType::F32) => "float",
307 TypeRef::Primitive(PrimitiveType::F64) => "double",
308 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
309 TypeRef::Primitive(PrimitiveType::Isize) => "long",
310 TypeRef::Duration => "ulong",
312 TypeRef::String
314 | TypeRef::Char
315 | TypeRef::Bytes
316 | TypeRef::Optional(_)
317 | TypeRef::Vec(_)
318 | TypeRef::Map(_, _)
319 | TypeRef::Named(_)
320 | TypeRef::Path
321 | TypeRef::Json => "IntPtr",
322 }
323}
324
325fn returns_string(ty: &TypeRef) -> bool {
327 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
328}
329
330fn returns_bool_via_int(ty: &TypeRef) -> bool {
332 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
333}
334
335fn returns_json_object(ty: &TypeRef) -> bool {
337 matches!(
338 ty,
339 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
340 )
341}
342
343fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
354 match ty {
355 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
356 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
358 TypeRef::Unit => "void",
359 TypeRef::Primitive(PrimitiveType::Bool) => "int",
360 TypeRef::Primitive(PrimitiveType::U8) => "byte",
361 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
362 TypeRef::Primitive(PrimitiveType::U32) => "uint",
363 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
364 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
365 TypeRef::Primitive(PrimitiveType::I16) => "short",
366 TypeRef::Primitive(PrimitiveType::I32) => "int",
367 TypeRef::Primitive(PrimitiveType::I64) => "long",
368 TypeRef::Primitive(PrimitiveType::F32) => "float",
369 TypeRef::Primitive(PrimitiveType::F64) => "double",
370 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
371 TypeRef::Primitive(PrimitiveType::Isize) => "long",
372 TypeRef::Duration => "ulong",
373 }
374}
375
376fn is_bridge_param(
383 param: &alef_core::ir::ParamDef,
384 bridge_param_names: &HashSet<String>,
385 bridge_type_aliases: &HashSet<String>,
386) -> bool {
387 bridge_param_names.contains(¶m.name)
388 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
389}
390
391#[allow(clippy::too_many_arguments)]
392fn gen_native_methods(
393 api: &ApiSurface,
394 namespace: &str,
395 lib_name: &str,
396 prefix: &str,
397 bridge_param_names: &HashSet<String>,
398 bridge_type_aliases: &HashSet<String>,
399 has_visitor_bridge: bool,
400 trait_bridges: &[alef_core::config::TraitBridgeConfig],
401) -> String {
402 let mut out = String::from(
403 "// This file is auto-generated by alef. DO NOT EDIT.\n\
404 using System;\n\
405 using System.Runtime.InteropServices;\n\n",
406 );
407
408 out.push_str(&format!("namespace {};\n\n", namespace));
409
410 out.push_str("internal static partial class NativeMethods\n{\n");
411 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
412
413 let mut emitted: HashSet<String> = HashSet::new();
416
417 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
420
421 let mut opaque_param_types: HashSet<String> = HashSet::new();
425 let mut opaque_return_types: HashSet<String> = HashSet::new();
426
427 for func in &api.functions {
428 for param in &func.params {
429 if let TypeRef::Named(name) = ¶m.ty {
430 if !enum_names.contains(name) {
431 opaque_param_types.insert(name.clone());
432 }
433 }
434 }
435 if let TypeRef::Named(name) = &func.return_type {
436 if !enum_names.contains(name) {
437 opaque_return_types.insert(name.clone());
438 }
439 }
440 }
441 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
442 for method in &typ.methods {
443 for param in &method.params {
444 if let TypeRef::Named(name) = ¶m.ty {
445 if !enum_names.contains(name) {
446 opaque_param_types.insert(name.clone());
447 }
448 }
449 }
450 if let TypeRef::Named(name) = &method.return_type {
451 if !enum_names.contains(name) {
452 opaque_return_types.insert(name.clone());
453 }
454 }
455 }
456 }
457
458 let true_opaque_types: HashSet<String> = api
460 .types
461 .iter()
462 .filter(|t| t.is_opaque)
463 .map(|t| t.name.clone())
464 .collect();
465
466 let mut sorted_param_types: Vec<&String> = opaque_param_types.iter().collect();
470 sorted_param_types.sort();
471 for type_name in sorted_param_types {
472 let snake = type_name.to_snake_case();
473 if !true_opaque_types.contains(type_name) {
474 let from_json_entry = format!("{prefix}_{snake}_from_json");
475 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
476 if emitted.insert(from_json_entry.clone()) {
477 out.push_str(&format!(
478 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
479 ));
480 out.push_str(&format!(
481 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
482 ));
483 }
484 }
485 let free_entry = format!("{prefix}_{snake}_free");
486 let free_cs = format!("{}Free", type_name.to_pascal_case());
487 if emitted.insert(free_entry.clone()) {
488 out.push_str(&format!(
489 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
490 ));
491 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
492 }
493 }
494
495 let mut sorted_return_types: Vec<&String> = opaque_return_types.iter().collect();
498 sorted_return_types.sort();
499 for type_name in sorted_return_types {
500 let snake = type_name.to_snake_case();
501 if !true_opaque_types.contains(type_name) {
502 let to_json_entry = format!("{prefix}_{snake}_to_json");
503 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
504 if emitted.insert(to_json_entry.clone()) {
505 out.push_str(&format!(
506 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
507 ));
508 out.push_str(&format!(
509 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
510 ));
511 }
512 }
513 let free_entry = format!("{prefix}_{snake}_free");
514 let free_cs = format!("{}Free", type_name.to_pascal_case());
515 if emitted.insert(free_entry.clone()) {
516 out.push_str(&format!(
517 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
518 ));
519 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
520 }
521 }
522
523 for func in &api.functions {
525 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
526 if emitted.insert(c_func_name.clone()) {
527 out.push_str(&gen_pinvoke_for_func(
528 &c_func_name,
529 func,
530 bridge_param_names,
531 bridge_type_aliases,
532 ));
533 }
534 }
535
536 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
538 let type_snake = typ.name.to_snake_case();
539 for method in &typ.methods {
540 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
541 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
545 if emitted.insert(c_method_name.clone()) {
546 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
547 }
548 }
549 }
550
551 out.push_str(&format!(
553 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
554 ));
555 out.push_str(" internal static extern int LastErrorCode();\n\n");
556
557 out.push_str(&format!(
558 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
559 ));
560 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
561
562 out.push_str(&format!(
563 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
564 ));
565 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
566
567 if has_visitor_bridge {
569 out.push('\n');
570 out.push_str(&crate::gen_visitor::gen_native_methods_visitor(
571 namespace, lib_name, prefix,
572 ));
573 }
574
575 if !trait_bridges.is_empty() {
577 let trait_defs: Vec<_> = api.types.iter().filter(|t| t.is_trait).collect();
579
580 let bridges: Vec<_> = trait_bridges
582 .iter()
583 .filter_map(|config| {
584 let trait_name = config.trait_name.clone();
585 trait_defs
586 .iter()
587 .find(|t| t.name == trait_name)
588 .map(|trait_def| (trait_name, config, *trait_def))
589 })
590 .collect();
591
592 if !bridges.is_empty() {
593 out.push('\n');
594 out.push_str(&crate::trait_bridge::gen_native_methods_trait_bridges(
595 namespace, prefix, &bridges,
596 ));
597 }
598 }
599
600 out.push_str("}\n");
601
602 out
603}
604
605fn gen_pinvoke_for_func(
606 c_name: &str,
607 func: &FunctionDef,
608 bridge_param_names: &HashSet<String>,
609 bridge_type_aliases: &HashSet<String>,
610) -> String {
611 let cs_name = to_csharp_name(&func.name);
612 let mut out =
613 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
614 out.push_str(" internal static extern ");
615
616 out.push_str(pinvoke_return_type(&func.return_type));
618
619 out.push_str(&format!(" {}(", cs_name));
620
621 let visible_params: Vec<_> = func
624 .params
625 .iter()
626 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
627 .collect();
628
629 if visible_params.is_empty() {
630 out.push_str(");\n\n");
631 } else {
632 out.push('\n');
633 for (i, param) in visible_params.iter().enumerate() {
634 out.push_str(" ");
635 let pinvoke_ty = pinvoke_param_type(¶m.ty);
636 if pinvoke_ty == "string" {
637 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
638 }
639 let param_name = param.name.to_lower_camel_case();
640 out.push_str(&format!("{pinvoke_ty} {param_name}"));
641
642 if i < visible_params.len() - 1 {
643 out.push(',');
644 }
645 out.push('\n');
646 }
647 out.push_str(" );\n\n");
648 }
649
650 out
651}
652
653fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
654 let mut out =
655 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
656 out.push_str(" internal static extern ");
657
658 out.push_str(pinvoke_return_type(&method.return_type));
660
661 out.push_str(&format!(" {}(", cs_name));
662
663 if method.params.is_empty() {
664 out.push_str(");\n\n");
665 } else {
666 out.push('\n');
667 for (i, param) in method.params.iter().enumerate() {
668 out.push_str(" ");
669 let pinvoke_ty = pinvoke_param_type(¶m.ty);
670 if pinvoke_ty == "string" {
671 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
672 }
673 let param_name = param.name.to_lower_camel_case();
674 out.push_str(&format!("{pinvoke_ty} {param_name}"));
675
676 if i < method.params.len() - 1 {
677 out.push(',');
678 }
679 out.push('\n');
680 }
681 out.push_str(" );\n\n");
682 }
683
684 out
685}
686
687fn gen_exception_class(namespace: &str, class_name: &str) -> String {
688 let mut out = String::from(
689 "// This file is auto-generated by alef. DO NOT EDIT.\n\
690 using System;\n\n",
691 );
692
693 out.push_str(&format!("namespace {};\n\n", namespace));
694
695 out.push_str(&format!("public class {} : Exception\n", class_name));
696 out.push_str("{\n");
697 out.push_str(" public int Code { get; }\n\n");
698 out.push_str(&format!(
699 " public {}(int code, string message) : base(message)\n",
700 class_name
701 ));
702 out.push_str(" {\n");
703 out.push_str(" Code = code;\n");
704 out.push_str(" }\n");
705 out.push_str("}\n");
706
707 out
708}
709
710#[allow(clippy::too_many_arguments)]
711fn gen_wrapper_class(
712 api: &ApiSurface,
713 namespace: &str,
714 class_name: &str,
715 exception_name: &str,
716 prefix: &str,
717 bridge_param_names: &HashSet<String>,
718 bridge_type_aliases: &HashSet<String>,
719 has_visitor_bridge: bool,
720) -> String {
721 let mut out = String::from(
722 "// This file is auto-generated by alef. DO NOT EDIT.\n\
723 using System;\n\
724 using System.Collections.Generic;\n\
725 using System.Runtime.InteropServices;\n\
726 using System.Text.Json;\n\
727 using System.Text.Json.Serialization;\n\
728 using System.Threading.Tasks;\n\n",
729 );
730
731 out.push_str(&format!("namespace {};\n\n", namespace));
732
733 out.push_str(&format!("public static class {}\n", class_name));
734 out.push_str("{\n");
735 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
736 out.push_str(" {\n");
737 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
738 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
739 out.push_str(" };\n\n");
740
741 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
743
744 let true_opaque_types: HashSet<String> = api
746 .types
747 .iter()
748 .filter(|t| t.is_opaque)
749 .map(|t| t.name.clone())
750 .collect();
751
752 for func in &api.functions {
754 out.push_str(&gen_wrapper_function(
755 func,
756 exception_name,
757 prefix,
758 &enum_names,
759 &true_opaque_types,
760 bridge_param_names,
761 bridge_type_aliases,
762 ));
763 }
764
765 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
767 if typ.is_opaque {
769 continue;
770 }
771 for method in &typ.methods {
772 out.push_str(&gen_wrapper_method(
773 method,
774 exception_name,
775 prefix,
776 &typ.name,
777 &enum_names,
778 &true_opaque_types,
779 bridge_param_names,
780 bridge_type_aliases,
781 ));
782 }
783 }
784
785 if has_visitor_bridge {
787 out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
788 exception_name,
789 prefix,
790 ));
791 }
792
793 out.push_str(" private static ");
795 out.push_str(&format!("{} GetLastError()\n", exception_name));
796 out.push_str(" {\n");
797 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
798 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
799 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
800 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
801 out.push_str(" }\n");
802
803 out.push_str("}\n");
804
805 out
806}
807
808fn emit_named_param_setup(
825 out: &mut String,
826 params: &[alef_core::ir::ParamDef],
827 indent: &str,
828 true_opaque_types: &HashSet<String>,
829) {
830 for param in params {
831 let param_name = param.name.to_lower_camel_case();
832 let json_var = format!("{param_name}Json");
833 let handle_var = format!("{param_name}Handle");
834
835 match ¶m.ty {
836 TypeRef::Named(type_name) => {
837 if true_opaque_types.contains(type_name) {
840 continue;
841 }
842 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
843 if param.optional {
844 out.push_str(&format!(
845 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
846 ));
847 } else {
848 out.push_str(&format!(
849 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
850 ));
851 }
852 out.push_str(&format!(
853 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
854 ));
855 }
856 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
857 out.push_str(&format!(
859 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
860 ));
861 out.push_str(&format!(
862 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
863 ));
864 }
865 _ => {}
866 }
867 }
868}
869
870fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
876 match ty {
877 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
878 let bang = if optional { "!" } else { "" };
880 format!("{param_name}{bang}.Handle")
881 }
882 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
883 format!("{param_name}Handle")
884 }
885 ty => {
886 if optional {
887 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
892 if needs_value_unwrap {
893 format!("{param_name}.Value")
894 } else {
895 format!("{param_name}!")
896 }
897 } else {
898 param_name.to_string()
899 }
900 }
901 }
902}
903
904fn emit_named_param_teardown(
909 out: &mut String,
910 params: &[alef_core::ir::ParamDef],
911 true_opaque_types: &HashSet<String>,
912) {
913 for param in params {
914 let param_name = param.name.to_lower_camel_case();
915 let handle_var = format!("{param_name}Handle");
916 match ¶m.ty {
917 TypeRef::Named(type_name) => {
918 if true_opaque_types.contains(type_name) {
919 continue;
921 }
922 let free_method = format!("{}Free", type_name.to_pascal_case());
923 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
924 }
925 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
926 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
927 }
928 _ => {}
929 }
930 }
931}
932
933fn emit_named_param_teardown_indented(
935 out: &mut String,
936 params: &[alef_core::ir::ParamDef],
937 indent: &str,
938 true_opaque_types: &HashSet<String>,
939) {
940 for param in params {
941 let param_name = param.name.to_lower_camel_case();
942 let handle_var = format!("{param_name}Handle");
943 match ¶m.ty {
944 TypeRef::Named(type_name) => {
945 if true_opaque_types.contains(type_name) {
946 continue;
948 }
949 let free_method = format!("{}Free", type_name.to_pascal_case());
950 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
951 }
952 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
953 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
954 }
955 _ => {}
956 }
957 }
958}
959
960fn gen_wrapper_function(
961 func: &FunctionDef,
962 _exception_name: &str,
963 _prefix: &str,
964 enum_names: &HashSet<String>,
965 true_opaque_types: &HashSet<String>,
966 bridge_param_names: &HashSet<String>,
967 bridge_type_aliases: &HashSet<String>,
968) -> String {
969 let mut out = String::with_capacity(1024);
970
971 let visible_params: Vec<alef_core::ir::ParamDef> = func
973 .params
974 .iter()
975 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
976 .cloned()
977 .collect();
978
979 if !func.doc.is_empty() {
981 out.push_str(" /// <summary>\n");
982 for line in func.doc.lines() {
983 out.push_str(&format!(" /// {}\n", line));
984 }
985 out.push_str(" /// </summary>\n");
986 for param in &visible_params {
987 out.push_str(&format!(
988 " /// <param name=\"{}\">{}</param>\n",
989 param.name.to_lower_camel_case(),
990 if param.optional { "Optional." } else { "" }
991 ));
992 }
993 }
994
995 out.push_str(" public static ");
996
997 if func.is_async {
999 if func.return_type == TypeRef::Unit {
1000 out.push_str("async Task");
1001 } else {
1002 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
1003 }
1004 } else if func.return_type == TypeRef::Unit {
1005 out.push_str("void");
1006 } else {
1007 out.push_str(&csharp_type(&func.return_type));
1008 }
1009
1010 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
1011 out.push('(');
1012
1013 for (i, param) in visible_params.iter().enumerate() {
1015 let param_name = param.name.to_lower_camel_case();
1016 let mapped = csharp_type(¶m.ty);
1017 if param.optional && !mapped.ends_with('?') {
1018 out.push_str(&format!("{mapped}? {param_name}"));
1019 } else {
1020 out.push_str(&format!("{mapped} {param_name}"));
1021 }
1022
1023 if i < visible_params.len() - 1 {
1024 out.push_str(", ");
1025 }
1026 }
1027
1028 out.push_str(")\n {\n");
1029
1030 for param in &visible_params {
1032 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1033 let param_name = param.name.to_lower_camel_case();
1034 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1035 }
1036 }
1037
1038 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1040
1041 let cs_native_name = to_csharp_name(&func.name);
1043
1044 if func.is_async {
1045 out.push_str(" return await Task.Run(() =>\n {\n");
1047
1048 if func.return_type != TypeRef::Unit {
1049 out.push_str(" var result = ");
1050 } else {
1051 out.push_str(" ");
1052 }
1053
1054 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1055
1056 if visible_params.is_empty() {
1057 out.push_str(");\n");
1058 } else {
1059 out.push('\n');
1060 for (i, param) in visible_params.iter().enumerate() {
1061 let param_name = param.name.to_lower_camel_case();
1062 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1063 out.push_str(&format!(" {arg}"));
1064 if i < visible_params.len() - 1 {
1065 out.push(',');
1066 }
1067 out.push('\n');
1068 }
1069 out.push_str(" );\n");
1070 }
1071
1072 if func.return_type != TypeRef::Unit {
1074 out.push_str(
1075 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
1076 );
1077 }
1078
1079 emit_return_marshalling_indented(
1080 &mut out,
1081 &func.return_type,
1082 " ",
1083 enum_names,
1084 true_opaque_types,
1085 );
1086 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1087 emit_return_statement_indented(&mut out, &func.return_type, " ");
1088 out.push_str(" });\n");
1089 } else {
1090 if func.return_type != TypeRef::Unit {
1091 out.push_str(" var result = ");
1092 } else {
1093 out.push_str(" ");
1094 }
1095
1096 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1097
1098 if visible_params.is_empty() {
1099 out.push_str(");\n");
1100 } else {
1101 out.push('\n');
1102 for (i, param) in visible_params.iter().enumerate() {
1103 let param_name = param.name.to_lower_camel_case();
1104 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1105 out.push_str(&format!(" {arg}"));
1106 if i < visible_params.len() - 1 {
1107 out.push(',');
1108 }
1109 out.push('\n');
1110 }
1111 out.push_str(" );\n");
1112 }
1113
1114 if func.return_type != TypeRef::Unit {
1116 out.push_str(
1117 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
1118 );
1119 }
1120
1121 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
1122 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1123 emit_return_statement(&mut out, &func.return_type);
1124 }
1125
1126 out.push_str(" }\n\n");
1127
1128 out
1129}
1130
1131#[allow(clippy::too_many_arguments)]
1132fn gen_wrapper_method(
1133 method: &MethodDef,
1134 _exception_name: &str,
1135 _prefix: &str,
1136 type_name: &str,
1137 enum_names: &HashSet<String>,
1138 true_opaque_types: &HashSet<String>,
1139 bridge_param_names: &HashSet<String>,
1140 bridge_type_aliases: &HashSet<String>,
1141) -> String {
1142 let mut out = String::with_capacity(1024);
1143
1144 let visible_params: Vec<alef_core::ir::ParamDef> = method
1146 .params
1147 .iter()
1148 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1149 .cloned()
1150 .collect();
1151
1152 if !method.doc.is_empty() {
1154 out.push_str(" /// <summary>\n");
1155 for line in method.doc.lines() {
1156 out.push_str(&format!(" /// {}\n", line));
1157 }
1158 out.push_str(" /// </summary>\n");
1159 for param in &visible_params {
1160 out.push_str(&format!(
1161 " /// <param name=\"{}\">{}</param>\n",
1162 param.name.to_lower_camel_case(),
1163 if param.optional { "Optional." } else { "" }
1164 ));
1165 }
1166 }
1167
1168 out.push_str(" public static ");
1170
1171 if method.is_async {
1173 if method.return_type == TypeRef::Unit {
1174 out.push_str("async Task");
1175 } else {
1176 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1177 }
1178 } else if method.return_type == TypeRef::Unit {
1179 out.push_str("void");
1180 } else {
1181 out.push_str(&csharp_type(&method.return_type));
1182 }
1183
1184 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1186 out.push_str(&format!(" {method_cs_name}"));
1187 out.push('(');
1188
1189 for (i, param) in visible_params.iter().enumerate() {
1191 let param_name = param.name.to_lower_camel_case();
1192 let mapped = csharp_type(¶m.ty);
1193 if param.optional && !mapped.ends_with('?') {
1194 out.push_str(&format!("{mapped}? {param_name}"));
1195 } else {
1196 out.push_str(&format!("{mapped} {param_name}"));
1197 }
1198
1199 if i < visible_params.len() - 1 {
1200 out.push_str(", ");
1201 }
1202 }
1203
1204 out.push_str(")\n {\n");
1205
1206 for param in &visible_params {
1208 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1209 let param_name = param.name.to_lower_camel_case();
1210 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1211 }
1212 }
1213
1214 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1216
1217 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1222
1223 if method.is_async {
1224 out.push_str(" return await Task.Run(() =>\n {\n");
1226
1227 if method.return_type != TypeRef::Unit {
1228 out.push_str(" var result = ");
1229 } else {
1230 out.push_str(" ");
1231 }
1232
1233 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1234
1235 if visible_params.is_empty() {
1236 out.push_str(");\n");
1237 } else {
1238 out.push('\n');
1239 for (i, param) in visible_params.iter().enumerate() {
1240 let param_name = param.name.to_lower_camel_case();
1241 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1242 out.push_str(&format!(" {arg}"));
1243 if i < visible_params.len() - 1 {
1244 out.push(',');
1245 }
1246 out.push('\n');
1247 }
1248 out.push_str(" );\n");
1249 }
1250
1251 emit_return_marshalling_indented(
1252 &mut out,
1253 &method.return_type,
1254 " ",
1255 enum_names,
1256 true_opaque_types,
1257 );
1258 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1259 emit_return_statement_indented(&mut out, &method.return_type, " ");
1260 out.push_str(" });\n");
1261 } else {
1262 if method.return_type != TypeRef::Unit {
1263 out.push_str(" var result = ");
1264 } else {
1265 out.push_str(" ");
1266 }
1267
1268 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1269
1270 if visible_params.is_empty() {
1271 out.push_str(");\n");
1272 } else {
1273 out.push('\n');
1274 for (i, param) in visible_params.iter().enumerate() {
1275 let param_name = param.name.to_lower_camel_case();
1276 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1277 out.push_str(&format!(" {arg}"));
1278 if i < visible_params.len() - 1 {
1279 out.push(',');
1280 }
1281 out.push('\n');
1282 }
1283 out.push_str(" );\n");
1284 }
1285
1286 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1287 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1288 emit_return_statement(&mut out, &method.return_type);
1289 }
1290
1291 out.push_str(" }\n\n");
1292
1293 out
1294}
1295
1296fn emit_return_marshalling(
1308 out: &mut String,
1309 return_type: &TypeRef,
1310 enum_names: &HashSet<String>,
1311 true_opaque_types: &HashSet<String>,
1312) {
1313 if *return_type == TypeRef::Unit {
1314 return;
1316 }
1317
1318 if returns_string(return_type) {
1319 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1321 out.push_str(" NativeMethods.FreeString(result);\n");
1322 } else if returns_bool_via_int(return_type) {
1323 out.push_str(" var returnValue = result != 0;\n");
1325 } else if let TypeRef::Named(type_name) = return_type {
1326 let pascal = type_name.to_pascal_case();
1327 if true_opaque_types.contains(type_name) {
1328 out.push_str(&format!(" var returnValue = new {pascal}(result);\n"));
1330 } else if !enum_names.contains(&pascal) {
1331 let to_json_method = format!("{pascal}ToJson");
1333 let free_method = format!("{pascal}Free");
1334 let cs_ty = csharp_type(return_type);
1335 out.push_str(&format!(
1336 " var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1337 ));
1338 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1339 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1340 out.push_str(&format!(" NativeMethods.{free_method}(result);\n"));
1341 out.push_str(&format!(
1342 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1343 cs_ty
1344 ));
1345 } else {
1346 let cs_ty = csharp_type(return_type);
1348 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1349 out.push_str(" NativeMethods.FreeString(result);\n");
1350 out.push_str(&format!(
1351 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1352 cs_ty
1353 ));
1354 }
1355 } else if returns_json_object(return_type) {
1356 let cs_ty = csharp_type(return_type);
1358 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1359 out.push_str(" NativeMethods.FreeString(result);\n");
1360 out.push_str(&format!(
1361 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1362 cs_ty
1363 ));
1364 } else {
1365 out.push_str(" var returnValue = result;\n");
1367 }
1368}
1369
1370fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1372 if *return_type != TypeRef::Unit {
1373 out.push_str(" return returnValue;\n");
1374 }
1375}
1376
1377fn emit_return_marshalling_indented(
1382 out: &mut String,
1383 return_type: &TypeRef,
1384 indent: &str,
1385 enum_names: &HashSet<String>,
1386 true_opaque_types: &HashSet<String>,
1387) {
1388 if *return_type == TypeRef::Unit {
1389 return;
1390 }
1391
1392 if returns_string(return_type) {
1393 out.push_str(&format!(
1394 "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1395 ));
1396 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1397 } else if returns_bool_via_int(return_type) {
1398 out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1399 } else if let TypeRef::Named(type_name) = return_type {
1400 let pascal = type_name.to_pascal_case();
1401 if true_opaque_types.contains(type_name) {
1402 out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1404 } else if !enum_names.contains(&pascal) {
1405 let to_json_method = format!("{pascal}ToJson");
1407 let free_method = format!("{pascal}Free");
1408 let cs_ty = csharp_type(return_type);
1409 out.push_str(&format!(
1410 "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1411 ));
1412 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1413 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1414 out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1415 out.push_str(&format!(
1416 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1417 cs_ty
1418 ));
1419 } else {
1420 let cs_ty = csharp_type(return_type);
1422 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1423 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1424 out.push_str(&format!(
1425 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1426 cs_ty
1427 ));
1428 }
1429 } else if returns_json_object(return_type) {
1430 let cs_ty = csharp_type(return_type);
1431 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1432 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1433 out.push_str(&format!(
1434 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1435 cs_ty
1436 ));
1437 } else {
1438 out.push_str(&format!("{indent}var returnValue = result;\n"));
1439 }
1440}
1441
1442fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1444 if *return_type != TypeRef::Unit {
1445 out.push_str(&format!("{indent}return returnValue;\n"));
1446 }
1447}
1448
1449fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1450 let mut out = String::from(
1451 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1452 using System;\n\n",
1453 );
1454
1455 out.push_str(&format!("namespace {};\n\n", namespace));
1456
1457 if !typ.doc.is_empty() {
1459 out.push_str("/// <summary>\n");
1460 for line in typ.doc.lines() {
1461 out.push_str(&format!("/// {}\n", line));
1462 }
1463 out.push_str("/// </summary>\n");
1464 }
1465
1466 let class_name = typ.name.to_pascal_case();
1467 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1468 out.push_str("{\n");
1469 out.push_str(" internal IntPtr Handle { get; }\n\n");
1470 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1471 out.push_str(" {\n");
1472 out.push_str(" Handle = handle;\n");
1473 out.push_str(" }\n\n");
1474 out.push_str(" public void Dispose()\n");
1475 out.push_str(" {\n");
1476 out.push_str(" // Native free will be called by the runtime\n");
1477 out.push_str(" }\n");
1478 out.push_str("}\n");
1479
1480 out
1481}
1482
1483fn gen_record_type(
1484 typ: &TypeDef,
1485 namespace: &str,
1486 enum_names: &HashSet<String>,
1487 complex_enums: &HashSet<String>,
1488 custom_converter_enums: &HashSet<String>,
1489 _lang_rename_all: &str,
1490) -> String {
1491 let mut out = String::from(
1492 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1493 using System;\n\
1494 using System.Collections.Generic;\n\
1495 using System.Text.Json;\n\
1496 using System.Text.Json.Serialization;\n\n",
1497 );
1498
1499 out.push_str(&format!("namespace {};\n\n", namespace));
1500
1501 if !typ.doc.is_empty() {
1503 out.push_str("/// <summary>\n");
1504 for line in typ.doc.lines() {
1505 out.push_str(&format!("/// {}\n", line));
1506 }
1507 out.push_str("/// </summary>\n");
1508 }
1509
1510 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1511 out.push_str("{\n");
1512
1513 for field in &typ.fields {
1514 if is_tuple_field(field) {
1516 continue;
1517 }
1518
1519 if !field.doc.is_empty() {
1521 out.push_str(" /// <summary>\n");
1522 for line in field.doc.lines() {
1523 out.push_str(&format!(" /// {}\n", line));
1524 }
1525 out.push_str(" /// </summary>\n");
1526 }
1527
1528 let field_base_type = match &field.ty {
1532 TypeRef::Named(n) => Some(n.to_pascal_case()),
1533 TypeRef::Optional(inner) => match inner.as_ref() {
1534 TypeRef::Named(n) => Some(n.to_pascal_case()),
1535 _ => None,
1536 },
1537 _ => None,
1538 };
1539 if let Some(ref base) = field_base_type {
1540 if custom_converter_enums.contains(base) {
1541 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1542 }
1543 }
1544
1545 let json_name = field.name.clone();
1549 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1550
1551 let cs_name = to_csharp_name(&field.name);
1552
1553 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1556
1557 if field.optional {
1558 let mapped = if is_complex {
1560 "JsonElement".to_string()
1561 } else {
1562 csharp_type(&field.ty).to_string()
1563 };
1564 let field_type = if mapped.ends_with('?') {
1565 mapped
1566 } else {
1567 format!("{mapped}?")
1568 };
1569 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1570 out.push_str(" = null;\n");
1571 } else if typ.has_default || field.default.is_some() {
1572 let field_type = if is_complex {
1575 "JsonElement".to_string()
1576 } else {
1577 csharp_type(&field.ty).to_string()
1578 };
1579 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1580 use alef_core::ir::DefaultValue;
1581 if matches!(&field.ty, TypeRef::Duration) {
1584 out.push_str(" = null;\n");
1585 out.push('\n');
1586 continue;
1587 }
1588 let default_val = match &field.typed_default {
1589 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1590 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1591 Some(DefaultValue::FloatLiteral(f)) => {
1592 let s = f.to_string();
1593 if s.contains('.') { s } else { format!("{s}.0") }
1594 }
1595 Some(DefaultValue::StringLiteral(s)) => format!("\"{}\"", s.replace('"', "\\\"")),
1596 Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1597 Some(DefaultValue::None) => "null".to_string(),
1598 Some(DefaultValue::Empty) | None => match &field.ty {
1599 TypeRef::Vec(_) => "[]".to_string(),
1600 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1601 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1602 TypeRef::Json => "null".to_string(),
1603 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1604 TypeRef::Primitive(p) => match p {
1605 PrimitiveType::Bool => "false".to_string(),
1606 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1607 _ => "0".to_string(),
1608 },
1609 TypeRef::Named(name) => {
1610 let pascal = name.to_pascal_case();
1611 if enum_names.contains(&pascal) {
1612 "default".to_string()
1613 } else {
1614 "default!".to_string()
1615 }
1616 }
1617 _ => "default!".to_string(),
1618 },
1619 };
1620 out.push_str(&format!(" = {};\n", default_val));
1621 } else {
1622 let field_type = if is_complex {
1626 "JsonElement".to_string()
1627 } else {
1628 csharp_type(&field.ty).to_string()
1629 };
1630 if matches!(&field.ty, TypeRef::Duration) {
1632 out.push_str(&format!(
1633 " public {} {} {{ get; set; }} = null;\n",
1634 field_type, cs_name
1635 ));
1636 } else {
1637 let default_val = match &field.ty {
1638 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1639 TypeRef::Vec(_) => "[]",
1640 TypeRef::Bytes => "Array.Empty<byte>()",
1641 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1642 TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1643 TypeRef::Primitive(_) => "0",
1644 _ => "default!",
1645 };
1646 out.push_str(&format!(
1647 " public {} {} {{ get; set; }} = {};\n",
1648 field_type, cs_name, default_val
1649 ));
1650 }
1651 }
1652
1653 out.push('\n');
1654 }
1655
1656 out.push_str("}\n");
1657
1658 out
1659}
1660
1661fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1663 match rename_all {
1664 Some("snake_case") => name.to_snake_case(),
1665 Some("camelCase") => name.to_lower_camel_case(),
1666 Some("PascalCase") => name.to_pascal_case(),
1667 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1668 Some("lowercase") => name.to_lowercase(),
1669 Some("UPPERCASE") => name.to_uppercase(),
1670 _ => name.to_lowercase(),
1671 }
1672}
1673
1674fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1675 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1676
1677 if enum_def.serde_tag.is_some() && has_data_variants {
1679 return gen_tagged_union(enum_def, namespace);
1680 }
1681
1682 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1690 if let Some(ref rename) = v.serde_rename {
1691 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1692 rename != &snake
1693 } else {
1694 false
1695 }
1696 });
1697
1698 let enum_pascal = enum_def.name.to_pascal_case();
1699
1700 let variants: Vec<(String, String)> = enum_def
1702 .variants
1703 .iter()
1704 .map(|v| {
1705 let json_name = v
1706 .serde_rename
1707 .clone()
1708 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1709 let pascal_name = v.name.to_pascal_case();
1710 (json_name, pascal_name)
1711 })
1712 .collect();
1713
1714 let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n");
1715 out.push_str("using System;\n");
1716 out.push_str("using System.Text.Json;\n");
1717 out.push_str("using System.Text.Json.Serialization;\n\n");
1718
1719 out.push_str(&format!("namespace {};\n\n", namespace));
1720
1721 if !enum_def.doc.is_empty() {
1723 out.push_str("/// <summary>\n");
1724 for line in enum_def.doc.lines() {
1725 out.push_str(&format!("/// {}\n", line));
1726 }
1727 out.push_str("/// </summary>\n");
1728 }
1729
1730 if needs_custom_converter {
1731 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1732 }
1733 out.push_str(&format!("public enum {enum_pascal}\n"));
1734 out.push_str("{\n");
1735
1736 for (json_name, pascal_name) in &variants {
1737 if let Some(v) = enum_def
1739 .variants
1740 .iter()
1741 .find(|v| v.name.to_pascal_case() == *pascal_name)
1742 {
1743 if !v.doc.is_empty() {
1744 out.push_str(" /// <summary>\n");
1745 for line in v.doc.lines() {
1746 out.push_str(&format!(" /// {}\n", line));
1747 }
1748 out.push_str(" /// </summary>\n");
1749 }
1750 }
1751 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1752 out.push_str(&format!(" {pascal_name},\n"));
1753 }
1754
1755 out.push_str("}\n");
1756
1757 if needs_custom_converter {
1759 out.push('\n');
1760 out.push_str(&format!(
1761 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1762 ));
1763 out.push_str(&format!(
1764 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1765 ));
1766 out.push_str("{\n");
1767
1768 out.push_str(&format!(
1770 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1771 ));
1772 out.push_str(" {\n");
1773 out.push_str(" var value = reader.GetString();\n");
1774 out.push_str(" return value switch\n");
1775 out.push_str(" {\n");
1776 for (json_name, pascal_name) in &variants {
1777 out.push_str(&format!(
1778 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1779 ));
1780 }
1781 out.push_str(&format!(
1782 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1783 ));
1784 out.push_str(" };\n");
1785 out.push_str(" }\n\n");
1786
1787 out.push_str(&format!(
1789 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1790 ));
1791 out.push_str(" {\n");
1792 out.push_str(" var str = value switch\n");
1793 out.push_str(" {\n");
1794 for (json_name, pascal_name) in &variants {
1795 out.push_str(&format!(
1796 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1797 ));
1798 }
1799 out.push_str(&format!(
1800 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1801 ));
1802 out.push_str(" };\n");
1803 out.push_str(" writer.WriteStringValue(str);\n");
1804 out.push_str(" }\n");
1805 out.push_str("}\n");
1806 }
1807
1808 out
1809}
1810
1811fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1818 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1819 let enum_pascal = enum_def.name.to_pascal_case();
1820 let converter_name = format!("{enum_pascal}JsonConverter");
1821 let ns = namespace;
1824
1825 let mut out = String::from(
1826 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1827 using System;\n\
1828 using System.Collections.Generic;\n\
1829 using System.Text.Json;\n\
1830 using System.Text.Json.Serialization;\n\n",
1831 );
1832 out.push_str(&format!("namespace {};\n\n", namespace));
1833
1834 if !enum_def.doc.is_empty() {
1836 out.push_str("/// <summary>\n");
1837 for line in enum_def.doc.lines() {
1838 out.push_str(&format!("/// {}\n", line));
1839 }
1840 out.push_str("/// </summary>\n");
1841 }
1842
1843 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
1845 out.push_str(&format!("public abstract record {enum_pascal}\n"));
1846 out.push_str("{\n");
1847
1848 for variant in &enum_def.variants {
1850 let pascal = variant.name.to_pascal_case();
1851
1852 if !variant.doc.is_empty() {
1853 out.push_str(" /// <summary>\n");
1854 for line in variant.doc.lines() {
1855 out.push_str(&format!(" /// {}\n", line));
1856 }
1857 out.push_str(" /// </summary>\n");
1858 }
1859
1860 if variant.fields.is_empty() {
1861 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
1863 } else {
1864 let is_copy_ctor_clash = variant.fields.len() == 1 && {
1869 let field_cs_type = csharp_type(&variant.fields[0].ty);
1870 field_cs_type.as_ref() == pascal
1871 };
1872
1873 if is_copy_ctor_clash {
1874 let cs_type = csharp_type(&variant.fields[0].ty);
1875 let qualified_cs_type = format!("global::{ns}.{cs_type}");
1879 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
1880 out.push_str(" {\n");
1881 out.push_str(&format!(
1882 " public required {qualified_cs_type} Value {{ get; init; }}\n"
1883 ));
1884 out.push_str(" }\n\n");
1885 } else {
1886 out.push_str(&format!(" public sealed record {pascal}(\n"));
1888 for (i, field) in variant.fields.iter().enumerate() {
1889 let cs_type = csharp_type(&field.ty);
1890 let cs_type = if field.optional && !cs_type.ends_with('?') {
1891 format!("{cs_type}?")
1892 } else {
1893 cs_type.to_string()
1894 };
1895 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1896 if is_tuple_field(field) {
1897 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1898 } else {
1899 let json_name = field.name.trim_start_matches('_');
1900 let cs_name = to_csharp_name(json_name);
1901 let clashes = cs_name == pascal || cs_name == cs_type;
1902 if clashes {
1903 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1904 } else {
1905 out.push_str(&format!(
1906 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1907 ));
1908 }
1909 }
1910 }
1911 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
1912 }
1913 }
1914 }
1915
1916 out.push_str("}\n\n");
1917
1918 out.push_str(&format!(
1920 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
1921 ));
1922 out.push_str(&format!(
1923 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
1924 ));
1925 out.push_str("{\n");
1926
1927 out.push_str(&format!(
1929 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1930 ));
1931 out.push_str(" {\n");
1932 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
1933 out.push_str(" var root = doc.RootElement;\n");
1934 out.push_str(&format!(
1935 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
1936 ));
1937 out.push_str(&format!(
1938 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
1939 ));
1940 out.push_str(" var tag = tagEl.GetString();\n");
1941 out.push_str(" var json = root.GetRawText();\n");
1942 out.push_str(" return tag switch\n");
1943 out.push_str(" {\n");
1944
1945 for variant in &enum_def.variants {
1946 let discriminator = variant
1947 .serde_rename
1948 .clone()
1949 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1950 let pascal = variant.name.to_pascal_case();
1951 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
1956 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
1957 let f = &variant.fields[0];
1958 let cs_type = csharp_type(&f.ty);
1959 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
1960 cs_name == pascal || cs_name == cs_type
1961 };
1962 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
1963 if is_newtype {
1964 let inner_cs_type = csharp_type(&variant.fields[0].ty);
1965 if inner_cs_type == pascal {
1968 out.push_str(&format!(
1969 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
1970 ));
1971 out.push_str(&format!(
1972 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
1973 ));
1974 } else {
1975 out.push_str(&format!(
1976 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
1977 ));
1978 out.push_str(&format!(
1979 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
1980 ));
1981 out.push_str(&format!(
1982 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
1983 ));
1984 }
1985 } else {
1986 out.push_str(&format!(
1987 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
1988 ));
1989 out.push_str(&format!(
1990 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
1991 ));
1992 }
1993 }
1994
1995 out.push_str(&format!(
1996 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
1997 ));
1998 out.push_str(" };\n");
1999 out.push_str(" }\n\n");
2000
2001 out.push_str(&format!(
2003 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
2004 ));
2005 out.push_str(" {\n");
2006
2007 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
2009 out.push_str(" switch (value)\n");
2010 out.push_str(" {\n");
2011
2012 for variant in &enum_def.variants {
2013 let discriminator = variant
2014 .serde_rename
2015 .clone()
2016 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2017 let pascal = variant.name.to_pascal_case();
2018 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
2022 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
2023 let f = &variant.fields[0];
2024 let cs_type = csharp_type(&f.ty);
2025 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
2026 cs_name == pascal || cs_name == cs_type
2027 };
2028 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
2029 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
2030 out.push_str(" {\n");
2031 if is_newtype {
2032 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
2033 } else {
2034 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
2035 }
2036 out.push_str(" writer.WriteStartObject();\n");
2037 out.push_str(&format!(
2038 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
2039 ));
2040 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2041 out.push_str(&format!(
2042 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2043 ));
2044 out.push_str(" writer.WriteEndObject();\n");
2045 out.push_str(" break;\n");
2046 out.push_str(" }\n");
2047 }
2048
2049 out.push_str(&format!(
2050 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2051 ));
2052 out.push_str(" }\n");
2053 out.push_str(" }\n");
2054 out.push_str("}\n");
2055
2056 out
2057}