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?
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; |