alef 0.25.57

Opinionated polyglot binding generator for Rust libraries
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
use crate::codegen::naming::csharp_type_name;
use crate::core::ir::{PrimitiveType, TypeDef, TypeRef};
use heck::ToLowerCamelCase;
use std::collections::HashSet;

/// Returns the C# type to use in a `[DllImport]` declaration for the given return type.
///
/// Key differences from the high-level `csharp_type`:
/// - Bool is marshalled as `int` (C FFI convention) — the wrapper compares != 0.
/// - String / Named / Vec / Map / Path / Json / Bytes all come back as `IntPtr`.
/// - Numeric primitives use their natural C# types (`nuint`, `int`, etc.).
pub(super) fn pinvoke_return_type(ty: &TypeRef) -> &'static str {
    match ty {
        TypeRef::Unit => "void",
        // Bool over FFI is a C int (0/1).
        TypeRef::Primitive(PrimitiveType::Bool) => "int",
        // Numeric primitives — use their real C# types.
        TypeRef::Primitive(PrimitiveType::U8) => "byte",
        TypeRef::Primitive(PrimitiveType::U16) => "ushort",
        TypeRef::Primitive(PrimitiveType::U32) => "uint",
        TypeRef::Primitive(PrimitiveType::U64) => "ulong",
        TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
        TypeRef::Primitive(PrimitiveType::I16) => "short",
        TypeRef::Primitive(PrimitiveType::I32) => "int",
        TypeRef::Primitive(PrimitiveType::I64) => "long",
        TypeRef::Primitive(PrimitiveType::F32) => "float",
        TypeRef::Primitive(PrimitiveType::F64) => "double",
        TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
        TypeRef::Primitive(PrimitiveType::Isize) => "long",
        // Duration as u64
        TypeRef::Duration => "ulong",
        // Everything else is a pointer that needs manual marshalling.
        TypeRef::String
        | TypeRef::Char
        | TypeRef::Bytes
        | TypeRef::Optional(_)
        | TypeRef::Vec(_)
        | TypeRef::Map(_, _)
        | TypeRef::Named(_)
        | TypeRef::Path
        | TypeRef::Json => "IntPtr",
    }
}

/// Returns the C# type to use for a parameter in a `[DllImport]` declaration.
///
/// Managed reference types (Named structs, Vec, Map, Bytes, Optional of Named, etc.)
/// cannot be directly marshalled by P/Invoke.  They must be passed as `IntPtr` (opaque
/// handle or JSON-string pointer).  Primitive types and plain strings use their natural
/// types.
pub(super) fn pinvoke_param_type(ty: &TypeRef) -> &'static str {
    match ty {
        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "string",
        // Managed objects — pass as opaque IntPtr (serialised to handle before call)
        TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes | TypeRef::Optional(_) => "IntPtr",
        TypeRef::Unit => "void",
        TypeRef::Primitive(PrimitiveType::Bool) => "int",
        TypeRef::Primitive(PrimitiveType::U8) => "byte",
        TypeRef::Primitive(PrimitiveType::U16) => "ushort",
        TypeRef::Primitive(PrimitiveType::U32) => "uint",
        TypeRef::Primitive(PrimitiveType::U64) => "ulong",
        TypeRef::Primitive(PrimitiveType::I8) => "sbyte",
        TypeRef::Primitive(PrimitiveType::I16) => "short",
        TypeRef::Primitive(PrimitiveType::I32) => "int",
        TypeRef::Primitive(PrimitiveType::I64) => "long",
        TypeRef::Primitive(PrimitiveType::F32) => "float",
        TypeRef::Primitive(PrimitiveType::F64) => "double",
        TypeRef::Primitive(PrimitiveType::Usize) => "ulong",
        TypeRef::Primitive(PrimitiveType::Isize) => "long",
        TypeRef::Duration => "ulong",
    }
}

/// Returns true if a parameter should be hidden from the public API because it is a
/// trait-bridge param (e.g. the FFI visitor handle).
pub(super) fn is_bridge_param(
    param: &crate::core::ir::ParamDef,
    bridge_param_names: &HashSet<String>,
    bridge_type_aliases: &HashSet<String>,
) -> bool {
    bridge_param_names.contains(&param.name)
        || matches!(&param.ty, crate::core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n))
}

/// Does the return type need IntPtr→string marshalling in the wrapper?
pub(super) fn returns_string(ty: &TypeRef) -> bool {
    matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json)
}

/// Does the return type come back as a C int that should be converted to bool?
pub(super) fn returns_bool_via_int(ty: &TypeRef) -> bool {
    matches!(ty, TypeRef::Primitive(PrimitiveType::Bool))
}

/// Does the return type need JSON deserialization from an IntPtr string?
pub(super) fn returns_json_object(ty: &TypeRef) -> bool {
    matches!(
        ty,
        TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) | TypeRef::Bytes | TypeRef::Optional(_)
    )
}

/// Returns true if the FFI return type is a pointer (IntPtr), as opposed to a numeric value.
/// Only pointer-returning functions use `IntPtr.Zero` as an error sentinel.
pub(super) fn returns_ptr(ty: &TypeRef) -> bool {
    matches!(
        ty,
        TypeRef::String
            | TypeRef::Char
            | TypeRef::Path
            | TypeRef::Json
            | TypeRef::Named(_)
            | TypeRef::Vec(_)
            | TypeRef::Map(_, _)
            | TypeRef::Bytes
            | TypeRef::Optional(_)
    )
}

/// Returns the argument expression to pass to the native method for a given parameter.
///
/// For truly opaque types (is_opaque = true), the C# class wraps an IntPtr; pass `.Handle`.
/// For data-struct `Named` types this is the handle variable (e.g. `optionsHandle`).
/// For everything else it is the parameter name (with `!` for optional).
pub(super) fn native_call_arg(
    ty: &TypeRef,
    param_name: &str,
    optional: bool,
    true_opaque_types: &HashSet<String>,
) -> String {
    match ty {
        TypeRef::Named(type_name) if true_opaque_types.contains(type_name) => {
            // Truly opaque: unwrap the IntPtr from the C# handle class.
            let bang = if optional { "!" } else { "" };
            format!("{param_name}{bang}.Handle")
        }
        TypeRef::Named(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) => {
            format!("{param_name}Handle")
        }
        TypeRef::Bytes => {
            format!("{param_name}Handle.AddrOfPinnedObject()")
        }
        TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool) => {
            // The P/Invoke declaration emits `[MarshalAs(UnmanagedType.U1)] bool`
            // (see gen_bindings::functions.rs), so the call site must pass a `bool`
            // value directly — C# does not implicitly convert `int` to `bool`.
            // For nullable bools we collapse `null` to `false`, matching the legacy
            // FFI semantics (0 = false). A future change could route bool? through
            // a dedicated optional sentinel if Some(false) vs None ever need to be
            // distinguished, but no caller relies on that today.
            if optional {
                format!("({param_name} ?? false)")
            } else {
                param_name.to_string()
            }
        }
        ty => {
            if optional {
                // For optional primitive types (e.g. ulong?, uint?), pass the FFI's
                // None sentinel when the value is null. The FFI shim decodes
                // `{prim}::MAX` (and NAN for floats) as None — passing 0 collides with
                // a legitimate zero from the caller, e.g. timeout_secs=0 = "no timeout"
                // would be silently treated as "unset" without this. Mirrors the
                // `alef-backend-ffi` `param_optional_numeric_conversion` decoder.
                // String/Char/Path/Json are reference types so `!` is correct for those.
                if let TypeRef::Primitive(prim) = ty {
                    use crate::core::ir::PrimitiveType;
                    let sentinel = match prim {
                        PrimitiveType::U8 => "byte.MaxValue",
                        PrimitiveType::U16 => "ushort.MaxValue",
                        PrimitiveType::U32 => "uint.MaxValue",
                        PrimitiveType::U64 | PrimitiveType::Usize => "ulong.MaxValue",
                        PrimitiveType::I8 => "sbyte.MaxValue",
                        PrimitiveType::I16 => "short.MaxValue",
                        PrimitiveType::I32 => "int.MaxValue",
                        PrimitiveType::I64 | PrimitiveType::Isize => "long.MaxValue",
                        PrimitiveType::F32 => "float.NaN",
                        PrimitiveType::F64 => "double.NaN",
                        PrimitiveType::Bool => unreachable!("handled above"),
                    };
                    format!("{param_name} ?? {sentinel}")
                } else if matches!(ty, TypeRef::Duration) {
                    format!("{param_name}.GetValueOrDefault()")
                } else {
                    format!("{param_name}!")
                }
            } else {
                param_name.to_string()
            }
        }
    }
}

/// Build the byte-slice length argument passed to a native call.
///
/// `cast` is the C# cast prefix the P/Invoke length parameter expects (e.g. `"(UIntPtr)"`
/// or `"(nuint)"`). For optional `byte[]?` parameters the array may be null — pinning a
/// null array yields `IntPtr.Zero` and a zero length, so we null-coalesce the length to
/// `0` rather than dereferencing `.Length` (which would trip CS8602 under
/// `<TreatWarningsAsErrors>` / nullable-reference analysis).
pub(super) fn bytes_len_arg(cast: &str, param_name: &str, optional: bool) -> String {
    if optional {
        format!("{cast}({param_name}?.Length ?? 0)")
    } else {
        format!("{cast}{param_name}.Length")
    }
}

/// Returns true when wrapper setup allocates a temporary handle that must be
/// released after the native call.
pub(super) fn needs_param_teardown(
    params: &[crate::core::ir::ParamDef],
    true_opaque_types: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> bool {
    params.iter().any(|param| match &param.ty {
        TypeRef::Named(type_name) => !true_opaque_types.contains(type_name) && !enum_names.contains(type_name),
        TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Bytes => true,
        _ => false,
    })
}

/// For each `Named` parameter, emit code to serialise it to JSON and obtain a native handle.
///
/// For truly opaque types (is_opaque = true), the C# class already wraps the native handle, so
/// we pass `param.Handle` directly without any JSON serialisation.
pub(super) fn emit_named_param_setup(
    out: &mut String,
    params: &[crate::core::ir::ParamDef],
    indent: &str,
    true_opaque_types: &HashSet<String>,
    exception_name: &str,
    _types: &[TypeDef],
    enum_names: &HashSet<String>,
) {
    use crate::backends::csharp::template_env::render;

    for param in params {
        let param_name = param.name.to_lower_camel_case();
        let json_var = format!("{param_name}Json");
        let handle_var = format!("{param_name}Handle");

        match &param.ty {
            TypeRef::Named(type_name) => {
                // Truly opaque handles: the C# wrapper class holds the IntPtr directly.
                // No from_json round-trip needed — pass .Handle directly in native_call_arg.
                if true_opaque_types.contains(type_name) {
                    continue;
                }
                // Enums: marshal as i32 (cast directly), not JSON
                if enum_names.contains(type_name) {
                    // For optional enums, emit: var enumHandle = paramName != null ? (int)paramName : -1;
                    if param.optional {
                        out.push_str(&render(
                            "named_param_enum_optional.jinja",
                            minijinja::context! { indent, handle_var, param_name },
                        ));
                    } else {
                        // For required enums, just cast: var enumHandle = (int)paramName;
                        out.push_str(&render(
                            "named_param_enum_required.jinja",
                            minijinja::context! { indent, handle_var, param_name },
                        ));
                    }
                    continue;
                }
                let from_json_method = format!("{}FromJson", csharp_type_name(type_name));

                if param.optional {
                    // Optional Named param: pass IntPtr.Zero through to native when the
                    // C# arg is null instead of round-tripping `"null"` through FromJson
                    // which would error with "invalid type: null, expected struct T".
                    out.push_str(&crate::backends::csharp::template_env::render(
                        "named_param_handle_from_json_optional.jinja",
                        minijinja::context! {
                            indent,
                            handle_var => &handle_var,
                            from_json_method => &from_json_method,
                            json_var => &json_var,
                            param_name => &param_name,
                            exception_name => exception_name,
                        },
                    ));
                } else {
                    out.push_str(&crate::backends::csharp::template_env::render(
                        "named_param_json_serialize.jinja",
                        minijinja::context! { indent, json_var => &json_var, param_name => &param_name },
                    ));
                    out.push_str(&crate::backends::csharp::template_env::render(
                        "named_param_handle_from_json.jinja",
                        minijinja::context! {
                            indent,
                            handle_var => &handle_var,
                            from_json_method => &from_json_method,
                            json_var => &json_var,
                            exception_name => exception_name,
                        },
                    ));
                }
            }
            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
                // Vec/Map: serialize to JSON string, marshal to native pointer
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_json_serialize.jinja",
                    minijinja::context! { indent, json_var => &json_var, param_name => &param_name },
                ));
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_handle_string.jinja",
                    minijinja::context! { indent, handle_var => &handle_var, json_var => &json_var },
                ));
            }
            TypeRef::Bytes => {
                // byte[]: pin the managed array and pass pointer to native
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_handle_pin.jinja",
                    minijinja::context! { indent, handle_var => &handle_var, param_name => &param_name },
                ));
            }
            _ => {}
        }
    }
}

/// Emit cleanup code to free native handles allocated for `Named` parameters.
///
/// Truly opaque handles (is_opaque = true) are NOT freed here — their lifetime is managed by
/// the C# wrapper class (IDisposable). Only data-struct handles (from_json-allocated) are freed.
/// Enums are not freed (they are stack values, not heap-allocated).
pub(super) fn emit_named_param_teardown(
    out: &mut String,
    params: &[crate::core::ir::ParamDef],
    true_opaque_types: &HashSet<String>,
    enum_names: &HashSet<String>,
) {
    for param in params {
        let param_name = param.name.to_lower_camel_case();
        let handle_var = format!("{param_name}Handle");
        match &param.ty {
            TypeRef::Named(type_name) => {
                if true_opaque_types.contains(type_name) {
                    // Caller owns the opaque handle — do not free it here.
                    continue;
                }
                if enum_names.contains(type_name) {
                    // Enums are passed as i32 (stack values); no cleanup needed.
                    continue;
                }
                let free_method = format!("{}Free", csharp_type_name(type_name));
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_teardown_free.jinja",
                    minijinja::context! { indent => "        ", free_method => &free_method, handle_var => &handle_var },
                ));
            }
            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_teardown_hglobal.jinja",
                    minijinja::context! { indent => "        ", handle_var => &handle_var },
                ));
            }
            TypeRef::Bytes => {
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_teardown_gchandle.jinja",
                    minijinja::context! { indent => "        ", handle_var => &handle_var },
                ));
            }
            _ => {}
        }
    }
}

/// Emit cleanup code with configurable indentation (used inside `Task.Run` lambdas).
pub(super) fn emit_named_param_teardown_indented(
    out: &mut String,
    params: &[crate::core::ir::ParamDef],
    indent: &str,
    true_opaque_types: &HashSet<String>,
    enum_names: &HashSet<String>,
) {
    for param in params {
        let param_name = param.name.to_lower_camel_case();
        let handle_var = format!("{param_name}Handle");
        match &param.ty {
            TypeRef::Named(type_name) => {
                if true_opaque_types.contains(type_name) {
                    // Caller owns the opaque handle — do not free it here.
                    continue;
                }
                if enum_names.contains(type_name) {
                    // Enums are passed as i32 (stack values); no cleanup needed.
                    continue;
                }
                let free_method = format!("{}Free", csharp_type_name(type_name));
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_teardown_free.jinja",
                    minijinja::context! { indent, free_method => &free_method, handle_var => &handle_var },
                ));
            }
            TypeRef::Vec(_) | TypeRef::Map(_, _) => {
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_teardown_hglobal.jinja",
                    minijinja::context! { indent, handle_var => &handle_var },
                ));
            }
            TypeRef::Bytes => {
                out.push_str(&crate::backends::csharp::template_env::render(
                    "named_param_teardown_gchandle.jinja",
                    minijinja::context! { indent, handle_var => &handle_var },
                ));
            }
            _ => {}
        }
    }
}