A channel is the logical connection between a client and a component; all communication between the two parties is said to travel through the channel. Channel hooks enable a developer to hook into this communication mechanism. If you're experienced with the Microsoft Win32 API, you can compare channel hooking to the capabilities offered by the SetWindowLong function, which lets you store custom data inside a window structure held by Windows and later retrieve the data using the GetWindowLong function. In a similar way, channel hooks allow you to store custom data in the communication channel and then retrieve that data on the other side.
Recall that the last field in the ORPCTHIS and ORPCTHAT structures is an ORPC_EXTENT_ARRAY structure named extensions. You can use the extensions field of the ORPCTHIS structure to pass additional data in the channel when a method is invoked; you can use the extensions field of the ORPCTHAT structure to return additional data in the channel when a method returns. The ORPC_EXTENT_ARRAY itself does not contain the actual extension data—it simply records the number of extents that follow, each stored in an ORPC_EXTENT structure. The memory layout of the ORPC_EXTENT_ARRAY structure is shown here in IDL notation:
// Array of extensions typedef struct tagORPC_EXTENT_ARRAY { unsigned long size; // Num extents unsigned long reserved; // Must be 0 [size_is((size+1)&~1,), unique] ORPC_EXTENT **extent; // Extents } ORPC_EXTENT_ARRAY; |
The actual extent data passed in the channel is stored in an array of ORPC_EXTENT structures. Each extent is identified by a unique GUID. The first field of the ORPC_EXTENT structure is the GUID of the extent data being transmitted. The second field of the ORPC_EXTENT structure declares the size of that data, followed by the extent data itself in the final field. The ORPC_EXTENT structure is shown here in IDL notation:
// Extension to implicit parameters typedef struct tagORPC_EXTENT { GUID id; // Extension identifier unsigned long size; // Extension size [size_is((size+7)&~7)] byte data[]; // Extension data } ORPC_EXTENT; |
To use channel hooks from an application, you must create a coclass that implements the IChannelHook interface. This interface consists of six methods, three that are called in the client process and three that are called in the server process. Two of the three client-side methods are called automatically before a client method call is made, and the third is called immediately upon its return. This technique lets you put extent data in the channel to be sent in the ORPCTHIS structure of the request PDU and then retrieve any extent data from the response PDU. On the server side, one method of the IChannelHook interface is called just before a method executes and the other two are called immediately before it returns. This technique enables the server process to obtain extent data stored in the request PDU and then store additional extent data to be transmitted in the ORPCTHAT structure of the response PDU. The IDL definition of the IChannelHook interface is shown here:
interface IChannelHook : IUnknown { // How big is your data? void ClientGetSize( [in] REFGUID uExtent, [in] REFIID riid, [out] ULONG *pDataSize ); // Put the data in the channel. void ClientFillBuffer( [in] REFGUID uExtent, [in] REFIID riid, [in, out] ULONG *pDataSize, [in] void *pDataBuffer ); // Data has arrived from the server. void ClientNotify( [in] REFGUID uExtent, [in] REFIID riid, [in] ULONG cbDataSize, [in] void *pDataBuffer, [in] DWORD lDataRep, [in] HRESULT hrFault ); // Data has arrived from the client. void ServerNotify( [in] REFGUID uExtent, [in] REFIID riid, [in] ULONG cbDataSize, [in] void *pDataBuffer, [in] DWORD lDataRep ); // How big is your data? void ServerGetSize( [in] REFGUID uExtent, [in] REFIID riid, [in] HRESULT hrFault, [out] ULONG *pDataSize ); // Put the data in the channel. void ServerFillBuffer( [in] REFGUID uExtent, [in] REFIID riid, [in, out] ULONG *pDataSize, [in] void *pDataBuffer, [in] HRESULT hrFault ); }; |
The riid parameter of every IChannelHook method is actually a structure of type SChannelHookCallInfo that is passed by value, not a reference to an IID, as the interface definition would have you believe. The SChannelHookCallInfo structure provides channel hooks with additional information about a method call. For example, the fields available in this structure include the causality identifier (uCausality), the server's process identifier (dwServerPid), and a pointer to the object (pObject). The declaration of the SChannelHookCallInfo structure is shown here:
typedef struct SChannelHookCallInfo { IID iid; DWORD cbSize; GUID uCausality; DWORD dwServerPid; DWORD iMethod; void *pObject; } SChannelHookCallInfo; |
After creating an object that implements the IChannelHook interface, you need to inform COM+ that you intend to hook into its communication channel. The CoRegisterChannelHook function is designed for this purpose. It accepts the GUID of the extent and a pointer to the object that implements the IChannelHook interface, as shown here:
WINOLEAPI CoRegisterChannelHook(REFGUID ExtensionUuid, IChannelHook* pChannelHook); |
At this stage, you might be wondering why anyone would want to use a channel hook. After all, any data that you might want to pass from client to server and back can be transmitted as one or more parameters of a particular method. This technique is definitely easier than hooking into the communications channel! The advantage of a channel hook, however, is that the data being transmitted is not visible in the interface definition. By hooking into the communications channel, you can send additional data with each method call—data that is invisible to someone examining the interface definition.
One example of how channel hooks can prove useful occurs when you want to determine the name of the computer on which a particular client is executing. This information can prove valuable for a server-based administration application, since COM+ provides no built-in way to obtain a client's computer name. The simplest solution to this problem is to add one additional parameter to every method of the interface implemented by the server process, enabling the client to voluntarily provide its computer name. However, this solution requires that changes be made to the interface definition, thereby requiring that a new IID be defined. This in turn means that the old interface must still be supported for those clients that have not yet been updated to use the new interface. The new parameter also indicates that the interface is designed for remote use; it doesn't make sense to have a computer name parameter if the client and the component are running on the same machine.
Because the data that we want to pass to the server is not related to the primary purpose of that interface, it is bad design to force the two together. A channel hook can solve these problems because it can pass data in the channel outside the scope of the interface definition. With this goal in mind, you can build a custom channel hook that sends the client's computer name to the server as part of the ORPCTHIS structure in every method invocation. On the server side, the computer name is read from the channel and made available to the component. This makes for a nifty solution to a thorny problem.
We built this custom channel hook as an in-process component that can be loaded into the address space of any client or server process simply by calling CoCreateInstance. The DllMain function that is called on startup registers the channel hook by calling CoRegisterChannelHook and then obtains the computer's name by calling the Win32 GetComputerName function, as shown in boldface in the following code:
GUID EXTENTID_MyHook = {0x12345678, 0xABCD, 0xABCD, {0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99}}; BOOL WINAPI DllMain(HINSTANCE h, DWORD dwReason, void* pv) { static CChannelHook ChannelHook; if(dwReason == DLL_PROCESS_ATTACH) { if(FAILED(CoRegisterChannelHook(EXTENTID_MyHook, &ChannelHook))) { cout << "CoRegisterChannelHook failed." << endl; return FALSE; } ULONG length = MAX_COMPUTERNAME_LENGTH + 1; GetComputerName(g_mhtClientComputerName.computer_name, &length); } return TRUE; } |
A structure named MYHOOK_THIS encapsulates the data transmitted, making this channel hook easily extensible. Currently, the MYHOOK_THIS structure simply contains the string name of the computer on which the client is running. That name is obtained in the DllMain function shown in the preceding code and stored in g_mhtClientComputerName, as shown here:
struct MYHOOK_THIS { char computer_name[MAX_COMPUTERNAME_LENGTH + 1]; } g_MYHOOK_THIS, g_mhtClientComputerName; |
Although hooking into the COM+ communication channel sounds complex, the code required to implement the IChannelHook interface is relatively simple. When COM+ assembles the request PDU for a method invocation, the IChannelHook::ClientGetSize method is called to determine the size of the data to be transmitted. In the following code, the pDataSize value is set to the size of the MYHOOK_THIS structure:
// How big is your data? void CChannelHook::ClientGetSize(REFGUID uExtent, REFIID riid, ULONG* pDataSize) { if(uExtent == EXTENTID_MyHook) *pDataSize = sizeof(MYHOOK_THIS); } |
The ClientGetSize method is followed by a call to the IChannelHook::ClientFillBuffer method to request the actual data that you want to transmit in the communication channel. In the code below, the data pointer is set to the address of the global g_mhtClientComputerName variable containing the client's computer name:
// Put the data in the channel. void CChannelHook::ClientFillBuffer(REFGUID uExtent, REFIID riid, ULONG* pDataSize, void* pDataBuffer) { if(uExtent == EXTENTID_MyHook) { MYHOOK_THIS *data = (MYHOOK_THIS*)pDataBuffer; *data = g_mhtClientComputerName; *pDataSize = sizeof(MYHOOK_THIS); } } |
COM+ now has sufficient information to build and transmit the request PDU to the server. The data transmitted in the channel hook travels coach in the ORPCTHIS structure of the request PDU. Once the server receives the request PDU, the IChannelHook::ServerNotify method is called in the server process, which means that the channel hook must be running on both the client and server computers to work properly. The ServerNotify method indicates that data has arrived from the client. The following code obtains that data from the pDataBuffer pointer and temporarily stores it in the g_MYHOOK_THIS variable for retrieval by the component:
// Data has arrived from the client. void CChannelHook::ServerNotify(REFGUID uExtent, REFIID riid, ULONG cbDataSize, void* pDataBuffer, DWORD lDataRep) { if(uExtent == EXTENTID_MyHook && lDataRep == NDR_LOCAL_DATA_REPRESENTATION) { MYHOOK_THIS* data = (MYHOOK_THIS*)pDataBuffer; strcpy(g_MYHOOK_THIS.computer_name, data->computer_name); } } |
Our channel hook is designed to transmit data from the client to the server, not vice-versa, so the ClientNotify, ServerGetSize, and ServerFillBuffer methods of the IChannelHook interface are all NO-OPs. Channel hooks that want to return data in the ORPCTHAT structure of a response PDU must implement these three methods as well. To make the client's computer name available to a component, our channel hook implements a custom interface called IClientInfo that offers only one method: GetClientComputerName. This method can be called by a server-side component from within a method invoked by the client. The implementation of this method, shown in the following code, simply retrieves and returns the client's computer name from the g_MYHOOK_THIS variable, where it was stored in the IChannelHook::ServerNotify method:
HRESULT CClientInfo::GetClientComputerName(BSTR* bstr) { int length = strlen(g_MYHOOK_THIS.computer_name); *bstr = SysAllocStringLen(0, length+1); strcpy((char*)*bstr, g_MYHOOK_THIS.computer_name); return S_OK; } |
To use ClientChannelHook, both the client and server processes must load the channel hook by calling CoCreateInstance, as shown here:
// Load the channel hook. void* silly; CoCreateInstance(CLSID_ClientChannelHook, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, &silly); |
In the client process, the channel hook is instantiated and every remote method call is sent with the client's computer name in the ORPCTHIS structure of the request PDU. In the server process, the channel hook retrieves the client's computer name and makes it available to the component using the IClientInfo::GetClientComputerName method. For example, a method in the component that wants to obtain the client's computer name instantiates the ClientChannelHook object, requesting an IClientInfo interface pointer. Then the GetClientComputerName method is called to obtain the name of the client's computer from the channel hook. This process is shown in boldface in the following implementation of the Sum method:
HRESULT CInsideCOM::Sum(int x, int y, int* retval) { IClientInfo* pClientInfo; CoCreateInstance(CLSID_ClientChannelHook, NULL, CLSCTX_INPROC_SERVER, IID_IClientInfo, (void**)&pClientInfo); BSTR bstr = 0; pClientInfo->GetClientComputerName(&bstr); MessageBox(NULL, (char*)bstr, "GetClientComputerName", MB_OK); SysFreeString(bstr); *retval = x + y; return S_OK; } |
Some overhead is associated with the second call to CoCreateInstance in the server process because the ClientChannelHook object is implemented as a singleton object. As described in Chapter 13, instead of instantiating a new object at each client request, a coclass designed to operate as a singleton always returns a reference to the same object. An easy way to implement a singleton is to declare a static object in the IClassFactory::CreateInstance method and then always provide a pointer to that one object, as shown in the following code. Like other singletons, the ClientChannelHook object is designed to stay in memory for the lifetime of its container process, so it does not support reference counting.
HRESULT CFactory::CreateInstance(IUnknown* pUnknownOuter, REFIID riid, void** ppv) { if(pUnknownOuter != NULL) return CLASS_E_NOAGGREGATION; static CClientInfo ClientInfo; return ClientInfo.QueryInterface(riid, ppv); } |