Share via


How to RPC to transfer data from native c++ to managed object

 

My Experience on using RPC in mixed mode assembly

 

 

I was assigned a project which use RPC cross unmanaged world (from SQL Server code) to managed world (test c# code). The main idea is transferring event data from SQL Server to test application synchronously, so test application could do some real-time action.

During this project, I gained a lot of experience related to cross unmanaged/managed world, and RPC.

After reading this post, you will know how to solve following problems:

1. How to use explicit context handle for RPC?

2. How to do dynamic binding for RPC when the user doesn’t want to specify port number for his connection?

3. How to call native windows API in mixed mode c++ code?

4. How to callback from unmanaged world to managed world?

5. How to using windows event between a regular process and a service?

6. How to access Managed object in specified AppDomain from native function?

7. How to synchronize managed objects in different AppDomain?

8. How to initialize process level variable in mixed assembly?

How to use explicit context handle for RPC

There are a lot of examples for using RPC in c++, rarely there is examples show how to use explicit handle and how to do dynamic binding when using tcp-ip protocol.

Here is the script for the idl file that defines the RPC interface:

//myinterface.idl

 

import "oaidl.idl";

import "ocidl.idl";

 

typedef enum UserActionEnum

{

                ACT_NONE = 0,

                ACT_SLEEP = (ACT_NONE +1),

                ACT_WAIT = (ACT_SLEEP +1)

} UserAction;

 

[

                uuid(95670AFD-42E6-414c-8600-9843AE1494D4),

  version(1.0)

]

 

//we use explicit handle and context_handle for the interface

//so that we can have multiple client on the same server

interface XETESTINTERFACE

{

                // on RPC server side, this will hold the unmanaged TargetRPCSession objects

                typedef [context_handle] void* PRPCTargetSession;

                // the session is created by the user, we just try to get it from TargetRPCManager

    BOOL OpenSession(handle_t hXeTestInterface, [out] PRPCTargetSession *ppSession, UINT64 targetID);

                // send the event data to specified TargetRPCSession

                UserAction EventFired ([in, out]PRPCTargetSession *ppSession, [in, string, size_is(cchXml)] const WCHAR* xml, UINT32 cchXml, [out] UINT64* actionParam);

                // close the target session

                void CloseSession([in, out] PRPCTargetSession *ppSession);

}

typedef [context_handle] void* PRPCTargetSession will define the context handle, what is in it? It really depends on the RPC server side; it can wrap up anything. In my project, I wrap up an object of following class:

//piece of code in server.cpp

 

public class SessionContext

{

public:

                SessionContext(

                                UnmanagedTargetSession* pSession,

                                UINT32 adID)

                {

                                m_pSession = pSession;

                                m_AppDomainID = adID;

                }

 

private:

                UnmanagedTargetSession* m_pSession;

                UINT32 m_AppDomainID;

};

 

When client wants to talk to RPC server, once it make connection to the server, it could call OpenSession to get the context handle.

//piece of code in client.cpp

 

handle_t hXeTestInterface; //RPC binding handle

PRPCTargetSession RPCcontextHandle; //RPC context handle

bSessionOpened = OpenSession(hXeTestInterface, &RPCcontextHandle, myTargetID);

 

Later, when calling other RPC functions, RPC server side will know which object the data will go to according to the context handle:

//piece of code in client.cpp

 

UserAction *pAction = EventFired (&RPCcontextHandle, eventXml, len, pParam);

 

Data (eventXml, len) will be sent to the object specified in RPCcontextHandle. How that was handled, it depends on how RPCcontextHandle is interpreted in the RPC call on the Server side. For me, I forward the data to the UnmanagedTargetSession object I saved in the context handle.

//piece of code in server.cpp

 

// send the event data to specified TargetRPCSession

int EventFired(PRPCTargetSession *ppSession,

                                                const WCHAR* xml, UINT32 cchXml, UINT64* actionParam)

{

                SessionContext* cxt = reinterpret_cast<SessionContext*>(*ppSession);

              UnmanagedTargetSession *pSession = cxt->SessionHandle();

                return pSession->TransferEvent(xml, cchXml, actionParam);

                return FAILPOINT_TARGET_ACT_NONE;

}

 

Whenever you are done, you can easily close the handle by calling CloseSession:

//piece of code in client.cpp

 

CloseSession(&RPCcontextHandle);

 

How to do dynamic binding for RPC

To do dynamic binding for RPC connection, there are following steps to follow on both server and client side.

Server side:

//code in server.cpp

 

//a RPC server must do folloing for dynamic binding to listen to remote calls:

//1. register the interface

//2. create binding information

//3. register the endpoints

//4. listen for client calls

//step 1: register the interface, we use autolisten

RPC_STATUS status = ::RpcServerRegisterIfEx (XETESTINTERFACE_v1_0_s_ifspec, NULL, NULL,

      RPC_IF_AUTOLISTEN,

      RPC_C_LISTEN_MAX_CALLS_DEFAULT, NULL);

if (status)

{

      return(status);

}

//step 2. Making the Server Available on the Network

status = RpcServerUseProtseqW(

      (RPC_WSTR)L"ncacn_ip_tcp",

      RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Protseq dependent parameter

      NULL);

if (status)

{

      return(status);

}

//step 3: servers that use dynamic endpoints

// must create binding information for each protocol sequence it uses.

RPC_BINDING_VECTOR *rpcBindingVector;

status = RpcServerInqBindings(&rpcBindingVector);

if (status)

{

      return (status);

}

//step 4: register endpoints

status = RpcEpRegisterW(

      XETESTINTERFACE_v1_0_s_ifspec,

      rpcBindingVector,

      NULL,

      NULL);

if (status)

{

      return(status);

}

status = RpcBindingVectorFree(&rpcBindingVector);

return(status);

 

 

Client side:

//code in client.cpp

//uuid from idl file

RPC_WSTR pszUuid = (RPC_WSTR)(L"95670AFD-42E6-414c-8600-9843AE1494D4");

RPC_WSTR pszProtocolSequence = (RPC_WSTR)(L"ncacn_ip_tcp");

RPC_WSTR pszNetworkAddress = (RPC_WSTR)(TCHAR*)”localhost”;

RPC_WSTR pszEndpoint = NULL;

RPC_WSTR pszOptions = NULL;

BOOL bSessionOpened = FALSE;

//generate the string for binding

RPC_STATUS status = ::RpcStringBindingComposeW (pszUuid,

                                                pszProtocolSequence,

                                                pszNetworkAddress,

                                                pszEndpoint,

                                                pszOptions,

                                                (RPC_WSTR*)(&m_pszStringBinding));

if (status != RPC_S_OK)

{

      return(status);

}

//dynamic binding

status = RpcBindingFromStringBindingW(m_pszStringBinding, &m_hXeTestInterface);

if (status != RPC_S_OK)

{

      return(status);

}

//resolve the half binding

status = RpcEpResolveBinding(m_hXeTestInterface, XETESTINTERFACE_v1_0_c_ifspec);

return(status);

 

How to call native windows API in mixed mode c++ code

It’s much easier to use win API in mixed mode of c++ code. You can do exactly what I did in above section for server.cpp, those RPC related functions are all native win API. Compile the file with /clr and link with rpcrt4.lib.

For c# code, if you want to use native windows API, you can use PInvoke, search online, you can find tons of examples.

How to callback from unmanaged world to managed world

Since I want to transfer my event data from SQL Server to a managed object in test application, I need to find a way to callback to tester’s object when the event arrives in the test application which is the RPC server side.

RPC calls are unmanaged, tester’s objects are managed object, and I need to build a bridge between these two. I spent a lot of time to find out the solution, it’s my pleasure that I can share my experience with you here.

Callback function for native code is just a pointer to a function. So if we want the RPC function to callback to user’s managed object, we need to convert that managed delegate to a pointer, and we cannot reference managed object in native RPC call function body, so also need another unmanaged object to hold this pointer. Here is how we can do it:

//code in server.h

//this delegate is for the managed test code

public delegate int EventReceivedHandler(System::String^ xml,unsigned int cchXml, IntPtr param);

//we build a bridge between managed code and unmanaged code by callback function at each end

//and a managed struct to hold the managed callback function and cast the managed struct

//into a unmanaged callback function pointer

//

//this delegate is for the managed code

public delegate int UnmanagedCallBack(const wchar_t * xml, UINT32 cchXml, UINT64 * actionParam);

//this is the callback function defined for unmanaged code

typedef int (WINAPI *TransferEventCallbackFunc)(const wchar_t * xml, UINT32 cchXml, UINT64 * actionParam);

[StructLayoutAttribute( LayoutKind::Sequential, CharSet = CharSet::Ansi )]

public ref struct Managed_Delegate_Wrapper

{

      [MarshalAsAttribute(UnmanagedType::FunctionPtr)]

      UnmanagedCallBack^ _Delegate;

};

public class UnmanagedTargetSession

{

public:

      UnmanagedTargetSession();

      ~UnmanagedTargetSession();

      //used to set callback function to transfer event

      //here we set the TargetRPCSession's TransferEvent as the callback function

      void setCallback(TransferEventCallbackFunc func);

      //send event data to user and ask for action w/o parameter

      //via TargetRPCSession's TransferEvent

      int TransferEvent(const wchar_t* xml, UINT32 cchXml, UINT64 * actionParam);

private:

      //pointer to hold TargetRPCSession's TransferEvent

      TransferEventCallbackFunc sm_func;

};

public ref class TargetRPCSession

{

public:

      TargetRPCSession(UINT64 targetID);

      ~TargetRPCSession();

      //set/clear event handler

      void setCallback( EventReceivedHandler^ pEventReceived);

      void clearCallBack();

private:

      int TransferEvent(const wchar_t * xml, unsigned int cchXml, UINT64 * actionParam);

     

private:

      UINT64 m_targetID; //target id

      EventReceivedHandler^ m_pEventHandler; //user's callback function

      Managed_Delegate_Wrapper^ m_delegate_wrapper; //managed wrapper for TransferEvent to unmanaged session

      UnmanagedTargetSession* m_pUnmanagedSession; //unmanaged session can be used by the RPC calls as context handle

};

//code in server.cpp

//this is done in the constructor of TargetRPCSession

//create the unmanaged session

m_pUnmanagedSession = new UnmanagedTargetSession();

//wrap up callback function, connect the managed world and unmanaged world

m_delegate_wrapper = gcnew Managed_Delegate_Wrapper();

m_delegate_wrapper->_Delegate =

      gcnew UnmanagedCallBack(this, &TargetRPCSession::TransferEvent);

TransferEventCallbackFunc callback;

Marshal::StructureToPtr (m_delegate_wrapper, IntPtr::IntPtr(&callback), false);

m_pUnmanagedSession->setCallback(callback);

 

 

User can set his delegate at any time, once it’s ready, he can start to listen to RPC calls and the receiver will start to get event data:

// code in user.cs

 

public class MyEventReceiver

{

    public UInt64 m_targetID;

    public String m_WinEventPrefix;

    public Boolean m_testPassed;

    public MyEventReceiver(UInt64 tid, String prefix)

    {

        m_targetID = tid;

        m_WinEventPrefix = prefix;

        m_testPassed = false;

    }

    private int myRand(int min, int max)

    {

        Random rand = new Random();

        int value = rand.Next();

        return (value % (max - min + 1) + min);

    }

    //callback delegate

    public int OnEventReceived(String xml, uint cch, IntPtr param)

    {

        //doing treatment for the event and return action code for target to take

    }

}

class Example

{

    public static void Main()

    {

        TargetRPCSession ts = new TargetRPCSession(tid, prefix);

        //step 4: create event receiver and handler

        MyEventReceiver rec = new MyEventReceiver(tid, prefix);

        ts.setCallback(new EventReceivedHandler(rec.OnEventReceived));

        //start listening for event

        //............

        //get signaled and quit

    }

}

 

 

How the data is transferred to user’s receiver?

When data arrive in the RPC call, RPC function will call:

            (UnmanagedTargetSession*)pSession->TransferEvent(xml, cchXml, actionParam);

 

What does UnmanagedTargetSession::TransferEvent do is:

            return (sm_func)(xml, cchXml, actionParam);

 

this (sm_func) is set in the constructor of TargetSession by calling UnmanagedTargetSession::setCallback:

    //this is done in the constructor of TargetRPCSession

//create the unmanaged session

m_pUnmanagedSession = new UnmanagedTargetSession();

//wrap up callback function, connect the managed world and unmanaged world

m_delegate_wrapper = gcnew Managed_Delegate_Wrapper();

m_delegate_wrapper->_Delegate =

      gcnew UnmanagedCallBack(this, &TargetRPCSession::TransferEvent);

TransferEventCallbackFunc callback;

Marshal::StructureToPtr (m_delegate_wrapper, IntPtr::IntPtr(&callback), false);

m_pUnmanagedSession->setCallback(callback);

 

Then by the magic wand of Managed_Delegate_Wrapper, TargetRPCSession::TransferEvent will be called indeed.

    //the callback function to unmanaged class

    int TargetRPCSession::TransferEvent(const wchar_t * xml, unsigned int cchXml, UINT64 * actionParam)

    {

      String^ xmlEvent = gcnew String(xml);

      int action = 0;

      if (m_pEventHandler != nullptr)

      {

            action = m_pEventHandler(xmlEvent, cchXml, IntPtr::IntPtr(actionParam));

      }

      return action;

    }

 m_pEventHandler is the event receiver that user set.

 

How to using windows event between a regular process and a service?

In my project I try to synchronize the test process and SQL Server process, so I used windows event. To use windows event to synchronize a service process with a regular process, you need to use global windows event since the service and the regular process is not in the same windows session. The service is in a global session, once it’s started, every user logged to that machine will aware of it. it’s very simple to specify global windows variable – just append “\\Global” to the windows event name, such as “\\Global\myEventForTarget”.

How to access Managed object in specified AppDomain from native function?

The test application are written in c# code, the assembly could be loaded and executed in a new AppDomain. How can my RPC functions know which AppDomain the test is running in and how can my RPC functions transfer the data to the right receiver?

The concept of AppDomain a light weight process, it separates the workset of user threads. If the objects are created in different AppDomain, they cannot access each other.

RPC function is native, it will never aware of existence of AppDomain. For the correct functionality, I have to remember the AppDomain ID for each RPC session I created in the session context handle, so when data comes in, I can deliver it to the right AppDomain by using call_in_appdomain function:

//code in server.cpp

 

// send the event data to specified TargetRPCSession

int EventFiredInRightAppDomain (

                                UnmanagedTargetSession *pSession,

                                const WCHAR* xml, UINT32 cchXml, UINT64* actionParam)

{

                if (pSession != NULL)

                {

                                return pSession->TransferEvent(xml, cchXml, actionParam);

                }

                return FAILPOINT_TARGET_ACT_NONE;

}

 

UserAction EventFired(

                                                PRPCTargetSession *ppSession,

                                                const WCHAR* xml, UINT32 cchXml, UINT64* actionParam)

{

                SessionContext* cxt = reinterpret_cast<SessionContext*>(*ppSession);

                int ret = call_in_appdomain(

                                cxt->AppDomainID(),

                                &EventFiredInRightAppDomain,

                                cxt->SessionHandle(),

                                xml,

                                cchXml,

                                actionParam);

                return static_cast<UserAction>(ret);

}

 

How to synchronize managed objects in different AppDomain?

Since the RPC server is in managed world, I don’t want every test thread register/unregister the RPC interface, I want to keep a global refcounter and only allow the first thread to register and the last thread to unregister.

In managed c++ code, it’s allowed to specify process level global variable via __declspec(process), it only allowed to apply to unmanaged object.

//server.h

 

__declspec(process) volatile long RPCInterfaceRefCount;

 

To synchronize the threads, blocking others from accessing critical section, we can use process level CRITICAL_SECION:

//server.h

 

__declspec(process) CRITICAL_SECTION g_TestlibCritSec;

 

You can call following windows API functions to enter/leave the critical section:

::EnterCriticalSection(&g_TestlibCritSec);

::LeaveCriticalSection(&g_TestlibCritSec);

How to initialize process level variable in mixed assembly

This critical section variable need to be automatically initialized, we couldn’t provide entry point for mixed project, so that we will not be initialize it in the entry function for the assembly. But there is a trick to fulfill this task: define a unmanaged class and initialize the critical section in its constructor. Define a process level global variable of this class. So whenever your assembly is loaded, it will try to initialize this variable, so the constructor will be called and your global variables will be initialized.

class AutoInit

{

public:

      AutoInit()

      {

            ::InitializeCriticalSection(&g_TestlibCritSec);

            RPCInterfaceRefCount = 0;

      }

};

__declspec(process) AutoInit g_autoInit;