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