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