Next Previous Contents

3.4 COM component examples

This section contains examples of how to write Haskell COM components, including how to generate the surrounding support code using HaskellDirect.

This currently only works together with ghc.

A telephone directory

All the example code given in this section can be found in the examples/comserv directory of the HaskellDirect distribution, together with a Makefile for building the component.

We want to write a simple phone directory abstraction which supports lookups and the addition of entries to a directory. The external programming interface to the directory would be as follows:

newPBook          :: PBook
addDirectoryEntry :: PBook  -> String -> String -> PBook
lookupName        :: PBool  -> String -> Maybe String
lookupNumber      :: PBook  -> String -> Maybe String

What the individual operations do should hopefully be clear - here's one possible implementation of the interface:

module PBook 
       ( PBook
       , newPBook
       , addEntry
       , lookupNumber
       , lookupName
       ) where

import List ( find )

newtype PBook = PBook [PBookEntry]

data PBookEntry 
 = PBookEntry 
        { entryName   :: String
        , entryNumber :: String
        }

newPBook :: PBook
newPBook = PBook []

addEntry :: PBook 
         -> String
         -> String
         -> PBook
addEntry (PBook ls) name no
  = PBook (PBookEntry{entryName=name, entryNumber=no} : ls)

lookupNumber :: PBook
             -> String
             -> Maybe String
lookupNumber (PBook ls) no
  = fmap (entryName) (find ((==no).entryNumber) ls)

lookupName :: PBook
           -> String
           -> Maybe String
lookupName (PBook ls) name
  = fmap (entryNumber) (find ((==name).entryName) ls)

Not much to it, so let's make things a little bit more interesting and try to wrap this phone book up as a COM component.

First step along the way is to try to express the interfaces of a component in IDL, splitting up the operations for looking up entries from that of adding one into separate interfaces:

[....]
interface ILookup : IUnknown { 
   HRESULT LookupName   ([in]BSTR name,   [out,retval]BSTR* number);
   HRESULT LookupNumber ([in]BSTR number, [out,retval]BSTR* name);
};

[....]
interface IDirectory : IUnknown { 
   HRESULT AddEntry ([in]BSTR number, [in]BSTR numb);
};

[....]
coclass PBook  {
    [default]interface ILookup; 
    interface IDirectory;
};

The PBook is declared to be a component supporting two interfaces, ILookup and IDirectory. Nice and clean. Unfortunately though, scripting languages such as VBScript and JavaScript forces us to rethink our pursuit of Component Beauty. For one, they only let you access a component via a scripting-friendly IDispatch interface, that is, the notion that a component can consist of more than one interface is just lost on these languages. Only one IDispatch derived interface can be used by these clients, so in order to be more scripting language friendly, let's rephrase the above interface definition:

There are ways and means of working around the problems that IDispatch-based clients such as VBScript poses to the component designer/implementor. An overview of proposed solutions and workarounds can be found at IDispatch workarounds. These solutions can be carried over into our setting, but haven't been (yet). One response to all this might be, ``why bother?''. The short answer to that is that we dearly want to be able to embed and script Haskell components from within web pages.

[....]
interface ILookup : IUnknown { 
   HRESULT LookupName   ([in]BSTR name,   [out,retval]BSTR* number);
   HRESULT LookupNumber ([in]BSTR number, [out,retval]BSTR* name);
   HRESULT AddEntry     ([in]BSTR number, [in]BSTR numb);
};

[....]
dispinterface DILookup { 
  interface ILookup;
};

[....]
library PhBook {

[....]
coclass PBook  {
    [default]dispinterface DILookup; 
};
};

The dispinterface declaration exposes the methods of ILookup via the aforementioned scripting-friendly IDispatch interface (details of which aren't that important here).

The above specification also embeds the declaration of PBook inside a library declaration. This is done in order for the Haskell implementation of IDispatch, which HaskellDirect generates code for, to work properly.

When operating in 'Haskell COM server' mode, HaskellDirect generates the underlying support code which takes care of the details of how to wrap up a Haskell implementation of a PBook component (see separate document for details of what's generated - ToDo: add xref). The HaskellDirect command line is as follows:

  ihc -s -c pbx.idl -o PbxProxy.hs --output-tlb=pbx.tlb

where -s puts the compiler in server-mode, and --output-tlb= is used to generate the type library which is used to implement IDispatch.

If you feed HaskellDirect the extra option --skeleton, it will generate the type signatures and empty definitions of the methods it expects the component implementor to supply. In the case of PBook, it generates the following:

module PBX where

data State = State

new :: IO State
new = ...

lookupName :: String -> State -> IO String
lookupName name st = ...

lookupNumber :: String -> State -> IO String
lookupNumber number st = ...

addEntry :: String -> String -> State -> IO ()
addEntry name numb st = ...

Each method is an IO action that takes as its last argument the state of the PBook component which it then updates as needed.

Not quite the signature of our original PBook abstraction, but impedance matching the two is not much work:

module PBX where

import IOExts
import qualified PBook

data State = State (IORef PBook.PBook)

new :: IO State
new = do
  let x = PBook.newPBook
  r <- newIORef x
  return (State r)

lookupName :: String -> State -> IO String
lookupName name (State st) = do
   pb <- readIORef st
   case PBook.lookupName pb name of
     Nothing -> return "not found"
     Just x  -> return x

lookupNumber :: String -> State -> IO String
lookupNumber number (State st) = do
   pb <- readIORef st
   case PBook.lookupNumber pb number of
     Nothing -> return "not found"
     Just x  -> return x

addEntry :: String -> String -> State -> IO ()
addEntry name number (State st) = do
   pb <- readIORef st
   let new_pb = PBook.addEntry pb name number 
   writeIORef st new_pb

Ideally, we shouldn't have to write this layer of code - the HaskellDirect generated code should just 'plug into' the PBook signature we wrote earlier, right? That would certainly be nice, but our experiences of writing components in Haskell are so far quite limited, so a pattern of how to express components functionally has yet to emerge. But when or if such a pattern steps forward, HaskellDirect should be extended to avoid having to write code such as the above.

That's it really, next step is to assemble the bits that make up the component, see the examples/comserv/Makefile for details of how this is done.

Once you've successfully built the component, make sure you register it to the world using

sh$ regsvr32 test.dll
sh$ 

With that done, you can now start to use the component - in examples/comserv you'll also find an example of how it can be embedded in a web page (and accessed via VBScript) + there's also some sample code which demonstrates how it can be accessed from a Visual Java client.


Next Previous Contents