Next Previous Contents

6.5 Generating the binary representation

The Haskell component stubs are turned into the binary COM representation by a per-component mk<Component>_iptr action that is generated along with the new constructor in the previous section:

mk<Component>_iptr :: a 
                         -> Pointer GUID 
                         -> Pointer (Pointer Interface)
                         -> IO HRESULT

Assuming we're only supporting one interface, Figure Binary representation of COM object shows the object layout that mk<Component>_iptr will generate.

Binary representation of COM object

For the IntRef example, the following action is generated:

mkIntRef_iptr :: a
              -> Pointer GUID 
              -> Pointer (Pointer Interface)
              -> IO HRESULT
mkIntRef_iptr obj_st piid ppvObject = do
    stbl     <- makeStablePtr obj_st
    let iface_list = [(iidIIntRef, mkIIntRef_iptr stbl)]
    if_list  <- mkIfaceList iface_list
    hr       <- findInterface piid ppvObject if_list
    return hr

It takes three arguments, the first being the component instance state, the second the interface at which we want to initially create the component at, and finally a pointer to a piece of memory where the caller expects the interface pointer we're about to create, to be filled in. mkIIntRef_iptr starts by creating a stable pointer holding the component instance state.

Aside: A stable pointer is an (indirect) reference to a Haskell heap object, and is used when you need to store a reference to a heap object outside of the heap. Creating a stable pointer to a heap object amounts to telling the storage manager that the heap object is to be considered live even when no other objects in the heap refer to it. To avoid imposing the constraint that this heap object cannot be moved around by the storage manager, a stable pointer is an indirect reference and not a pointer to the heap object itself. So, to get at the heap object you'll need to dereference the stable pointer first.
Using the stable pointer, we construct an association list which maps from the identifiers (IIDs) of the interfaces that IntRef supports to the actions that will create the binary representation of that particular interface. This list is then used by the library provided action mkIfaceList:

-- library provided --
type IFList = 
  [(GUID, 
    IORef
      (Either
         (Pointer Interface)
         (StablePtr IFList -> IO (Pointer Interface))
      )
   )]

mkIfaceList :: [(GUID, StablePtr IFList -> IO (Pointer Interface))]
            -> IO (StablePtr IFList)
mkIfaceList ls = do
  ls' <- mapM initIf ls
  makeStablePtr ls'
 where
   initIf (iid, mkIf) = do
      v <- newIORef (Right mkIf)
      return (iid, v)

mkIfaceList creates an IFList, which is an association list between an interface identifier and the interface pointer representing the interface. We choose to generate binary representations of a component's interfaces lazily (so called tear-off interfaces), delaying the construction until the client of the component asks for the interface.

Since we only want to create one binary representation of a particular interface regardless of how many times the interface is asked for (via QueryInterface()), the IFList representation allows us to explicitly cache a pointer to an interface once it has been generated.

Returning to the implementation of mkIntRef_iptr, it finally calls findInterface passing it an IFList together with a pointer to the IID of the interface we want to initially create the component at. findInterface is another library provided function:

-- library provided too --
findInterface :: Pointer GUID
              -> Pointer (Pointer Interface) 
              -> StablePtr IFList
              -> IO HRESULT
findInterface piid ppv if_list = do
    iid <- unmarshalGUID piid
    ls  <- derefStablePtr if_list
    if (iid == iidIUnknown)
     then 
       case ls of
          [] -> error "will not ever happen. Promise."
          ((_,if_cons_v):_) -> createIface if_cons_v
     else 
       go ls
  where
   go [] = do
     writeAddrOffAddr ppv nullAddr -- help to flush out some bugs, client-side --
     return e_NOINTERFACE
   go ((x,if_cons_v):xs)
     | iid == x  = createIface if_cons_v
     | otherwise = go xs

   createIface if_cons_v = do
        if_cons <- readIORef if_cons_v
        newip   <- 
           case if_cons of
             Left ip -> -- already created, reuse --
                return ip
             Right mkIf -> do  -- create and cache --
                 newip <- mkIf if_list
                 writeIORef if_cons_v (Left newip)
                 return newip
        writeAddrOffAddr ppv newip
        addRef newip
        return s_OK

It simply walks down the IFList of a component, trying to find an entry for the requested interface. If found, a pointer to the binary representation is returned (this last step may involve creating the binary interface representation if it hasn't been already).

To summarise, the generated component constructor new performs the following tasks:

The binary representations are generated by automatically generated actions, one per interface that a component supports. In the case of our example, only mkIIntRef is generated:

-- generated in IntRefProxy --

mkIIntRef :: StablePtr a 
          -> StablePtr IFList
          -> IO (Interface IIntRef)
mkIIntRef iface_st ifaces = mkIfacePointer iface_st ifaces iIntRef_vtbl

sizeof_IIntRef_vtbl :: Word32
sizeof_IIntRef_vtbl = 
    sizeof_IUnknown_vtbl -- library provided constant --
  + 2*sizeofAddr         -- IIntRef meths  --

--type VTBL a = Addr  --

iIntRef_vtbl :: VTBL IIntRef
iIntRef_vtbl = unsafePerformIO  $ do 
    vtbl <- allocMemory sizeof_IIntRef_vtbl
     -- IUnknown implementation. --
    writeAddrOffAddr vtbl 0 addrOf_queryInterface
    writeAddrOffAddr vtbl 1 addrOf_addRef
    writeAddrOffAddr vtbl 2 addrOf_release
     -- IIntRef methods --
    writeAddrOffAddr vtbl 3 addrOf_set
    writeAddrOffAddr vtbl 4 addrOf_get
    return vtbl

foreign label "queryInterface" addrOf_queryInterface :: Addr
foreign label "addRef" addrOf_addRef :: Addr
foreign label "release" addrOf_release :: Addr
foreign label "set" addrOf_set :: Addr
foreign label "get" addrOf_get :: Addr

The binary representation of an interface in Figure Binary representation of COM object shows that it consist of two parts: an interface header and a method dispatch table. The method table is constant across all instances of a component (at that particular interface), so to achieve this property here we use a cheeky unsafePerformIO trick on the RHS of a CAF to ensure that the method table is only allocated once. For IIntRef, the iIntRef_vtbl constant does this, filling the method table with the addresses of IIntRef's methods.

The library function mkIfacePointer takes care of creating the (per-instance) interface header:

-- library provided function. --
mkIfacePointer :: StablePtr a 
               -> StablePtr IFList
               -> VTBL b 
               -> IO (Interface b)
mkIfacePointer iface_st ifaces vtbl = do
   pre <- allocMemory sizeofIfaceHeader
   writeAddrOffAddr pre 0 vtbl
   writeStablePtrOffAddr pre 1 iface_st
   writeStablePtrOffAddr pre 2 ifaces
   writeWord32OffAddr pre 3 0  -- ref count. --
   return pre

-- library provided constant. --
sizeofIfaceHeader :: Word32
sizeofIfaceHeader =
     sizeofAddr        -- lpVtbl
 + 2*sizeofStablePtr   -- interface state + interface list.
 +   sizeofWord32      -- reference count. --

The per-instance interface overhead is kept down to the allocation of a chunk of memory big enough to hold the reference count, stable pointers to the component state and the assoc list of supported interfaces, plus a pointer to the shared method dispatch table.

Given an interface pointer, we can now provide an implementation for the getIfaceState used by the method stubs earlier:

getIfaceState :: Pointer Interface -> IO a
getIfaceState iptr = do 
   stbl <- readStablePtrOffAddr (iptr `plusAddr` sizeofAddr) 0
   derefStablePtr stbl


Next Previous Contents