I'm looking for guidance on how to handle callbacks from native code when using the LibraryImport source generator. I may have missed it but I didn't see this particular usage covered in the documentation.
Whenever the parameters to a callback are not blittable, it becomes necessary to intercept it, marshal the parameters back to their managed representation, and then invoke the managed callback with the appropriate parameters. As far as I can tell, this is not something currently handled by the source generator.
For example, given a hypothetical native API that opens a connection and takes a callback to be notified of connection state changes.
typedef struct ConnectionState
{
int status_code;
char* error_message;
} ConnectionState;
typedef void (*ConnectionStateChange)(ConnectionState* state);
ConnectionHandle OpenConnection(ConnectionStateChange callback);
This could be handled by the following managed code.
class ConnectionHandle : SafeHandle
{
...
}
public struct ConnectionState
{
public int status_code { get; set; }
public string error_message { get; set; }
internal unsafe struct Native
{
public int status_code;
public sbyte* error_message;
}
}
delegate void ConnectionStateChange(ConnectionState connectionState);
[LibraryImport("some_lib")]
static partial ConnectionHandle OpenConnection([MarshalUsing(typeof(ConnectionStateChangeMarshaller))] ConnectionStateChange callback);
where the callback parameter of the OpenConnection function is marshalled with the following custom marshaller. By the way, the marshaller below generates warning SYSLIB1057 about not having a Free method that doesn't seem right as there's nothing to be freed in this case. Nevertheless, the code compiles and works as expected when the warning is suppressed.
[CustomMarshaller(typeof(ConnectionStateChange), MarshalMode.ManagedToUnmanagedIn, typeof(ConnectionStateChangeMarshaller))]
internal unsafe struct ConnectionStateChangeMarshaller
{
private ConnectionStateChange _callback;
// intercepts the callback with a native representation of the connectionState parameter,
// converts it to its managed representation, and invokes the original callback
private unsafe void Interceptor(ConnectionState.Native* connectionState)
{
ConnectionState marshaller = new();
try
{
marshaller.FromUnmanaged(connectionState);
_callback(marshaller.ToManaged());
}
finally
{
marshaller.Free();
}
}
public void FromManaged(ConnectionStateChange managed) => _callback = managed;
public delegate* managed<ConnectionState.Native*, void> ToUnmanaged()
{
Delegate nativeCallback = Interceptor;
GCHandle.Alloc(nativeCallback); // HACK: to keep delegate alive
return (delegate* managed<ConnectionState.Native*, void>) Marshal.GetFunctionPointerForDelegate(nativeCallback);
}
}
Note the need to keep a reference to the original _callback so that the interceptor can invoke it once it has unmarshalled the parameters, which basically rules out using a static method for the interceptor and requires using a delegate instead. Moreover, given that the callback can be invoked at any time, the lifetime of the delegate cannot be tied to that of the marshaller. For my quick test, and to prevent the delegate from being garbage collected, I allocated a GCHandle that is basically leaked, but this was only a temporary solution.
Prior to the LibraryImport generator, I used code similar in structure to that generated by the source generator where I also intercepted the callback using a delegate. However, in that case, the code had visibility of all the parameters involved in the call and of its return value, so I could store a reference to the delegate in a member of the ConnectionHandle returned by the call to ensure that the delegate was kept alive for as long the connection was open.
However, it seems a custom marshaller designed for the LibraryImport generator has no context and is not aware of other parameters or of the return value of the call where it is being used. Is there a way to associate the delegate reference to the lifetime of some other object?
I'm looking for guidance on how to handle callbacks from native code when using the LibraryImport source generator. I may have missed it but I didn't see this particular usage covered in the documentation.
Whenever the parameters to a callback are not blittable, it becomes necessary to intercept it, marshal the parameters back to their managed representation, and then invoke the managed callback with the appropriate parameters. As far as I can tell, this is not something currently handled by the source generator.
For example, given a hypothetical native API that opens a connection and takes a callback to be notified of connection state changes.
This could be handled by the following managed code.
where the
callbackparameter of theOpenConnectionfunction is marshalled with the following custom marshaller. By the way, the marshaller below generates warning SYSLIB1057 about not having aFreemethod that doesn't seem right as there's nothing to be freed in this case. Nevertheless, the code compiles and works as expected when the warning is suppressed.Note the need to keep a reference to the original
_callbackso that the interceptor can invoke it once it has unmarshalled the parameters, which basically rules out using a static method for the interceptor and requires using a delegate instead. Moreover, given that the callback can be invoked at any time, the lifetime of the delegate cannot be tied to that of the marshaller. For my quick test, and to prevent the delegate from being garbage collected, I allocated a GCHandle that is basically leaked, but this was only a temporary solution.Prior to the LibraryImport generator, I used code similar in structure to that generated by the source generator where I also intercepted the callback using a delegate. However, in that case, the code had visibility of all the parameters involved in the call and of its return value, so I could store a reference to the delegate in a member of the
ConnectionHandlereturned by the call to ensure that the delegate was kept alive for as long the connection was open.However, it seems a custom marshaller designed for the LibraryImport generator has no context and is not aware of other parameters or of the return value of the call where it is being used. Is there a way to associate the delegate reference to the lifetime of some other object?