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

1

8

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

Ilona answered 6/3, 2012 at 20:44 Comment(6)
Are you trying to interact with the C# program (e.g. send a COM interface as a parameter or get one back) or just call some method from C++ to C#?Midships
I really just need to call one C# function from C++ (and get an IntPtr back from C#, not an int). I can take care of the rest, but I don't see a way to do this.Ilona
I think some more detail as to what you are doing would help. Are you just trying to get the C# code to do one thing then return, or are you expecting a more continuous interop. Why is C++/CLI not a possibility if you control both the managed and unmanaged sides? Finally is there any way you could redefine the cross over to be managed initiated (thus using DllImport).Sophistic
BTW, part of the problem is there are tons of complexities to handling managed/unmanaged interop that are hidden by these helper methods, for instance marshalling of data, that would probably have to be handled by you if you don't use one of the standard interop methods.Sophistic
Yes, I'm well aware of the complexities. I framed the question as simply as possible so you don't have to read 10 pages of text to get at the heart of what I'm asking. I can handle all of the interop and marshaling. That's well tread territory for me. The question I'm asking though (specifically about how to get an intptr out of this) is the part I couldn't figure out.Ilona
Hi, I don't know if you got a notification for it, but I updated my answer to include shared memory as a data transport between C# and C++.Midships
M
10

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.
Midships answered 7/3, 2012 at 0:0 Comment(4)
You are assuming the IntPtr will fit in a 32 bit integer with will not always be the case. I suppose I could call twice to get the high/low half of the integer which is such a giant hack. =/Ilona
Agreed. I switched on my PC just now to write a disclaimer about the ToInt32() and 64-bit gap and I see you're already there. I don't think you need to call twice, but something should be done. A MemoryMappedFile solution comes to mind, but it's wayyy too late right now so I'll have to update my answer later on.Midships
@Midships Noob question: In the case of 64 bits, is memory mapped files really the easiest route? Apart from that just in order to pass a value of a bigger type around it seems strange that a whole new set of tools is needed. And, we can return values of type String^ back to C++ land (using it as a string: static_cast<PCWSTR>(retVal.bstrVal), see http://blogs.msdn.com/b/msdnforum/archive/2010/07/09/use-clr4-hosting-api-to-invoke-net-assembly-from-native-c.aspx), and strings should be capable of carrying 64 bit values.Caroncarotene
@Midships I put that comment into a new question, including sample code excerpts: stackoverflow.com/questions/11388510 In case you are interested...Caroncarotene

© 2022 - 2024 — McMap. All rights reserved.