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.
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 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:
this pointer instead.VTable could easily maintain the number of entries
it has. With that in hand, we could then provide basic method
table operators (e.g., union, that's either IUnknown
savvy or not.)IUnknown.IO actions,
they could be functions. However, since they're commonly used from
within an IO context, we keep them as actions (for now.)
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:
mkIface work over VTables or ComVTables?
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:
IUnknown. Why? Because, effectively, a PrimIP is the
'this' pointer to a COM method, so we might as well perform the
unsafe operation of fetching the component state immediately, rather
than waste our time dressing it first up as an IUnknown before
performing the unsafe op. Fetching the component state is the only
operation that a method proxy uses the the 'this' pointer for.PrimIP can be converted into an interface pointer, IUnknown,
through the use of Com.addrToIface.
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:
IDispatch.IDispatch.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.
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 :-)
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)