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.
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