Interface definition language - idl

Introduction

The main points in this section were borrowed from "Understanding Interface Definition Language : A Developer's Survival Guide". Available in MSDN.

Whenever thinking about marshaling, the ultimate objective is to copy data from the address space of one side (client or server) to the address space of the other side (server or client.) Whatever exists in the address space of one side at the time of the call must be replicated in the address  space of the other side. Therefore,

When to use IDL

An IDL is used to accomplish the following

  1. To generate proxy/stub DLLs to marshal data across apartment / process / machine boundaries. This is only necessary when exposing custom interfaces that are not automation compatible.
  2. To generate type libraries, which are used in 3 occasions:
Universal Marshaler (Type library marshaler.)

In the following note that Automation Compatible means anything that can go into a VARIANT

MIDL-gerenated files
File Name Purpose
<IDLFileName>.tlb Type library
Example IDL File

SampleIDL.IDL
/* The following import statements will cause SampleIDL.h to contain 
    #include "oaidl.h"
    #include "ocidl.h"
*/

import "oaidl.idl";
import "ocidl.idl";

/* This section will only generate proxy stub code since it is outside the 
library section */

[
    /* object tells MIDL to produce files for a COM interface. Without 
    it, MIDL generates files necessary for an RPC interface */
    object,

    /* uuid() is required to generate proxy/stub code. */
    uuid(FBBF2DEF-1391-11D4-8ED9-000000000000),

    /* TO DO */
    dual,
    helpstring("ITestIDL Interface"),

    /* pointer_default(ref | ptr | unique) specifies the default pointer 
    attribute for all pointers except top level pointers. This includes 
    pointers returned by functions, pointers within a struct, pointer to a
    pointer, etc. For top level pointers, the default is 'ref' which means
    that it cannot be NULL and it should always point to a valid address. */
    pointer_default(unique)

    /* local attribute prevents the MIDL from generating proxy/stub 
    (networking) code for this interface. This also frees the interface
    functions from returning HRESULT since the interface methods can only
    be invoked in-proc.*/
   
// local
]
interface ITestIDL : IDispatch
{

    /* [in] and [out]
    Marshaling data across network is expensive. To optimize, [in] tells
    the marshaler that its variable is only marshaled (moved) when traffic
    is from client to server. [out] is used to marshal data from server to
    client. 
    An [out] parameter MUST be a pointer. Why? For an [out] param, the 
    server allocates data and moves it back to client space. But where in 
    the client space should the server move the data to? Using a pointer for
    [out] params tells the server where the resulting data is to be placed.
    This means that the pointer on the client side must represent valid 
    address. And since this is a top level pointer, the default is ref */
    [ id(1), helpstring("method SetGetInteger") ] 
    HRESULT SetGetInteger( [in] long lNum, [out, retval] long *plNum );

    /* [ref] pointers are reference pointers that should always point to a 
    valid address. This is the default for top level pointers. */
   
[ id(2), helpstring("method PassPtrWithRefAttribute") ] 
    HRESULT PassPtrWithRefAttribute( [in, ref] const long* plVal );

    /* [unique] pointers can be passed as null. This should be the attribute
    for pointers within a struct or to pointers returned by a function */
   
[ id(3), helpstring("method PassPtrWithUniqueAttribute") ] 
    HRESULT PassPtrWithUniqueAttribute( [in, unique] long *plValue );

    /* [full] pointers are used to emulate duplication: A full pointer 
    performs duplicate pointer detection and will make sure that duplicate 
    pointers are unmarshaled as duplicate pointers pointing to one memory 
    location instead of two. Break into PassPtrWithFullAttribute() function
    in the  server code and watch the addresses */

    [ id(4), helpstring("method PassPtrWithFullAttribute") ] 
    HRESULT PassPtrWithFullAttribute( [in,ptr] long *plVal1, 
                                      [in,ptr] long *plVal2 );

    /* [string] lets the marshaler pass the data the pointer is pointing to 
    until it hits a NULL. Two important points here:
    1. Without any attribute for a pointer, pointers are assumed to point
    to a single instance of a data type, ie, marshaler will only marshal
    one data item.
    2. When using [string] the string type should be OLECHAR and not CHAR */
   
[ id(5), helpstring("method PassString") ] 
    HRESULT PassString( [in, string] OLECHAR *pwszName );

    /* Passing an array. Again we need to indicate how much to marshal. In 
    fact, using size_is would accomplish the same thing as [string]
    Note that max_is() is equivalent to size_is() except that max_is()
    specifies the maximum index of the array */
 
   [ id(6), helpstring("method PassArray") ] 
    HRESULT PassArray( [in] long lSize, [in, size_is(lSize)] long *plArray );

    /* iid_is tells the marshaler that we are passing an interface pointer whose
    IID is indicated by the parameter in iid_is() */
    [ id(7), helpstring("method GetInterfacePointer") ] 
    HRESULT GetInterfacePointer( [in] REFIID riid, [out, iid_is(riid)] void** ppv );
};

    /* The following is a dispinterface:
        uuid(CC7954B0-14B3-11d4-8EDD-000000000000)
    ]
    dispinterface ITestIDL2
    {
        properties:
            [id(1)] long lAttribute;
        methods:
    };
    */


    /* Everything that is inside the library statement will be added to 
    the type library. A tlb can contains only the following five 
        typedefs
        modules - check out the [module] IDL keyword
        interfaces
        dispinterfaces
        coclasses */
[

    uuid(FBBF2DE3-1391-11D4-8ED9-000000000000),
    version(1.0),
    helpstring("IDL 1.0 Type Library")
]
library SampleIDLLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    /* If ITestIDL2 was a uncommented below, it will be added to the type
    library. Upon registering the type library (when the server code calls 
    CComModule::RegisterServer() function), the dispinterface will be added
    to the \interfaces registry key and designated to use oleaut32.dll 
    (the universal marshaler) as its marshaler.*/

    //dispinterface ITestIDL2;

    /* Coclasses are not registered by the type library. */
    [
        uuid(FBBF2DF0-1391-11D4-8ED9-000000000000),
        helpstring("TestIDL Class")
    ]
    coclass TestIDL
    {
        /* ITestIDL will be registered. Because its is [dual], it will be 
        assigned the universal marshaler.*/
       
[default] interface ITestIDL;
    };
};

    

Example Client Code

#import "..\SampleIDL.tlb"
using namespace SampleIDLLib;

// Thread function to introduce threading issues
DWORD WINAPI ThreadProc( LPVOID );

// Helper functions
void TestInOutParams();
void TestPointerParam();
void TestStringsAndArrays();

int main(int argc, char* argv[])
{
    /* Run the objects on a separate thread than that of the main program.
    This should serve to introudce marshaling issues */
    DWORD dwID;
    HANDLE hThread = CreateThread( NULL, 0, ThreadProc, NULL, 0, &dwID );

    if (NULL != hThread)
    {
        WaitForSingleObject( hThread, INFINITE);
    }

    CloseHandle( hThread );
    return 0;
}

DWORD WINAPI ThreadProc( LPVOID )
{
    /* Recall that CoInitialize() must be called on the thread where the object
    is running, and not on the primary thread created by the application */
    CoInitialize( NULL );
    {
        TestInOutParams();
        TestPointerParam();
        TestStringsAndArrays();
    }
    CoUninitialize();

    return 0;
}

void TestInOutParams()
{
    // Create test object
    ITestIDLPtr gspIDLTest;
    gspIDLTest.CreateInstance( __uuidof( TestIDL ) );

    /* 'lNum' is an [out] param. It is allocated in the server and freed 
    in the client. Note the an [out] param tells the server where to place
    its data which can be a simple integer or a pointer to an array of 
    chars (ie, string) Therefore, we must always pass a valid client 
    address (pointer) to the server. The server allocates its data
    and puts it in the address passed in by the client
. */ 

    long lNum;
    gspIDLTest->raw_SetGetInteger( 10, &lNum );

    /* This is INCORRECT since we're passing an invalid address to the server.
    The server has no place to copy its allocated data.
        long *plNum;
        spIDLTest->raw_SetGetInteger( 20, plNum );
        delete plNum;
    */
}

void TestPointerParam()
{
    // Create object
    ITestIDLPtr gspIDLTest;
    gspIDLTest.CreateInstance( __uuidof( TestIDL ) );

    /* PassPtrWithRefAttribute() has [in, ref] for its input parameter. This 
    means the input param is a reference parameter which MUST always contain 
    a valid address. If it is null, the marshaling code will return an error. 
   
TODO: This didn't work for me! */
    long *plNum1;
    gspIDLTest->PassPtrWithRefAttribute( plNum1 );

    /* PassPtrWithUniqueAttribute() has [in, unique] for its input parameter. 
    This means the input pointer can be NULL.*/
   
long *plNum2;    
    gspIDLTest->PassPtrWithUniqueAttribute( plNum2 );

    /* PassPtrWithFullAttribute() has [in,ptr] for its input param. This 
    means  the input pointer is treated as a full pointer. Note that we are
    passing the address of the same pointer. On the server side, unless we
    use the [ptr] IDL attribute, the server will allocate two memory blocks
    for the same variable. The [ptr] attribute detected pointer duplications
    and makes sure that they are marshaled as a duplicate pointer.*/

    long lVal = 10;
    gspIDLTest->PassPtrWithFullAttribute( &lVal, &lVal );
}

void TestStringsAndArrays()
{
    // Create object
    ITestIDLPtr gspIDLTest;
    gspIDLTest.CreateInstance( __uuidof( TestIDL ) );

    /* Passing a string using [string] attribute as opposed to using a BSTR. 
    [string] tells the marshaler to compute the length of data the pointer is
    pointing to */
    LPWSTR lpszString = L"hello there";
    gspIDLTest->PassString( lpszString );

    /* Passing an array using size_is() attribute */
    long lArr[2] = {1,2};
    gspIDLTest->PassArray( 2, lArr );

    /* Testing iid_is() attribute */
    ITestIDL *pIDL;
    IID IID_TestIDL = __uuidof(ITestIDL);
    HRESULT hr = gspIDLTest->raw_GetInterfacePointer( &IID_TestIDL, (void**) &pIDL);
    pIDL->Release();
}