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