Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.)

like image 247
LCC Avatar asked Mar 06 '12 20:03

LCC


1 Answers

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.
like image 105
AVIDeveloper Avatar answered Nov 20 '22 18:11

AVIDeveloper