CppSharp icon indicating copy to clipboard operation
CppSharp copied to clipboard

Generating C# delegates from function pointers

Open imaras opened this issue 2 years ago • 5 comments

I am generating C# code using header file

#ifndef MY_HEADER_H
#define MY_HEADER_H

class my_class;
class my_args;
typedef void (*my_callback)(my_class*, my_args);

class MY_EXPORT my_args
{
public:
    my_args(int val) : m_Val(val) {}
    int get_val() { return m_Val; }
private:
    int m_Val;
};

class MY_EXPORT my_class
{
public:
    my_class(int id, my_callback callback) : m_Id(id), m_Callback(callback) {}
    void publish(int n) { m_Callback(this, my_args(n)); }
    int get_id() { return m_Id; }
private:
    my_callback m_Callback;
    int m_Id;
};

#endif

Genarated C# file is used to build manged dll. Code in client application using the managed dll is:

static void Main(string[] args) 
{
    MyCallback callback = new MyCallback((ptr, args) => 
    {
        // ptr is of type IntPtr and it can be marshaled to MyClass.__Internal
        // args is MyArgs.__Internal
    });
    MyClass o = new MyClass(33, callback);
    o.Publish(10);
}

I would like to be able to get values stored in fields m_Id of MyClass and m_Val of My_args from within the delegate. What is the best way to achieve this using CppSharp?

At the moment I can't achieve this since MyCallback is generated as:

[SuppressUnmanagedCodeSecurity, UnmanagedFunctionPointer(__CallingConvention.Cdecl)]
public unsafe delegate void MyCallback(__IntPtr __0, global::MyArgs.__Internal __1);

I can get from IntPtr to MyClass.__Internal but generated type MyClass.__Internal does not have public access to the GetId method.

OS: Windows

imaras avatar Mar 27 '23 11:03 imaras

I think you should be able to use either the managed constructor my_class, or maybe its __CreateInstance method, and passing the pointer you have on the callback.

Then you should be able to use the managed instance as normal. Just be careful with the lifetimes, you may or not need to call Dispose on the instance.

tritao avatar Mar 27 '23 11:03 tritao

The generated C# code for MyClass type is:

public unsafe partial class MyClass : IDisposable
{
    [StructLayout(LayoutKind.Sequential, Size = 16)]
    public partial struct __Internal
    {
        internal __IntPtr m_Callback;
        internal int m_Id;

        [SuppressUnmanagedCodeSecurity, DllImport("testdll", EntryPoint = "??0my_class@@QEAA@HP6AXPEAV0@Vmy_args@@@Z@Z", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern __IntPtr ctor(__IntPtr __instance, int id, __IntPtr callback);

        [SuppressUnmanagedCodeSecurity, DllImport("ManagedTestdll", EntryPoint = "??0my_class@@QEAA@AEBV0@@Z", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern __IntPtr cctor(__IntPtr __instance, __IntPtr _0);

        [SuppressUnmanagedCodeSecurity, DllImport("testdll", EntryPoint = "?publish@my_class@@QEAAXH@Z", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern void Publish(__IntPtr __instance, int n);

        [SuppressUnmanagedCodeSecurity, DllImport("testdll", EntryPoint = "?get_id@my_class@@QEAAHXZ", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern int GetId(__IntPtr __instance);
    }

    public __IntPtr __Instance { get; protected set; }

    internal static readonly new global::System.Collections.Concurrent.ConcurrentDictionary<IntPtr, global::MyClass> NativeToManagedMap =
        new global::System.Collections.Concurrent.ConcurrentDictionary<IntPtr, global::MyClass>();

    internal static void __RecordNativeToManagedMapping(IntPtr native, global::MyClass managed)
    {
        NativeToManagedMap[native] = managed;
    }

    internal static bool __TryGetNativeToManagedMapping(IntPtr native, out global::MyClass managed)
    {
    
        return NativeToManagedMap.TryGetValue(native, out managed);
    }

    protected bool __ownsNativeInstance;

    internal static global::MyClass __CreateInstance(__IntPtr native, bool skipVTables = false)
    {
        if (native == __IntPtr.Zero)
            return null;
        return new global::MyClass(native.ToPointer(), skipVTables);
    }

    internal static global::MyClass __GetOrCreateInstance(__IntPtr native, bool saveInstance = false, bool skipVTables = false)
    {
        if (native == __IntPtr.Zero)
            return null;
        if (__TryGetNativeToManagedMapping(native, out var managed))
            return (global::MyClass)managed;
        var result = __CreateInstance(native, skipVTables);
        if (saveInstance)
            __RecordNativeToManagedMapping(native, result);
        return result;
    }

    internal static global::MyClass __CreateInstance(__Internal native, bool skipVTables = false)
    {
        return new global::MyClass(native, skipVTables);
    }

    private static void* __CopyValue(__Internal native)
    {
        var ret = Marshal.AllocHGlobal(sizeof(__Internal));
        *(__Internal*) ret = native;
        return ret.ToPointer();
    }

    private MyClass(__Internal native, bool skipVTables = false)
        : this(__CopyValue(native), skipVTables)
    {
        __ownsNativeInstance = true;
        __RecordNativeToManagedMapping(__Instance, this);
    }

    protected MyClass(void* native, bool skipVTables = false)
    {
        if (native == null)
            return;
        __Instance = new __IntPtr(native);
    }

    public MyClass(int id, global::MyCallback callback)
    {
        __Instance = Marshal.AllocHGlobal(sizeof(global::MyClass.__Internal));
        __ownsNativeInstance = true;
        __RecordNativeToManagedMapping(__Instance, this);
        var __arg1 = callback == null ? global::System.IntPtr.Zero : Marshal.GetFunctionPointerForDelegate(callback);
        __Internal.ctor(__Instance, id, __arg1);
    }

    public MyClass(global::MyClass _0)
    {
        __Instance = Marshal.AllocHGlobal(sizeof(global::MyClass.__Internal));
        __ownsNativeInstance = true;
        __RecordNativeToManagedMapping(__Instance, this);
        *((global::MyClass.__Internal*) __Instance) = *((global::MyClass.__Internal*) _0.__Instance);
    }

    public void Dispose()
    {
        Dispose(disposing: true, callNativeDtor : __ownsNativeInstance );
    }

    partial void DisposePartial(bool disposing);

    internal protected virtual void Dispose(bool disposing, bool callNativeDtor )
    {
        if (__Instance == IntPtr.Zero)
            return;
        NativeToManagedMap.TryRemove(__Instance, out _);
        DisposePartial(disposing);
        if (__ownsNativeInstance)
            Marshal.FreeHGlobal(__Instance);
        __Instance = IntPtr.Zero;
    }

    public void Publish(int n)
    {
        __Internal.Publish(__Instance, n);
    }

    public int Id
    {
        get
        {
            var ___ret = __Internal.GetId(__Instance);
            return ___ret;
        }
    }
}

Since the method you suggested to use (__CreateInstance) is generated as internal it can't be accesses from client application. Am I missing some generator option to make it public?

imaras avatar Mar 27 '23 12:03 imaras

The generated C# code for MyClass type is:

public unsafe partial class MyClass : IDisposable
{
    [StructLayout(LayoutKind.Sequential, Size = 16)]
    public partial struct __Internal
    {
        internal __IntPtr m_Callback;
        internal int m_Id;

        [SuppressUnmanagedCodeSecurity, DllImport("testdll", EntryPoint = "??0my_class@@QEAA@HP6AXPEAV0@Vmy_args@@@Z@Z", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern __IntPtr ctor(__IntPtr __instance, int id, __IntPtr callback);

        [SuppressUnmanagedCodeSecurity, DllImport("ManagedTestdll", EntryPoint = "??0my_class@@QEAA@AEBV0@@Z", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern __IntPtr cctor(__IntPtr __instance, __IntPtr _0);

        [SuppressUnmanagedCodeSecurity, DllImport("testdll", EntryPoint = "?publish@my_class@@QEAAXH@Z", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern void Publish(__IntPtr __instance, int n);

        [SuppressUnmanagedCodeSecurity, DllImport("testdll", EntryPoint = "?get_id@my_class@@QEAAHXZ", CallingConvention = __CallingConvention.Cdecl)]
        internal static extern int GetId(__IntPtr __instance);
    }

    public __IntPtr __Instance { get; protected set; }

    internal static readonly new global::System.Collections.Concurrent.ConcurrentDictionary<IntPtr, global::MyClass> NativeToManagedMap =
        new global::System.Collections.Concurrent.ConcurrentDictionary<IntPtr, global::MyClass>();

    internal static void __RecordNativeToManagedMapping(IntPtr native, global::MyClass managed)
    {
        NativeToManagedMap[native] = managed;
    }

    internal static bool __TryGetNativeToManagedMapping(IntPtr native, out global::MyClass managed)
    {
    
        return NativeToManagedMap.TryGetValue(native, out managed);
    }

    protected bool __ownsNativeInstance;

    internal static global::MyClass __CreateInstance(__IntPtr native, bool skipVTables = false)
    {
        if (native == __IntPtr.Zero)
            return null;
        return new global::MyClass(native.ToPointer(), skipVTables);
    }

    internal static global::MyClass __GetOrCreateInstance(__IntPtr native, bool saveInstance = false, bool skipVTables = false)
    {
        if (native == __IntPtr.Zero)
            return null;
        if (__TryGetNativeToManagedMapping(native, out var managed))
            return (global::MyClass)managed;
        var result = __CreateInstance(native, skipVTables);
        if (saveInstance)
            __RecordNativeToManagedMapping(native, result);
        return result;
    }

    internal static global::MyClass __CreateInstance(__Internal native, bool skipVTables = false)
    {
        return new global::MyClass(native, skipVTables);
    }

    private static void* __CopyValue(__Internal native)
    {
        var ret = Marshal.AllocHGlobal(sizeof(__Internal));
        *(__Internal*) ret = native;
        return ret.ToPointer();
    }

    private MyClass(__Internal native, bool skipVTables = false)
        : this(__CopyValue(native), skipVTables)
    {
        __ownsNativeInstance = true;
        __RecordNativeToManagedMapping(__Instance, this);
    }

    protected MyClass(void* native, bool skipVTables = false)
    {
        if (native == null)
            return;
        __Instance = new __IntPtr(native);
    }

    public MyClass(int id, global::MyCallback callback)
    {
        __Instance = Marshal.AllocHGlobal(sizeof(global::MyClass.__Internal));
        __ownsNativeInstance = true;
        __RecordNativeToManagedMapping(__Instance, this);
        var __arg1 = callback == null ? global::System.IntPtr.Zero : Marshal.GetFunctionPointerForDelegate(callback);
        __Internal.ctor(__Instance, id, __arg1);
    }

    public MyClass(global::MyClass _0)
    {
        __Instance = Marshal.AllocHGlobal(sizeof(global::MyClass.__Internal));
        __ownsNativeInstance = true;
        __RecordNativeToManagedMapping(__Instance, this);
        *((global::MyClass.__Internal*) __Instance) = *((global::MyClass.__Internal*) _0.__Instance);
    }

    public void Dispose()
    {
        Dispose(disposing: true, callNativeDtor : __ownsNativeInstance );
    }

    partial void DisposePartial(bool disposing);

    internal protected virtual void Dispose(bool disposing, bool callNativeDtor )
    {
        if (__Instance == IntPtr.Zero)
            return;
        NativeToManagedMap.TryRemove(__Instance, out _);
        DisposePartial(disposing);
        if (__ownsNativeInstance)
            Marshal.FreeHGlobal(__Instance);
        __Instance = IntPtr.Zero;
    }

    public void Publish(int n)
    {
        __Internal.Publish(__Instance, n);
    }

    public int Id
    {
        get
        {
            var ___ret = __Internal.GetId(__Instance);
            return ___ret;
        }
    }
}

Since the method you suggested to use (__CreateInstance) is generated as internal it can't be accesses from client application. Am I missing some generator option to make it public?

imaras avatar Mar 27 '23 12:03 imaras

Since MyClass is generated as partial class I can add another C# file which provides public method for creating managed type from MyClass.__Internal type.

public unsafe partial class MyClass
{
    public static global::MyClass CreateInstance(__Internal native)
    {
        return __CreateInstance(native);
    }
}

imaras avatar Mar 27 '23 13:03 imaras

Since MyClass is generated as partial class I can add another C# file which provides public method for creating managed type from MyClass.__Internal type.

public unsafe partial class MyClass
{
    public static global::MyClass CreateInstance(__Internal native)
    {
        return __CreateInstance(native);
    }
}

Cool, that seems OK.

Another possible alternative would be to use IntervalsVisibleTo: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.internalsvisibletoattribute?view=net-8.0

Or patch the generator to do a bit better, we have https://github.com/mono/CppSharp/issues/511 open for that.

tritao avatar Mar 27 '23 13:03 tritao