/// <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;
}
}
}