Next Previous Contents

7.5 The ComServ library

This section presents ComServ, a Haskell library for creating COM components. The library is intended to be used by the code generated by a tool such as HaskellDirect and applications that want to directly and dynamically create COM component instances.

Some COM Concepts

A COM component supports one or more interfaces. COM defines the binary representation of a pointer to (an instance of) an interface as follows:


     
  i-pointer     +-----+        method table
  ----------->  |     +-------->+-------+
                +-----+         |       +-----> QueryInterface()
                                +-------+
                                |       +-----> AddRef()
                                +-------+
                                |       +-----> Release()
                                +-------+
                                |       |
                                +-------+
                                |       |
                                +-------+

A pointer to an interface is represented as a pointer to a pointer to a method table (vtable, for short.) How the state of the object/ component instance is represented isn't prescribed by COM, but a common (and sensible :-) strategy is to store it next to the pointer the method table.

The state of a component instance is shared by all the interfaces that the component supports.

To navigate between the different interfaces a component supports, COM guarantees that all components provides an implementation of the interface IUnknown. Through it, you can get from one interface to any other (that the component supports.)

A COM interface can extend/inherit another; indeed, all COM interfaces extend the IUnknown interface. At the level of method tables, interface extension is simply method table extension. Consequently, all COM method tables will have in their first three entries the implementation of the IUnknown methods.

Building a method table

Building an interface pointer is a two staged affair, the first of which is the construction of the table of methods that a particular interface supports. A method table is represented by the Haskell type:

data VTable iid objState = -- abstract, not finalised.

The method table has to be allocated in a chunk of memory that's not moveable; i.e., it's likely to be allocated somewhere external to the Haskell heap. So, while abstract, the concrete representation of the method table is likely to be a pointer to this piece of memory.

To make the manipulation of such pointers a tad safer, we parameterise them with respect to the type of the interface that a method table is an implementation of, and the state of the component which supports that interface.

Method tables are constructed using the following library provided action:

createVTable :: [Addr] -> IO (VTable iid st)

It creates a table big enough to hold all the method pointers of its argument list, filling in the table from top to bottom.

When creating method tables representing COM interfaces, the top three entries of all method tables collectively implement the IUnknown interface. You normally won't be interested in implementing these methods, so the library provides a default implementation of them.

To create a method table with the standard IUnknown methods prepended, the library provides:

createComVTable :: [Addr] -> IO (ComVTable iid objState)

type ComVTable iid objState = VTable iid (objState,IUnkState)

It returns a ComVTable, which is a standard method table but with an object state that also includes the state required to implement IUnknown. IUnkState is an abstract type.

Remarks:

Building a component

With a method table in hand, we can now create component instances/objects:

createInstance :: objState
               -> VTable iid objState
               -> IO (IUnknown iid)

Given the state of the component instance and its method table, this action does the bare minimum of creating an interface pointer. The interface pointer has type IUnknown iid, parameterising the pointer type over the interface it implements.

Using createInstance, you can then provide your own Haskell implementation of any COM interface (including IUnknown.) Regarding IUnknown; to let your implementation of QueryInterface() be able to create new interface pointers from old, the following operation is also provided

cloneIPointer :: VTable iid_new objState
              -> IP iid_old
              -> IO (IP iid_new)

However, most of the time you'll be more than happy (we hope ;-) with using the library provided implementation of IUnknown, in which case you'll be wanting to use the createComInstance constructor instead:

createComInstance :: String
                  -> objState
                  -> IO ()
                  -> [ComInterface objState]
                  -> IID (IUnknown iid)
                  -> IO (IUnknown iid)

The arguments to createComInstance are:

Since all the interfaces that a component supports work over the same object state, we parameterise them over that very state using the ComInterface type:

data ComInterface objState = -- abstract
mkIface :: IID iid
        -> ComVTable iid objState 
        -> ComInterface objState

You associate a particular interface ID with the method table that implements it by applying mkIface. The method table was constructed using createComVTable.

Back to createComInstance, it also takes the interface ID of the interface we want to initially create the component at. If that interface cannot be found in the list of interfaces that the component supports, an IO exception is raised.

Remarks:

Accessing the object state

To get at their object state, methods use:

type PrimIP a = Addr

getComObjState  :: PrimIP (objState, IUnkState) -> IO objState
getObjState     :: PrimIP objState -> IO objState

What's the difference between the two? getComObjState is used by methods contained in vtables that have been created via createComVTable, whereas getRealObjState is used by methods of DIY components that had their method tables constructed with createVTable.

Safe? I think not, any Addr could be used here. However, since the vast majority of the code that calls either of these functions will be automatically generated, I can live with this.

Remarks:

Creating dispatch interfaces

The ComServ library also supports the creation of components that implement the scripting-friendly IDispatch interface. You can create both 'pure' and 'dual' dispinterfaces:

createDispInterface :: IUnknown iid
                    -> Either LIBID String
                    -> IID iid
                    -> IO (IDispatch ()) 

createDualInterface :: StablePtr objState
                    -> ComVTable iid objState
                    -> Either LIBID String
                    -> IID iid
                    -> IO (IDispatch ())

The arguments to createDispInterface are:

The creation of dual interface is a little bit more complex, since we have to combine together the IDispatch method table with the table that actually implements the functionality. The vtable argument that createDualInterface implements is assumed to be IUnknown derived.

The details of how to call createDispInterface and createDualInterface is taken care of by the implementation of IUnknown that ComServ provides. To indicate that a particular interface implementation is of a IDispatch nature, use the following two constructors

mkDispIface, mkDualIface :: Maybe LIBID
                         -> IID iid
                         -> VTable iid objState
                         -> ComInterface objState

instead of mkIface.

An example

To demonstrate how to use the above services, here's an example interface we want to provide an implementation of in Haskell:

   interface IWho : IUnknown {
      HRESULT SongTitle ( void );
   };

The component supports just the one interface, IWho, containing the single method SongTitle. It's implemented as follows:

module Who where

import MessageBox

songTitle :: String -> IO ()
songTitle title = messageBox title

Next step is to create the method table for the component. To do this we make use of the Hugs/GHC foreign function interface:

iWho_vtbl :: (ComVTable (IWho a) String)
iWho_vtbl = unsafePerformIO $ do
   s_title <- mkSongTitle (primSongTitle)
   createComVTable [s_title]

primSongTitle :: PrimIP String -> IO HRESULT
primSongTitle this = do
  title <- getComObjState this
  songTitle title
  return s_OK

foreign export stdcall dynamic mkSongTitle :: (Addr -> IO HRESULT) -> IO Addr

Creating an instance of the component is straightforward too:

new ::  String -> IO (IUnknown (IWho ()))
new initState = 
  createComInstance ""
                    initState
                    (return ())
                    [mkIface iidIWho iWho_vtbl]
                    iidIWho

To use this component from Haskell, let's assume that we've used HaskellDirect to generate a client stub for songTitle:

module Main(main) where

import qualified Who ( new )

-- HaskellDirect generated *client* stub.
songTitle :: IWho a -> IO ()
songTitle = ...

main :: IO ()
main = do
  ip1 <- Who.new "My Generation"
  ip2 <- Who.new "Much Too Much"
  ip1 # songTitle
  ip2 # songTitle

A very indirect way of calling a Haskell function :-)

API signature

To summarise, here's the module signature for ComServ:

module ComServ where

import Com ( IUnknown, PrimIP, IID )

data ComInterface objState = --abstract
mkIface     :: IID iid 
            -> ComVTable iid objState
            -> ComInterface objState

mkDispIface :: Maybe LIBID
            -> IID iid
            -> ComVTable iid objState
            -> ComInterface objState

mkDualIface :: Maybe LIBID
            -> IID iid
            -> ComVTable iid objState
            -> ComInterface objState

data IID iid -- defined by Com library.
data VTable iid Com = .. -- externally allocated

type ComVTable iid objState = VTable iid (objState, IUnkState)

data IUnkState = ... -- abstract

type PrimIP objState = Addr  
  -- raw interface pointer (i.e., *not* finalised.)
  -- Com library should also define this.

createInstance :: objState
               -> VTable iid objState
               -> IO (IUnknown iid)

createComInstance :: String
                  -> objState
                  -> IO ()
                  -> [ComInterface objState]
                  -> IID (IUnknown iid)
                  -> IO (IUnknown iid)


createVTable    :: [Addr] -> IO (VTable iid objState)
    -- prepends 'standard' IU implementation.
createComVTable :: [Addr] -> IO (ComVTable iid objState)

cloneIPointer   :: IUnknown a -> VTable b -> IO (PrimIP b)

getObjState      :: PrimIP (objState, IUnkState) -> IO objState
getRealObjState  :: PrimIP objState -> IO objState

createDualInterface :: StablePtr objState
                    -> ComVTable (IUnknown iid) objState
                    -> Either LIBID String
                    -> IID (IUnknown iid)
                    -> IO (IUnknown iid)

createDispInterface :: IUnknown iid
                    -> Either LIBID String
                    -> IID (IUnknown iid)
                    -> IO (IUnknown iid)


Next Previous Contents