alef 0.25.37

Opinionated polyglot binding generator for Rust libraries
Documentation
/// <summary>
/// Manages the FFI vtable and delegates for a {{ trait_pascal }} implementation
/// </summary>
public sealed class {{ trait_pascal }}Bridge : IDisposable {

    internal readonly I{{ trait_pascal }} _impl;
    private readonly GCHandle _implHandle;
    internal IntPtr _vtable;
    private bool _disposed;
    // Keep all delegates alive for the lifetime of the bridge.
    // Delegates must not be GC'd because Rust holds function pointers obtained via
    // GetFunctionPointerForDelegate; those pointers become invalid if the delegate is collected.
    private readonly List<object> _delegateRoots;
    internal readonly IntPtr _bridgeId;
    private int _callbackRefCount = 0;

    // Static registry: maps bridge ID (IntPtr) to bridge instance
    // This prevents GC while Rust holds the bridge ID as userData.
    internal static readonly Dictionary<IntPtr, {{ trait_pascal }}Bridge> _bridgeRegistry = new();
    internal static int _nextBridgeId = 1;
    internal static readonly object _registryLock = new();

    // Vtable slot delegates ({{ num_vtable_fields }})
{% if has_super_trait %}

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int NameFn(IntPtr userData, out IntPtr outName, out IntPtr outError);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int VersionFn(IntPtr userData, out IntPtr outVersion, out IntPtr outError);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int InitializeFn(IntPtr userData, out IntPtr outError);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate int ShutdownFn(IntPtr userData, out IntPtr outError);
{% endif %}
{% for method in methods %}

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
{% if method.is_primitive_return %}
{# Primitive returns: return directly, no out params #}
{% if method.params_empty %}
    private delegate {{ method.delegate_return_type }} {{ method.pascal_name }}Fn(IntPtr userData);
{% else %}
    private delegate {{ method.delegate_return_type }} {{ method.pascal_name }}Fn(IntPtr userData, {{ method.unmanaged_params }});
{% endif %}
{% else %}
{# Complex returns: use out params #}
{% if method.params_empty %}
{% if is_options_field %}
    private delegate int {{ method.pascal_name }}Fn(IntPtr userData, out IntPtr outResult);
{% else %}
    private delegate int {{ method.pascal_name }}Fn(IntPtr userData, out IntPtr outResult, out IntPtr outError);
{% endif %}
{% else %}
{% if is_options_field %}
    private delegate int {{ method.pascal_name }}Fn(IntPtr userData, {{ method.unmanaged_params }}, out IntPtr outResult);
{% else %}
    private delegate int {{ method.pascal_name }}Fn(IntPtr userData, {{ method.unmanaged_params }}, out IntPtr outResult, out IntPtr outError);
{% endif %}
{% endif %}
{% endif %}
{% endfor %}

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void FreeStringFn(IntPtr ptr);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void FreeUserDataFn(IntPtr userData);

    public {{ trait_pascal }}Bridge(I{{ trait_pascal }} impl) {
        _impl = impl ?? throw new ArgumentNullException(nameof(impl));
        // Keep impl alive via normal GCHandle (sufficient for callback rooting).
        // The impl instance itself is not pinned, just kept in the GC root set.
        _implHandle = GCHandle.Alloc(impl, GCHandleType.Normal);
        // Delegate list roots all delegates by reference to prevent GC.
        // This avoids trying to pin an object array containing references, which .NET 10 forbids.
        _delegateRoots = new List<object>({{ num_vtable_fields }});
        _vtable = IntPtr.Zero;
        _disposed = false;
        // Allocate unique bridge ID for registry lookup during callbacks
        lock (_registryLock) {
            _bridgeId = new IntPtr(_nextBridgeId++);
        }
        BuildVtable();
    }

    private void BuildVtable() {
        // Allocate unmanaged vtable struct (array of function pointers)
        _vtable = global::System.Runtime.InteropServices.Marshal.AllocHGlobal(IntPtr.Size * {{ num_vtable_fields }});

{{ vtable_slots }}
    }

    private static string ToJsonString<T>(T value) {
        return JsonSerializer.Serialize(value, FfiJsonExtensions.FfiJsonOptions);
    }

    /// <summary>Called by Rust via FreeUserDataCallback when done with this bridge</summary>
    internal static void FreeUserData(IntPtr bridgeId) {
        lock (_registryLock) {
            // Mark bridge as disposed but DON'T remove from registry yet.
            // Callbacks in flight will still be able to look it up and execute safely.
            // The bridge will stay alive as long as _callbackRefCount > 0.
            if (_bridgeRegistry.TryGetValue(bridgeId, out var bridge)) {
                bridge._disposed = true;
            }
        }
    }

    private void IncrementCallbackRef() {
        lock (_registryLock) {
            _callbackRefCount++;
        }
    }

    private void DecrementCallbackRef() {
        lock (_registryLock) {
            if (_callbackRefCount > 0) {
                _callbackRefCount--;
            }
            // Once refcount reaches 0 and bridge is disposed, remove from registry
            if (_callbackRefCount == 0 && _disposed) {
                _bridgeRegistry.Remove(_bridgeId);
            }
        }
    }

{{ callbacks }}

    public void Dispose() {
        if (_disposed) return;
        _disposed = true;

        if (_vtable != IntPtr.Zero) {
            global::System.Runtime.InteropServices.Marshal.FreeHGlobal(_vtable);
            _vtable = IntPtr.Zero;
        }

        if (_implHandle.IsAllocated) {
            _implHandle.Free();
        }

        // _delegateRoots is kept as a managed list; GC handles it automatically.
        // No need to free individual delegates or the list.
    }

    /// <summary>Register a {{ trait_pascal }} implementation and return its native handle</summary>
{% if has_super_trait %}
    public static IntPtr Register(I{{ trait_pascal }} impl) {
        if (impl == null)
            throw new ArgumentNullException(nameof(impl));

        var name = impl.Name;

{% else %}
    public static IntPtr Register(I{{ trait_pascal }} impl, string name) {
        if (impl == null)
            throw new ArgumentNullException(nameof(impl));

{% endif %}

        var bridge = new {{ trait_pascal }}Bridge(impl);

        try {
            // Register bridge in the static registry using its unique ID.
            // This keeps the bridge alive while Rust holds the ID (userData).
            lock (_registryLock) {
                _bridgeRegistry[bridge._bridgeId] = bridge;
            }

            var vtablePtr = bridge._vtable;
            var userData = bridge._bridgeId;

            var result = NativeMethods.Register{{ trait_pascal }}(name, vtablePtr, userData, out var outError);
            if (result != 0) {
                lock (_registryLock) {
                    _bridgeRegistry.Remove(bridge._bridgeId);
                }
                bridge.Dispose();
                var errorMsg = global::System.Runtime.InteropServices.Marshal.PtrToStringUTF8(outError) ?? "Unknown error";
                global::System.Runtime.InteropServices.Marshal.FreeCoTaskMem(outError);
                throw new InvalidOperationException($"Failed to register {name}: {errorMsg}");
            }

            // Return the bridge ID (userData).
            // The FFI layer holds this ID and will call FreeUserDataCallback with it when done.
            return userData;
        } catch {
            lock (_registryLock) {
                _bridgeRegistry.Remove(bridge._bridgeId);
            }
            bridge.Dispose();
            throw;
        }
    }
}