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 )),
77 generated_header: true,
78 });
79
80 if !api.errors.is_empty() {
82 for error in &api.errors {
83 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
84 for (class_name, content) in error_files {
85 files.push(GeneratedFile {
86 path: base_path.join(format!("{}.cs", class_name)),
87 content: strip_trailing_whitespace(&content),
88 generated_header: false, });
90 }
91 }
92 }
93
94 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
96 if api.errors.is_empty()
97 || !api
98 .errors
99 .iter()
100 .any(|e| format!("{}Exception", e.name) == exception_class_name)
101 {
102 files.push(GeneratedFile {
103 path: base_path.join(format!("{}.cs", exception_class_name)),
104 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
105 generated_header: true,
106 });
107 }
108
109 let base_class_name = api.crate_name.to_pascal_case();
111 let wrapper_class_name = if namespace == base_class_name {
112 format!("{}Lib", base_class_name)
113 } else {
114 base_class_name
115 };
116 files.push(GeneratedFile {
117 path: base_path.join(format!("{}.cs", wrapper_class_name)),
118 content: strip_trailing_whitespace(&gen_wrapper_class(
119 api,
120 &namespace,
121 &wrapper_class_name,
122 &exception_class_name,
123 &prefix,
124 &bridge_param_names,
125 &bridge_type_aliases,
126 has_visitor_bridge,
127 )),
128 generated_header: true,
129 });
130
131 if has_visitor_bridge {
133 for (filename, content) in crate::gen_visitor::gen_visitor_files(&namespace) {
134 files.push(GeneratedFile {
135 path: base_path.join(filename),
136 content: strip_trailing_whitespace(&content),
137 generated_header: true,
138 });
139 }
140 }
141
142 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
144 if typ.is_opaque {
145 let type_filename = typ.name.to_pascal_case();
146 files.push(GeneratedFile {
147 path: base_path.join(format!("{}.cs", type_filename)),
148 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
149 generated_header: true,
150 });
151 }
152 }
153
154 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
156
157 let complex_enums: HashSet<String> = api
162 .enums
163 .iter()
164 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
165 .map(|e| e.name.to_pascal_case())
166 .collect();
167
168 let custom_converter_enums: HashSet<String> = api
172 .enums
173 .iter()
174 .filter(|e| {
175 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
177 || e.variants.iter().any(|v| {
179 if let Some(ref rename) = v.serde_rename {
180 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
181 rename != &snake
182 } else {
183 false
184 }
185 })
186 })
187 .map(|e| e.name.to_pascal_case())
188 .collect();
189
190 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
192
193 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
195 if !typ.is_opaque {
196 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
199 if !typ.fields.is_empty() && !has_named_fields {
200 continue;
201 }
202
203 let type_filename = typ.name.to_pascal_case();
204 files.push(GeneratedFile {
205 path: base_path.join(format!("{}.cs", type_filename)),
206 content: strip_trailing_whitespace(&gen_record_type(
207 typ,
208 &namespace,
209 &enum_names,
210 &complex_enums,
211 &custom_converter_enums,
212 &lang_rename_all,
213 )),
214 generated_header: true,
215 });
216 }
217 }
218
219 for enum_def in &api.enums {
221 let enum_filename = enum_def.name.to_pascal_case();
222 files.push(GeneratedFile {
223 path: base_path.join(format!("{}.cs", enum_filename)),
224 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
225 generated_header: true,
226 });
227 }
228
229 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
231
232 Ok(files)
233 }
234
235 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
240 Ok(vec![])
242 }
243
244 fn build_config(&self) -> Option<BuildConfig> {
245 Some(BuildConfig {
246 tool: "dotnet",
247 crate_suffix: "",
248 depends_on_ffi: true,
249 post_build: vec![],
250 })
251 }
252}
253
254fn is_tuple_field(field: &FieldDef) -> bool {
256 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
257 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
258}
259
260fn strip_trailing_whitespace(content: &str) -> String {
262 let mut result: String = content
263 .lines()
264 .map(|line| line.trim_end())
265 .collect::<Vec<_>>()
266 .join("\n");
267 if !result.ends_with('\n') {
268 result.push('\n');
269 }
270 result
271}
272
273fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
284 match ty {
285 TypeRef::Unit => "void",
286 TypeRef::Primitive(PrimitiveType::Bool) => "int",
288 TypeRef::Primitive(PrimitiveType::U8) => "byte",
290 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
291 TypeRef::Primitive(PrimitiveType::U32) => "uint",
292 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
293 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
294 TypeRef::Primitive(PrimitiveType::I16) => "short",
295 TypeRef::Primitive(PrimitiveType::I32) => "int",
296 TypeRef::Primitive(PrimitiveType::I64) => "long",
297 TypeRef::Primitive(PrimitiveType::F32) => "float",
298 TypeRef::Primitive(PrimitiveType::F64) => "double",
299 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
300 TypeRef::Primitive(PrimitiveType::Isize) => "long",
301 TypeRef::Duration => "ulong",
303 TypeRef::String
305 | TypeRef::Char
306 | TypeRef::Bytes
307 | TypeRef::Optional(_)
308 | TypeRef::Vec(_)
309 | TypeRef::Map(_, _)
310 | TypeRef::Named(_)
311 | TypeRef::Path
312 | TypeRef::Json => "IntPtr",
313 }
314}
315
316fn returns_string(ty: &TypeRef) -> bool {
318 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
319}
320
321fn returns_bool_via_int(ty: &TypeRef) -> bool {
323 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
324}
325
326fn returns_json_object(ty: &TypeRef) -> bool {
328 matches!(
329 ty,
330 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
331 )
332}
333
334fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
345 match ty {
346 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
347 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
349 TypeRef::Unit => "void",
350 TypeRef::Primitive(PrimitiveType::Bool) => "int",
351 TypeRef::Primitive(PrimitiveType::U8) => "byte",
352 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
353 TypeRef::Primitive(PrimitiveType::U32) => "uint",
354 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
355 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
356 TypeRef::Primitive(PrimitiveType::I16) => "short",
357 TypeRef::Primitive(PrimitiveType::I32) => "int",
358 TypeRef::Primitive(PrimitiveType::I64) => "long",
359 TypeRef::Primitive(PrimitiveType::F32) => "float",
360 TypeRef::Primitive(PrimitiveType::F64) => "double",
361 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
362 TypeRef::Primitive(PrimitiveType::Isize) => "long",
363 TypeRef::Duration => "ulong",
364 }
365}
366
367fn is_bridge_param(
374 param: &alef_core::ir::ParamDef,
375 bridge_param_names: &HashSet<String>,
376 bridge_type_aliases: &HashSet<String>,
377) -> bool {
378 bridge_param_names.contains(¶m.name)
379 || matches!(¶m.ty, alef_core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
380}
381
382fn gen_native_methods(
383 api: &ApiSurface,
384 namespace: &str,
385 lib_name: &str,
386 prefix: &str,
387 bridge_param_names: &HashSet<String>,
388 bridge_type_aliases: &HashSet<String>,
389 has_visitor_bridge: bool,
390) -> String {
391 let mut out = String::from(
392 "// This file is auto-generated by alef. DO NOT EDIT.\n\
393 using System;\n\
394 using System.Runtime.InteropServices;\n\n",
395 );
396
397 out.push_str(&format!("namespace {};\n\n", namespace));
398
399 out.push_str("internal static partial class NativeMethods\n{\n");
400 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
401
402 let mut emitted: HashSet<String> = HashSet::new();
405
406 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
409
410 let mut opaque_param_types: HashSet<String> = HashSet::new();
414 let mut opaque_return_types: HashSet<String> = HashSet::new();
415
416 for func in &api.functions {
417 for param in &func.params {
418 if let TypeRef::Named(name) = ¶m.ty {
419 if !enum_names.contains(name) {
420 opaque_param_types.insert(name.clone());
421 }
422 }
423 }
424 if let TypeRef::Named(name) = &func.return_type {
425 if !enum_names.contains(name) {
426 opaque_return_types.insert(name.clone());
427 }
428 }
429 }
430 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
431 for method in &typ.methods {
432 for param in &method.params {
433 if let TypeRef::Named(name) = ¶m.ty {
434 if !enum_names.contains(name) {
435 opaque_param_types.insert(name.clone());
436 }
437 }
438 }
439 if let TypeRef::Named(name) = &method.return_type {
440 if !enum_names.contains(name) {
441 opaque_return_types.insert(name.clone());
442 }
443 }
444 }
445 }
446
447 let true_opaque_types: HashSet<String> = api
449 .types
450 .iter()
451 .filter(|t| t.is_opaque)
452 .map(|t| t.name.clone())
453 .collect();
454
455 for type_name in &opaque_param_types {
459 let snake = type_name.to_snake_case();
460 if !true_opaque_types.contains(type_name) {
461 let from_json_entry = format!("{prefix}_{snake}_from_json");
462 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
463 if emitted.insert(from_json_entry.clone()) {
464 out.push_str(&format!(
465 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
466 ));
467 out.push_str(&format!(
468 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
469 ));
470 }
471 }
472 let free_entry = format!("{prefix}_{snake}_free");
473 let free_cs = format!("{}Free", type_name.to_pascal_case());
474 if emitted.insert(free_entry.clone()) {
475 out.push_str(&format!(
476 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
477 ));
478 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
479 }
480 }
481
482 for type_name in &opaque_return_types {
485 let snake = type_name.to_snake_case();
486 if !true_opaque_types.contains(type_name) {
487 let to_json_entry = format!("{prefix}_{snake}_to_json");
488 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
489 if emitted.insert(to_json_entry.clone()) {
490 out.push_str(&format!(
491 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
492 ));
493 out.push_str(&format!(
494 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
495 ));
496 }
497 }
498 let free_entry = format!("{prefix}_{snake}_free");
499 let free_cs = format!("{}Free", type_name.to_pascal_case());
500 if emitted.insert(free_entry.clone()) {
501 out.push_str(&format!(
502 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
503 ));
504 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
505 }
506 }
507
508 for func in &api.functions {
510 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
511 if emitted.insert(c_func_name.clone()) {
512 out.push_str(&gen_pinvoke_for_func(
513 &c_func_name,
514 func,
515 bridge_param_names,
516 bridge_type_aliases,
517 ));
518 }
519 }
520
521 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
523 let type_snake = typ.name.to_snake_case();
524 for method in &typ.methods {
525 let c_method_name = format!("{}_{}_{}", prefix, type_snake, method.name.to_lowercase());
526 let cs_method_name = format!("{}{}", typ.name.to_pascal_case(), to_csharp_name(&method.name));
530 if emitted.insert(c_method_name.clone()) {
531 out.push_str(&gen_pinvoke_for_method(&c_method_name, &cs_method_name, method));
532 }
533 }
534 }
535
536 out.push_str(&format!(
538 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
539 ));
540 out.push_str(" internal static extern int LastErrorCode();\n\n");
541
542 out.push_str(&format!(
543 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
544 ));
545 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
546
547 out.push_str(&format!(
548 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
549 ));
550 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
551
552 if has_visitor_bridge {
554 out.push('\n');
555 out.push_str(&crate::gen_visitor::gen_native_methods_visitor(
556 namespace, lib_name, prefix,
557 ));
558 }
559
560 out.push_str("}\n");
561
562 out
563}
564
565fn gen_pinvoke_for_func(
566 c_name: &str,
567 func: &FunctionDef,
568 bridge_param_names: &HashSet<String>,
569 bridge_type_aliases: &HashSet<String>,
570) -> String {
571 let cs_name = to_csharp_name(&func.name);
572 let mut out =
573 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
574 out.push_str(" internal static extern ");
575
576 out.push_str(pinvoke_return_type(&func.return_type));
578
579 out.push_str(&format!(" {}(", cs_name));
580
581 let visible_params: Vec<_> = func
584 .params
585 .iter()
586 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
587 .collect();
588
589 if visible_params.is_empty() {
590 out.push_str(");\n\n");
591 } else {
592 out.push('\n');
593 for (i, param) in visible_params.iter().enumerate() {
594 out.push_str(" ");
595 let pinvoke_ty = pinvoke_param_type(¶m.ty);
596 if pinvoke_ty == "string" {
597 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
598 }
599 let param_name = param.name.to_lower_camel_case();
600 out.push_str(&format!("{pinvoke_ty} {param_name}"));
601
602 if i < visible_params.len() - 1 {
603 out.push(',');
604 }
605 out.push('\n');
606 }
607 out.push_str(" );\n\n");
608 }
609
610 out
611}
612
613fn gen_pinvoke_for_method(c_name: &str, cs_name: &str, method: &MethodDef) -> String {
614 let mut out =
615 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
616 out.push_str(" internal static extern ");
617
618 out.push_str(pinvoke_return_type(&method.return_type));
620
621 out.push_str(&format!(" {}(", cs_name));
622
623 if method.params.is_empty() {
624 out.push_str(");\n\n");
625 } else {
626 out.push('\n');
627 for (i, param) in method.params.iter().enumerate() {
628 out.push_str(" ");
629 let pinvoke_ty = pinvoke_param_type(¶m.ty);
630 if pinvoke_ty == "string" {
631 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
632 }
633 let param_name = param.name.to_lower_camel_case();
634 out.push_str(&format!("{pinvoke_ty} {param_name}"));
635
636 if i < method.params.len() - 1 {
637 out.push(',');
638 }
639 out.push('\n');
640 }
641 out.push_str(" );\n\n");
642 }
643
644 out
645}
646
647fn gen_exception_class(namespace: &str, class_name: &str) -> String {
648 let mut out = String::from(
649 "// This file is auto-generated by alef. DO NOT EDIT.\n\
650 using System;\n\n",
651 );
652
653 out.push_str(&format!("namespace {};\n\n", namespace));
654
655 out.push_str(&format!("public class {} : Exception\n", class_name));
656 out.push_str("{\n");
657 out.push_str(" public int Code { get; }\n\n");
658 out.push_str(&format!(
659 " public {}(int code, string message) : base(message)\n",
660 class_name
661 ));
662 out.push_str(" {\n");
663 out.push_str(" Code = code;\n");
664 out.push_str(" }\n");
665 out.push_str("}\n");
666
667 out
668}
669
670#[allow(clippy::too_many_arguments)]
671fn gen_wrapper_class(
672 api: &ApiSurface,
673 namespace: &str,
674 class_name: &str,
675 exception_name: &str,
676 prefix: &str,
677 bridge_param_names: &HashSet<String>,
678 bridge_type_aliases: &HashSet<String>,
679 has_visitor_bridge: bool,
680) -> String {
681 let mut out = String::from(
682 "// This file is auto-generated by alef. DO NOT EDIT.\n\
683 using System;\n\
684 using System.Collections.Generic;\n\
685 using System.Runtime.InteropServices;\n\
686 using System.Text.Json;\n\
687 using System.Text.Json.Serialization;\n\
688 using System.Threading.Tasks;\n\n",
689 );
690
691 out.push_str(&format!("namespace {};\n\n", namespace));
692
693 out.push_str(&format!("public static class {}\n", class_name));
694 out.push_str("{\n");
695 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
696 out.push_str(" {\n");
697 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
698 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
699 out.push_str(" };\n\n");
700
701 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
703
704 let true_opaque_types: HashSet<String> = api
706 .types
707 .iter()
708 .filter(|t| t.is_opaque)
709 .map(|t| t.name.clone())
710 .collect();
711
712 for func in &api.functions {
714 out.push_str(&gen_wrapper_function(
715 func,
716 exception_name,
717 prefix,
718 &enum_names,
719 &true_opaque_types,
720 bridge_param_names,
721 bridge_type_aliases,
722 ));
723 }
724
725 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
727 if typ.is_opaque {
729 continue;
730 }
731 for method in &typ.methods {
732 out.push_str(&gen_wrapper_method(
733 method,
734 exception_name,
735 prefix,
736 &typ.name,
737 &enum_names,
738 &true_opaque_types,
739 bridge_param_names,
740 bridge_type_aliases,
741 ));
742 }
743 }
744
745 if has_visitor_bridge {
747 out.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(
748 exception_name,
749 prefix,
750 ));
751 }
752
753 out.push_str(" private static ");
755 out.push_str(&format!("{} GetLastError()\n", exception_name));
756 out.push_str(" {\n");
757 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
758 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
759 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
760 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
761 out.push_str(" }\n");
762
763 out.push_str("}\n");
764
765 out
766}
767
768fn emit_named_param_setup(
785 out: &mut String,
786 params: &[alef_core::ir::ParamDef],
787 indent: &str,
788 true_opaque_types: &HashSet<String>,
789) {
790 for param in params {
791 let param_name = param.name.to_lower_camel_case();
792 let json_var = format!("{param_name}Json");
793 let handle_var = format!("{param_name}Handle");
794
795 match ¶m.ty {
796 TypeRef::Named(type_name) => {
797 if true_opaque_types.contains(type_name) {
800 continue;
801 }
802 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
803 if param.optional {
804 out.push_str(&format!(
805 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
806 ));
807 } else {
808 out.push_str(&format!(
809 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
810 ));
811 }
812 out.push_str(&format!(
813 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
814 ));
815 }
816 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
817 out.push_str(&format!(
819 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
820 ));
821 out.push_str(&format!(
822 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
823 ));
824 }
825 _ => {}
826 }
827 }
828}
829
830fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
836 match ty {
837 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
838 let bang = if optional { "!" } else { "" };
840 format!("{param_name}{bang}.Handle")
841 }
842 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
843 format!("{param_name}Handle")
844 }
845 ty => {
846 if optional {
847 let needs_value_unwrap = matches!(ty, TypeRef::Primitive(_) | TypeRef::Duration);
852 if needs_value_unwrap {
853 format!("{param_name}.Value")
854 } else {
855 format!("{param_name}!")
856 }
857 } else {
858 param_name.to_string()
859 }
860 }
861 }
862}
863
864fn emit_named_param_teardown(
869 out: &mut String,
870 params: &[alef_core::ir::ParamDef],
871 true_opaque_types: &HashSet<String>,
872) {
873 for param in params {
874 let param_name = param.name.to_lower_camel_case();
875 let handle_var = format!("{param_name}Handle");
876 match ¶m.ty {
877 TypeRef::Named(type_name) => {
878 if true_opaque_types.contains(type_name) {
879 continue;
881 }
882 let free_method = format!("{}Free", type_name.to_pascal_case());
883 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
884 }
885 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
886 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
887 }
888 _ => {}
889 }
890 }
891}
892
893fn emit_named_param_teardown_indented(
895 out: &mut String,
896 params: &[alef_core::ir::ParamDef],
897 indent: &str,
898 true_opaque_types: &HashSet<String>,
899) {
900 for param in params {
901 let param_name = param.name.to_lower_camel_case();
902 let handle_var = format!("{param_name}Handle");
903 match ¶m.ty {
904 TypeRef::Named(type_name) => {
905 if true_opaque_types.contains(type_name) {
906 continue;
908 }
909 let free_method = format!("{}Free", type_name.to_pascal_case());
910 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
911 }
912 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
913 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
914 }
915 _ => {}
916 }
917 }
918}
919
920fn gen_wrapper_function(
921 func: &FunctionDef,
922 _exception_name: &str,
923 _prefix: &str,
924 enum_names: &HashSet<String>,
925 true_opaque_types: &HashSet<String>,
926 bridge_param_names: &HashSet<String>,
927 bridge_type_aliases: &HashSet<String>,
928) -> String {
929 let mut out = String::with_capacity(1024);
930
931 let visible_params: Vec<alef_core::ir::ParamDef> = func
933 .params
934 .iter()
935 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
936 .cloned()
937 .collect();
938
939 if !func.doc.is_empty() {
941 out.push_str(" /// <summary>\n");
942 for line in func.doc.lines() {
943 out.push_str(&format!(" /// {}\n", line));
944 }
945 out.push_str(" /// </summary>\n");
946 for param in &visible_params {
947 out.push_str(&format!(
948 " /// <param name=\"{}\">{}</param>\n",
949 param.name.to_lower_camel_case(),
950 if param.optional { "Optional." } else { "" }
951 ));
952 }
953 }
954
955 out.push_str(" public static ");
956
957 if func.is_async {
959 if func.return_type == TypeRef::Unit {
960 out.push_str("async Task");
961 } else {
962 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
963 }
964 } else if func.return_type == TypeRef::Unit {
965 out.push_str("void");
966 } else {
967 out.push_str(&csharp_type(&func.return_type));
968 }
969
970 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
971 out.push('(');
972
973 for (i, param) in visible_params.iter().enumerate() {
975 let param_name = param.name.to_lower_camel_case();
976 let mapped = csharp_type(¶m.ty);
977 if param.optional && !mapped.ends_with('?') {
978 out.push_str(&format!("{mapped}? {param_name}"));
979 } else {
980 out.push_str(&format!("{mapped} {param_name}"));
981 }
982
983 if i < visible_params.len() - 1 {
984 out.push_str(", ");
985 }
986 }
987
988 out.push_str(")\n {\n");
989
990 for param in &visible_params {
992 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
993 let param_name = param.name.to_lower_camel_case();
994 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
995 }
996 }
997
998 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1000
1001 let cs_native_name = to_csharp_name(&func.name);
1003
1004 if func.is_async {
1005 out.push_str(" return await Task.Run(() =>\n {\n");
1007
1008 if func.return_type != TypeRef::Unit {
1009 out.push_str(" var result = ");
1010 } else {
1011 out.push_str(" ");
1012 }
1013
1014 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1015
1016 if visible_params.is_empty() {
1017 out.push_str(");\n");
1018 } else {
1019 out.push('\n');
1020 for (i, param) in visible_params.iter().enumerate() {
1021 let param_name = param.name.to_lower_camel_case();
1022 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1023 out.push_str(&format!(" {arg}"));
1024 if i < visible_params.len() - 1 {
1025 out.push(',');
1026 }
1027 out.push('\n');
1028 }
1029 out.push_str(" );\n");
1030 }
1031
1032 if func.return_type != TypeRef::Unit {
1034 out.push_str(
1035 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
1036 );
1037 }
1038
1039 emit_return_marshalling_indented(
1040 &mut out,
1041 &func.return_type,
1042 " ",
1043 enum_names,
1044 true_opaque_types,
1045 );
1046 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1047 emit_return_statement_indented(&mut out, &func.return_type, " ");
1048 out.push_str(" });\n");
1049 } else {
1050 if func.return_type != TypeRef::Unit {
1051 out.push_str(" var result = ");
1052 } else {
1053 out.push_str(" ");
1054 }
1055
1056 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1057
1058 if visible_params.is_empty() {
1059 out.push_str(");\n");
1060 } else {
1061 out.push('\n');
1062 for (i, param) in visible_params.iter().enumerate() {
1063 let param_name = param.name.to_lower_camel_case();
1064 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1065 out.push_str(&format!(" {arg}"));
1066 if i < visible_params.len() - 1 {
1067 out.push(',');
1068 }
1069 out.push('\n');
1070 }
1071 out.push_str(" );\n");
1072 }
1073
1074 if func.return_type != TypeRef::Unit {
1076 out.push_str(
1077 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
1078 );
1079 }
1080
1081 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
1082 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1083 emit_return_statement(&mut out, &func.return_type);
1084 }
1085
1086 out.push_str(" }\n\n");
1087
1088 out
1089}
1090
1091#[allow(clippy::too_many_arguments)]
1092fn gen_wrapper_method(
1093 method: &MethodDef,
1094 _exception_name: &str,
1095 _prefix: &str,
1096 type_name: &str,
1097 enum_names: &HashSet<String>,
1098 true_opaque_types: &HashSet<String>,
1099 bridge_param_names: &HashSet<String>,
1100 bridge_type_aliases: &HashSet<String>,
1101) -> String {
1102 let mut out = String::with_capacity(1024);
1103
1104 let visible_params: Vec<alef_core::ir::ParamDef> = method
1106 .params
1107 .iter()
1108 .filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
1109 .cloned()
1110 .collect();
1111
1112 if !method.doc.is_empty() {
1114 out.push_str(" /// <summary>\n");
1115 for line in method.doc.lines() {
1116 out.push_str(&format!(" /// {}\n", line));
1117 }
1118 out.push_str(" /// </summary>\n");
1119 for param in &visible_params {
1120 out.push_str(&format!(
1121 " /// <param name=\"{}\">{}</param>\n",
1122 param.name.to_lower_camel_case(),
1123 if param.optional { "Optional." } else { "" }
1124 ));
1125 }
1126 }
1127
1128 out.push_str(" public static ");
1130
1131 if method.is_async {
1133 if method.return_type == TypeRef::Unit {
1134 out.push_str("async Task");
1135 } else {
1136 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1137 }
1138 } else if method.return_type == TypeRef::Unit {
1139 out.push_str("void");
1140 } else {
1141 out.push_str(&csharp_type(&method.return_type));
1142 }
1143
1144 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1146 out.push_str(&format!(" {method_cs_name}"));
1147 out.push('(');
1148
1149 for (i, param) in visible_params.iter().enumerate() {
1151 let param_name = param.name.to_lower_camel_case();
1152 let mapped = csharp_type(¶m.ty);
1153 if param.optional && !mapped.ends_with('?') {
1154 out.push_str(&format!("{mapped}? {param_name}"));
1155 } else {
1156 out.push_str(&format!("{mapped} {param_name}"));
1157 }
1158
1159 if i < visible_params.len() - 1 {
1160 out.push_str(", ");
1161 }
1162 }
1163
1164 out.push_str(")\n {\n");
1165
1166 for param in &visible_params {
1168 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1169 let param_name = param.name.to_lower_camel_case();
1170 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1171 }
1172 }
1173
1174 emit_named_param_setup(&mut out, &visible_params, " ", true_opaque_types);
1176
1177 let cs_native_name = format!("{}{}", type_name.to_pascal_case(), to_csharp_name(&method.name));
1182
1183 if method.is_async {
1184 out.push_str(" return await Task.Run(() =>\n {\n");
1186
1187 if method.return_type != TypeRef::Unit {
1188 out.push_str(" var result = ");
1189 } else {
1190 out.push_str(" ");
1191 }
1192
1193 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1194
1195 if visible_params.is_empty() {
1196 out.push_str(");\n");
1197 } else {
1198 out.push('\n');
1199 for (i, param) in visible_params.iter().enumerate() {
1200 let param_name = param.name.to_lower_camel_case();
1201 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1202 out.push_str(&format!(" {arg}"));
1203 if i < visible_params.len() - 1 {
1204 out.push(',');
1205 }
1206 out.push('\n');
1207 }
1208 out.push_str(" );\n");
1209 }
1210
1211 emit_return_marshalling_indented(
1212 &mut out,
1213 &method.return_type,
1214 " ",
1215 enum_names,
1216 true_opaque_types,
1217 );
1218 emit_named_param_teardown_indented(&mut out, &visible_params, " ", true_opaque_types);
1219 emit_return_statement_indented(&mut out, &method.return_type, " ");
1220 out.push_str(" });\n");
1221 } else {
1222 if method.return_type != TypeRef::Unit {
1223 out.push_str(" var result = ");
1224 } else {
1225 out.push_str(" ");
1226 }
1227
1228 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1229
1230 if visible_params.is_empty() {
1231 out.push_str(");\n");
1232 } else {
1233 out.push('\n');
1234 for (i, param) in visible_params.iter().enumerate() {
1235 let param_name = param.name.to_lower_camel_case();
1236 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1237 out.push_str(&format!(" {arg}"));
1238 if i < visible_params.len() - 1 {
1239 out.push(',');
1240 }
1241 out.push('\n');
1242 }
1243 out.push_str(" );\n");
1244 }
1245
1246 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1247 emit_named_param_teardown(&mut out, &visible_params, true_opaque_types);
1248 emit_return_statement(&mut out, &method.return_type);
1249 }
1250
1251 out.push_str(" }\n\n");
1252
1253 out
1254}
1255
1256fn emit_return_marshalling(
1268 out: &mut String,
1269 return_type: &TypeRef,
1270 enum_names: &HashSet<String>,
1271 true_opaque_types: &HashSet<String>,
1272) {
1273 if *return_type == TypeRef::Unit {
1274 return;
1276 }
1277
1278 if returns_string(return_type) {
1279 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1281 out.push_str(" NativeMethods.FreeString(result);\n");
1282 } else if returns_bool_via_int(return_type) {
1283 out.push_str(" var returnValue = result != 0;\n");
1285 } else if let TypeRef::Named(type_name) = return_type {
1286 let pascal = type_name.to_pascal_case();
1287 if true_opaque_types.contains(type_name) {
1288 out.push_str(&format!(" var returnValue = new {pascal}(result);\n"));
1290 } else if !enum_names.contains(&pascal) {
1291 let to_json_method = format!("{pascal}ToJson");
1293 let free_method = format!("{pascal}Free");
1294 let cs_ty = csharp_type(return_type);
1295 out.push_str(&format!(
1296 " var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1297 ));
1298 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1299 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1300 out.push_str(&format!(" NativeMethods.{free_method}(result);\n"));
1301 out.push_str(&format!(
1302 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1303 cs_ty
1304 ));
1305 } else {
1306 let cs_ty = csharp_type(return_type);
1308 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1309 out.push_str(" NativeMethods.FreeString(result);\n");
1310 out.push_str(&format!(
1311 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1312 cs_ty
1313 ));
1314 }
1315 } else if returns_json_object(return_type) {
1316 let cs_ty = csharp_type(return_type);
1318 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1319 out.push_str(" NativeMethods.FreeString(result);\n");
1320 out.push_str(&format!(
1321 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1322 cs_ty
1323 ));
1324 } else {
1325 out.push_str(" var returnValue = result;\n");
1327 }
1328}
1329
1330fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1332 if *return_type != TypeRef::Unit {
1333 out.push_str(" return returnValue;\n");
1334 }
1335}
1336
1337fn emit_return_marshalling_indented(
1342 out: &mut String,
1343 return_type: &TypeRef,
1344 indent: &str,
1345 enum_names: &HashSet<String>,
1346 true_opaque_types: &HashSet<String>,
1347) {
1348 if *return_type == TypeRef::Unit {
1349 return;
1350 }
1351
1352 if returns_string(return_type) {
1353 out.push_str(&format!(
1354 "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1355 ));
1356 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1357 } else if returns_bool_via_int(return_type) {
1358 out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1359 } else if let TypeRef::Named(type_name) = return_type {
1360 let pascal = type_name.to_pascal_case();
1361 if true_opaque_types.contains(type_name) {
1362 out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1364 } else if !enum_names.contains(&pascal) {
1365 let to_json_method = format!("{pascal}ToJson");
1367 let free_method = format!("{pascal}Free");
1368 let cs_ty = csharp_type(return_type);
1369 out.push_str(&format!(
1370 "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1371 ));
1372 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1373 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1374 out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1375 out.push_str(&format!(
1376 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1377 cs_ty
1378 ));
1379 } else {
1380 let cs_ty = csharp_type(return_type);
1382 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1383 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1384 out.push_str(&format!(
1385 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1386 cs_ty
1387 ));
1388 }
1389 } else if returns_json_object(return_type) {
1390 let cs_ty = csharp_type(return_type);
1391 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1392 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1393 out.push_str(&format!(
1394 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1395 cs_ty
1396 ));
1397 } else {
1398 out.push_str(&format!("{indent}var returnValue = result;\n"));
1399 }
1400}
1401
1402fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1404 if *return_type != TypeRef::Unit {
1405 out.push_str(&format!("{indent}return returnValue;\n"));
1406 }
1407}
1408
1409fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1410 let mut out = String::from(
1411 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1412 using System;\n\n",
1413 );
1414
1415 out.push_str(&format!("namespace {};\n\n", namespace));
1416
1417 if !typ.doc.is_empty() {
1419 out.push_str("/// <summary>\n");
1420 for line in typ.doc.lines() {
1421 out.push_str(&format!("/// {}\n", line));
1422 }
1423 out.push_str("/// </summary>\n");
1424 }
1425
1426 let class_name = typ.name.to_pascal_case();
1427 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1428 out.push_str("{\n");
1429 out.push_str(" internal IntPtr Handle { get; }\n\n");
1430 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1431 out.push_str(" {\n");
1432 out.push_str(" Handle = handle;\n");
1433 out.push_str(" }\n\n");
1434 out.push_str(" public void Dispose()\n");
1435 out.push_str(" {\n");
1436 out.push_str(" // Native free will be called by the runtime\n");
1437 out.push_str(" }\n");
1438 out.push_str("}\n");
1439
1440 out
1441}
1442
1443fn gen_record_type(
1444 typ: &TypeDef,
1445 namespace: &str,
1446 enum_names: &HashSet<String>,
1447 complex_enums: &HashSet<String>,
1448 custom_converter_enums: &HashSet<String>,
1449 _lang_rename_all: &str,
1450) -> String {
1451 let mut out = String::from(
1452 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1453 using System;\n\
1454 using System.Collections.Generic;\n\
1455 using System.Text.Json;\n\
1456 using System.Text.Json.Serialization;\n\n",
1457 );
1458
1459 out.push_str(&format!("namespace {};\n\n", namespace));
1460
1461 if !typ.doc.is_empty() {
1463 out.push_str("/// <summary>\n");
1464 for line in typ.doc.lines() {
1465 out.push_str(&format!("/// {}\n", line));
1466 }
1467 out.push_str("/// </summary>\n");
1468 }
1469
1470 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1471 out.push_str("{\n");
1472
1473 for field in &typ.fields {
1474 if is_tuple_field(field) {
1476 continue;
1477 }
1478
1479 if !field.doc.is_empty() {
1481 out.push_str(" /// <summary>\n");
1482 for line in field.doc.lines() {
1483 out.push_str(&format!(" /// {}\n", line));
1484 }
1485 out.push_str(" /// </summary>\n");
1486 }
1487
1488 let field_base_type = match &field.ty {
1492 TypeRef::Named(n) => Some(n.to_pascal_case()),
1493 TypeRef::Optional(inner) => match inner.as_ref() {
1494 TypeRef::Named(n) => Some(n.to_pascal_case()),
1495 _ => None,
1496 },
1497 _ => None,
1498 };
1499 if let Some(ref base) = field_base_type {
1500 if custom_converter_enums.contains(base) {
1501 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1502 }
1503 }
1504
1505 let json_name = field.name.clone();
1509 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1510
1511 let cs_name = to_csharp_name(&field.name);
1512
1513 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1516
1517 if field.optional {
1518 let mapped = if is_complex {
1520 "JsonElement".to_string()
1521 } else {
1522 csharp_type(&field.ty).to_string()
1523 };
1524 let field_type = if mapped.ends_with('?') {
1525 mapped
1526 } else {
1527 format!("{mapped}?")
1528 };
1529 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1530 out.push_str(" = null;\n");
1531 } else if typ.has_default || field.default.is_some() {
1532 let field_type = if is_complex {
1535 "JsonElement".to_string()
1536 } else {
1537 csharp_type(&field.ty).to_string()
1538 };
1539 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1540 use alef_core::ir::DefaultValue;
1541 if matches!(&field.ty, TypeRef::Duration) {
1544 out.push_str(" = null;\n");
1545 out.push('\n');
1546 continue;
1547 }
1548 let default_val = match &field.typed_default {
1549 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1550 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1551 Some(DefaultValue::FloatLiteral(f)) => {
1552 let s = f.to_string();
1553 if s.contains('.') { s } else { format!("{s}.0") }
1554 }
1555 Some(DefaultValue::StringLiteral(s)) => format!("\"{}\"", s.replace('"', "\\\"")),
1556 Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1557 Some(DefaultValue::None) => "null".to_string(),
1558 Some(DefaultValue::Empty) | None => match &field.ty {
1559 TypeRef::Vec(_) => "[]".to_string(),
1560 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1561 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1562 TypeRef::Json => "null".to_string(),
1563 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1564 TypeRef::Primitive(p) => match p {
1565 PrimitiveType::Bool => "false".to_string(),
1566 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1567 _ => "0".to_string(),
1568 },
1569 TypeRef::Named(name) => {
1570 let pascal = name.to_pascal_case();
1571 if enum_names.contains(&pascal) {
1572 "default".to_string()
1573 } else {
1574 "default!".to_string()
1575 }
1576 }
1577 _ => "default!".to_string(),
1578 },
1579 };
1580 out.push_str(&format!(" = {};\n", default_val));
1581 } else {
1582 let field_type = if is_complex {
1586 "JsonElement".to_string()
1587 } else {
1588 csharp_type(&field.ty).to_string()
1589 };
1590 if matches!(&field.ty, TypeRef::Duration) {
1592 out.push_str(&format!(
1593 " public {} {} {{ get; set; }} = null;\n",
1594 field_type, cs_name
1595 ));
1596 } else {
1597 let default_val = match &field.ty {
1598 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1599 TypeRef::Vec(_) => "[]",
1600 TypeRef::Bytes => "Array.Empty<byte>()",
1601 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1602 TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1603 TypeRef::Primitive(_) => "0",
1604 _ => "default!",
1605 };
1606 out.push_str(&format!(
1607 " public {} {} {{ get; set; }} = {};\n",
1608 field_type, cs_name, default_val
1609 ));
1610 }
1611 }
1612
1613 out.push('\n');
1614 }
1615
1616 out.push_str("}\n");
1617
1618 out
1619}
1620
1621fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1623 match rename_all {
1624 Some("snake_case") => name.to_snake_case(),
1625 Some("camelCase") => name.to_lower_camel_case(),
1626 Some("PascalCase") => name.to_pascal_case(),
1627 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1628 Some("lowercase") => name.to_lowercase(),
1629 Some("UPPERCASE") => name.to_uppercase(),
1630 _ => name.to_lowercase(),
1631 }
1632}
1633
1634fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1635 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1636
1637 if enum_def.serde_tag.is_some() && has_data_variants {
1639 return gen_tagged_union(enum_def, namespace);
1640 }
1641
1642 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1650 if let Some(ref rename) = v.serde_rename {
1651 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1652 rename != &snake
1653 } else {
1654 false
1655 }
1656 });
1657
1658 let enum_pascal = enum_def.name.to_pascal_case();
1659
1660 let variants: Vec<(String, String)> = enum_def
1662 .variants
1663 .iter()
1664 .map(|v| {
1665 let json_name = v
1666 .serde_rename
1667 .clone()
1668 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1669 let pascal_name = v.name.to_pascal_case();
1670 (json_name, pascal_name)
1671 })
1672 .collect();
1673
1674 let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n");
1675 out.push_str("using System;\n");
1676 out.push_str("using System.Text.Json;\n");
1677 out.push_str("using System.Text.Json.Serialization;\n\n");
1678
1679 out.push_str(&format!("namespace {};\n\n", namespace));
1680
1681 if !enum_def.doc.is_empty() {
1683 out.push_str("/// <summary>\n");
1684 for line in enum_def.doc.lines() {
1685 out.push_str(&format!("/// {}\n", line));
1686 }
1687 out.push_str("/// </summary>\n");
1688 }
1689
1690 if needs_custom_converter {
1691 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1692 }
1693 out.push_str(&format!("public enum {enum_pascal}\n"));
1694 out.push_str("{\n");
1695
1696 for (json_name, pascal_name) in &variants {
1697 if let Some(v) = enum_def
1699 .variants
1700 .iter()
1701 .find(|v| v.name.to_pascal_case() == *pascal_name)
1702 {
1703 if !v.doc.is_empty() {
1704 out.push_str(" /// <summary>\n");
1705 for line in v.doc.lines() {
1706 out.push_str(&format!(" /// {}\n", line));
1707 }
1708 out.push_str(" /// </summary>\n");
1709 }
1710 }
1711 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1712 out.push_str(&format!(" {pascal_name},\n"));
1713 }
1714
1715 out.push_str("}\n");
1716
1717 if needs_custom_converter {
1719 out.push('\n');
1720 out.push_str(&format!(
1721 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1722 ));
1723 out.push_str(&format!(
1724 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1725 ));
1726 out.push_str("{\n");
1727
1728 out.push_str(&format!(
1730 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1731 ));
1732 out.push_str(" {\n");
1733 out.push_str(" var value = reader.GetString();\n");
1734 out.push_str(" return value switch\n");
1735 out.push_str(" {\n");
1736 for (json_name, pascal_name) in &variants {
1737 out.push_str(&format!(
1738 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1739 ));
1740 }
1741 out.push_str(&format!(
1742 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1743 ));
1744 out.push_str(" };\n");
1745 out.push_str(" }\n\n");
1746
1747 out.push_str(&format!(
1749 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1750 ));
1751 out.push_str(" {\n");
1752 out.push_str(" var str = value switch\n");
1753 out.push_str(" {\n");
1754 for (json_name, pascal_name) in &variants {
1755 out.push_str(&format!(
1756 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1757 ));
1758 }
1759 out.push_str(&format!(
1760 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1761 ));
1762 out.push_str(" };\n");
1763 out.push_str(" writer.WriteStringValue(str);\n");
1764 out.push_str(" }\n");
1765 out.push_str("}\n");
1766 }
1767
1768 out
1769}
1770
1771fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1778 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1779 let enum_pascal = enum_def.name.to_pascal_case();
1780 let converter_name = format!("{enum_pascal}JsonConverter");
1781 let ns = namespace;
1784
1785 let mut out = String::from(
1786 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1787 using System;\n\
1788 using System.Collections.Generic;\n\
1789 using System.Text.Json;\n\
1790 using System.Text.Json.Serialization;\n\n",
1791 );
1792 out.push_str(&format!("namespace {};\n\n", namespace));
1793
1794 if !enum_def.doc.is_empty() {
1796 out.push_str("/// <summary>\n");
1797 for line in enum_def.doc.lines() {
1798 out.push_str(&format!("/// {}\n", line));
1799 }
1800 out.push_str("/// </summary>\n");
1801 }
1802
1803 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
1805 out.push_str(&format!("public abstract record {enum_pascal}\n"));
1806 out.push_str("{\n");
1807
1808 for variant in &enum_def.variants {
1810 let pascal = variant.name.to_pascal_case();
1811
1812 if !variant.doc.is_empty() {
1813 out.push_str(" /// <summary>\n");
1814 for line in variant.doc.lines() {
1815 out.push_str(&format!(" /// {}\n", line));
1816 }
1817 out.push_str(" /// </summary>\n");
1818 }
1819
1820 if variant.fields.is_empty() {
1821 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
1823 } else {
1824 let is_copy_ctor_clash = variant.fields.len() == 1 && {
1829 let field_cs_type = csharp_type(&variant.fields[0].ty);
1830 field_cs_type.as_ref() == pascal
1831 };
1832
1833 if is_copy_ctor_clash {
1834 let cs_type = csharp_type(&variant.fields[0].ty);
1835 let qualified_cs_type = format!("global::{ns}.{cs_type}");
1839 out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
1840 out.push_str(" {\n");
1841 out.push_str(&format!(
1842 " public required {qualified_cs_type} Value {{ get; init; }}\n"
1843 ));
1844 out.push_str(" }\n\n");
1845 } else {
1846 out.push_str(&format!(" public sealed record {pascal}(\n"));
1848 for (i, field) in variant.fields.iter().enumerate() {
1849 let cs_type = csharp_type(&field.ty);
1850 let cs_type = if field.optional && !cs_type.ends_with('?') {
1851 format!("{cs_type}?")
1852 } else {
1853 cs_type.to_string()
1854 };
1855 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1856 if is_tuple_field(field) {
1857 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1858 } else {
1859 let json_name = field.name.trim_start_matches('_');
1860 let cs_name = to_csharp_name(json_name);
1861 let clashes = cs_name == pascal || cs_name == cs_type;
1862 if clashes {
1863 out.push_str(&format!(" {cs_type} Value{comma}\n"));
1864 } else {
1865 out.push_str(&format!(
1866 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1867 ));
1868 }
1869 }
1870 }
1871 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
1872 }
1873 }
1874 }
1875
1876 out.push_str("}\n\n");
1877
1878 out.push_str(&format!(
1880 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
1881 ));
1882 out.push_str(&format!(
1883 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
1884 ));
1885 out.push_str("{\n");
1886
1887 out.push_str(&format!(
1889 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1890 ));
1891 out.push_str(" {\n");
1892 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
1893 out.push_str(" var root = doc.RootElement;\n");
1894 out.push_str(&format!(
1895 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
1896 ));
1897 out.push_str(&format!(
1898 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
1899 ));
1900 out.push_str(" var tag = tagEl.GetString();\n");
1901 out.push_str(" var json = root.GetRawText();\n");
1902 out.push_str(" return tag switch\n");
1903 out.push_str(" {\n");
1904
1905 for variant in &enum_def.variants {
1906 let discriminator = variant
1907 .serde_rename
1908 .clone()
1909 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1910 let pascal = variant.name.to_pascal_case();
1911 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
1916 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
1917 let f = &variant.fields[0];
1918 let cs_type = csharp_type(&f.ty);
1919 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
1920 cs_name == pascal || cs_name == cs_type
1921 };
1922 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
1923 if is_newtype {
1924 let inner_cs_type = csharp_type(&variant.fields[0].ty);
1925 if inner_cs_type == pascal {
1928 out.push_str(&format!(
1929 " \"{discriminator}\" => new {enum_pascal}.{pascal} {{ Value = JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
1930 ));
1931 out.push_str(&format!(
1932 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\") }},\n"
1933 ));
1934 } else {
1935 out.push_str(&format!(
1936 " \"{discriminator}\" => new {enum_pascal}.{pascal}(\n"
1937 ));
1938 out.push_str(&format!(
1939 " JsonSerializer.Deserialize<{inner_cs_type}>(json, options)!\n"
1940 ));
1941 out.push_str(&format!(
1942 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}.Value\")),\n"
1943 ));
1944 }
1945 } else {
1946 out.push_str(&format!(
1947 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
1948 ));
1949 out.push_str(&format!(
1950 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
1951 ));
1952 }
1953 }
1954
1955 out.push_str(&format!(
1956 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
1957 ));
1958 out.push_str(" };\n");
1959 out.push_str(" }\n\n");
1960
1961 out.push_str(&format!(
1963 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1964 ));
1965 out.push_str(" {\n");
1966
1967 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
1969 out.push_str(" switch (value)\n");
1970 out.push_str(" {\n");
1971
1972 for variant in &enum_def.variants {
1973 let discriminator = variant
1974 .serde_rename
1975 .clone()
1976 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1977 let pascal = variant.name.to_pascal_case();
1978 let is_tuple_newtype = variant.fields.len() == 1 && is_tuple_field(&variant.fields[0]);
1982 let is_named_clash_newtype = variant.fields.len() == 1 && !is_tuple_field(&variant.fields[0]) && {
1983 let f = &variant.fields[0];
1984 let cs_type = csharp_type(&f.ty);
1985 let cs_name = to_csharp_name(f.name.trim_start_matches('_'));
1986 cs_name == pascal || cs_name == cs_type
1987 };
1988 let is_newtype = is_tuple_newtype || is_named_clash_newtype;
1989 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
1990 out.push_str(" {\n");
1991 if is_newtype {
1992 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v.Value, options);\n");
1993 } else {
1994 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
1995 }
1996 out.push_str(" writer.WriteStartObject();\n");
1997 out.push_str(&format!(
1998 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
1999 ));
2000 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
2001 out.push_str(&format!(
2002 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
2003 ));
2004 out.push_str(" writer.WriteEndObject();\n");
2005 out.push_str(" break;\n");
2006 out.push_str(" }\n");
2007 }
2008
2009 out.push_str(&format!(
2010 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
2011 ));
2012 out.push_str(" }\n");
2013 out.push_str(" }\n");
2014 out.push_str("}\n");
2015
2016 out
2017}