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 output_dir = resolve_output_dir(
42 config.output.csharp.as_ref(),
43 &config.crate_config.name,
44 "packages/csharp/",
45 );
46
47 let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
48
49 let mut files = Vec::new();
50
51 files.push(GeneratedFile {
53 path: base_path.join("NativeMethods.cs"),
54 content: strip_trailing_whitespace(&gen_native_methods(api, &namespace, &lib_name, &prefix)),
55 generated_header: true,
56 });
57
58 if !api.errors.is_empty() {
60 for error in &api.errors {
61 let error_files = alef_codegen::error_gen::gen_csharp_error_types(error, &namespace);
62 for (class_name, content) in error_files {
63 files.push(GeneratedFile {
64 path: base_path.join(format!("{}.cs", class_name)),
65 content: strip_trailing_whitespace(&content),
66 generated_header: false, });
68 }
69 }
70 }
71
72 let exception_class_name = format!("{}Exception", api.crate_name.to_pascal_case());
74 if api.errors.is_empty()
75 || !api
76 .errors
77 .iter()
78 .any(|e| format!("{}Exception", e.name) == exception_class_name)
79 {
80 files.push(GeneratedFile {
81 path: base_path.join(format!("{}.cs", exception_class_name)),
82 content: strip_trailing_whitespace(&gen_exception_class(&namespace, &exception_class_name)),
83 generated_header: true,
84 });
85 }
86
87 let base_class_name = api.crate_name.to_pascal_case();
89 let wrapper_class_name = if namespace == base_class_name {
90 format!("{}Lib", base_class_name)
91 } else {
92 base_class_name
93 };
94 files.push(GeneratedFile {
95 path: base_path.join(format!("{}.cs", wrapper_class_name)),
96 content: strip_trailing_whitespace(&gen_wrapper_class(
97 api,
98 &namespace,
99 &wrapper_class_name,
100 &exception_class_name,
101 &prefix,
102 )),
103 generated_header: true,
104 });
105
106 for typ in &api.types {
108 if typ.is_opaque {
109 let type_filename = typ.name.to_pascal_case();
110 files.push(GeneratedFile {
111 path: base_path.join(format!("{}.cs", type_filename)),
112 content: strip_trailing_whitespace(&gen_opaque_handle(typ, &namespace)),
113 generated_header: true,
114 });
115 }
116 }
117
118 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
120
121 let complex_enums: HashSet<String> = api
126 .enums
127 .iter()
128 .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
129 .map(|e| e.name.to_pascal_case())
130 .collect();
131
132 let custom_converter_enums: HashSet<String> = api
136 .enums
137 .iter()
138 .filter(|e| {
139 (e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty()))
141 || e.variants.iter().any(|v| {
143 if let Some(ref rename) = v.serde_rename {
144 let snake = apply_rename_all(&v.name, e.serde_rename_all.as_deref());
145 rename != &snake
146 } else {
147 false
148 }
149 })
150 })
151 .map(|e| e.name.to_pascal_case())
152 .collect();
153
154 let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
156
157 for typ in &api.types {
159 if !typ.is_opaque {
160 let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
163 if !typ.fields.is_empty() && !has_named_fields {
164 continue;
165 }
166
167 let type_filename = typ.name.to_pascal_case();
168 files.push(GeneratedFile {
169 path: base_path.join(format!("{}.cs", type_filename)),
170 content: strip_trailing_whitespace(&gen_record_type(
171 typ,
172 &namespace,
173 &enum_names,
174 &complex_enums,
175 &custom_converter_enums,
176 &lang_rename_all,
177 )),
178 generated_header: true,
179 });
180 }
181 }
182
183 for enum_def in &api.enums {
185 let enum_filename = enum_def.name.to_pascal_case();
186 files.push(GeneratedFile {
187 path: base_path.join(format!("{}.cs", enum_filename)),
188 content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
189 generated_header: true,
190 });
191 }
192
193 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
195
196 Ok(files)
197 }
198
199 fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
204 Ok(vec![])
206 }
207
208 fn build_config(&self) -> Option<BuildConfig> {
209 Some(BuildConfig {
210 tool: "dotnet",
211 crate_suffix: "",
212 depends_on_ffi: true,
213 post_build: vec![],
214 })
215 }
216}
217
218fn is_tuple_field(field: &FieldDef) -> bool {
220 (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
221 || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
222}
223
224fn strip_trailing_whitespace(content: &str) -> String {
226 let mut result: String = content
227 .lines()
228 .map(|line| line.trim_end())
229 .collect::<Vec<_>>()
230 .join("\n");
231 if !result.ends_with('\n') {
232 result.push('\n');
233 }
234 result
235}
236
237fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
248 match ty {
249 TypeRef::Unit => "void",
250 TypeRef::Primitive(PrimitiveType::Bool) => "int",
252 TypeRef::Primitive(PrimitiveType::U8) => "byte",
254 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
255 TypeRef::Primitive(PrimitiveType::U32) => "uint",
256 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
257 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
258 TypeRef::Primitive(PrimitiveType::I16) => "short",
259 TypeRef::Primitive(PrimitiveType::I32) => "int",
260 TypeRef::Primitive(PrimitiveType::I64) => "long",
261 TypeRef::Primitive(PrimitiveType::F32) => "float",
262 TypeRef::Primitive(PrimitiveType::F64) => "double",
263 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
264 TypeRef::Primitive(PrimitiveType::Isize) => "long",
265 TypeRef::Duration => "ulong",
267 TypeRef::String
269 | TypeRef::Char
270 | TypeRef::Bytes
271 | TypeRef::Optional(_)
272 | TypeRef::Vec(_)
273 | TypeRef::Map(_, _)
274 | TypeRef::Named(_)
275 | TypeRef::Path
276 | TypeRef::Json => "IntPtr",
277 }
278}
279
280fn returns_string(ty: &TypeRef) -> bool {
282 matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
283}
284
285fn returns_bool_via_int(ty: &TypeRef) -> bool {
287 matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
288}
289
290fn returns_json_object(ty: &TypeRef) -> bool {
292 matches!(
293 ty,
294 TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
295 )
296}
297
298fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
309 match ty {
310 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
311 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
313 TypeRef::Unit => "void",
314 TypeRef::Primitive(PrimitiveType::Bool) => "int",
315 TypeRef::Primitive(PrimitiveType::U8) => "byte",
316 TypeRef::Primitive(PrimitiveType::U16) => "ushort",
317 TypeRef::Primitive(PrimitiveType::U32) => "uint",
318 TypeRef::Primitive(PrimitiveType::U64) => "ulong",
319 TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
320 TypeRef::Primitive(PrimitiveType::I16) => "short",
321 TypeRef::Primitive(PrimitiveType::I32) => "int",
322 TypeRef::Primitive(PrimitiveType::I64) => "long",
323 TypeRef::Primitive(PrimitiveType::F32) => "float",
324 TypeRef::Primitive(PrimitiveType::F64) => "double",
325 TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
326 TypeRef::Primitive(PrimitiveType::Isize) => "long",
327 TypeRef::Duration => "ulong",
328 }
329}
330
331fn gen_native_methods(api: &ApiSurface, namespace: &str, lib_name: &str, prefix: &str) -> String {
336 let mut out = String::from(
337 "// This file is auto-generated by alef. DO NOT EDIT.\n\
338 using System.Runtime.InteropServices;\n\n",
339 );
340
341 out.push_str(&format!("namespace {};\n\n", namespace));
342
343 out.push_str("internal static partial class NativeMethods\n{\n");
344 out.push_str(&format!(" private const string LibName = \"{}\";\n\n", lib_name));
345
346 let mut emitted: HashSet<String> = HashSet::new();
349
350 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
353
354 let mut opaque_param_types: HashSet<String> = HashSet::new();
358 let mut opaque_return_types: HashSet<String> = HashSet::new();
359
360 for func in &api.functions {
361 for param in &func.params {
362 if let TypeRef::Named(name) = ¶m.ty {
363 if !enum_names.contains(name) {
364 opaque_param_types.insert(name.clone());
365 }
366 }
367 }
368 if let TypeRef::Named(name) = &func.return_type {
369 if !enum_names.contains(name) {
370 opaque_return_types.insert(name.clone());
371 }
372 }
373 }
374 for typ in &api.types {
375 for method in &typ.methods {
376 for param in &method.params {
377 if let TypeRef::Named(name) = ¶m.ty {
378 if !enum_names.contains(name) {
379 opaque_param_types.insert(name.clone());
380 }
381 }
382 }
383 if let TypeRef::Named(name) = &method.return_type {
384 if !enum_names.contains(name) {
385 opaque_return_types.insert(name.clone());
386 }
387 }
388 }
389 }
390
391 let true_opaque_types: HashSet<String> = api
393 .types
394 .iter()
395 .filter(|t| t.is_opaque)
396 .map(|t| t.name.clone())
397 .collect();
398
399 for type_name in &opaque_param_types {
403 let snake = type_name.to_snake_case();
404 if !true_opaque_types.contains(type_name) {
405 let from_json_entry = format!("{prefix}_{snake}_from_json");
406 let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
407 if emitted.insert(from_json_entry.clone()) {
408 out.push_str(&format!(
409 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
410 ));
411 out.push_str(&format!(
412 " internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
413 ));
414 }
415 }
416 let free_entry = format!("{prefix}_{snake}_free");
417 let free_cs = format!("{}Free", type_name.to_pascal_case());
418 if emitted.insert(free_entry.clone()) {
419 out.push_str(&format!(
420 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
421 ));
422 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
423 }
424 }
425
426 for type_name in &opaque_return_types {
429 let snake = type_name.to_snake_case();
430 if !true_opaque_types.contains(type_name) {
431 let to_json_entry = format!("{prefix}_{snake}_to_json");
432 let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
433 if emitted.insert(to_json_entry.clone()) {
434 out.push_str(&format!(
435 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
436 ));
437 out.push_str(&format!(
438 " internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
439 ));
440 }
441 }
442 let free_entry = format!("{prefix}_{snake}_free");
443 let free_cs = format!("{}Free", type_name.to_pascal_case());
444 if emitted.insert(free_entry.clone()) {
445 out.push_str(&format!(
446 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
447 ));
448 out.push_str(&format!(" internal static extern void {free_cs}(IntPtr ptr);\n\n"));
449 }
450 }
451
452 for func in &api.functions {
454 let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
455 if emitted.insert(c_func_name.clone()) {
456 out.push_str(&gen_pinvoke_for_func(&c_func_name, func));
457 }
458 }
459
460 for typ in &api.types {
462 for method in &typ.methods {
463 let c_method_name = format!("{}_{}", prefix, method.name.to_lowercase());
464 if emitted.insert(c_method_name.clone()) {
465 out.push_str(&gen_pinvoke_for_method(&c_method_name, method));
466 }
467 }
468 }
469
470 out.push_str(&format!(
472 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
473 ));
474 out.push_str(" internal static extern int LastErrorCode();\n\n");
475
476 out.push_str(&format!(
477 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
478 ));
479 out.push_str(" internal static extern IntPtr LastErrorContext();\n\n");
480
481 out.push_str(&format!(
482 " [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
483 ));
484 out.push_str(" internal static extern void FreeString(IntPtr ptr);\n");
485
486 out.push_str("}\n");
487
488 out
489}
490
491fn gen_pinvoke_for_func(c_name: &str, func: &FunctionDef) -> String {
492 let cs_name = to_csharp_name(&func.name);
493 let mut out =
494 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
495 out.push_str(" internal static extern ");
496
497 out.push_str(pinvoke_return_type(&func.return_type));
499
500 out.push_str(&format!(" {}(", cs_name));
501
502 if func.params.is_empty() {
503 out.push_str(");\n\n");
504 } else {
505 out.push('\n');
506 for (i, param) in func.params.iter().enumerate() {
507 out.push_str(" ");
508 let pinvoke_ty = pinvoke_param_type(¶m.ty);
509 if pinvoke_ty == "string" {
510 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
511 }
512 let param_name = param.name.to_lower_camel_case();
513 out.push_str(&format!("{pinvoke_ty} {param_name}"));
514
515 if i < func.params.len() - 1 {
516 out.push(',');
517 }
518 out.push('\n');
519 }
520 out.push_str(" );\n\n");
521 }
522
523 out
524}
525
526fn gen_pinvoke_for_method(c_name: &str, method: &MethodDef) -> String {
527 let cs_name = to_csharp_name(&method.name);
528 let mut out =
529 format!(" [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
530 out.push_str(" internal static extern ");
531
532 out.push_str(pinvoke_return_type(&method.return_type));
534
535 out.push_str(&format!(" {}(", cs_name));
536
537 if method.params.is_empty() {
538 out.push_str(");\n\n");
539 } else {
540 out.push('\n');
541 for (i, param) in method.params.iter().enumerate() {
542 out.push_str(" ");
543 let pinvoke_ty = pinvoke_param_type(¶m.ty);
544 if pinvoke_ty == "string" {
545 out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
546 }
547 let param_name = param.name.to_lower_camel_case();
548 out.push_str(&format!("{pinvoke_ty} {param_name}"));
549
550 if i < method.params.len() - 1 {
551 out.push(',');
552 }
553 out.push('\n');
554 }
555 out.push_str(" );\n\n");
556 }
557
558 out
559}
560
561fn gen_exception_class(namespace: &str, class_name: &str) -> String {
562 let mut out = String::from(
563 "// This file is auto-generated by alef. DO NOT EDIT.\n\
564 using System;\n\n",
565 );
566
567 out.push_str(&format!("namespace {};\n\n", namespace));
568
569 out.push_str(&format!("public class {} : Exception\n", class_name));
570 out.push_str("{\n");
571 out.push_str(" public int Code { get; }\n\n");
572 out.push_str(&format!(
573 " public {}(int code, string message) : base(message)\n",
574 class_name
575 ));
576 out.push_str(" {\n");
577 out.push_str(" Code = code;\n");
578 out.push_str(" }\n");
579 out.push_str("}\n");
580
581 out
582}
583
584fn gen_wrapper_class(
585 api: &ApiSurface,
586 namespace: &str,
587 class_name: &str,
588 exception_name: &str,
589 prefix: &str,
590) -> String {
591 let mut out = String::from(
592 "// This file is auto-generated by alef. DO NOT EDIT.\n\
593 using System;\n\
594 using System.Collections.Generic;\n\
595 using System.Runtime.InteropServices;\n\
596 using System.Text.Json;\n\
597 using System.Text.Json.Serialization;\n\
598 using System.Threading.Tasks;\n\n",
599 );
600
601 out.push_str(&format!("namespace {};\n\n", namespace));
602
603 out.push_str(&format!("public static class {}\n", class_name));
604 out.push_str("{\n");
605 out.push_str(" private static readonly JsonSerializerOptions JsonOptions = new()\n");
606 out.push_str(" {\n");
607 out.push_str(" Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
608 out.push_str(" DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault\n");
609 out.push_str(" };\n\n");
610
611 let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
613
614 let true_opaque_types: HashSet<String> = api
616 .types
617 .iter()
618 .filter(|t| t.is_opaque)
619 .map(|t| t.name.clone())
620 .collect();
621
622 for func in &api.functions {
624 out.push_str(&gen_wrapper_function(
625 func,
626 exception_name,
627 prefix,
628 &enum_names,
629 &true_opaque_types,
630 ));
631 }
632
633 for typ in &api.types {
635 if typ.is_opaque {
637 continue;
638 }
639 for method in &typ.methods {
640 out.push_str(&gen_wrapper_method(
641 method,
642 exception_name,
643 prefix,
644 &typ.name,
645 &enum_names,
646 &true_opaque_types,
647 ));
648 }
649 }
650
651 out.push_str(" private static ");
653 out.push_str(&format!("{} GetLastError()\n", exception_name));
654 out.push_str(" {\n");
655 out.push_str(" var code = NativeMethods.LastErrorCode();\n");
656 out.push_str(" var ctxPtr = NativeMethods.LastErrorContext();\n");
657 out.push_str(" var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
658 out.push_str(&format!(" return new {}(code, message);\n", exception_name));
659 out.push_str(" }\n");
660
661 out.push_str("}\n");
662
663 out
664}
665
666fn emit_named_param_setup(
683 out: &mut String,
684 params: &[alef_core::ir::ParamDef],
685 indent: &str,
686 true_opaque_types: &HashSet<String>,
687) {
688 for param in params {
689 let param_name = param.name.to_lower_camel_case();
690 let json_var = format!("{param_name}Json");
691 let handle_var = format!("{param_name}Handle");
692
693 match ¶m.ty {
694 TypeRef::Named(type_name) => {
695 if true_opaque_types.contains(type_name) {
698 continue;
699 }
700 let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
701 if param.optional {
702 out.push_str(&format!(
703 "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
704 ));
705 } else {
706 out.push_str(&format!(
707 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
708 ));
709 }
710 out.push_str(&format!(
711 "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
712 ));
713 }
714 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
715 out.push_str(&format!(
717 "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
718 ));
719 out.push_str(&format!(
720 "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
721 ));
722 }
723 _ => {}
724 }
725 }
726}
727
728fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
734 match ty {
735 TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
736 let bang = if optional { "!" } else { "" };
738 format!("{param_name}{bang}.Handle")
739 }
740 TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
741 format!("{param_name}Handle")
742 }
743 _ => {
744 let bang = if optional { "!" } else { "" };
745 format!("{param_name}{bang}")
746 }
747 }
748}
749
750fn emit_named_param_teardown(
755 out: &mut String,
756 params: &[alef_core::ir::ParamDef],
757 true_opaque_types: &HashSet<String>,
758) {
759 for param in params {
760 let param_name = param.name.to_lower_camel_case();
761 let handle_var = format!("{param_name}Handle");
762 match ¶m.ty {
763 TypeRef::Named(type_name) => {
764 if true_opaque_types.contains(type_name) {
765 continue;
767 }
768 let free_method = format!("{}Free", type_name.to_pascal_case());
769 out.push_str(&format!(" NativeMethods.{free_method}({handle_var});\n"));
770 }
771 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
772 out.push_str(&format!(" Marshal.FreeHGlobal({handle_var});\n"));
773 }
774 _ => {}
775 }
776 }
777}
778
779fn emit_named_param_teardown_indented(
781 out: &mut String,
782 params: &[alef_core::ir::ParamDef],
783 indent: &str,
784 true_opaque_types: &HashSet<String>,
785) {
786 for param in params {
787 let param_name = param.name.to_lower_camel_case();
788 let handle_var = format!("{param_name}Handle");
789 match ¶m.ty {
790 TypeRef::Named(type_name) => {
791 if true_opaque_types.contains(type_name) {
792 continue;
794 }
795 let free_method = format!("{}Free", type_name.to_pascal_case());
796 out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
797 }
798 TypeRef::Vec(_) | TypeRef::Map(_, _) => {
799 out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
800 }
801 _ => {}
802 }
803 }
804}
805
806fn gen_wrapper_function(
807 func: &FunctionDef,
808 _exception_name: &str,
809 _prefix: &str,
810 enum_names: &HashSet<String>,
811 true_opaque_types: &HashSet<String>,
812) -> String {
813 let mut out = String::with_capacity(1024);
814
815 if !func.doc.is_empty() {
817 out.push_str(" /// <summary>\n");
818 for line in func.doc.lines() {
819 out.push_str(&format!(" /// {}\n", line));
820 }
821 out.push_str(" /// </summary>\n");
822 for param in &func.params {
823 out.push_str(&format!(
824 " /// <param name=\"{}\">{}</param>\n",
825 param.name.to_lower_camel_case(),
826 if param.optional { "Optional." } else { "" }
827 ));
828 }
829 }
830
831 out.push_str(" public static ");
832
833 if func.is_async {
835 if func.return_type == TypeRef::Unit {
836 out.push_str("async Task");
837 } else {
838 out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
839 }
840 } else if func.return_type == TypeRef::Unit {
841 out.push_str("void");
842 } else {
843 out.push_str(&csharp_type(&func.return_type));
844 }
845
846 out.push_str(&format!(" {}", to_csharp_name(&func.name)));
847 out.push('(');
848
849 for (i, param) in func.params.iter().enumerate() {
851 let param_name = param.name.to_lower_camel_case();
852 let mapped = csharp_type(¶m.ty);
853 if param.optional && !mapped.ends_with('?') {
854 out.push_str(&format!("{mapped}? {param_name}"));
855 } else {
856 out.push_str(&format!("{mapped} {param_name}"));
857 }
858
859 if i < func.params.len() - 1 {
860 out.push_str(", ");
861 }
862 }
863
864 out.push_str(")\n {\n");
865
866 for param in &func.params {
868 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
869 let param_name = param.name.to_lower_camel_case();
870 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
871 }
872 }
873
874 emit_named_param_setup(&mut out, &func.params, " ", true_opaque_types);
876
877 let cs_native_name = to_csharp_name(&func.name);
879
880 if func.is_async {
881 out.push_str(" return await Task.Run(() =>\n {\n");
883
884 if func.return_type != TypeRef::Unit {
885 out.push_str(" var result = ");
886 } else {
887 out.push_str(" ");
888 }
889
890 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
891
892 if func.params.is_empty() {
893 out.push_str(");\n");
894 } else {
895 out.push('\n');
896 for (i, param) in func.params.iter().enumerate() {
897 let param_name = param.name.to_lower_camel_case();
898 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
899 out.push_str(&format!(" {arg}"));
900 if i < func.params.len() - 1 {
901 out.push(',');
902 }
903 out.push('\n');
904 }
905 out.push_str(" );\n");
906 }
907
908 if func.return_type != TypeRef::Unit {
910 out.push_str(
911 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
912 );
913 }
914
915 emit_return_marshalling_indented(
916 &mut out,
917 &func.return_type,
918 " ",
919 enum_names,
920 true_opaque_types,
921 );
922 emit_named_param_teardown_indented(&mut out, &func.params, " ", true_opaque_types);
923 emit_return_statement_indented(&mut out, &func.return_type, " ");
924 out.push_str(" });\n");
925 } else {
926 if func.return_type != TypeRef::Unit {
927 out.push_str(" var result = ");
928 } else {
929 out.push_str(" ");
930 }
931
932 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
933
934 if func.params.is_empty() {
935 out.push_str(");\n");
936 } else {
937 out.push('\n');
938 for (i, param) in func.params.iter().enumerate() {
939 let param_name = param.name.to_lower_camel_case();
940 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
941 out.push_str(&format!(" {arg}"));
942 if i < func.params.len() - 1 {
943 out.push(',');
944 }
945 out.push('\n');
946 }
947 out.push_str(" );\n");
948 }
949
950 if func.return_type != TypeRef::Unit {
952 out.push_str(
953 " if (result == IntPtr.Zero) { var err = GetLastError(); if (err.Code != 0) throw err; }\n",
954 );
955 }
956
957 emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
958 emit_named_param_teardown(&mut out, &func.params, true_opaque_types);
959 emit_return_statement(&mut out, &func.return_type);
960 }
961
962 out.push_str(" }\n\n");
963
964 out
965}
966
967fn gen_wrapper_method(
968 method: &MethodDef,
969 _exception_name: &str,
970 _prefix: &str,
971 type_name: &str,
972 enum_names: &HashSet<String>,
973 true_opaque_types: &HashSet<String>,
974) -> String {
975 let mut out = String::with_capacity(1024);
976
977 if !method.doc.is_empty() {
979 out.push_str(" /// <summary>\n");
980 for line in method.doc.lines() {
981 out.push_str(&format!(" /// {}\n", line));
982 }
983 out.push_str(" /// </summary>\n");
984 for param in &method.params {
985 out.push_str(&format!(
986 " /// <param name=\"{}\">{}</param>\n",
987 param.name.to_lower_camel_case(),
988 if param.optional { "Optional." } else { "" }
989 ));
990 }
991 }
992
993 out.push_str(" public static ");
995
996 if method.is_async {
998 if method.return_type == TypeRef::Unit {
999 out.push_str("async Task");
1000 } else {
1001 out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
1002 }
1003 } else if method.return_type == TypeRef::Unit {
1004 out.push_str("void");
1005 } else {
1006 out.push_str(&csharp_type(&method.return_type));
1007 }
1008
1009 let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
1011 out.push_str(&format!(" {method_cs_name}"));
1012 out.push('(');
1013
1014 for (i, param) in method.params.iter().enumerate() {
1016 let param_name = param.name.to_lower_camel_case();
1017 let mapped = csharp_type(¶m.ty);
1018 if param.optional && !mapped.ends_with('?') {
1019 out.push_str(&format!("{mapped}? {param_name}"));
1020 } else {
1021 out.push_str(&format!("{mapped} {param_name}"));
1022 }
1023
1024 if i < method.params.len() - 1 {
1025 out.push_str(", ");
1026 }
1027 }
1028
1029 out.push_str(")\n {\n");
1030
1031 for param in &method.params {
1033 if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
1034 let param_name = param.name.to_lower_camel_case();
1035 out.push_str(&format!(" ArgumentNullException.ThrowIfNull({param_name});\n"));
1036 }
1037 }
1038
1039 emit_named_param_setup(&mut out, &method.params, " ", true_opaque_types);
1041
1042 let cs_native_name = to_csharp_name(&method.name);
1044
1045 if method.is_async {
1046 out.push_str(" return await Task.Run(() =>\n {\n");
1048
1049 if method.return_type != TypeRef::Unit {
1050 out.push_str(" var result = ");
1051 } else {
1052 out.push_str(" ");
1053 }
1054
1055 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1056
1057 if method.params.is_empty() {
1058 out.push_str(");\n");
1059 } else {
1060 out.push('\n');
1061 for (i, param) in method.params.iter().enumerate() {
1062 let param_name = param.name.to_lower_camel_case();
1063 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1064 out.push_str(&format!(" {arg}"));
1065 if i < method.params.len() - 1 {
1066 out.push(',');
1067 }
1068 out.push('\n');
1069 }
1070 out.push_str(" );\n");
1071 }
1072
1073 emit_return_marshalling_indented(
1074 &mut out,
1075 &method.return_type,
1076 " ",
1077 enum_names,
1078 true_opaque_types,
1079 );
1080 emit_named_param_teardown_indented(&mut out, &method.params, " ", true_opaque_types);
1081 emit_return_statement_indented(&mut out, &method.return_type, " ");
1082 out.push_str(" });\n");
1083 } else {
1084 if method.return_type != TypeRef::Unit {
1085 out.push_str(" var result = ");
1086 } else {
1087 out.push_str(" ");
1088 }
1089
1090 out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1091
1092 if method.params.is_empty() {
1093 out.push_str(");\n");
1094 } else {
1095 out.push('\n');
1096 for (i, param) in method.params.iter().enumerate() {
1097 let param_name = param.name.to_lower_camel_case();
1098 let arg = native_call_arg(¶m.ty, ¶m_name, param.optional, true_opaque_types);
1099 out.push_str(&format!(" {arg}"));
1100 if i < method.params.len() - 1 {
1101 out.push(',');
1102 }
1103 out.push('\n');
1104 }
1105 out.push_str(" );\n");
1106 }
1107
1108 emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1109 emit_named_param_teardown(&mut out, &method.params, true_opaque_types);
1110 emit_return_statement(&mut out, &method.return_type);
1111 }
1112
1113 out.push_str(" }\n\n");
1114
1115 out
1116}
1117
1118fn emit_return_marshalling(
1130 out: &mut String,
1131 return_type: &TypeRef,
1132 enum_names: &HashSet<String>,
1133 true_opaque_types: &HashSet<String>,
1134) {
1135 if *return_type == TypeRef::Unit {
1136 return;
1138 }
1139
1140 if returns_string(return_type) {
1141 out.push_str(" var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1143 out.push_str(" NativeMethods.FreeString(result);\n");
1144 } else if returns_bool_via_int(return_type) {
1145 out.push_str(" var returnValue = result != 0;\n");
1147 } else if let TypeRef::Named(type_name) = return_type {
1148 let pascal = type_name.to_pascal_case();
1149 if true_opaque_types.contains(type_name) {
1150 out.push_str(&format!(" var returnValue = new {pascal}(result);\n"));
1152 } else if !enum_names.contains(&pascal) {
1153 let to_json_method = format!("{pascal}ToJson");
1155 let free_method = format!("{pascal}Free");
1156 let cs_ty = csharp_type(return_type);
1157 out.push_str(&format!(
1158 " var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1159 ));
1160 out.push_str(" var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1161 out.push_str(" NativeMethods.FreeString(jsonPtr);\n");
1162 out.push_str(&format!(" NativeMethods.{free_method}(result);\n"));
1163 out.push_str(&format!(
1164 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1165 cs_ty
1166 ));
1167 } else {
1168 let cs_ty = csharp_type(return_type);
1170 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1171 out.push_str(" NativeMethods.FreeString(result);\n");
1172 out.push_str(&format!(
1173 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1174 cs_ty
1175 ));
1176 }
1177 } else if returns_json_object(return_type) {
1178 let cs_ty = csharp_type(return_type);
1180 out.push_str(" var json = Marshal.PtrToStringUTF8(result);\n");
1181 out.push_str(" NativeMethods.FreeString(result);\n");
1182 out.push_str(&format!(
1183 " var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1184 cs_ty
1185 ));
1186 } else {
1187 out.push_str(" var returnValue = result;\n");
1189 }
1190}
1191
1192fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1194 if *return_type != TypeRef::Unit {
1195 out.push_str(" return returnValue;\n");
1196 }
1197}
1198
1199fn emit_return_marshalling_indented(
1204 out: &mut String,
1205 return_type: &TypeRef,
1206 indent: &str,
1207 enum_names: &HashSet<String>,
1208 true_opaque_types: &HashSet<String>,
1209) {
1210 if *return_type == TypeRef::Unit {
1211 return;
1212 }
1213
1214 if returns_string(return_type) {
1215 out.push_str(&format!(
1216 "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1217 ));
1218 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1219 } else if returns_bool_via_int(return_type) {
1220 out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1221 } else if let TypeRef::Named(type_name) = return_type {
1222 let pascal = type_name.to_pascal_case();
1223 if true_opaque_types.contains(type_name) {
1224 out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1226 } else if !enum_names.contains(&pascal) {
1227 let to_json_method = format!("{pascal}ToJson");
1229 let free_method = format!("{pascal}Free");
1230 let cs_ty = csharp_type(return_type);
1231 out.push_str(&format!(
1232 "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1233 ));
1234 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1235 out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1236 out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1237 out.push_str(&format!(
1238 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1239 cs_ty
1240 ));
1241 } else {
1242 let cs_ty = csharp_type(return_type);
1244 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1245 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1246 out.push_str(&format!(
1247 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1248 cs_ty
1249 ));
1250 }
1251 } else if returns_json_object(return_type) {
1252 let cs_ty = csharp_type(return_type);
1253 out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1254 out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1255 out.push_str(&format!(
1256 "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1257 cs_ty
1258 ));
1259 } else {
1260 out.push_str(&format!("{indent}var returnValue = result;\n"));
1261 }
1262}
1263
1264fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1266 if *return_type != TypeRef::Unit {
1267 out.push_str(&format!("{indent}return returnValue;\n"));
1268 }
1269}
1270
1271fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1272 let mut out = String::from(
1273 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1274 using System;\n\n",
1275 );
1276
1277 out.push_str(&format!("namespace {};\n\n", namespace));
1278
1279 if !typ.doc.is_empty() {
1281 out.push_str("/// <summary>\n");
1282 for line in typ.doc.lines() {
1283 out.push_str(&format!("/// {}\n", line));
1284 }
1285 out.push_str("/// </summary>\n");
1286 }
1287
1288 let class_name = typ.name.to_pascal_case();
1289 out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1290 out.push_str("{\n");
1291 out.push_str(" internal IntPtr Handle { get; }\n\n");
1292 out.push_str(&format!(" internal {}(IntPtr handle)\n", class_name));
1293 out.push_str(" {\n");
1294 out.push_str(" Handle = handle;\n");
1295 out.push_str(" }\n\n");
1296 out.push_str(" public void Dispose()\n");
1297 out.push_str(" {\n");
1298 out.push_str(" // Native free will be called by the runtime\n");
1299 out.push_str(" }\n");
1300 out.push_str("}\n");
1301
1302 out
1303}
1304
1305fn gen_record_type(
1306 typ: &TypeDef,
1307 namespace: &str,
1308 enum_names: &HashSet<String>,
1309 complex_enums: &HashSet<String>,
1310 custom_converter_enums: &HashSet<String>,
1311 _lang_rename_all: &str,
1312) -> String {
1313 let mut out = String::from(
1314 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1315 using System;\n\
1316 using System.Collections.Generic;\n\
1317 using System.Text.Json;\n\
1318 using System.Text.Json.Serialization;\n\n",
1319 );
1320
1321 out.push_str(&format!("namespace {};\n\n", namespace));
1322
1323 if !typ.doc.is_empty() {
1325 out.push_str("/// <summary>\n");
1326 for line in typ.doc.lines() {
1327 out.push_str(&format!("/// {}\n", line));
1328 }
1329 out.push_str("/// </summary>\n");
1330 }
1331
1332 out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1333 out.push_str("{\n");
1334
1335 for field in &typ.fields {
1336 if is_tuple_field(field) {
1338 continue;
1339 }
1340
1341 if !field.doc.is_empty() {
1343 out.push_str(" /// <summary>\n");
1344 for line in field.doc.lines() {
1345 out.push_str(&format!(" /// {}\n", line));
1346 }
1347 out.push_str(" /// </summary>\n");
1348 }
1349
1350 let field_base_type = match &field.ty {
1354 TypeRef::Named(n) => Some(n.to_pascal_case()),
1355 TypeRef::Optional(inner) => match inner.as_ref() {
1356 TypeRef::Named(n) => Some(n.to_pascal_case()),
1357 _ => None,
1358 },
1359 _ => None,
1360 };
1361 if let Some(ref base) = field_base_type {
1362 if custom_converter_enums.contains(base) {
1363 out.push_str(&format!(" [JsonConverter(typeof({base}JsonConverter))]\n"));
1364 }
1365 }
1366
1367 let json_name = field.name.clone();
1371 out.push_str(&format!(" [JsonPropertyName(\"{}\")]\n", json_name));
1372
1373 let cs_name = to_csharp_name(&field.name);
1374
1375 let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1378
1379 if field.optional {
1380 let mapped = if is_complex {
1382 "JsonElement".to_string()
1383 } else {
1384 csharp_type(&field.ty).to_string()
1385 };
1386 let field_type = if mapped.ends_with('?') {
1387 mapped
1388 } else {
1389 format!("{mapped}?")
1390 };
1391 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1392 out.push_str(" = null;\n");
1393 } else if typ.has_default || field.default.is_some() {
1394 let field_type = if is_complex {
1397 "JsonElement".to_string()
1398 } else {
1399 csharp_type(&field.ty).to_string()
1400 };
1401 out.push_str(&format!(" public {} {} {{ get; set; }}", field_type, cs_name));
1402 use alef_core::ir::DefaultValue;
1403 if matches!(&field.ty, TypeRef::Duration) {
1406 out.push_str(" = null;\n");
1407 out.push('\n');
1408 continue;
1409 }
1410 let default_val = match &field.typed_default {
1411 Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1412 Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1413 Some(DefaultValue::FloatLiteral(f)) => {
1414 let s = f.to_string();
1415 if s.contains('.') { s } else { format!("{s}.0") }
1416 }
1417 Some(DefaultValue::StringLiteral(s)) => format!("\"{}\"", s.replace('"', "\\\"")),
1418 Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1419 Some(DefaultValue::None) => "null".to_string(),
1420 Some(DefaultValue::Empty) | None => match &field.ty {
1421 TypeRef::Vec(_) => "[]".to_string(),
1422 TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1423 TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
1424 TypeRef::Json => "null".to_string(),
1425 TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1426 TypeRef::Primitive(p) => match p {
1427 PrimitiveType::Bool => "false".to_string(),
1428 PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1429 _ => "0".to_string(),
1430 },
1431 TypeRef::Named(name) => {
1432 let pascal = name.to_pascal_case();
1433 if enum_names.contains(&pascal) {
1434 "default".to_string()
1435 } else {
1436 "default!".to_string()
1437 }
1438 }
1439 _ => "default!".to_string(),
1440 },
1441 };
1442 out.push_str(&format!(" = {};\n", default_val));
1443 } else {
1444 let field_type = if is_complex {
1448 "JsonElement".to_string()
1449 } else {
1450 csharp_type(&field.ty).to_string()
1451 };
1452 if matches!(&field.ty, TypeRef::Duration) {
1454 out.push_str(&format!(
1455 " public {} {} {{ get; set; }} = null;\n",
1456 field_type, cs_name
1457 ));
1458 } else {
1459 let default_val = match &field.ty {
1460 TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1461 TypeRef::Vec(_) => "[]",
1462 TypeRef::Bytes => "Array.Empty<byte>()",
1463 TypeRef::Primitive(PrimitiveType::Bool) => "false",
1464 TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1465 TypeRef::Primitive(_) => "0",
1466 _ => "default!",
1467 };
1468 out.push_str(&format!(
1469 " public {} {} {{ get; set; }} = {};\n",
1470 field_type, cs_name, default_val
1471 ));
1472 }
1473 }
1474
1475 out.push('\n');
1476 }
1477
1478 out.push_str("}\n");
1479
1480 out
1481}
1482
1483fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1485 match rename_all {
1486 Some("snake_case") => name.to_snake_case(),
1487 Some("camelCase") => name.to_lower_camel_case(),
1488 Some("PascalCase") => name.to_pascal_case(),
1489 Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1490 Some("lowercase") => name.to_lowercase(),
1491 Some("UPPERCASE") => name.to_uppercase(),
1492 _ => name.to_lowercase(),
1493 }
1494}
1495
1496fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1497 let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1498
1499 if enum_def.serde_tag.is_some() && has_data_variants {
1501 return gen_tagged_union(enum_def, namespace);
1502 }
1503
1504 let needs_custom_converter = enum_def.variants.iter().any(|v| {
1512 if let Some(ref rename) = v.serde_rename {
1513 let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
1514 rename != &snake
1515 } else {
1516 false
1517 }
1518 });
1519
1520 let enum_pascal = enum_def.name.to_pascal_case();
1521
1522 let variants: Vec<(String, String)> = enum_def
1524 .variants
1525 .iter()
1526 .map(|v| {
1527 let json_name = v
1528 .serde_rename
1529 .clone()
1530 .unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
1531 let pascal_name = v.name.to_pascal_case();
1532 (json_name, pascal_name)
1533 })
1534 .collect();
1535
1536 let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n");
1537 out.push_str("using System.Text.Json;\n");
1538 out.push_str("using System.Text.Json.Serialization;\n\n");
1539
1540 out.push_str(&format!("namespace {};\n\n", namespace));
1541
1542 if !enum_def.doc.is_empty() {
1544 out.push_str("/// <summary>\n");
1545 for line in enum_def.doc.lines() {
1546 out.push_str(&format!("/// {}\n", line));
1547 }
1548 out.push_str("/// </summary>\n");
1549 }
1550
1551 if needs_custom_converter {
1552 out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
1553 }
1554 out.push_str(&format!("public enum {enum_pascal}\n"));
1555 out.push_str("{\n");
1556
1557 for (json_name, pascal_name) in &variants {
1558 if let Some(v) = enum_def
1560 .variants
1561 .iter()
1562 .find(|v| v.name.to_pascal_case() == *pascal_name)
1563 {
1564 if !v.doc.is_empty() {
1565 out.push_str(" /// <summary>\n");
1566 for line in v.doc.lines() {
1567 out.push_str(&format!(" /// {}\n", line));
1568 }
1569 out.push_str(" /// </summary>\n");
1570 }
1571 }
1572 out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
1573 out.push_str(&format!(" {pascal_name},\n"));
1574 }
1575
1576 out.push_str("}\n");
1577
1578 if needs_custom_converter {
1580 out.push('\n');
1581 out.push_str(&format!(
1582 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
1583 ));
1584 out.push_str(&format!(
1585 "internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
1586 ));
1587 out.push_str("{\n");
1588
1589 out.push_str(&format!(
1591 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1592 ));
1593 out.push_str(" {\n");
1594 out.push_str(" var value = reader.GetString();\n");
1595 out.push_str(" return value switch\n");
1596 out.push_str(" {\n");
1597 for (json_name, pascal_name) in &variants {
1598 out.push_str(&format!(
1599 " \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
1600 ));
1601 }
1602 out.push_str(&format!(
1603 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1604 ));
1605 out.push_str(" };\n");
1606 out.push_str(" }\n\n");
1607
1608 out.push_str(&format!(
1610 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1611 ));
1612 out.push_str(" {\n");
1613 out.push_str(" var str = value switch\n");
1614 out.push_str(" {\n");
1615 for (json_name, pascal_name) in &variants {
1616 out.push_str(&format!(
1617 " {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
1618 ));
1619 }
1620 out.push_str(&format!(
1621 " _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
1622 ));
1623 out.push_str(" };\n");
1624 out.push_str(" writer.WriteStringValue(str);\n");
1625 out.push_str(" }\n");
1626 out.push_str("}\n");
1627 }
1628
1629 out
1630}
1631
1632fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1639 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1640 let enum_pascal = enum_def.name.to_pascal_case();
1641 let converter_name = format!("{enum_pascal}JsonConverter");
1642
1643 let mut out = String::from(
1644 "// This file is auto-generated by alef. DO NOT EDIT.\n\
1645 using System.Collections.Generic;\n\
1646 using System.Text.Json;\n\
1647 using System.Text.Json.Serialization;\n\n",
1648 );
1649 out.push_str(&format!("namespace {};\n\n", namespace));
1650
1651 if !enum_def.doc.is_empty() {
1653 out.push_str("/// <summary>\n");
1654 for line in enum_def.doc.lines() {
1655 out.push_str(&format!("/// {}\n", line));
1656 }
1657 out.push_str("/// </summary>\n");
1658 }
1659
1660 out.push_str(&format!("[JsonConverter(typeof({converter_name}))]\n"));
1662 out.push_str(&format!("public abstract record {enum_pascal}\n"));
1663 out.push_str("{\n");
1664
1665 for variant in &enum_def.variants {
1667 let pascal = variant.name.to_pascal_case();
1668
1669 if !variant.doc.is_empty() {
1670 out.push_str(" /// <summary>\n");
1671 for line in variant.doc.lines() {
1672 out.push_str(&format!(" /// {}\n", line));
1673 }
1674 out.push_str(" /// </summary>\n");
1675 }
1676
1677 if variant.fields.is_empty() {
1678 out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
1680 } else {
1681 out.push_str(&format!(" public sealed record {pascal}(\n"));
1683 for (i, field) in variant.fields.iter().enumerate() {
1684 let json_name = field.name.trim_start_matches('_');
1685 let cs_type = csharp_type(&field.ty);
1686 let cs_type = if field.optional && !cs_type.ends_with('?') {
1687 format!("{cs_type}?")
1688 } else {
1689 cs_type.to_string()
1690 };
1691 let cs_name = to_csharp_name(json_name);
1692 let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1693 out.push_str(&format!(
1694 " [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1695 ));
1696 }
1697 out.push_str(&format!(" ) : {enum_pascal};\n\n"));
1698 }
1699 }
1700
1701 out.push_str("}\n\n");
1702
1703 out.push_str(&format!(
1705 "/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that reads the \"{tag_field}\" discriminator from any position.</summary>\n"
1706 ));
1707 out.push_str(&format!(
1708 "internal sealed class {converter_name} : JsonConverter<{enum_pascal}>\n"
1709 ));
1710 out.push_str("{\n");
1711
1712 out.push_str(&format!(
1714 " public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
1715 ));
1716 out.push_str(" {\n");
1717 out.push_str(" using var doc = JsonDocument.ParseValue(ref reader);\n");
1718 out.push_str(" var root = doc.RootElement;\n");
1719 out.push_str(&format!(
1720 " if (!root.TryGetProperty(\"{tag_field}\", out var tagEl))\n"
1721 ));
1722 out.push_str(&format!(
1723 " throw new JsonException(\"{enum_pascal}: missing \\\"{tag_field}\\\" discriminator\");\n"
1724 ));
1725 out.push_str(" var tag = tagEl.GetString();\n");
1726 out.push_str(" var json = root.GetRawText();\n");
1727 out.push_str(" return tag switch\n");
1728 out.push_str(" {\n");
1729
1730 for variant in &enum_def.variants {
1731 let discriminator = variant
1732 .serde_rename
1733 .clone()
1734 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1735 let pascal = variant.name.to_pascal_case();
1736 out.push_str(&format!(
1737 " \"{discriminator}\" => JsonSerializer.Deserialize<{enum_pascal}.{pascal}>(json, options)!\n"
1738 ));
1739 out.push_str(&format!(
1740 " ?? throw new JsonException(\"Failed to deserialize {enum_pascal}.{pascal}\"),\n"
1741 ));
1742 }
1743
1744 out.push_str(&format!(
1745 " _ => throw new JsonException($\"Unknown {enum_pascal} discriminator: {{tag}}\")\n"
1746 ));
1747 out.push_str(" };\n");
1748 out.push_str(" }\n\n");
1749
1750 out.push_str(&format!(
1752 " public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
1753 ));
1754 out.push_str(" {\n");
1755
1756 out.push_str(" // Serialize the concrete type, then inject the discriminator\n");
1758 out.push_str(" switch (value)\n");
1759 out.push_str(" {\n");
1760
1761 for variant in &enum_def.variants {
1762 let discriminator = variant
1763 .serde_rename
1764 .clone()
1765 .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1766 let pascal = variant.name.to_pascal_case();
1767 out.push_str(&format!(" case {enum_pascal}.{pascal} v:\n"));
1768 out.push_str(" {\n");
1769 out.push_str(" var doc = JsonSerializer.SerializeToDocument(v, options);\n");
1770 out.push_str(" writer.WriteStartObject();\n");
1771 out.push_str(&format!(
1772 " writer.WriteString(\"{tag_field}\", \"{discriminator}\");\n"
1773 ));
1774 out.push_str(" foreach (var prop in doc.RootElement.EnumerateObject())\n");
1775 out.push_str(&format!(
1776 " if (prop.Name != \"{tag_field}\") prop.WriteTo(writer);\n"
1777 ));
1778 out.push_str(" writer.WriteEndObject();\n");
1779 out.push_str(" break;\n");
1780 out.push_str(" }\n");
1781 }
1782
1783 out.push_str(&format!(
1784 " default: throw new JsonException($\"Unknown {enum_pascal} subtype: {{value.GetType().Name}}\");\n"
1785 ));
1786 out.push_str(" }\n");
1787 out.push_str(" }\n");
1788 out.push_str("}\n");
1789
1790 out
1791}