Search code examples
c#clrclr-hosting

CLR Hosting: Call a function with an arbitrary method signature?


I need to take a C++ program, load CLR and call a function in a C# library. The function I need to call takes in a COM interface as the parameter.

My problem is, the CLR hosting interface only seems to let you call a method with this signature:

int Foo(String arg)

Example, this C++ code loads CLR and runs the P.Test function in "test.exe":

ICLRRuntimeHost *pClrHost = NULL;
HRESULT hrCorBind = CorBindToRuntimeEx(NULL, L"wks", 0, CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (PVOID*)&pClrHost);

HRESULT hrStart = pClrHost->Start();

DWORD retVal;
HRESULT hrExecute = pClrHost->ExecuteInDefaultAppDomain(L"C:\\Test.exe", L"P", L"Test", L"", &retVal);

What I need to do is call a function with this method signature (note I own the C# code, so I can change it):

void SomeFunction(IFoo interface)

Where IFoo is a com interface. I could even do what I need to if I could call a function like this:

IntPtr SomeFunction();

I could have SomeFunction construct a correct delegate then use Marshal.GetFunctionPointerForDelegate. However, I can't figure out how to make the hosting interfaces do anything other than call a function with an int func(string) signature.

Does anyone know how to call a C# function from C++ code with a different signature?

(Note I cannot use C++/CLI for this. I need to use the hosting APIs.)


Solution

  • Edit: I promised to update my answer to include the code for passing 64-bit values, so here's goes..

    • I left the original answer if someone's interested in a less complicated solution for a 32-bit system.

    Note: Since you're using CorBindToRuntimeEx, which is obsolete in .net 4.0, I'll assume a .net 2.0 compliant solution using good old Win32 API.

    So, in order to pass data between C# and C++ (in our case - the IntPtr of a delegate), we'll create a small Win32 DLL project, named SharedMem, with two straight-forward methods.

    SharedMem.h

    #pragma once
    
    #ifdef SHAREDMEM_EXPORTS
    #define SHAREDMEM_API __declspec(dllexport)
    #else
    #define SHAREDMEM_API __declspec(dllimport)
    #endif
    
    #define SHAREDMEM_CALLING_CONV __cdecl
    
    extern "C" {
        SHAREDMEM_API BOOL SHAREDMEM_CALLING_CONV SetSharedMem(ULONGLONG _64bitValue);
        SHAREDMEM_API BOOL SHAREDMEM_CALLING_CONV GetSharedMem(ULONGLONG* p64bitValue);
    }
    

    Now for the implementation file:

    SharedMem.cpp

    #include "stdafx.h"
    #include "SharedMem.h"
    
    HANDLE      hMappedFileObject = NULL;  // handle to mapped file
    LPVOID      lpvSharedMem = NULL;       // pointer to shared memory
    const int   SHARED_MEM_SIZE = sizeof(ULONGLONG);
    
    BOOL CreateSharedMem()
    {
        // Create a named file mapping object
        hMappedFileObject = CreateFileMapping(
                                INVALID_HANDLE_VALUE,
                                NULL,
                                PAGE_READWRITE,
                                0,
                                SHARED_MEM_SIZE,
                                TEXT("shmemfile") // Name of shared mem file
                            );
    
        if (hMappedFileObject == NULL) 
        {
            return FALSE;
        }
    
        BOOL bFirstInit = (ERROR_ALREADY_EXISTS != GetLastError());
    
        // Get a ptr to the shared memory
        lpvSharedMem = MapViewOfFile( hMappedFileObject, FILE_MAP_WRITE, 0, 0, 0);
    
        if (lpvSharedMem == NULL) 
        {
            return FALSE; 
        }
    
        if (bFirstInit) // First time the shared memory is accessed?
        {
            ZeroMemory(lpvSharedMem, SHARED_MEM_SIZE); 
        }
    
        return TRUE;
    }
    
    BOOL SetSharedMem(ULONGLONG _64bitValue) 
    { 
        BOOL bOK = CreateSharedMem();
    
        if ( bOK )
        {
            ULONGLONG* pSharedMem = (ULONGLONG*)lpvSharedMem;
            *pSharedMem = _64bitValue;
        }
    
        return bOK;
    }
    
    BOOL GetSharedMem(ULONGLONG* p64bitValue) 
    { 
        if ( p64bitValue == NULL ) return FALSE;
    
        BOOL bOK = CreateSharedMem();
    
        if ( bOK )
        {
            ULONGLONG* pSharedMem = (ULONGLONG*)lpvSharedMem;
            *p64bitValue = *pSharedMem;
        }
    
        return bOK;
    }
    
    • Note that for simplicity I'm just sharing a 64-bit value, but this is a general way of sharing memory between C# and C++. Feel free to enlarge SHARED_MEM_SIZE and/or add functions in order to share other data types as you see fit.

    This is how we'll consume the above methods: we'll use SetSharedMem() on the C# side in order to set the delegate's IntPtr as a 64-bit value (regardless if the code runs on a 32 or a 64 bit system).

    C# Code

    namespace CSharpCode
    {
        delegate void VoidDelegate();
    
        static public class COMInterfaceClass
        {
            [DllImport( "SharedMem.dll" )]
            static extern bool SetSharedMem( Int64 value );
    
            static GCHandle gcDelegateHandle;
    
            public static int EntryPoint(string ignored)
            {
                IntPtr pFunc = IntPtr.Zero;
                Delegate myFuncDelegate = new VoidDelegate( SomeMethod );
                gcDelegateHandle = GCHandle.Alloc( myFuncDelegate );
                pFunc = Marshal.GetFunctionPointerForDelegate( myFuncDelegate );
                bool bSetOK = SetSharedMem( pFunc.ToInt64() );
                return bSetOK ? 1 : 0;
            }
    
            public static void SomeMethod()
            {
                MessageBox.Show( "Hello from C# SomeMethod!" );
                gcDelegateHandle.Free();
            }
        }
    }
    
    • Note the use of GCHandle in order to prevent the delegate from being garbage-collected.
    • For good measures, we'll use the return value as a success/failure flag.

    On the C++ side we'll extract the 64-bit value using GetSharedMem(), convert it to a function pointer and invoke the C# delegate.

    C++ Code

    #include "SharedMem.h"
    typedef void (*VOID_FUNC_PTR)();
    
    void ExecCSharpCode()
    {
        ICLRRuntimeHost *pClrHost = NULL;
        HRESULT hrCorBind = CorBindToRuntimeEx(
                                    NULL,
                                    L"wks",
                                    0,
                                    CLSID_CLRRuntimeHost,
                                    IID_ICLRRuntimeHost,
                                    (PVOID*)&pClrHost
                                );
    
        HRESULT hrStart = pClrHost->Start();
    
        DWORD retVal;
        HRESULT hrExecute = pClrHost->ExecuteInDefaultAppDomain(
                                    szPathToAssembly,
                                    L"CSharpCode.COMInterfaceClass",
                                    L"EntryPoint",
                                    L"",
                                    &retVal // 1 for success, 0 is a failure
                                );
    
        if ( hrExecute == S_OK && retVal == 1 )
        {
            ULONGLONG nSharedMemValue = 0;
            BOOL bGotValue = GetSharedMem(&nSharedMemValue);
            if ( bGotValue )
            {
                VOID_FUNC_PTR CSharpFunc = (VOID_FUNC_PTR)nSharedMemValue;
                CSharpFunc();
            }
        }
    }
    

    The Original Answer - Good for 32-bit Systems

    Here's a solution that is based on using IntPtr.ToInt32() in order to convert the delegate func. ptr. to the int which is returned from the static C# EntryPoint method.

    (*) Note the use of GCHandle in order to prevent the delegate from being garbage-collected.

    C# Code

    namespace CSharpCode
    {
        delegate void VoidDelegate();
    
        public class COMInterfaceClass
        {
            static GCHandle gcDelegateHandle;
    
            public static int EntryPoint(string ignored)
            {
                IntPtr pFunc = IntPtr.Zero;
                Delegate myFuncDelegate = new VoidDelegate( SomeMethod );
                gcDelegateHandle = GCHandle.Alloc( myFuncDelegate );
                pFunc = Marshal.GetFunctionPointerForDelegate( myFuncDelegate );
                return (int)pFunc.ToInt32();
            }
    
            public static void SomeMethod()
            {
                MessageBox.Show( "Hello from C# SomeMethod!" );
                gcDelegateHandle.Free();
            }
        }
    }
    

    C++ Code We'll need to convert the returned int value to a function pointer, so we'll start off by defining a void function ptr. type:

    typedef void (*VOID_FUNC_PTR)();
    

    And the rest of the code looks pretty much like your original code, with the addition of converting and executing the function ptr.

    ICLRRuntimeHost *pClrHost = NULL;
    HRESULT hrCorBind = CorBindToRuntimeEx(
                                NULL,
                                L"wks",
                                0,
                                CLSID_CLRRuntimeHost,
                                IID_ICLRRuntimeHost,
                                (PVOID*)&pClrHost
                            );
    
    HRESULT hrStart = pClrHost->Start();
    
    DWORD retVal;
    HRESULT hrExecute = pClrHost->ExecuteInDefaultAppDomain(
                                szPathToAssembly,
                                L"CSharpCode.COMInterfaceClass",
                                L"EntryPoint",
                                L"",
                                &retVal
                            );
    
    if ( hrExecute == S_OK )
    {
        VOID_FUNC_PTR func = (VOID_FUNC_PTR)retVal;
        func();
    }
    

    A Little Bit of Extra

    You can also make use of the string input in order to choose which method to execute:

    public static int EntryPoint( string interfaceName )
    {
        IntPtr pFunc = IntPtr.Zero;
    
        if ( interfaceName == "SomeMethod" )
        {
            Delegate myFuncDelegate = new VoidDelegate( SomeMethod );
            gcDelegateHandle = GCHandle.Alloc( myFuncDelegate );
            pFunc = Marshal.GetFunctionPointerForDelegate( myFuncDelegate );
        }
    
        return (int)pFunc.ToInt32();
    }
    
    • You can get even more generic by using reflection in order to find the correct method according to the given string input.