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};
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 record types (structs)
107        for typ in &api.types {
108            if !typ.is_opaque {
109                // Skip types where all fields are unnamed tuple positions — they have no
110                // meaningful properties to expose in C#.
111                let has_named_fields = typ.fields.iter().any(|f| !is_tuple_field(f));
112                if !typ.fields.is_empty() && !has_named_fields {
113                    continue;
114                }
115
116                let type_filename = typ.name.to_pascal_case();
117                files.push(GeneratedFile {
118                    path: base_path.join(format!("{}.cs", type_filename)),
119                    content: strip_trailing_whitespace(&gen_record_type(typ, &namespace)),
120                    generated_header: true,
121                });
122            }
123        }
124
125        // 5. Generate enums
126        for enum_def in &api.enums {
127            let enum_filename = enum_def.name.to_pascal_case();
128            files.push(GeneratedFile {
129                path: base_path.join(format!("{}.cs", enum_filename)),
130                content: strip_trailing_whitespace(&gen_enum(enum_def, &namespace)),
131                generated_header: true,
132            });
133        }
134
135        // Build adapter body map (consumed by generators via body substitution)
136        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Csharp)?;
137
138        Ok(files)
139    }
140
141    /// C# wrapper class is already the public API.
142    /// The `gen_wrapper_class` (generated in `generate_bindings`) provides high-level public methods
143    /// that wrap NativeMethods (P/Invoke), marshal types, and handle errors.
144    /// No additional facade is needed.
145    fn generate_public_api(&self, _api: &ApiSurface, _config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
146        // C#'s wrapper class IS the public API — no additional wrapper needed.
147        Ok(vec![])
148    }
149
150    fn build_config(&self) -> Option<BuildConfig> {
151        Some(BuildConfig {
152            tool: "dotnet",
153            crate_suffix: "",
154            depends_on_ffi: true,
155            post_build: vec![],
156        })
157    }
158}
159
160/// Returns true if a field is a tuple struct positional field (e.g., `_0`, `_1`, `0`, `1`).
161fn is_tuple_field(field: &FieldDef) -> bool {
162    (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
163        || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
164}
165
166/// Strip trailing whitespace from every line and ensure the file ends with a single newline.
167fn strip_trailing_whitespace(content: &str) -> String {
168    let mut result: String = content
169        .lines()
170        .map(|line| line.trim_end())
171        .collect::<Vec<_>>()
172        .join("\n");
173    if !result.ends_with('\n') {
174        result.push('\n');
175    }
176    result
177}
178
179// ---------------------------------------------------------------------------
180// Helpers: P/Invoke return type mapping
181// ---------------------------------------------------------------------------
182
183/// Returns the C# type to use in a `[DllImport]` declaration for the given return type.
184///
185/// Key differences from the high-level `csharp_type`:
186/// - Bool is marshalled as `int` (C FFI convention) — the wrapper compares != 0.
187/// - String / Named / Vec / Map / Path / Json / Bytes all come back as `IntPtr`.
188/// - Numeric primitives use their natural C# types (`nuint`, `int`, etc.).
189fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
190    match ty {
191        TypeRef::Unit => "void",
192        // Bool over FFI is a C int (0/1).
193        TypeRef::Primitive(PrimitiveType::Bool) => "int",
194        // Numeric primitives — use their real C# types.
195        TypeRef::Primitive(PrimitiveType::U8) => "byte",
196        TypeRef::Primitive(PrimitiveType::U16) => "ushort",
197        TypeRef::Primitive(PrimitiveType::U32) => "uint",
198        TypeRef::Primitive(PrimitiveType::U64) => "ulong",
199        TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
200        TypeRef::Primitive(PrimitiveType::I16) => "short",
201        TypeRef::Primitive(PrimitiveType::I32) => "int",
202        TypeRef::Primitive(PrimitiveType::I64) => "long",
203        TypeRef::Primitive(PrimitiveType::F32) => "float",
204        TypeRef::Primitive(PrimitiveType::F64) => "double",
205        TypeRef::Primitive(PrimitiveType::Usize) => "nuint",
206        TypeRef::Primitive(PrimitiveType::Isize) => "nint",
207        // Duration as u64
208        TypeRef::Duration => "ulong",
209        // Everything else is a pointer that needs manual marshalling.
210        TypeRef::String
211        | TypeRef::Char
212        | TypeRef::Bytes
213        | TypeRef::Optional(_)
214        | TypeRef::Vec(_)
215        | TypeRef::Map(_, _)
216        | TypeRef::Named(_)
217        | TypeRef::Path
218        | TypeRef::Json => "IntPtr",
219    }
220}
221
222/// Does the return type need IntPtr→string marshalling in the wrapper?
223fn returns_string(ty: &TypeRef) -> bool {
224    matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
225}
226
227/// Does the return type come back as a C int that should be converted to bool?
228fn returns_bool_via_int(ty: &TypeRef) -> bool {
229    matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
230}
231
232/// Does the return type need JSON deserialization from an IntPtr string?
233fn returns_json_object(ty: &TypeRef) -> bool {
234    matches!(
235        ty,
236        TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
237    )
238}
239
240// ---------------------------------------------------------------------------
241// Code generation functions
242// ---------------------------------------------------------------------------
243
244fn gen_native_methods(api: &ApiSurface, namespace: &str, lib_name: &str, prefix: &str) -> String {
245    let mut out = String::from(
246        "// This file is auto-generated by alef. DO NOT EDIT.\n\
247         using System.Runtime.InteropServices;\n\n",
248    );
249
250    out.push_str(&format!("namespace {};\n\n", namespace));
251
252    out.push_str("internal static partial class NativeMethods\n{\n");
253    out.push_str(&format!("    private const string LibName = \"{}\";\n\n", lib_name));
254
255    // Track emitted C entry-point names to avoid duplicates when the same FFI
256    // function appears both as a free function and as a type method.
257    let mut emitted: HashSet<String> = HashSet::new();
258
259    // Generate P/Invoke declarations for functions
260    for func in &api.functions {
261        let c_func_name = format!("{}_{}", prefix, func.name.to_lowercase());
262        if emitted.insert(c_func_name.clone()) {
263            out.push_str(&gen_pinvoke_for_func(&c_func_name, func));
264        }
265    }
266
267    // Generate P/Invoke declarations for type methods
268    for typ in &api.types {
269        for method in &typ.methods {
270            let c_method_name = format!("{}_{}", prefix, method.name.to_lowercase());
271            if emitted.insert(c_method_name.clone()) {
272                out.push_str(&gen_pinvoke_for_method(&c_method_name, method));
273            }
274        }
275    }
276
277    // Add error handling functions with PascalCase names
278    out.push_str(&format!(
279        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_code\")]\n"
280    ));
281    out.push_str("    internal static extern int LastErrorCode();\n\n");
282
283    out.push_str(&format!(
284        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_last_error_context\")]\n"
285    ));
286    out.push_str("    internal static extern IntPtr LastErrorContext();\n\n");
287
288    out.push_str(&format!(
289        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_free_string\")]\n"
290    ));
291    out.push_str("    internal static extern void FreeString(IntPtr ptr);\n");
292
293    out.push_str("}\n");
294
295    out
296}
297
298fn gen_pinvoke_for_func(c_name: &str, func: &FunctionDef) -> String {
299    let cs_name = to_csharp_name(&func.name);
300    let mut out =
301        format!("    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
302    out.push_str("    internal static extern ");
303
304    // Return type — use the correct P/Invoke type for each kind.
305    out.push_str(pinvoke_return_type(&func.return_type));
306
307    out.push_str(&format!(" {}(", cs_name));
308
309    if func.params.is_empty() {
310        out.push_str(");\n\n");
311    } else {
312        out.push('\n');
313        for (i, param) in func.params.iter().enumerate() {
314            out.push_str("        ");
315            if matches!(param.ty, TypeRef::String | TypeRef::Char) {
316                out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
317            }
318            let param_name = param.name.to_lower_camel_case();
319            out.push_str(&format!("{} {}", csharp_type(&param.ty), param_name));
320
321            if i < func.params.len() - 1 {
322                out.push(',');
323            }
324            out.push('\n');
325        }
326        out.push_str("    );\n\n");
327    }
328
329    out
330}
331
332fn gen_pinvoke_for_method(c_name: &str, method: &MethodDef) -> String {
333    let cs_name = to_csharp_name(&method.name);
334    let mut out =
335        format!("    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{c_name}\")]\n");
336    out.push_str("    internal static extern ");
337
338    // Return type — use the correct P/Invoke type for each kind.
339    out.push_str(pinvoke_return_type(&method.return_type));
340
341    out.push_str(&format!(" {}(", cs_name));
342
343    if method.params.is_empty() {
344        out.push_str(");\n\n");
345    } else {
346        out.push('\n');
347        for (i, param) in method.params.iter().enumerate() {
348            out.push_str("        ");
349            if matches!(param.ty, TypeRef::String | TypeRef::Char) {
350                out.push_str("[MarshalAs(UnmanagedType.LPStr)] ");
351            }
352            let param_name = param.name.to_lower_camel_case();
353            out.push_str(&format!("{} {}", csharp_type(&param.ty), param_name));
354
355            if i < method.params.len() - 1 {
356                out.push(',');
357            }
358            out.push('\n');
359        }
360        out.push_str("    );\n\n");
361    }
362
363    out
364}
365
366fn gen_exception_class(namespace: &str, class_name: &str) -> String {
367    let mut out = String::from(
368        "// This file is auto-generated by alef. DO NOT EDIT.\n\
369         using System;\n\n",
370    );
371
372    out.push_str(&format!("namespace {};\n\n", namespace));
373
374    out.push_str(&format!("public class {} : Exception\n", class_name));
375    out.push_str("{\n");
376    out.push_str("    public int Code { get; }\n\n");
377    out.push_str(&format!(
378        "    public {}(int code, string message) : base(message)\n",
379        class_name
380    ));
381    out.push_str("    {\n");
382    out.push_str("        Code = code;\n");
383    out.push_str("    }\n");
384    out.push_str("}\n");
385
386    out
387}
388
389fn gen_wrapper_class(
390    api: &ApiSurface,
391    namespace: &str,
392    class_name: &str,
393    exception_name: &str,
394    prefix: &str,
395) -> String {
396    let mut out = String::from(
397        "// This file is auto-generated by alef. DO NOT EDIT.\n\
398         using System;\n\
399         using System.Collections.Generic;\n\
400         using System.Runtime.InteropServices;\n\
401         using System.Text.Json;\n\
402         using System.Text.Json.Serialization;\n\
403         using System.Threading.Tasks;\n\n",
404    );
405
406    out.push_str(&format!("namespace {};\n\n", namespace));
407
408    out.push_str(&format!("public static class {}\n", class_name));
409    out.push_str("{\n");
410
411    // Generate wrapper methods for functions
412    for func in &api.functions {
413        out.push_str(&gen_wrapper_function(func, exception_name, prefix));
414    }
415
416    // Generate wrapper methods for type methods (prefixed with type name to avoid collisions)
417    for typ in &api.types {
418        // Skip opaque types (no C# representation for their methods)
419        if typ.is_opaque {
420            continue;
421        }
422        for method in &typ.methods {
423            // Skip methods that return opaque types not representable in C#
424            if let alef_core::ir::TypeRef::Named(ref name) = method.return_type {
425                if api.types.iter().any(|t| t.name == *name && t.is_opaque) {
426                    continue;
427                }
428            }
429            out.push_str(&gen_wrapper_method(method, exception_name, prefix, &typ.name));
430        }
431    }
432
433    // Add error handling helper
434    out.push_str("    private static ");
435    out.push_str(&format!("{} GetLastError()\n", exception_name));
436    out.push_str("    {\n");
437    out.push_str("        var code = NativeMethods.LastErrorCode();\n");
438    out.push_str("        var ctxPtr = NativeMethods.LastErrorContext();\n");
439    out.push_str("        var message = Marshal.PtrToStringAnsi(ctxPtr) ?? \"Unknown error\";\n");
440    out.push_str(&format!("        return new {}(code, message);\n", exception_name));
441    out.push_str("    }\n");
442
443    out.push_str("}\n");
444
445    out
446}
447
448fn gen_wrapper_function(func: &FunctionDef, _exception_name: &str, _prefix: &str) -> String {
449    let mut out = String::with_capacity(1024);
450
451    out.push_str("    public static ");
452
453    // Return type
454    if func.return_type == TypeRef::Unit {
455        out.push_str("void");
456    } else {
457        out.push_str(&csharp_type(&func.return_type));
458    }
459
460    out.push_str(&format!(" {}", to_csharp_name(&func.name)));
461    out.push('(');
462
463    // Parameters
464    for (i, param) in func.params.iter().enumerate() {
465        let param_name = param.name.to_lower_camel_case();
466        let mapped = csharp_type(&param.ty);
467        if param.optional && !mapped.ends_with('?') {
468            out.push_str(&format!("{mapped}? {param_name}"));
469        } else {
470            out.push_str(&format!("{mapped} {param_name}"));
471        }
472
473        if i < func.params.len() - 1 {
474            out.push_str(", ");
475        }
476    }
477
478    out.push_str(")\n    {\n");
479
480    // Method body - delegation to native method with proper marshalling
481    let cs_native_name = to_csharp_name(&func.name);
482
483    if func.return_type != TypeRef::Unit {
484        out.push_str("        var result = ");
485    } else {
486        out.push_str("        ");
487    }
488
489    out.push_str(&format!("NativeMethods.{}(", cs_native_name));
490
491    if func.params.is_empty() {
492        out.push_str(");\n");
493    } else {
494        out.push('\n');
495        for (i, param) in func.params.iter().enumerate() {
496            let param_name = param.name.to_lower_camel_case();
497            out.push_str(&format!("            {}", param_name));
498            if i < func.params.len() - 1 {
499                out.push(',');
500            }
501            out.push('\n');
502        }
503        out.push_str("        );\n");
504    }
505
506    emit_return_marshalling(&mut out, &func.return_type);
507
508    out.push_str("    }\n\n");
509
510    out
511}
512
513fn gen_wrapper_method(method: &MethodDef, _exception_name: &str, _prefix: &str, type_name: &str) -> String {
514    let mut out = String::with_capacity(1024);
515
516    // The wrapper class is always `static class`, so all methods must be static.
517    out.push_str("    public static ");
518
519    // Return type
520    if method.return_type == TypeRef::Unit {
521        out.push_str("void");
522    } else {
523        out.push_str(&csharp_type(&method.return_type));
524    }
525
526    // Prefix method name with type name to avoid collisions (e.g., MetadataConfigDefault)
527    let method_cs_name = format!("{}{}", type_name, to_csharp_name(&method.name));
528    out.push_str(&format!(" {method_cs_name}"));
529    out.push('(');
530
531    // Parameters
532    for (i, param) in method.params.iter().enumerate() {
533        let param_name = param.name.to_lower_camel_case();
534        let mapped = csharp_type(&param.ty);
535        if param.optional && !mapped.ends_with('?') {
536            out.push_str(&format!("{mapped}? {param_name}"));
537        } else {
538            out.push_str(&format!("{mapped} {param_name}"));
539        }
540
541        if i < method.params.len() - 1 {
542            out.push_str(", ");
543        }
544    }
545
546    out.push_str(")\n    {\n");
547
548    // Method body - delegation to native method with proper marshalling
549    let cs_native_name = to_csharp_name(&method.name);
550
551    if method.return_type != TypeRef::Unit {
552        out.push_str("        var result = ");
553    } else {
554        out.push_str("        ");
555    }
556
557    out.push_str(&format!("NativeMethods.{}(", cs_native_name));
558
559    if method.params.is_empty() {
560        out.push_str(");\n");
561    } else {
562        out.push('\n');
563        for (i, param) in method.params.iter().enumerate() {
564            let param_name = param.name.to_lower_camel_case();
565            out.push_str(&format!("            {}", param_name));
566            if i < method.params.len() - 1 {
567                out.push(',');
568            }
569            out.push('\n');
570        }
571        out.push_str("        );\n");
572    }
573
574    emit_return_marshalling(&mut out, &method.return_type);
575
576    out.push_str("    }\n\n");
577
578    out
579}
580
581/// Emit the return-value marshalling code shared by both function and method wrappers.
582///
583/// `result` is the local variable holding the native call's return value.
584fn emit_return_marshalling(out: &mut String, return_type: &TypeRef) {
585    if *return_type == TypeRef::Unit {
586        // void — nothing to return
587        return;
588    }
589
590    if returns_string(return_type) {
591        // IntPtr → string, then free the native buffer.
592        out.push_str("        var str = Marshal.PtrToStringUTF8(result);\n");
593        out.push_str("        NativeMethods.FreeString(result);\n");
594        out.push_str("        return str ?? string.Empty;\n");
595    } else if returns_bool_via_int(return_type) {
596        // C int → bool
597        out.push_str("        return result != 0;\n");
598    } else if returns_json_object(return_type) {
599        // IntPtr → JSON string → deserialized object, then free the native buffer.
600        let cs_ty = csharp_type(return_type);
601        out.push_str("        var json = Marshal.PtrToStringUTF8(result);\n");
602        out.push_str("        NativeMethods.FreeString(result);\n");
603        out.push_str(&format!(
604            "        return JsonSerializer.Deserialize<{}>(json ?? \"null\")!;\n",
605            cs_ty
606        ));
607    } else {
608        // Numeric primitives — direct return.
609        out.push_str("        return result;\n");
610    }
611}
612
613fn gen_record_type(typ: &TypeDef, namespace: &str) -> String {
614    let mut out = String::from(
615        "// This file is auto-generated by alef. DO NOT EDIT.\n\
616         using System;\n\
617         using System.Collections.Generic;\n\
618         using System.Text.Json.Serialization;\n\n",
619    );
620
621    out.push_str(&format!("namespace {};\n\n", namespace));
622
623    // Generate doc comment if available
624    if !typ.doc.is_empty() {
625        out.push_str("/// <summary>\n");
626        for line in typ.doc.lines() {
627            out.push_str(&format!("/// {}\n", line));
628        }
629        out.push_str("/// </summary>\n");
630    }
631
632    out.push_str(&format!("public sealed class {}\n", typ.name.to_pascal_case()));
633    out.push_str("{\n");
634
635    for field in &typ.fields {
636        // Skip unnamed tuple struct fields (e.g., _0, _1, 0, 1, etc.)
637        if is_tuple_field(field) {
638            continue;
639        }
640
641        // Doc comment for field
642        if !field.doc.is_empty() {
643            out.push_str("    /// <summary>\n");
644            for line in field.doc.lines() {
645                out.push_str(&format!("    /// {}\n", line));
646            }
647            out.push_str("    /// </summary>\n");
648        }
649
650        // [JsonPropertyName("camelCaseName")]
651        let json_name = field.name.to_lower_camel_case();
652        out.push_str(&format!("    [JsonPropertyName(\"{}\")]\n", json_name));
653
654        let cs_name = to_csharp_name(&field.name);
655
656        if field.optional {
657            // Optional fields: nullable type, no `required`, default = null
658            let mapped = csharp_type(&field.ty);
659            let field_type = if mapped.ends_with('?') {
660                mapped.to_string()
661            } else {
662                format!("{mapped}?")
663            };
664            out.push_str(&format!("    public {} {} {{ get; set; }}", field_type, cs_name));
665            out.push_str(" = null;\n");
666        } else if typ.has_default || field.default.is_some() {
667            // Field with an explicit default value or part of a type with defaults
668            let field_type = csharp_type(&field.ty).to_string();
669            out.push_str(&format!("    public {} {} {{ get; set; }}", field_type, cs_name));
670            if let Some(default) = &field.default {
671                out.push_str(&format!(" = {};\n", default));
672            } else {
673                // Use type-appropriate zero value
674                let default_val = match &field.ty {
675                    TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "\"\"".to_string(),
676                    TypeRef::Bytes => "Array.Empty<byte>()".to_string(),
677                    TypeRef::Primitive(p) => match p {
678                        PrimitiveType::Bool => "false".to_string(),
679                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
680                        _ => "0".to_string(),
681                    },
682                    TypeRef::Vec(_) => "[]".to_string(),
683                    TypeRef::Map(_, _) => "new Dictionary<>()".to_string(),
684                    TypeRef::Duration => "0".to_string(),
685                    _ => "null".to_string(),
686                };
687                out.push_str(&format!(" = {};\n", default_val));
688            }
689        } else {
690            // Required field: no default, not optional
691            let field_type = csharp_type(&field.ty).to_string();
692            out.push_str(&format!(
693                "    public required {} {} {{ get; set; }}\n",
694                field_type, cs_name
695            ));
696        }
697
698        out.push('\n');
699    }
700
701    out.push_str("}\n");
702
703    out
704}
705
706fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
707    let mut out = String::from("// This file is auto-generated by alef. DO NOT EDIT.\n\n");
708
709    out.push_str(&format!("namespace {};\n\n", namespace));
710
711    // Generate doc comment if available
712    if !enum_def.doc.is_empty() {
713        out.push_str("/// <summary>\n");
714        for line in enum_def.doc.lines() {
715            out.push_str(&format!("/// {}\n", line));
716        }
717        out.push_str("/// </summary>\n");
718    }
719
720    out.push_str(&format!("public enum {}\n", enum_def.name.to_pascal_case()));
721    out.push_str("{\n");
722
723    // Enum variants
724    for variant in &enum_def.variants {
725        if !variant.doc.is_empty() {
726            out.push_str("    /// <summary>\n");
727            for line in variant.doc.lines() {
728                out.push_str(&format!("    /// {}\n", line));
729            }
730            out.push_str("    /// </summary>\n");
731        }
732
733        out.push_str(&format!("    {},\n", variant.name.to_pascal_case()));
734    }
735
736    out.push_str("}\n");
737
738    out
739}