Next Previous Contents

7.4 The Automation library

To make it easier for scripting languages to access and utilise COM components, a separate 'technology' exists called Automation built on top of the basic COM component model.

Automation eases the task of using components from within a scripting language in that method invocation is done by name, rather than by performing indexed jumps via an interface pointer.

There's no intrinsic reason why a scripting language couldn't also do this, it's just harder and requires extra effort by the scripting client (at least that's the claim!)
Being able to invoke methods by name has more of a dynamic flavour to it, allowing for later binding.

See Automation documentation for other arguments as to why it is a Good Thing. To us Haskellers, Automation is a Good Thing in the sense that there's scores of Automation objects 'out there' that we can immediately interface to if we can 'do Automation'. Enter the Automation library - it provides a comprehensive and Haskell friendly view of Automation (and the interface that implements it, IDispatch).

The IDispatch interface

At the heart of Automation is the IDispatch interface, which collectively lets you invoke methods by name. The IID and types used for IDispatch are as follows:

data IDispatch_ a = IDispatch_
type IDispatch a  = IUnknown (IDispatch_ a)

iidIDispatch :: IID (IDispatch ())

Actually, when push comes to shove, Automation and IDispatch doesn't perform method invocation by name, but via (unique) numbers, so-called dispatch identifiers (DISPIDs). The IDispatch interface provides a separate method for mapping between method names and their DISPIDs:

type Member = String
type DISPID = Word32

getMemberID :: Member -> IDispatch a -> IO DISPID

The DISPID for the methods of a published interface is considered constant, so you can, if you wish, perform method invocation via those instead - the Automation library gives you the alternative.

The Variant class

Central to IDispatch is the VARIANT type, which is a big and ugly union/sum type that encodes the range of possible types an Automation object can be passed as arguments and return as results. In Haskell, we do not provide a type which mirrors VARIANT directly, instead a type class is put to good use:

class Variant a where
  inVariant      :: ArgIn a
  inVarList      :: ArgIn [a]
  inVarIUnknown  :: ArgIn (IUnknown a)
  vtEltType      :: a -> VARENUM
  resVariant     :: ArgRes a
  defaultVariant :: a

  resVarList      :: ArgRes [a]
  resVarIUnknown  :: ArgRes (IUnknown a)
  resVarIDispatch :: ArgRes (IDispatch a)

What the individual class methods do is only of interest should you want to declare your own instance - something you shouldn't have to do :), since the Automation library supplies the instances for you. But, unsurprisingly, what the class methods collectively provide is the bi-directional conversion between a Haskell value and the VARIANT values that an Automation object expects to see, hiding the details of that type from direct view to the Haskell programmer.

The important thing to notice is that if you have an overloaded type signature of the form

addVals :: (Variant a, Variant b)
        => a
        -> b
        -> IDispatch i
        -> IO ()
addVals x y = ...

and you invoke the method giving it arguments of one of the following types,

Char
String
IUnknown a
IDispatch a
Bool
Int
Int32
Float
Double

the type checker will successfully resolve the overloading and supply the dictionary containing the methods which marshals the arguments of the applied type into VARIANT form.

Invoking Automation methods

The right hand side of the addVals stub in the previous section is normally generated by HaskellDirect for you, but writing it out by hand is not much work, and useful should you only want to use a selection of the methods that a component exposes. Here's how to do it - suppose the addVals method was specified in IDL as follows:

 [id(1)]HRESULT addVals([in]int i, [in]int j);

The Automation library supplies a family of actions for invoking Automation methods, grouped by the number of results that the methods return. There's no results here, i.e., [out] parameters, so we make use of method0:

method0 :: String   -- method name
        -> [VarIn]
        -> IDispatch i
        -> IO ()

addVals :: Int -> Int -> IDispatch i -> IO ()
addVals i j = method0 "addVals" [inInt i, inInt j]

Input parameters are passed via a list of VarIn values. The VarIn type is itself abstract, but you construct values of it using a family of coercion functions, all of the form:

inTy :: Ty -> VarIn

where Ty is the name of one of the types that are instance of the Variant class. If you prefer to instead overload the arguments to addVals, you do that by using inVariant:

addVals :: (Variant a, Variant b)
        => a
        -> b
        -> IDispatch i
        -> IO ()
addVals i j = method0 "addVals" [inVariant i, inVariant j]

It's worth remarking here that one important feature of Automation is that it is by-design accommodating to typeless programming environments (not surprising, considering the VB development team at Microsoft implemented it in the first place!). So, as long as you can coerce your arguments into a VARIANT, you can pass it to an Automation component, even if the IDL / type information for the method states that it expects an int (say). The burden of converting the incoming VARIANT arguments into an int is on the Automation object/server, not its client.

Automation methods can either be accessed by name (as method0 did), or via a unique (per interface) dispatch identifier, DISPID. If you want to do the latter, and save having to look it up each time you invoke the method, use the methodIDn family of functions instead:

addVals :: Int -> Int -> IDispatch i -> IO ()
addVals i j = methodID0 1 [inInt i, inInt j]

It's part of the contract of an interface not to change the DISPID associated with its methods once it has been published.

There's one further twist to method invocation - if the last argument is an [out,retval], you'll need to use functionn instead,

[id(2)]HRESULT addVals([in]int i,[in]int j,[out,retval]int* res);

This method is invoked using function1:

function1 :: Member
          -> [VarIn]
          -> ArgOut a1
          -> IDispatch i
          -> IO a1

addVals :: Int -> Int -> IDispatch i -> IO Int
addVals i j = function1 "addVals" [inVariant i, inVariant j] outInt

Accessing properties

Automation objects has special support for setting and getting properties on an object. The Automation library allows you to access properties through propertyGet and propertySet:

propertySet :: Member -> [VarIn] -> IDispatch d -> IO ()
propertyGet :: Member -> [VarIn] -> ArgOut a -> IDispatch d -> IO a

For example, the property value,

[..]
interface IValue : IDispatch {
[propget,id(3)]HRESULT value([out]int* i);
[propset,id(3)]HRESULT value([in]int i);
 ...
};

can be accessed by the following pair:

getValue :: IValue a -> IO Int
getValue = propertyGet "value" [] outInt

setValue :: Int -> IValue a -> IO ()
setValue i = propertySet "value" [inInt i]

As with methods, properties can also be accessed via their DISPIDs:

propertyGetID :: DISPID -> [VarIn] -> ArgOut a -> IDispatch d -> IO a
propertySetID :: DISPID -> [VarIn] -> IDispatch d -> IO ()

Automation utilities

The library provides a couple of Automation-friendly versions of the component creation actions that the Com library supplies:

createObject    :: ProgID -> IO (IDispatch a)
getFileObject   :: String -> ProgID -> IO (IAutomation a)
getActiveObject :: ProgID -> IO (IAutomation a)
getObject       :: String -> IO (IAutomation a)

type ProgID = String

where a ProgID is a semi-readable string with the following format <Vendor>.<Component>.<Version> (if you don't have supply the version number, you're implicitly referring to the latest version of a component installed on your machine). An example of a ProgID would be MSWinsock.Winsock, and offer a somewhat legible way of referring to components in your programs.


Next Previous Contents