❔ `delegate*` wrapper throws `System.BadImageFormatException`

I wrote some wrapper for delegate* unmanaged to use it in F# since it doesn't support function pointers:
using Microsoft.FSharp.Core;

public unsafe struct FuncPtr<TArg, TRet>
{
private readonly nint ptr;

public FuncPtr(nint ptr) => this.ptr = ptr;

public TRet Call(TArg arg)
{
if (typeof(TRet) == typeof(Unit))
{
if (typeof(TArg) == typeof(Unit))
((delegate* unmanaged<void>)ptr)();
else
((delegate* unmanaged<TArg, void>)ptr)(arg);

return default!;
}
else
{
return typeof(TArg) == typeof(Unit) ?
((delegate* unmanaged<TRet>)ptr)() :
((delegate* unmanaged<TArg, TRet>)ptr)(arg);
}
}
}
using Microsoft.FSharp.Core;

public unsafe struct FuncPtr<TArg, TRet>
{
private readonly nint ptr;

public FuncPtr(nint ptr) => this.ptr = ptr;

public TRet Call(TArg arg)
{
if (typeof(TRet) == typeof(Unit))
{
if (typeof(TArg) == typeof(Unit))
((delegate* unmanaged<void>)ptr)();
else
((delegate* unmanaged<TArg, void>)ptr)(arg);

return default!;
}
else
{
return typeof(TArg) == typeof(Unit) ?
((delegate* unmanaged<TRet>)ptr)() :
((delegate* unmanaged<TArg, TRet>)ptr)(arg);
}
}
}
But when I try to call Call() method:
using KiDev.IlAsm.FuncPtr;
using Microsoft.FSharp.Core;
using System.Runtime.InteropServices;

unsafe
{
delegate* unmanaged<int> x = &Test;
Console.WriteLine($"delegate* = {x()}"); // fine

var ptr = new FuncPtr<Unit, int>((nint)x);
Console.WriteLine($"FuncPtr = {ptr.Call(null!)}"); // exception
}

[UnmanagedCallersOnly]
static int Test() => 12;
using KiDev.IlAsm.FuncPtr;
using Microsoft.FSharp.Core;
using System.Runtime.InteropServices;

unsafe
{
delegate* unmanaged<int> x = &Test;
Console.WriteLine($"delegate* = {x()}"); // fine

var ptr = new FuncPtr<Unit, int>((nint)x);
Console.WriteLine($"FuncPtr = {ptr.Call(null!)}"); // exception
}

[UnmanagedCallersOnly]
static int Test() => 12;
I get System.BadImageFormatException: "Bad element type in SizeOf.". What I am doing wrong?
43 Replies
nukleer bomb
nukleer bomb2y ago
For those who don't know: roughly, F# Unit is same as C# void and it is assumed that any value of type Unit is null
HowNiceOfYou
HowNiceOfYou2y ago
You can pass the default value for the TArg type instead of null. For example:
var ptr = new FuncPtr<Unit, int>((nint)x);
Console.WriteLine($"FuncPtr = {ptr.Call(default(Unit))}");
var ptr = new FuncPtr<Unit, int>((nint)x);
Console.WriteLine($"FuncPtr = {ptr.Call(default(Unit))}");
nukleer bomb
nukleer bomb2y ago
This does not matter at all, because this is just an example and this struct is intended to be used in F#, where Unit is treated as void and can be not passed at all:
FuncPtr<unit, int>(address1).Call() |> ignore
FuncPtr<int, unit>(address2).Call(123)
FuncPtr<unit, int>(address1).Call() |> ignore
FuncPtr<int, unit>(address2).Call(123)
HowNiceOfYou
HowNiceOfYou2y ago
Sorry, didn't read that part. Uhm you can make changes to the Call. I think this will work.
public TRet Call(TArg arg = default)
{
if (typeof(TRet) == typeof(Unit))
{
if (typeof(TArg) == typeof(Unit))
((delegate* unmanaged<void>)ptr)();
else
((delegate* unmanaged<TArg, void>)ptr)(arg);

return default!;
}
else
{
return typeof(TArg) == typeof(Unit) ?
((delegate* unmanaged<TRet>)ptr)() :
((delegate* unmanaged<TArg, TRet>)ptr)(arg);
}
}
public TRet Call(TArg arg = default)
{
if (typeof(TRet) == typeof(Unit))
{
if (typeof(TArg) == typeof(Unit))
((delegate* unmanaged<void>)ptr)();
else
((delegate* unmanaged<TArg, void>)ptr)(arg);

return default!;
}
else
{
return typeof(TArg) == typeof(Unit) ?
((delegate* unmanaged<TRet>)ptr)() :
((delegate* unmanaged<TArg, TRet>)ptr)(arg);
}
}
Petris
Petris2y ago
Is Unit a struct or a class?
nukleer bomb
nukleer bomb2y ago
class
Petris
Petris2y ago
Then why do you expect it to work with an unmanaged function pointer Make it an empty struct
nukleer bomb
nukleer bomb2y ago
Unit is a part of F#'s stdlib, I can't change it
HowNiceOfYou
HowNiceOfYou2y ago
Have you tried my suggested code?
nukleer bomb
nukleer bomb2y ago
Adding default is just a visual change that doesn't do anything (but yes, I tried) And both 'TArg and 'TRet are either Unit or some value type, this is checked at F# site
jakobbotsch
jakobbotsch2y ago
what unmanaged function would you call that takes an object reference as an argument? It doesn't make sense
Petris
Petris2y ago
Well a class won't work with an unmanaged function pointer So it probably refuses to load il with one Even if the branch isn't reachable You'd have to make some hidden static method that'd be constrained to unmanaged and pass a dummy valuetype there for Unit Also you'd probably want to make this a readonly struct and use a nuint or a void* instead of nint @tannergooding which is more appropriate for storing a function pointer, nuint or a void*
tannergooding
tannergooding2y ago
void*, but nuint is also "fine" and at least matches on being unsigned The general issues is that Unit is an F# only and a managed only concept Because it doesn't get niche filled to void and because it is not runtime specialized, it has hidden overhead/cost/incompatibilities with a non-F# ABI It would work fine for a managed function pointer that binds to an F# function but not for a managed function pointer for a C# function and not for an unmanaged function pointer
Petris
Petris2y ago
Although ldftn is documented to load a signed native int on the stack
tannergooding
tannergooding2y ago
the runtime spec doesn't have unsigned types on the stack see "cls compliance"
Petris
Petris2y ago
(while calli is documented to take a function pointer)
tannergooding
tannergooding2y ago
everything on the runtime stack is a signed, and often normalized type it is up to the subsequent instructions to determine how it is handled (signed or unsigned) and how much of the value is used
Petris
Petris2y ago
The ecma says that there are signed types in cil?
tannergooding
tannergooding2y ago
I said unsigned and on the stack The runtime is aware of signed vs unsigned types the runtime stack only contains signed and normalized types e.g. there is no float vs double on the stack only R likewise there is no string vs Attribute vs ..., only O and there is no byte/sbyte/short/ushort/uint, only int
Petris
Petris2y ago
?
tannergooding
tannergooding2y ago
again, the runtime stack the instruction, such as cgt vs cgt.un dictates whether the values are interpreted as signed vs unsigned
intermediate types – only a subset of the built-in value types can be represented on the evaluation stack (§I.12.1). Values of other built-in value types are translated to/from their intermediate type when loaded onto/stored from the evaluation stack. The intermediate types are a subset of the verification types plus the floating-point type F (which is not a member of the above four subsets).
Petris
Petris2y ago
tannergooding
tannergooding2y ago
this is why, in the runtime HIL, you only get TYP_INT and why you see me regularly complaining about the lack of tracking for "small types" and for unsigned types in general yes, that's the part that covers that the evaluation stack is not signed vs unsigned some implementation could do that, but none really do its up to the instruction to determine how the evaluation stack entry is handled particularly given that some instructions allow implicit upcasting
nukleer bomb
nukleer bomb2y ago
I created unmanaged constrained version of Call, CallImpl and some reflection wrapper, Call to still use it with Unit https://paste.mod.gg/rfyuzaszrvnr/0 All delegate creation logic (in FuncPtr static constructor) works fine, so unmanaged constraints are passed... ...but it still throws
BlazeBin - rfyuzaszrvnr
A tool for sharing your source code with the world!
tannergooding
tannergooding2y ago
What is the definition of FuncPtr? There is a fundamental incompatibility between Unit and unmanaged function pointers, the runtime will completely block it due to being a managed type You could create a managed wrapper which tracks an unmanaged fnptr field and then dispatches to it that way but in general it's not going to be nice
nukleer bomb
nukleer bomb2y ago
Just look sources I check if TArg and TRet are Unit or not, and choose which backing function to call So delegate* logic will take unmanaged types (including struct PseudoUnit)
tannergooding
tannergooding2y ago
I think you should probably just wait for the F# support even if you got this working "as is", you're allocating a delegate and losing all benefits of function pointers There are potentially things you could do to workaround this, but in general its not going to work or run well You'd be better off not supporting Unit here at all and just providing 1-to-1 ABI equivalent fnptrs and then requiring users to not pass down units themselves
nukleer bomb
nukleer bomb2y ago
I don't want to have some special benefits of function pointers, I just need to call runtime generated machine code
tannergooding
tannergooding2y ago
Use a delegate then its cheaper and more efficient than trying to hack something together
nukleer bomb
nukleer bomb2y ago
Looks like it's the only good option right now
tannergooding
tannergooding2y ago
Function pointers are a lowlevel utility designed for a very specific use/purpose they are not designed to work "well" with higher level language concepts, like Unit they are meant to provide a 1-to-1 with the actual ABI and do so in a non-allocating and non GC tracked fashion
nukleer bomb
nukleer bomb2y ago
runtime generated machine code
I just have an address where it is located, so I have to use function pointers
tannergooding
tannergooding2y ago
Marshal.GetDelegateForFunctionPointer -or- just don't support unit and require the caller to handle the ABI difference for the common case, you can write an F# inline function to do that
nukleer bomb
nukleer bomb2y ago
Doesn't support generic delegates, so no Action and Func
tannergooding
tannergooding2y ago
That is, if you define this in C#:
public struct UnmangedFnptr<TRet>
{
private delegate* unmanaged<TRet> _field;

public TRet Invoke() => _field();
}
public struct UnmangedFnptr<TRet>
{
private delegate* unmanaged<TRet> _field;

public TRet Invoke() => _field();
}
You can then write an F# inline fn that does the relevant unit handling to call Invoke() you'd still need to have a different fnptr type for "action" vs "func", but that's expected at this level and is something that the actual F# support will likely be limited around as well
nukleer bomb
nukleer bomb2y ago
Oh you're right Thanks a lot
tannergooding
tannergooding2y ago
@TIHan might have a good suggestion on a way to simplify that he's a lot more familiar with F# than I am, given he used to be on the team
.tihan
.tihan2y ago
You probably want to use Marshal.GetDelegateForFunctionPointer and create a delegate in F# -> https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/delegates
Delegates in F#
Learn how to work with delegates in F#.
.tihan
.tihan2y ago
I wanted to implement function pointer support in F# but it wasn't a priority so it never happened
nukleer bomb
nukleer bomb2y ago
Doesn't support generic delegates Wait . The problem was even not in Unit class Looks like delegate* unmanaged can't handle any generics
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.