Skip to main content

alef_backend_csharp/
gen_bindings.rs

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    // lib_name comes from config.ffi_lib_name()
14}
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        // 1. Generate NativeMethods.cs
52        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        // 2. Generate error types from thiserror enums (if any), otherwise generic exception
59        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, // already has header
67                    });
68                }
69            }
70        }
71
72        // Fallback generic exception class (always generated for GetLastError)
73        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        // 3. Generate main wrapper class
88        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        // 4. Generate opaque handle classes
107        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        // Collect enum names so record generation can distinguish enum fields from class fields.
119        let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
120
121        // Collect complex enums (enums with data variants and no serde tag) — these can't be
122        // simple C# enums and should be represented as JsonElement for flexible deserialization.
123        // Tagged unions (serde_tag is set) are now generated as proper abstract records
124        // and can be deserialized as their concrete types, so they are NOT complex_enums.
125        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        // Resolve the language-level serde rename_all strategy (always wins over IR type-level).
133        let lang_rename_all = config.serde_rename_all_for_language(Language::Csharp);
134
135        // 5. Generate record types (structs)
136        for typ in &api.types {
137            if !typ.is_opaque {
138                // Skip types where all fields are unnamed tuple positions — they have no
139                // meaningful properties to expose in C#.
140                let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
141                if !typ.fields.is_empty() && !has_named_fields {
142                    continue;
143                }
144
145                let type_filename = typ.name.to_pascal_case();
146                files.push(GeneratedFile {
147                    path: base_path.join(format!("{}.cs", type_filename)),
148                    content: strip_trailing_whitespace(&gen_record_type(
149                        typ,
150                        &namespace,
151                        &enum_names,
152                        &complex_enums,
153                        &lang_rename_all,
154                    )),
155                    generated_header: true,
156                });
157            }
158        }
159
160        // 6. Generate enums
161        for enum_def in &api.enums {
162            let enum_filename = enum_def.name.to_pascal_case();
163            files.push(GeneratedFile {
164                path: base_path.join(format!("{}.cs", enum_filename)),
165                content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
166                generated_header: true,
167            });
168        }
169
170        // Build adapter body map (consumed by generators via body substitution)
171        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
172
173        Ok(files)
174    }
175
176    /// C# wrapper class is already the public API.
177    /// The `gen_wrapper_class` (generated in `generate_bindings`) provides high-level public methods
178    /// that wrap NativeMethods (P/Invoke), marshal types, and handle errors.
179    /// No additional facade is needed.
180    fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
181        // C#'s wrapper class IS the public API — no additional wrapper needed.
182        Ok(vec![])
183    }
184
185    fn build_config(&self) -> Option<BuildConfig> {
186        Some(BuildConfig {
187            tool: "dotnet",
188            crate_suffix: "",
189            depends_on_ffi: true,
190            post_build: vec![],
191        })
192    }
193}
194
195/// Returns true if a field is a tuple struct positional field (e.g., `_0`, `_1`, `0`, `1`).
196fn is_tuple_field(field: &FieldDef) -> bool {
197    (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
198        || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
199}
200
201/// Strip trailing whitespace from every line and ensure the file ends with a single newline.
202fn strip_trailing_whitespace(content: &str) -> String {
203    let mut result: String = content
204        .lines()
205        .map(|line| line.trim_end())
206        .collect::<Vec<_>>()
207        .join("\n");
208    if !result.ends_with('\n') {
209        result.push('\n');
210    }
211    result
212}
213
214// ---------------------------------------------------------------------------
215// Helpers: P/Invoke return type mapping
216// ---------------------------------------------------------------------------
217
218/// Returns the C# type to use in a `[DllImport]` declaration for the given return type.
219///
220/// Key differences from the high-level `csharp_type`:
221/// - Bool is marshalled as `int` (C FFI convention) — the wrapper compares != 0.
222/// - String / Named / Vec / Map / Path / Json / Bytes all come back as `IntPtr`.
223/// - Numeric primitives use their natural C# types (`nuint`, `int`, etc.).
224fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
225    match ty {
226        TypeRef::Unit => "void",
227        // Bool over FFI is a C int (0/1).
228        TypeRef::Primitive(PrimitiveType::Bool) => "int",
229        // Numeric primitives — use their real C# types.
230        TypeRef::Primitive(PrimitiveType::U8) => "byte",
231        TypeRef::Primitive(PrimitiveType::U16) => "ushort",
232        TypeRef::Primitive(PrimitiveType::U32) => "uint",
233        TypeRef::Primitive(PrimitiveType::U64) => "ulong",
234        TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
235        TypeRef::Primitive(PrimitiveType::I16) => "short",
236        TypeRef::Primitive(PrimitiveType::I32) => "int",
237        TypeRef::Primitive(PrimitiveType::I64) => "long",
238        TypeRef::Primitive(PrimitiveType::F32) => "float",
239        TypeRef::Primitive(PrimitiveType::F64) => "double",
240        TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
241        TypeRef::Primitive(PrimitiveType::Isize) => "long",
242        // Duration as u64
243        TypeRef::Duration => "ulong",
244        // Everything else is a pointer that needs manual marshalling.
245        TypeRef::String
246        | TypeRef::Char
247        | TypeRef::Bytes
248        | TypeRef::Optional(_)
249        | TypeRef::Vec(_)
250        | TypeRef::Map(_, _)
251        | TypeRef::Named(_)
252        | TypeRef::Path
253        | TypeRef::Json => "IntPtr",
254    }
255}
256
257/// Does the return type need IntPtr→string marshalling in the wrapper?
258fn returns_string(ty: &TypeRef) -> bool {
259    matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
260}
261
262/// Does the return type come back as a C int that should be converted to bool?
263fn returns_bool_via_int(ty: &TypeRef) -> bool {
264    matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
265}
266
267/// Does the return type need JSON deserialization from an IntPtr string?
268fn returns_json_object(ty: &TypeRef) -> bool {
269    matches!(
270        ty,
271        TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
272    )
273}
274
275/// Does this return type represent an opaque handle (Named struct type) that needs special marshalling?
276///
277/// Opaque handles are returned as `IntPtr` from P/Invoke.  The wrapper must call
278/// `{prefix}_{type_snake}_to_json(ptr)` to obtain a JSON string, then deserialise it,
279/// Returns the C# type to use for a parameter in a `[DllImport]` declaration.
280///
281/// Managed reference types (Named structs, Vec, Map, Bytes, Optional of Named, etc.)
282/// cannot be directly marshalled by P/Invoke.  They must be passed as `IntPtr` (opaque
283/// handle or JSON-string pointer).  Primitive types and plain strings use their natural
284/// types.
285fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
286    match ty {
287        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
288        // Managed objects — pass as opaque IntPtr (serialised to handle before call)
289        TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
290        TypeRef::Unit => "void",
291        TypeRef::Primitive(PrimitiveType::Bool) => "int",
292        TypeRef::Primitive(PrimitiveType::U8) => "byte",
293        TypeRef::Primitive(PrimitiveType::U16) => "ushort",
294        TypeRef::Primitive(PrimitiveType::U32) => "uint",
295        TypeRef::Primitive(PrimitiveType::U64) => "ulong",
296        TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
297        TypeRef::Primitive(PrimitiveType::I16) => "short",
298        TypeRef::Primitive(PrimitiveType::I32) => "int",
299        TypeRef::Primitive(PrimitiveType::I64) => "long",
300        TypeRef::Primitive(PrimitiveType::F32) => "float",
301        TypeRef::Primitive(PrimitiveType::F64) => "double",
302        TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
303        TypeRef::Primitive(PrimitiveType::Isize) => "long",
304        TypeRef::Duration => "ulong",
305    }
306}
307
308// ---------------------------------------------------------------------------
309// Code generation functions
310// ---------------------------------------------------------------------------
311
312fn gen_native_methods(api: &ApiSurface, namespace: &str, lib_name: &str, prefix: &str) -> String {
313    let mut out = String::from(
314        "// This file is auto-generated by alef. DO NOT EDIT.\n\
315         using System.Runtime.InteropServices;\n\n",
316    );
317
318    out.push_str(&format!("namespace {};\n\n", namespace));
319
320    out.push_str("internal static partial class NativeMethods\n{\n");
321    out.push_str(&format!("    private const string LibName = \"{}\";\n\n", lib_name));
322
323    // Track emitted C entry-point names to avoid duplicates when the same FFI
324    // function appears both as a free function and as a type method.
325    let mut emitted: HashSet<String> = HashSet::new();
326
327    // Enum type names — these are NOT opaque handles and must not have from_json / to_json / free
328    // helpers emitted for them.
329    let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
330
331    // Collect opaque struct type names that appear as parameters or return types so we can
332    // emit their from_json / to_json / free P/Invoke helpers.
333    // Enum types are excluded.
334    let mut opaque_param_types: HashSet<String> = HashSet::new();
335    let mut opaque_return_types: HashSet<String> = HashSet::new();
336
337    for func in &api.functions {
338        for param in &func.params {
339            if let TypeRef::Named(name) = &param.ty {
340                if !enum_names.contains(name) {
341                    opaque_param_types.insert(name.clone());
342                }
343            }
344        }
345        if let TypeRef::Named(name) = &func.return_type {
346            if !enum_names.contains(name) {
347                opaque_return_types.insert(name.clone());
348            }
349        }
350    }
351    for typ in &api.types {
352        for method in &typ.methods {
353            for param in &method.params {
354                if let TypeRef::Named(name) = &param.ty {
355                    if !enum_names.contains(name) {
356                        opaque_param_types.insert(name.clone());
357                    }
358                }
359            }
360            if let TypeRef::Named(name) = &method.return_type {
361                if !enum_names.contains(name) {
362                    opaque_return_types.insert(name.clone());
363                }
364            }
365        }
366    }
367
368    // Collect truly opaque types (is_opaque = true in IR) — these have no to_json/from_json FFI.
369    let true_opaque_types: HashSet<String> = api
370        .types
371        .iter()
372        .filter(|t| t.is_opaque)
373        .map(|t| t.name.clone())
374        .collect();
375
376    // Emit from_json + free helpers for opaque types used as parameters.
377    // Truly opaque handles (is_opaque = true) have no from_json — only free.
378    // E.g. `htm_conversion_options_from_json(const char *json) -> HTMConversionOptions*`
379    for type_name in &opaque_param_types {
380        let snake = type_name.to_snake_case();
381        if !true_opaque_types.contains(type_name) {
382            let from_json_entry = format!("{prefix}_{snake}_from_json");
383            let from_json_cs = format!("{}FromJson", type_name.to_pascal_case());
384            if emitted.insert(from_json_entry.clone()) {
385                out.push_str(&format!(
386                    "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{from_json_entry}\")]\n"
387                ));
388                out.push_str(&format!(
389                    "    internal static extern IntPtr {from_json_cs}([MarshalAs(UnmanagedType.LPStr)] string json);\n\n"
390                ));
391            }
392        }
393        let free_entry = format!("{prefix}_{snake}_free");
394        let free_cs = format!("{}Free", type_name.to_pascal_case());
395        if emitted.insert(free_entry.clone()) {
396            out.push_str(&format!(
397                "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
398            ));
399            out.push_str(&format!("    internal static extern void {free_cs}(IntPtr ptr);\n\n"));
400        }
401    }
402
403    // Emit to_json + free helpers for opaque types returned from functions.
404    // Truly opaque handles (is_opaque = true) have no to_json — only free.
405    for type_name in &opaque_return_types {
406        let snake = type_name.to_snake_case();
407        if !true_opaque_types.contains(type_name) {
408            let to_json_entry = format!("{prefix}_{snake}_to_json");
409            let to_json_cs = format!("{}ToJson", type_name.to_pascal_case());
410            if emitted.insert(to_json_entry.clone()) {
411                out.push_str(&format!(
412                    "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{to_json_entry}\")]\n"
413                ));
414                out.push_str(&format!(
415                    "    internal static extern IntPtr {to_json_cs}(IntPtr ptr);\n\n"
416                ));
417            }
418        }
419        let free_entry = format!("{prefix}_{snake}_free");
420        let free_cs = format!("{}Free", type_name.to_pascal_case());
421        if emitted.insert(free_entry.clone()) {
422            out.push_str(&format!(
423                "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{free_entry}\")]\n"
424            ));
425            out.push_str(&format!("    internal static extern void {free_cs}(IntPtr ptr);\n\n"));
426        }
427    }
428
429    // Generate P/Invoke declarations for functions
430    for func in &api.functions {
431        let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
432        if emitted.insert(c_func_name.clone()) {
433            out.push_str(&gen_pinvoke_for_func(&c_func_name, func));
434        }
435    }
436
437    // Generate P/Invoke declarations for type methods
438    for typ in &api.types {
439        for method in &typ.methods {
440            let c_method_name = format!("{}_{}", prefix, method.name.to_lowercase());
441            if emitted.insert(c_method_name.clone()) {
442                out.push_str(&gen_pinvoke_for_method(&c_method_name, method));
443            }
444        }
445    }
446
447    // Add error handling functions with PascalCase names
448    out.push_str(&format!(
449        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
450    ));
451    out.push_str("    internal static extern int LastErrorCode();\n\n");
452
453    out.push_str(&format!(
454        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
455    ));
456    out.push_str("    internal static extern IntPtr LastErrorContext();\n\n");
457
458    out.push_str(&format!(
459        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
460    ));
461    out.push_str("    internal static extern void FreeString(IntPtr ptr);\n");
462
463    out.push_str("}\n");
464
465    out
466}
467
468fn gen_pinvoke_for_func(c_name: &str, func: &FunctionDef) -> String {
469    let cs_name = to_csharp_name(&func.name);
470    let mut out =
471        format!("    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
472    out.push_str("    internal static extern ");
473
474    // Return type — use the correct P/Invoke type for each kind.
475    out.push_str(pinvoke_return_type(&func.return_type));
476
477    out.push_str(&format!(" {}(", cs_name));
478
479    if func.params.is_empty() {
480        out.push_str(");\n\n");
481    } else {
482        out.push('\n');
483        for (i, param) in func.params.iter().enumerate() {
484            out.push_str("        ");
485            let pinvoke_ty = pinvoke_param_type(&param.ty);
486            if pinvoke_ty == "string" {
487                out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
488            }
489            let param_name = param.name.to_lower_camel_case();
490            out.push_str(&format!("{pinvoke_ty} {param_name}"));
491
492            if i < func.params.len() - 1 {
493                out.push(',');
494            }
495            out.push('\n');
496        }
497        out.push_str("    );\n\n");
498    }
499
500    out
501}
502
503fn gen_pinvoke_for_method(c_name: &str, method: &MethodDef) -> String {
504    let cs_name = to_csharp_name(&method.name);
505    let mut out =
506        format!("    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
507    out.push_str("    internal static extern ");
508
509    // Return type — use the correct P/Invoke type for each kind.
510    out.push_str(pinvoke_return_type(&method.return_type));
511
512    out.push_str(&format!(" {}(", cs_name));
513
514    if method.params.is_empty() {
515        out.push_str(");\n\n");
516    } else {
517        out.push('\n');
518        for (i, param) in method.params.iter().enumerate() {
519            out.push_str("        ");
520            let pinvoke_ty = pinvoke_param_type(&param.ty);
521            if pinvoke_ty == "string" {
522                out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
523            }
524            let param_name = param.name.to_lower_camel_case();
525            out.push_str(&format!("{pinvoke_ty} {param_name}"));
526
527            if i < method.params.len() - 1 {
528                out.push(',');
529            }
530            out.push('\n');
531        }
532        out.push_str("    );\n\n");
533    }
534
535    out
536}
537
538fn gen_exception_class(namespace: &str, class_name: &str) -> String {
539    let mut out = String::from(
540        "// This file is auto-generated by alef. DO NOT EDIT.\n\
541         using System;\n\n",
542    );
543
544    out.push_str(&format!("namespace {};\n\n", namespace));
545
546    out.push_str(&format!("public class {} : Exception\n", class_name));
547    out.push_str("{\n");
548    out.push_str("    public int Code { get; }\n\n");
549    out.push_str(&format!(
550        "    public {}(int code, string message) : base(message)\n",
551        class_name
552    ));
553    out.push_str("    {\n");
554    out.push_str("        Code = code;\n");
555    out.push_str("    }\n");
556    out.push_str("}\n");
557
558    out
559}
560
561fn gen_wrapper_class(
562    api: &ApiSurface,
563    namespace: &str,
564    class_name: &str,
565    exception_name: &str,
566    prefix: &str,
567) -> String {
568    let mut out = String::from(
569        "// This file is auto-generated by alef. DO NOT EDIT.\n\
570         using System;\n\
571         using System.Collections.Generic;\n\
572         using System.Runtime.InteropServices;\n\
573         using System.Text.Json;\n\
574         using System.Text.Json.Serialization;\n\
575         using System.Threading.Tasks;\n\n",
576    );
577
578    out.push_str(&format!("namespace {};\n\n", namespace));
579
580    out.push_str(&format!("public static class {}\n", class_name));
581    out.push_str("{\n");
582    out.push_str("    private static readonly JsonSerializerOptions JsonOptions = new()\n");
583    out.push_str("    {\n");
584    out.push_str("        Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) },\n");
585    out.push_str("        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull\n");
586    out.push_str("    };\n\n");
587
588    // Enum names: used to distinguish opaque struct handles from enum return types.
589    let enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.to_pascal_case()).collect();
590
591    // Truly opaque types (is_opaque = true) — returned/passed as handles, no JSON serialization.
592    let true_opaque_types: HashSet<String> = api
593        .types
594        .iter()
595        .filter(|t| t.is_opaque)
596        .map(|t| t.name.clone())
597        .collect();
598
599    // Generate wrapper methods for functions
600    for func in &api.functions {
601        out.push_str(&gen_wrapper_function(
602            func,
603            exception_name,
604            prefix,
605            &enum_names,
606            &true_opaque_types,
607        ));
608    }
609
610    // Generate wrapper methods for type methods (prefixed with type name to avoid collisions)
611    for typ in &api.types {
612        // Skip opaque types — their methods belong on the opaque handle class, not the static wrapper
613        if typ.is_opaque {
614            continue;
615        }
616        for method in &typ.methods {
617            out.push_str(&gen_wrapper_method(
618                method,
619                exception_name,
620                prefix,
621                &typ.name,
622                &enum_names,
623                &true_opaque_types,
624            ));
625        }
626    }
627
628    // Add error handling helper
629    out.push_str("    private static ");
630    out.push_str(&format!("{} GetLastError()\n", exception_name));
631    out.push_str("    {\n");
632    out.push_str("        var code = NativeMethods.LastErrorCode();\n");
633    out.push_str("        var ctxPtr = NativeMethods.LastErrorContext();\n");
634    out.push_str("        var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
635    out.push_str(&format!("        return new {}(code, message);\n", exception_name));
636    out.push_str("    }\n");
637
638    out.push_str("}\n");
639
640    out
641}
642
643// ---------------------------------------------------------------------------
644// Helpers: Named-param setup/teardown for opaque handle marshalling
645// ---------------------------------------------------------------------------
646
647/// For each `Named` parameter, emit code to serialise it to JSON and obtain a native handle.
648///
649/// For truly opaque types (is_opaque = true), the C# class already wraps the native handle, so
650/// we pass `param.Handle` directly without any JSON serialisation.
651///
652/// ```text
653/// // Data struct (has from_json):
654/// var optionsJson = JsonSerializer.Serialize(options);
655/// var optionsHandle = NativeMethods.ConversionOptionsFromJson(optionsJson);
656///
657/// // Truly opaque handle: passed as engineHandle.Handle directly — no setup needed.
658/// ```
659fn emit_named_param_setup(
660    out: &mut String,
661    params: &[alef_core::ir::ParamDef],
662    indent: &str,
663    true_opaque_types: &HashSet<String>,
664) {
665    for param in params {
666        let param_name = param.name.to_lower_camel_case();
667        let json_var = format!("{param_name}Json");
668        let handle_var = format!("{param_name}Handle");
669
670        match &param.ty {
671            TypeRef::Named(type_name) => {
672                // Truly opaque handles: the C# wrapper class holds the IntPtr directly.
673                // No from_json round-trip needed — pass .Handle directly in native_call_arg.
674                if true_opaque_types.contains(type_name) {
675                    continue;
676                }
677                let from_json_method = format!("{}FromJson", type_name.to_pascal_case());
678                if param.optional {
679                    out.push_str(&format!(
680                        "{indent}var {json_var} = {param_name} != null ? JsonSerializer.Serialize({param_name}, JsonOptions) : \"null\";\n"
681                    ));
682                } else {
683                    out.push_str(&format!(
684                        "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
685                    ));
686                }
687                out.push_str(&format!(
688                    "{indent}var {handle_var} = NativeMethods.{from_json_method}({json_var});\n"
689                ));
690            }
691            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
692                // Vec/Map: serialize to JSON string, marshal to native pointer
693                out.push_str(&format!(
694                    "{indent}var {json_var} = JsonSerializer.Serialize({param_name}, JsonOptions);\n"
695                ));
696                out.push_str(&format!(
697                    "{indent}var {handle_var} = Marshal.StringToHGlobalAnsi({json_var});\n"
698                ));
699            }
700            _ => {}
701        }
702    }
703}
704
705/// Returns the argument expression to pass to the native method for a given parameter.
706///
707/// For truly opaque types (is_opaque = true), the C# class wraps an IntPtr; pass `.Handle`.
708/// For data-struct `Named` types this is the handle variable (e.g. `optionsHandle`).
709/// For everything else it is the parameter name (with `!` for optional).
710fn native_call_arg(ty: &TypeRef, param_name: &str, optional: bool, true_opaque_types: &HashSet<String>) -> String {
711    match ty {
712        TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
713            // Truly opaque: unwrap the IntPtr from the C# handle class.
714            let bang = if optional { "!" } else { "" };
715            format!("{param_name}{bang}.Handle")
716        }
717        TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
718            format!("{param_name}Handle")
719        }
720        _ => {
721            let bang = if optional { "!" } else { "" };
722            format!("{param_name}{bang}")
723        }
724    }
725}
726
727/// Emit cleanup code to free native handles allocated for `Named` parameters.
728///
729/// Truly opaque handles (is_opaque = true) are NOT freed here — their lifetime is managed by
730/// the C# wrapper class (IDisposable). Only data-struct handles (from_json-allocated) are freed.
731fn emit_named_param_teardown(
732    out: &mut String,
733    params: &[alef_core::ir::ParamDef],
734    true_opaque_types: &HashSet<String>,
735) {
736    for param in params {
737        let param_name = param.name.to_lower_camel_case();
738        let handle_var = format!("{param_name}Handle");
739        match &param.ty {
740            TypeRef::Named(type_name) => {
741                if true_opaque_types.contains(type_name) {
742                    // Caller owns the opaque handle — do not free it here.
743                    continue;
744                }
745                let free_method = format!("{}Free", type_name.to_pascal_case());
746                out.push_str(&format!("        NativeMethods.{free_method}({handle_var});\n"));
747            }
748            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
749                out.push_str(&format!("        Marshal.FreeHGlobal({handle_var});\n"));
750            }
751            _ => {}
752        }
753    }
754}
755
756/// Emit cleanup code with configurable indentation (used inside `Task.Run` lambdas).
757fn emit_named_param_teardown_indented(
758    out: &mut String,
759    params: &[alef_core::ir::ParamDef],
760    indent: &str,
761    true_opaque_types: &HashSet<String>,
762) {
763    for param in params {
764        let param_name = param.name.to_lower_camel_case();
765        let handle_var = format!("{param_name}Handle");
766        match &param.ty {
767            TypeRef::Named(type_name) => {
768                if true_opaque_types.contains(type_name) {
769                    // Caller owns the opaque handle — do not free it here.
770                    continue;
771                }
772                let free_method = format!("{}Free", type_name.to_pascal_case());
773                out.push_str(&format!("{indent}NativeMethods.{free_method}({handle_var});\n"));
774            }
775            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
776                out.push_str(&format!("{indent}Marshal.FreeHGlobal({handle_var});\n"));
777            }
778            _ => {}
779        }
780    }
781}
782
783fn gen_wrapper_function(
784    func: &FunctionDef,
785    _exception_name: &str,
786    _prefix: &str,
787    enum_names: &HashSet<String>,
788    true_opaque_types: &HashSet<String>,
789) -> String {
790    let mut out = String::with_capacity(1024);
791
792    // XML doc comment
793    if !func.doc.is_empty() {
794        out.push_str("    /// <summary>\n");
795        for line in func.doc.lines() {
796            out.push_str(&format!("    /// {}\n", line));
797        }
798        out.push_str("    /// </summary>\n");
799        for param in &func.params {
800            out.push_str(&format!(
801                "    /// <param name=\"{}\">{}</param>\n",
802                param.name.to_lower_camel_case(),
803                if param.optional { "Optional." } else { "" }
804            ));
805        }
806    }
807
808    out.push_str("    public static ");
809
810    // Return type — use async Task<T> for async methods
811    if func.is_async {
812        if func.return_type == TypeRef::Unit {
813            out.push_str("async Task");
814        } else {
815            out.push_str(&format!("async Task<{}>", csharp_type(&func.return_type)));
816        }
817    } else if func.return_type == TypeRef::Unit {
818        out.push_str("void");
819    } else {
820        out.push_str(&csharp_type(&func.return_type));
821    }
822
823    out.push_str(&format!(" {}", to_csharp_name(&func.name)));
824    out.push('(');
825
826    // Parameters
827    for (i, param) in func.params.iter().enumerate() {
828        let param_name = param.name.to_lower_camel_case();
829        let mapped = csharp_type(&param.ty);
830        if param.optional && !mapped.ends_with('?') {
831            out.push_str(&format!("{mapped}? {param_name}"));
832        } else {
833            out.push_str(&format!("{mapped} {param_name}"));
834        }
835
836        if i < func.params.len() - 1 {
837            out.push_str(", ");
838        }
839    }
840
841    out.push_str(")\n    {\n");
842
843    // Null checks for required string/object parameters
844    for param in &func.params {
845        if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
846            let param_name = param.name.to_lower_camel_case();
847            out.push_str(&format!("        ArgumentNullException.ThrowIfNull({param_name});\n"));
848        }
849    }
850
851    // Serialize Named (opaque handle) params to JSON and obtain native handles.
852    emit_named_param_setup(&mut out, &func.params, "        ", true_opaque_types);
853
854    // Method body - delegation to native method with proper marshalling
855    let cs_native_name = to_csharp_name(&func.name);
856
857    if func.is_async {
858        // Async: wrap in Task.Run for non-blocking execution
859        out.push_str("        return await Task.Run(() =>\n        {\n");
860
861        if func.return_type != TypeRef::Unit {
862            out.push_str("            var result = ");
863        } else {
864            out.push_str("            ");
865        }
866
867        out.push_str(&format!("NativeMethods.{}(", cs_native_name));
868
869        if func.params.is_empty() {
870            out.push_str(");\n");
871        } else {
872            out.push('\n');
873            for (i, param) in func.params.iter().enumerate() {
874                let param_name = param.name.to_lower_camel_case();
875                let arg = native_call_arg(&param.ty, &param_name, param.optional, true_opaque_types);
876                out.push_str(&format!("                {arg}"));
877                if i < func.params.len() - 1 {
878                    out.push(',');
879                }
880                out.push('\n');
881            }
882            out.push_str("            );\n");
883        }
884
885        emit_return_marshalling_indented(
886            &mut out,
887            &func.return_type,
888            "            ",
889            enum_names,
890            true_opaque_types,
891        );
892        emit_named_param_teardown_indented(&mut out, &func.params, "            ", true_opaque_types);
893        emit_return_statement_indented(&mut out, &func.return_type, "            ");
894        out.push_str("        });\n");
895    } else {
896        if func.return_type != TypeRef::Unit {
897            out.push_str("        var result = ");
898        } else {
899            out.push_str("        ");
900        }
901
902        out.push_str(&format!("NativeMethods.{}(", cs_native_name));
903
904        if func.params.is_empty() {
905            out.push_str(");\n");
906        } else {
907            out.push('\n');
908            for (i, param) in func.params.iter().enumerate() {
909                let param_name = param.name.to_lower_camel_case();
910                let arg = native_call_arg(&param.ty, &param_name, param.optional, true_opaque_types);
911                out.push_str(&format!("            {arg}"));
912                if i < func.params.len() - 1 {
913                    out.push(',');
914                }
915                out.push('\n');
916            }
917            out.push_str("        );\n");
918        }
919
920        emit_return_marshalling(&mut out, &func.return_type, enum_names, true_opaque_types);
921        emit_named_param_teardown(&mut out, &func.params, true_opaque_types);
922        emit_return_statement(&mut out, &func.return_type);
923    }
924
925    out.push_str("    }\n\n");
926
927    out
928}
929
930fn gen_wrapper_method(
931    method: &MethodDef,
932    _exception_name: &str,
933    _prefix: &str,
934    type_name: &str,
935    enum_names: &HashSet<String>,
936    true_opaque_types: &HashSet<String>,
937) -> String {
938    let mut out = String::with_capacity(1024);
939
940    // XML doc comment
941    if !method.doc.is_empty() {
942        out.push_str("    /// <summary>\n");
943        for line in method.doc.lines() {
944            out.push_str(&format!("    /// {}\n", line));
945        }
946        out.push_str("    /// </summary>\n");
947        for param in &method.params {
948            out.push_str(&format!(
949                "    /// <param name=\"{}\">{}</param>\n",
950                param.name.to_lower_camel_case(),
951                if param.optional { "Optional." } else { "" }
952            ));
953        }
954    }
955
956    // The wrapper class is always `static class`, so all methods must be static.
957    out.push_str("    public static ");
958
959    // Return type — use async Task<T> for async methods
960    if method.is_async {
961        if method.return_type == TypeRef::Unit {
962            out.push_str("async Task");
963        } else {
964            out.push_str(&format!("async Task<{}>", csharp_type(&method.return_type)));
965        }
966    } else if method.return_type == TypeRef::Unit {
967        out.push_str("void");
968    } else {
969        out.push_str(&csharp_type(&method.return_type));
970    }
971
972    // Prefix method name with type name to avoid collisions (e.g., MetadataConfigDefault)
973    let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
974    out.push_str(&format!(" {method_cs_name}"));
975    out.push('(');
976
977    // Parameters
978    for (i, param) in method.params.iter().enumerate() {
979        let param_name = param.name.to_lower_camel_case();
980        let mapped = csharp_type(&param.ty);
981        if param.optional && !mapped.ends_with('?') {
982            out.push_str(&format!("{mapped}? {param_name}"));
983        } else {
984            out.push_str(&format!("{mapped} {param_name}"));
985        }
986
987        if i < method.params.len() - 1 {
988            out.push_str(", ");
989        }
990    }
991
992    out.push_str(")\n    {\n");
993
994    // Null checks for required string/object parameters
995    for param in &method.params {
996        if !param.optional && matches!(param.ty, TypeRef::String | TypeRef::Named(_) | TypeRef::Bytes) {
997            let param_name = param.name.to_lower_camel_case();
998            out.push_str(&format!("        ArgumentNullException.ThrowIfNull({param_name});\n"));
999        }
1000    }
1001
1002    // Serialize Named (opaque handle) params to JSON and obtain native handles.
1003    emit_named_param_setup(&mut out, &method.params, "        ", true_opaque_types);
1004
1005    // Method body - delegation to native method with proper marshalling
1006    let cs_native_name = to_csharp_name(&method.name);
1007
1008    if method.is_async {
1009        // Async: wrap in Task.Run for non-blocking execution
1010        out.push_str("        return await Task.Run(() =>\n        {\n");
1011
1012        if method.return_type != TypeRef::Unit {
1013            out.push_str("            var result = ");
1014        } else {
1015            out.push_str("            ");
1016        }
1017
1018        out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1019
1020        if method.params.is_empty() {
1021            out.push_str(");\n");
1022        } else {
1023            out.push('\n');
1024            for (i, param) in method.params.iter().enumerate() {
1025                let param_name = param.name.to_lower_camel_case();
1026                let arg = native_call_arg(&param.ty, &param_name, param.optional, true_opaque_types);
1027                out.push_str(&format!("                {arg}"));
1028                if i < method.params.len() - 1 {
1029                    out.push(',');
1030                }
1031                out.push('\n');
1032            }
1033            out.push_str("            );\n");
1034        }
1035
1036        emit_return_marshalling_indented(
1037            &mut out,
1038            &method.return_type,
1039            "            ",
1040            enum_names,
1041            true_opaque_types,
1042        );
1043        emit_named_param_teardown_indented(&mut out, &method.params, "            ", true_opaque_types);
1044        emit_return_statement_indented(&mut out, &method.return_type, "            ");
1045        out.push_str("        });\n");
1046    } else {
1047        if method.return_type != TypeRef::Unit {
1048            out.push_str("        var result = ");
1049        } else {
1050            out.push_str("        ");
1051        }
1052
1053        out.push_str(&format!("NativeMethods.{}(", cs_native_name));
1054
1055        if method.params.is_empty() {
1056            out.push_str(");\n");
1057        } else {
1058            out.push('\n');
1059            for (i, param) in method.params.iter().enumerate() {
1060                let param_name = param.name.to_lower_camel_case();
1061                let arg = native_call_arg(&param.ty, &param_name, param.optional, true_opaque_types);
1062                out.push_str(&format!("            {arg}"));
1063                if i < method.params.len() - 1 {
1064                    out.push(',');
1065                }
1066                out.push('\n');
1067            }
1068            out.push_str("        );\n");
1069        }
1070
1071        emit_return_marshalling(&mut out, &method.return_type, enum_names, true_opaque_types);
1072        emit_named_param_teardown(&mut out, &method.params, true_opaque_types);
1073        emit_return_statement(&mut out, &method.return_type);
1074    }
1075
1076    out.push_str("    }\n\n");
1077
1078    out
1079}
1080
1081/// Emit the return-value marshalling code shared by both function and method wrappers.
1082///
1083/// This function emits the code to convert the raw P/Invoke `result` into the managed return
1084/// type and store it in a local variable `returnValue`.  It intentionally does **not** emit
1085/// the `return` statement so that callers can interpose cleanup (param handle teardown) between
1086/// the value computation and the return.
1087///
1088/// `enum_names`: the set of C# type names that are enums (not opaque handles).
1089/// `true_opaque_types`: types with `is_opaque = true` — wrapped in `new CsType(result)`.
1090///
1091/// Callers must invoke `emit_return_statement` after their cleanup to complete the method body.
1092fn emit_return_marshalling(
1093    out: &mut String,
1094    return_type: &TypeRef,
1095    enum_names: &HashSet<String>,
1096    true_opaque_types: &HashSet<String>,
1097) {
1098    if *return_type == TypeRef::Unit {
1099        // void — nothing to return
1100        return;
1101    }
1102
1103    if returns_string(return_type) {
1104        // IntPtr → string, then free the native buffer.
1105        out.push_str("        var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n");
1106        out.push_str("        NativeMethods.FreeString(result);\n");
1107    } else if returns_bool_via_int(return_type) {
1108        // C int → bool
1109        out.push_str("        var returnValue = result != 0;\n");
1110    } else if let TypeRef::Named(type_name) = return_type {
1111        let pascal = type_name.to_pascal_case();
1112        if true_opaque_types.contains(type_name) {
1113            // Truly opaque handle: wrap the IntPtr in the C# handle class.
1114            out.push_str(&format!("        var returnValue = new {pascal}(result);\n"));
1115        } else if !enum_names.contains(&pascal) {
1116            // Data struct with to_json: call to_json, deserialise, then free both.
1117            let to_json_method = format!("{pascal}ToJson");
1118            let free_method = format!("{pascal}Free");
1119            let cs_ty = csharp_type(return_type);
1120            out.push_str(&format!(
1121                "        var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1122            ));
1123            out.push_str("        var json = Marshal.PtrToStringUTF8(jsonPtr);\n");
1124            out.push_str("        NativeMethods.FreeString(jsonPtr);\n");
1125            out.push_str(&format!("        NativeMethods.{free_method}(result);\n"));
1126            out.push_str(&format!(
1127                "        var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1128                cs_ty
1129            ));
1130        } else {
1131            // Enum returned as JSON string IntPtr.
1132            let cs_ty = csharp_type(return_type);
1133            out.push_str("        var json = Marshal.PtrToStringUTF8(result);\n");
1134            out.push_str("        NativeMethods.FreeString(result);\n");
1135            out.push_str(&format!(
1136                "        var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1137                cs_ty
1138            ));
1139        }
1140    } else if returns_json_object(return_type) {
1141        // IntPtr → JSON string → deserialized object, then free the native buffer.
1142        let cs_ty = csharp_type(return_type);
1143        out.push_str("        var json = Marshal.PtrToStringUTF8(result);\n");
1144        out.push_str("        NativeMethods.FreeString(result);\n");
1145        out.push_str(&format!(
1146            "        var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1147            cs_ty
1148        ));
1149    } else {
1150        // Numeric primitives — direct return.
1151        out.push_str("        var returnValue = result;\n");
1152    }
1153}
1154
1155/// Emit the final `return returnValue;` statement after cleanup.
1156fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
1157    if *return_type != TypeRef::Unit {
1158        out.push_str("        return returnValue;\n");
1159    }
1160}
1161
1162/// Emit the return-value marshalling code with configurable indentation.
1163///
1164/// Like `emit_return_marshalling` this stores the value in `returnValue` without emitting
1165/// the final `return` statement.  Callers must call `emit_return_statement_indented` after.
1166fn emit_return_marshalling_indented(
1167    out: &mut String,
1168    return_type: &TypeRef,
1169    indent: &str,
1170    enum_names: &HashSet<String>,
1171    true_opaque_types: &HashSet<String>,
1172) {
1173    if *return_type == TypeRef::Unit {
1174        return;
1175    }
1176
1177    if returns_string(return_type) {
1178        out.push_str(&format!(
1179            "{indent}var returnValue = Marshal.PtrToStringUTF8(result) ?? string.Empty;\n"
1180        ));
1181        out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1182    } else if returns_bool_via_int(return_type) {
1183        out.push_str(&format!("{indent}var returnValue = result != 0;\n"));
1184    } else if let TypeRef::Named(type_name) = return_type {
1185        let pascal = type_name.to_pascal_case();
1186        if true_opaque_types.contains(type_name) {
1187            // Truly opaque handle: wrap the IntPtr in the C# handle class.
1188            out.push_str(&format!("{indent}var returnValue = new {pascal}(result);\n"));
1189        } else if !enum_names.contains(&pascal) {
1190            // Data struct with to_json: call to_json, deserialise, then free both.
1191            let to_json_method = format!("{pascal}ToJson");
1192            let free_method = format!("{pascal}Free");
1193            let cs_ty = csharp_type(return_type);
1194            out.push_str(&format!(
1195                "{indent}var jsonPtr = NativeMethods.{to_json_method}(result);\n"
1196            ));
1197            out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(jsonPtr);\n"));
1198            out.push_str(&format!("{indent}NativeMethods.FreeString(jsonPtr);\n"));
1199            out.push_str(&format!("{indent}NativeMethods.{free_method}(result);\n"));
1200            out.push_str(&format!(
1201                "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1202                cs_ty
1203            ));
1204        } else {
1205            // Enum returned as JSON string IntPtr.
1206            let cs_ty = csharp_type(return_type);
1207            out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1208            out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1209            out.push_str(&format!(
1210                "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1211                cs_ty
1212            ));
1213        }
1214    } else if returns_json_object(return_type) {
1215        let cs_ty = csharp_type(return_type);
1216        out.push_str(&format!("{indent}var json = Marshal.PtrToStringUTF8(result);\n"));
1217        out.push_str(&format!("{indent}NativeMethods.FreeString(result);\n"));
1218        out.push_str(&format!(
1219            "{indent}var returnValue = JsonSerializer.Deserialize<{}>(json ?? \"null\", JsonOptions)!;\n",
1220            cs_ty
1221        ));
1222    } else {
1223        out.push_str(&format!("{indent}var returnValue = result;\n"));
1224    }
1225}
1226
1227/// Emit the final `return returnValue;` with configurable indentation.
1228fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
1229    if *return_type != TypeRef::Unit {
1230        out.push_str(&format!("{indent}return returnValue;\n"));
1231    }
1232}
1233
1234fn gen_opaque_handle(typ: &TypeDef, namespace: &str) -> String {
1235    let mut out = String::from(
1236        "// This file is auto-generated by alef. DO NOT EDIT.\n\
1237         using System;\n\n",
1238    );
1239
1240    out.push_str(&format!("namespace {};\n\n", namespace));
1241
1242    // Generate doc comment if available
1243    if !typ.doc.is_empty() {
1244        out.push_str("/// <summary>\n");
1245        for line in typ.doc.lines() {
1246            out.push_str(&format!("/// {}\n", line));
1247        }
1248        out.push_str("/// </summary>\n");
1249    }
1250
1251    let class_name = typ.name.to_pascal_case();
1252    out.push_str(&format!("public sealed class {} : IDisposable\n", class_name));
1253    out.push_str("{\n");
1254    out.push_str("    internal IntPtr Handle { get; }\n\n");
1255    out.push_str(&format!("    internal {}(IntPtr handle)\n", class_name));
1256    out.push_str("    {\n");
1257    out.push_str("        Handle = handle;\n");
1258    out.push_str("    }\n\n");
1259    out.push_str("    public void Dispose()\n");
1260    out.push_str("    {\n");
1261    out.push_str("        // Native free will be called by the runtime\n");
1262    out.push_str("    }\n");
1263    out.push_str("}\n");
1264
1265    out
1266}
1267
1268fn gen_record_type(
1269    typ: &TypeDef,
1270    namespace: &str,
1271    enum_names: &HashSet<String>,
1272    complex_enums: &HashSet<String>,
1273    _lang_rename_all: &str,
1274) -> String {
1275    let mut out = String::from(
1276        "// This file is auto-generated by alef. DO NOT EDIT.\n\
1277         using System;\n\
1278         using System.Collections.Generic;\n\
1279         using System.Text.Json;\n\
1280         using System.Text.Json.Serialization;\n\n",
1281    );
1282
1283    out.push_str(&format!("namespace {};\n\n", namespace));
1284
1285    // Generate doc comment if available
1286    if !typ.doc.is_empty() {
1287        out.push_str("/// <summary>\n");
1288        for line in typ.doc.lines() {
1289            out.push_str(&format!("/// {}\n", line));
1290        }
1291        out.push_str("/// </summary>\n");
1292    }
1293
1294    out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
1295    out.push_str("{\n");
1296
1297    for field in &typ.fields {
1298        // Skip unnamed tuple struct fields (e.g., _0, _1, 0, 1, etc.)
1299        if is_tuple_field(field) {
1300            continue;
1301        }
1302
1303        // Doc comment for field
1304        if !field.doc.is_empty() {
1305            out.push_str("    /// <summary>\n");
1306            for line in field.doc.lines() {
1307                out.push_str(&format!("    /// {}\n", line));
1308            }
1309            out.push_str("    /// </summary>\n");
1310        }
1311
1312        // [JsonPropertyName("json_name")]
1313        // FFI-based languages serialize to JSON that Rust serde deserializes.
1314        // Since Rust uses default snake_case, JSON property names must be snake_case.
1315        let json_name = field.name.clone();
1316        out.push_str(&format!("    [JsonPropertyName(\"{}\")]\n", json_name));
1317
1318        let cs_name = to_csharp_name(&field.name);
1319
1320        // Check if field type is a complex enum (tagged enum with data variants).
1321        // These can't be simple C# enums — use JsonElement for flexible deserialization.
1322        let is_complex = matches!(&field.ty, TypeRef::Named(n) if complex_enums.contains(&n.to_pascal_case()));
1323
1324        if field.optional {
1325            // Optional fields: nullable type, no `required`, default = null
1326            let mapped = if is_complex {
1327                "JsonElement".to_string()
1328            } else {
1329                csharp_type(&field.ty).to_string()
1330            };
1331            let field_type = if mapped.ends_with('?') {
1332                mapped
1333            } else {
1334                format!("{mapped}?")
1335            };
1336            out.push_str(&format!("    public {} {} {{ get; set; }}", field_type, cs_name));
1337            out.push_str(" = null;\n");
1338        } else if typ.has_default || field.default.is_some() {
1339            // Field with an explicit default value or part of a type with defaults.
1340            // Use typed_default from IR to get Rust-compatible defaults.
1341            let field_type = if is_complex {
1342                "JsonElement".to_string()
1343            } else {
1344                csharp_type(&field.ty).to_string()
1345            };
1346            out.push_str(&format!("    public {} {} {{ get; set; }}", field_type, cs_name));
1347            use alef_core::ir::DefaultValue;
1348            let default_val = match &field.typed_default {
1349                Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
1350                Some(DefaultValue::IntLiteral(n)) => n.to_string(),
1351                Some(DefaultValue::FloatLiteral(f)) => {
1352                    let s = f.to_string();
1353                    if s.contains('.') { s } else { format!("{s}.0") }
1354                }
1355                Some(DefaultValue::StringLiteral(s)) => format!("\"{}\"", s.replace('"', "\\\"")),
1356                Some(DefaultValue::EnumVariant(v)) => format!("{}.{}", field_type, v.to_pascal_case()),
1357                Some(DefaultValue::None) => "null".to_string(),
1358                Some(DefaultValue::Empty) | None => match &field.ty {
1359                    TypeRef::Vec(_) => "[]".to_string(),
1360                    TypeRef::Map(k, v) => format!("new Dictionary<{}, {}>()", csharp_type(k), csharp_type(v)),
1361                    TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"".to_string(),
1362                    TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
1363                    TypeRef::Primitive(p) => match p {
1364                        PrimitiveType::Bool => "false".to_string(),
1365                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
1366                        _ => "0".to_string(),
1367                    },
1368                    TypeRef::Named(name) => {
1369                        let pascal = name.to_pascal_case();
1370                        if enum_names.contains(&pascal) {
1371                            "default".to_string()
1372                        } else {
1373                            "default!".to_string()
1374                        }
1375                    }
1376                    _ => "default!".to_string(),
1377                },
1378            };
1379            out.push_str(&format!(" = {};\n", default_val));
1380        } else {
1381            // Non-optional field without explicit default.
1382            // Use type-appropriate zero values instead of `required` to avoid
1383            // JSON deserialization failures when fields are omitted via serde skip_serializing_if.
1384            let field_type = if is_complex {
1385                "JsonElement".to_string()
1386            } else {
1387                csharp_type(&field.ty).to_string()
1388            };
1389            let default_val = match &field.ty {
1390                TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"",
1391                TypeRef::Vec(_) => "[]",
1392                TypeRef::Bytes => "Array.Empty<byte>()",
1393                TypeRef::Primitive(PrimitiveType::Bool) => "false",
1394                TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => "0.0",
1395                TypeRef::Primitive(_) => "0",
1396                _ => "default!",
1397            };
1398            out.push_str(&format!(
1399                "    public {} {} {{ get; set; }} = {};\n",
1400                field_type, cs_name, default_val
1401            ));
1402        }
1403
1404        out.push('\n');
1405    }
1406
1407    out.push_str("}\n");
1408
1409    out
1410}
1411
1412/// Apply a serde `rename_all` strategy to a variant name.
1413fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1414    match rename_all {
1415        Some("snake_case") => name.to_snake_case(),
1416        Some("camelCase") => name.to_lower_camel_case(),
1417        Some("PascalCase") => name.to_pascal_case(),
1418        Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1419        Some("lowercase") => name.to_lowercase(),
1420        Some("UPPERCASE") => name.to_uppercase(),
1421        _ => name.to_lowercase(),
1422    }
1423}
1424
1425fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
1426    let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1427
1428    // Tagged union: enum has a serde tag AND data variants → generate abstract record hierarchy
1429    if enum_def.serde_tag.is_some() && has_data_variants {
1430        return gen_tagged_union(enum_def, namespace);
1431    }
1432
1433    let mut out = String::from(
1434        "// This file is auto-generated by alef. DO NOT EDIT.\n\
1435         using System.Text.Json.Serialization;\n\n",
1436    );
1437
1438    out.push_str(&format!("namespace {};\n\n", namespace));
1439
1440    // Generate doc comment if available
1441    if !enum_def.doc.is_empty() {
1442        out.push_str("/// <summary>\n");
1443        for line in enum_def.doc.lines() {
1444            out.push_str(&format!("/// {}\n", line));
1445        }
1446        out.push_str("/// </summary>\n");
1447    }
1448
1449    let enum_pascal = enum_def.name.to_pascal_case();
1450    out.push_str(&format!("public enum {enum_pascal}\n"));
1451    out.push_str("{\n");
1452
1453    // Enum variants with JsonPropertyName for serde-compatible serialization
1454    for variant in &enum_def.variants {
1455        if !variant.doc.is_empty() {
1456            out.push_str("    /// <summary>\n");
1457            for line in variant.doc.lines() {
1458                out.push_str(&format!("    /// {}\n", line));
1459            }
1460            out.push_str("    /// </summary>\n");
1461        }
1462
1463        // Use serde_rename if available, otherwise apply rename_all strategy
1464        let json_name = variant
1465            .serde_rename
1466            .clone()
1467            .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1468        let pascal_name = variant.name.to_pascal_case();
1469        out.push_str(&format!("    [JsonPropertyName(\"{json_name}\")]\n"));
1470        out.push_str(&format!("    {pascal_name},\n"));
1471    }
1472
1473    out.push_str("}\n");
1474
1475    out
1476}
1477
1478/// Generate a C# abstract record hierarchy for internally tagged enums.
1479///
1480/// Maps `#[serde(tag = "type_field", rename_all = "snake_case")]` Rust enums to
1481/// `[JsonPolymorphic]` C# abstract records with nested sealed record subtypes.
1482fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
1483    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1484    let enum_pascal = enum_def.name.to_pascal_case();
1485
1486    let mut out = String::from(
1487        "// This file is auto-generated by alef. DO NOT EDIT.\n\
1488         using System.Text.Json;\n\
1489         using System.Text.Json.Serialization;\n\n",
1490    );
1491    out.push_str(&format!("namespace {};\n\n", namespace));
1492
1493    // Doc comment
1494    if !enum_def.doc.is_empty() {
1495        out.push_str("/// <summary>\n");
1496        for line in enum_def.doc.lines() {
1497            out.push_str(&format!("/// {}\n", line));
1498        }
1499        out.push_str("/// </summary>\n");
1500    }
1501
1502    // [JsonPolymorphic] + [JsonDerivedType] attributes
1503    out.push_str(&format!(
1504        "[JsonPolymorphic(TypeDiscriminatorPropertyName = \"{tag_field}\")]\n"
1505    ));
1506    for variant in &enum_def.variants {
1507        let discriminator = variant
1508            .serde_rename
1509            .clone()
1510            .unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1511        let pascal = variant.name.to_pascal_case();
1512        out.push_str(&format!(
1513            "[JsonDerivedType(typeof({enum_pascal}.{pascal}), typeDiscriminator: \"{discriminator}\")]\n"
1514        ));
1515    }
1516
1517    out.push_str(&format!("public abstract record {enum_pascal}\n"));
1518    out.push_str("{\n");
1519
1520    // Nested sealed records for each variant
1521    for variant in &enum_def.variants {
1522        let pascal = variant.name.to_pascal_case();
1523
1524        if !variant.doc.is_empty() {
1525            out.push_str("    /// <summary>\n");
1526            for line in variant.doc.lines() {
1527                out.push_str(&format!("    /// {}\n", line));
1528            }
1529            out.push_str("    /// </summary>\n");
1530        }
1531
1532        if variant.fields.is_empty() {
1533            // Unit variant → sealed record with no fields
1534            out.push_str(&format!("    public sealed record {pascal}() : {enum_pascal};\n\n"));
1535        } else {
1536            // Data variant → sealed record with fields as constructor params
1537            out.push_str(&format!("    public sealed record {pascal}(\n"));
1538            for (i, field) in variant.fields.iter().enumerate() {
1539                let json_name = field.name.trim_start_matches('_');
1540                let cs_type = csharp_type(&field.ty);
1541                let cs_type = if field.optional && !cs_type.ends_with('?') {
1542                    format!("{cs_type}?")
1543                } else {
1544                    cs_type.to_string()
1545                };
1546                let cs_name = to_csharp_name(json_name);
1547                let comma = if i < variant.fields.len() - 1 { "," } else { "" };
1548                out.push_str(&format!(
1549                    "        [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
1550                ));
1551            }
1552            out.push_str(&format!("    ) : {enum_pascal};\n\n"));
1553        }
1554    }
1555
1556    out.push_str("}\n");
1557    out
1558}