Skip to content

monkey0506/native-generic-delegates

Repository files navigation

NativeGenericDelegates

NativeGenericDelegates is a C# project designed to provide Delegate class types that are generic and can be used from native code with platform invoke (P/Invoke). There is a historical caveat to .NET generics in that they cannot be used with P/Invoke. Marshal.GetFunctionPointerForDelegate in particular will throw an ArgumentException if passed a generic delegate type. This means that you are often left with no option but to create your own delegate types as-needed, and there is little to no room for code reusability.

This project was inspired by a StackOverflow question for which the correct solution was, in fact, to use the DllImportAttribute. However, there are still scenarios where it may be desirable to utilize generic delegates with P/Invoke and this project aims to facilitate those use cases.

This solution involves a lot of boilerplate code and may not be a good fit for every project, particularly if you are only using a small number of delegates.

What this project does

IMPORTANT NOTE: This project relies on dynamic code using types from the System.Reflection.Emit namespace. These types are not supported on all .NET platforms.

This project provides a set of interfaces that mirror the System.Action and System.Func delegate types with the following features:

  • Generic interface (e.g., INativeAction<int>, INativeFunc<double, bool>, etc.)
  • Full interoperability with APIs consuming Delegate objects (via cast to Delegate)
  • Passing delegate to unmanaged code (via GetFunctionPointer)
  • Construct delegates from managed and unmanaged functions with common interface
  • Non-dynamic invocation from managed code (via Invoke, no calls to Delegate.DynamicInvoke)
  • Define unmanaged function pointer calling convention
  • Optionally define marshaling behavior for return value and parameters
  • Convert to System.Action and System.Func delegate types (ToAction and ToFunc)

How to use this project

You just DO WHAT THE FUCK YOU WANT TO.

As per the license terms, you are free to use this project however you see fit, but the following contains an overview of the public API that the code exposes:

INativeAction and INativeFunc

INativeAction<T..16>
INativeFunc<T..16, TResult>

There are 17 variations each of the INativeAction and INativeFunc interfaces that mirror System.Action and System.Func respectively. Similarly, INativeAction represents a method with no return value (void return type) and INativeFunc takes the return type as the final generic type parameter (TResult). For brevity, the shorthand <T..16>/<T..16, TResult> will be used to denote all 17 variations of the interface.

Converting to and from Delegate

Every instance of INativeAction<T..16> and INativeFunc<T..16, TResult> that is exposed by the methods and properties below are actual instances of a class that is derived from System.MulticastDelegate (which in turn is derived from System.Delegate). Any APIs using these base classes can accept INativeAction or INativeFunc instances, but this requires an explicit cast (as implicit conversions to or from interfaces are forbidden):

private void Foo(int i) { }
private void Bar(Delegate d) { }

var foo = INativeAction<int>.FromDelegate(Foo, CallingConvention.Cdecl);
Bar((Delegate)foo);

The conversion in the other direction can also be done with an explicit cast, and conversions in both directions can use the is or as operators:

if (d is INativeAction<int> action)
{
	action.Invoke(42); // avoid expensive call to DynamicInvoke
}
INativeAction<int>? maybeAction = d as INativeAction<int>;
INativeAction<int> invalidCastExceptionIfWrong = (INativeAction<int>)d;

Custom marshaling

The methods below permit you to specify custom marshaling behavior for the native generic delegates that you create. The default behavior when constructing these delegates using FromDelegate or FromMethod is to copy the marshaling behavior from the method. In those method's marshalReturnAs and marshalParamAs parameters, a null value will be used to represent this default behavior. If you explicitly want the delegate to have no custom marshaling behavior defined, you may use either INativeAction.NoCustomMarshaling or INativeFunc.NoCustomMarshaling.

static MarshalAsAttribute INativeAction.NoCustomMarshaling
static MarshalAsAttribute INativeFunc.NoCustomMarshaling

(NOTE: INativeFunc is a static class to mirror INativeAction when accessing this property, which is only accessible through the parameterless INativeAction interface or the static INativeFunc class. The two references are equivalent.)

This does not turn off custom marshaling of the underlying methods that your native generic delegates will represent. This only affects the marshaling behavior defined for the delegate type itself.

When using FromFunctionPointer, values of null and NoCustomMarshaling are the same, as there is no managed method to copy marshaling behaviors from.

FromDelegate

static INativeAction<T..16> INativeAction<T..16>.FromDelegate(Delegate d, CallingConvention callingConvention, [optional] MarshalAsAttribute?[]? marshalParamAs)
static INativeFunc<T..16, TResult> INativeFunc<T..16, TResult>.FromDelegate(Delegate d, CallingConvention callingConvention, [optional] MarshalAsAttribute? marshalReturnAs, [optional] MarshalAsAttribute?[]? marshalParamAs)

Creates a native generic delegate with the same signature as the interface that will invoke the same method (and target, if any) as the given delegate d.

d is the delegate from which the invocation method and target will be copied.

callingConvention is the calling convention of the unmanaged function pointer returned by GetFunctionPointer.

marshalReturnAs is an optional MarshalAsAttribute that controls the marshaling behavior of the managed delegate return value, if any. INativeAction<T..16> omits this parameter as there is no return value. If this parameter is null, the marshaling behavior for the new delegate's return value will be copied from the marshaling of the managed method (d.Method; this is the default). To specify that the new delegate should have no explicit marshaling, you may pass NoCustomMarshaling. Any custom marshaling of the underlying method's return value will still be preserved, but not represented in the new delegate type.

marshalParamAs is an optional array of MarshalAsAttributes that control the marshaling behavior of the managed delegate parameters. Delegates that accept no parameters (INativeAction and INativeFunc<TResult>) omit this parameter. The length and order of the array must match the function signature. If this parameter is null, the marshaling behavior for the new delegate's parameters will be copied from the marshaling of the managed method's (d.Method's) parameters (this is the default). To specify that a parameter in the new delegate type should have no explicit marshaling, you may pass NoCustomMarshaling. Any custom marshaling of the underlying method's parameters will still be preserved, but not represented in the new delegate type.

Returns: The new delegate instance.

Example:

public static void OnNativeEvent(int eventID)
{
	Console.WriteLine($"Native event {eventID} raised.");
}

var nativeEventHandler = INativeAction<int>.FromDelegate(OnNativeEvent, CallingConvention.Cdecl);
NativeMethods.SetNativeEventCallback(nativeEventHandler.GetFunctionPointer());

See also: FromFunctionPointer, FromMethod, GetFunctionPointer, Invoke, Method, Target

FromFunctionPointer

static INativeAction INativeAction.FromFunctionPointer(nint functionPtr, CallingConvention callingConvention)
static INativeAction<T..16> INativeAction<T..16>.FromFunctionPointer(nint functionPtr, CallingConvention callingConvention, [optional] MarshalAsAttribute?[]? marshalParamAs)
static unsafe INativeAction INativeAction.FromFunctionPointer(delegate* unmanaged[CALL_CONV]<void> functionPtr)
static unsafe INativeAction<T..16> INativeAction<T..16>.FromFunctionPointer(delegate* unmanaged[CALL_CONV]<T..16, void> functionPtr, [optional] MarshalAsAttribute?[]? marshalParamAs)
static unsafe INativeAction<T..16> INativeAction<T..16>.FromFunctionPointer<U..16>(delegate* unmanaged[CALL_CONV]<U..16, void> functionPtr, [optional] MarshalAsAttribute?[]? marshalParamAs)
static INativeFunc<TResult> INativeFunc<TResult>.FromFunctionPointer(nint functionPtr, CallingConvention callingConvention, [optional] MarshalAsAttribute? marshalReturnAs)
static INativeFunc<T..16, TResult> INativeFunc<T..16, TResult>.FromFunctionPointer(nint functionPtr, CallingConvention callingConvention, [optional] MarshalAsAttribute? marshalReturnAs, [optional] MarshalAsAttribute?[]? marshalParamAs)
static unsafe INativeFunc<TResult> INativeFunc<TResult>.FromFunctionPointer(delegate* unmanaged[CALL_CONV]<TResult> functionPtr, [optional] MarshalAsAttribute? marshalReturnAs)
static unsafe INativeFunc<T..16, TResult> INativeFunc<T..16, TResult>.FromFunctionPointer(delegate* unmanaged[CALL_CONV]<T..16, TResult> functionPtr, [optional] MarshalAsAttribute? marshalReturnAs, [optional] MarshalAsAttribute?[]? marshalParamAs)
static unsafe INativeFunc<T..16, TResult> INativeFunc<T..16, TResult>.FromFunctionPointer<U..16, UResult>(delegate* unmanaged[CALL_CONV]<U..16, UResult> functionPtr, [optional] MarshalAsAttribute? marshalReturnAs, [optional] MarshalAsAttribute?[]? marshalParamAs)

Methods indicated as unsafe require the /unsafe compiler switch.

CALL_CONV is one of Cdecl, Stdcall, or Thiscall. Fastcall is not supported.

Creates a native generic delegate with the same signature as the interface that will invoke an unmanaged function pointer.

The overloads that accept type parameters for the unmanaged function pointer <U..16>/<U..16, UResult> permit you to change the managed delegate parameter types from the native function's parameter types (<T..16>/<T..T16, TResult> refers to the managed types and <U..16>/<U..16, UResult> refers to the unmanaged types). If you do change any of the parameter types, you must use marshalParamAs to define the correct marshaling behavior.

functionPtr is the unmanaged function pointer that will be invoked by the delegate.

callingConvention is the calling convention of the unmanaged function pointer returned by GetFunctionPointer. The overloads that take an unmanaged function pointer omit this parameter, as it is inferred from CALL_CONV instead.

marshalReturnAs is an optional MarshalAsAttribute that controls the marshaling behavior of the managed delegate return value, if any. INativeAction<T..16> omits this parameter as there is no return value. If no custom marshaling behavior is needed for the return value, this should be null (this is the default).

marshalParamAs is an optional array of MarshalAsAttributes that control the marshaling behavior of the managed delegate parameters. Delegates that accept no parameters (INativeAction and INativeFunc<TResult>) omit this parameter. The length and order of the array must match the function signature. If no custom marshaling behavior is needed for a parameter, the corresponding index in the array should be null. If no custom marshaling is needed for any parameters, the array may be null (this is the default).

Returns: The new delegate instance.

Example:

nint pMyDllFunc = NativeMethods.GetProcAddress(myDllHandle, "MyDllFunc"); // get some function pointer from native code
var myDllFuncHandler = INativeFunc<int, string>.FromFunctionPointer
(
	functionPtr: pMyDllFunc,
	callingConvention: CallingConvention.StdCall,
	marshalReturnAs: new MarshalAsAttribute(UnmanagedType.LPUTF8Str)
);
Console.WriteLine(myDllFuncHandler.Invoke(42));

Where possible, DllImportAttribute or LibraryImportAttribute are likely a better option to import a method from an unmanaged library. This method may be useful if an unmanaged library exposes method handles that are not exported or are expensive to load, or if you need to invoke the method from managed code while maintaining a pointer to the method.

See also: FromDelegate, FromMethod GetFunctionPointer, Invoke

FromMethod

static INativeAction<T..16> INativeAction<T..16>.FromMethod(object? target, MethodInfo method, CallingConvention callingConvention, [optional] MarshalAsAttribute?[]? marshalParamAs)
static INativeFunc<T..16, TResult> INativeFunc<T..16, TResult>.FromMethod(object? target, MethodInfo method, CallingConvention callingConvention, [optional] MarshalAsAttribute? marshalReturnAs, [optional] MarshalAsAttribute?[]? marshalParamAs)

Creates a native generic delegate with the same signature as the interface that will invoke the given method method with the given target target.

Marshaling behavior matches any MarshalAsAttributes that are applied to the parameters and return value of method.

target is the class instance on which the method will be invoked. Should be null if method is a static method.

method is the method that will be invoked by this delegate.

callingConvention is the calling convention of the unmanaged function pointer returned by GetFunctionPointer.

marshalReturnAs is an optional MarshalAsAttribute that controls the marshaling behavior of the managed delegate return value, if any. INativeAction<T..16> omits this parameter as there is no return value. If this parameter is null, the marshaling behavior for the new delegate's return value will be copied from the marshaling of the managed method (this is the default). To specify that the new delegate should have no explicit marshaling, you may pass NoCustomMarshaling. Any custom marshaling of the underlying method's return value will still be preserved, but not represented in the new delegate type.

marshalParamAs is an optional array of MarshalAsAttributes that control the marshaling behavior of the managed delegate parameters. Delegates that accept no parameters (INativeAction and INativeFunc<TResult>) omit this parameter. The length and order of the array must match the function signature. If this parameter is null, the marshaling behavior for the new delegate's parameters will be copied from the marshaling of the managed method's parameters (this is the default). To specify that a parameter in the new delegate type should have no explicit marshaling, you may pass NoCustomMarshaling. Any custom marshaling of the underlying method's parameters will still be preserved, but not represented in the new delegate type.

Returns: The new delegate instance.

Example:

public static void OnNativeEvent(int eventID)
{
	Console.WriteLine($"Native event {eventID} raised.");
}

var nativeEventHandler = INativeAction<int>.FromMethod
(
	target: null,
	method: typeof(Program).GetMethod(nameof(OnNativeEvent))!,
	callingConvention: CallingConvention.Cdecl
);
NativeMethods.SetNativeEventCallback(nativeEventHandler.GetFunctionPointer());

See also: FromDelegate FromFunctionPointer, GetFunctionPointer, Invoke, Method, Target

GetFunctionPointer

nint INativeAction<T..16>.GetFunctionPointer()
nint INativeFunc<T..16, TResult>.GetFunctionPointer()

Converts the delegate into a function pointer that is callable from unmanaged code.

You must keep a managed reference to the delegate for the lifetime of the unmanaged function pointer.

Returns: A value that can be passed to unmanaged code, which, in turn, can use it to call the underlying managed delegate.

Example:

public static void OnNativeEvent(int eventID)
{
	Console.WriteLine($"Native event {eventID} raised.");
}

var nativeEventHandler = INativeAction<int>.FromMethod
(
	target: null,
	method: typeof(Program).GetMethod(nameof(OnNativeEvent))!,
	callingConvention: CallingConvention.Cdecl
);
NativeMethods.SetNativeEventCallback(nativeEventHandler.GetFunctionPointer());

Invoke

void INativeAction<T..16>.Invoke(T..16 t..16);
TResult INativeFunc<T..16, TResult>.Invoke(T..16 t..16)

Invokes the managed or unmanaged method that the delegate represents with the given parameters.

Note: This does not call Delegate.DynamicInvoke and does not incur the performance penalty associated with that method.

Returns: Nothing (INativeAction<T..16>) or TResult (INativeFunc<T..16, TResult>).

Example:

var printPoint = INativeAction<int, int>.FromDelegate
(
	(int x, int y) => Console.WriteLine($"Point {{ X = {x}, Y = {y} }}"),
	CallingConvention.Cdecl
);
printPoint.Invoke(420, 69);

See also: FromDelegate FromFunctionPointer, FromMethod, GetFunctionPointer, Method, Target

ToAction and ToFunc

Action<T..16> INativeAction<T..16>.ToAction()
Func<T..16, TResult> INativeFunc<T..16, TResult>.ToFunc()

Creates an System.Action or System.Func with the same Target and Method as the delegate.

Note: The returned delegate is not one that implements the INativeAction or INativeFunc interfaces. You can convert back to an equivalent delegate using the FromDelegate method.

Returns: The requested delegate.

Example:

public static void InvokeAction(Action<int, int> action, int x, int y)
{
	action(x, y);
}

var printPoint = INativeAction<int, int>.FromDelegate
(
	(int x, int y) => Console.WriteLine($"Point {{ X = {x}, Y = {y} }}"),
	CallingConvention.Cdecl
);
InvokeAction(printPoint.ToAction(), 420, 69);

See also: GetFunctionPointer, Method, Target

Method

MethodInfo INativeAction<T..16>.Method { get; }
MethodInfo INativeFunc<T..16, TResult>.Method { get; }

Gets the method that is represented by this delegate.

Example:

public static void Foo() { }

var foo = INativeAction.FromDelegate(Foo, CallingConvention.Cdecl);
Console.WriteLine($"{nameof(foo)} represents the method {foo.Method.Name}");

See also: FromDelegate, FromMethod Target

Target

object? INativeAction<T..16>.Target { get; }
object? INativeFunc<T..16, TResult>.Target { get; }

Gets the class instance on which the current delegate invokes the instance method, or null if this delegate represents a static method.

Returns: The object on which the current delegate invokes the instance method, if the delegate represents an instance method; null if the delegate represents a static method.

Example:

public class Foo
{
	public void Bar(int i) { }
}

Foo foo = new();
var bar = INativeAction<int>.FromDelegate(foo.Bar, CallingConvention.Cdecl);
Console.WriteLine($"{nameof(bar)} target is {nameof(foo)}? {object.ReferenceEquals(bar.Target, foo)}"); // true

Technical implementation detail

The "magic" behind the scenes here is that we use types from System.Reflection.Emit to create a class type that inherits from System.MulticastDelegate. It is explicitly disallowed to inherit from MulitcastDelegate at compile-time, but types emitted dynamically at runtime are permitted to do so. This emitted class is then also permitted to permitted to implement an interface (while compile-time delegates cannot).

In the INativeAction<T..16> and INativeFunc<T..16, TResult> interfaces, the only member that does not have a default implementation (and thus must be implemented by the class) is the Invoke method. Because the runtime class type we define has this method, we can cast our runtime Delegate-derived class to and from the interface that it implements.

See DelegateFactory.cs for the implementation.