$Revision: 5.0.2.3 $
Described in this document is a preliminary implementation of an interface to OLE, available only on Microsoft Windows. We will be evolving this initial implementation and your feedback will be appreciated. |
The facility described in this document simplifies OLE programming without imposing any limit on the programmer's access to OLE functionality. To a CLOS program, Microsoft's COM/OLE/ActiveX facilities look like a foreign library consisting of data types, named API entries, and interfaces, the latter being C++-like objects with associated virtual tables. Allegro's OLE support provides tools for dealing with these foreign entities. Every API point is reachable, every interface can be used. The support also includes a library of CLOS classes and functions that make it easier to manage an application's OLE component in a CLOS development environment. This CLOS-OLE layer is not yet complete as we are continuing to extend it. All the most useful OLE capabilities will be as readily available in CLOS as in any other environment, and will benefit from the unique dynamic power inherent in CLOS.
This file contains an overview of ACL OLE 's treatment of OLE concepts. It lays out the overall organization of the ACL OLE tools and an ACL OLE application. Detailed documentation of each element in the ACL OLE system appears in the Reference document. Look there for information about individual CLOS functions, macros, classes and data types.
You will find a set of sample ACL OLE programs to illustrate writing client and server applications using ACL OLE. Each sample appears in a directory ole/samples/samplenn, and includes a readme.txt file explaining how to compile and run the example.
Basic Unit of OLE |
Treatment in ACL OLE |
Data Types | Most of OLE's primitive data structures are defined and manipulated using the Allegro foreign data interface. A few receive special treatment. See the entries for Unicode, GUID's, BSTR's, and Interface Pointers. |
Interface Definition | An OLE Interface definition specifies the methods of an interface, giving
their order in the VTBL and the arguments and return-value type for each. An interface is
seen from two sides, client and server. On the client side, an interface allows the
program to invoke its methods without knowing how they are implemented. The method
implementations exist on the the server side. ACL OLE provides separate macros to
def-ole-interface
|
Reference Counting | OLE uses a reference counting scheme to allow it to do garbage collection
on OLE resources. Every OLE interface inherits from the IUnknown interface, which provides
AddRef and Release methods to record object usage. ACL OLE
reflects these OLE methods as the CLOS generic functions add-ref and release .
Functions in the ACL OLE subsystem make add-ref and release calls at the appropriate times. Allegro garbage collection finalizations perform a release call on each client-interface object that dies without already having been released. |
Starting an OLE Session | Before using any OLE facilities, an application has to call certain OLE
API functions to initialize state. ACL OLE provides start-ole and stop-ole ,
functions that perform all necessary initialization calls and disconnect from OLE,
respectively. |
Important CLOS Issues for an ACL OLE Application
ole
package;
exported symbols name the documented CLOS functions, macros, constants, classes and
foreign data types that ACL OLE supports.Users should not place (in-package
#:ole)
forms in their source files. They should either include a (use-package
#:ole)
form or use explicit qualification on ACL OLE symbols, as in (ole:query-interface
foo ole:IID_IUnknown)
.
FORMAT
and DEFUN
) appear in upper
case. Allegro CLOS can operate in this mode; it can also operate in a mode that is more
consistent with UNIX and C usage. In this second mode, called :case-sensitive-lower,
symbol names are read without case conversion and all Common Lisp symbols (e.g., format
and defun
) appear in lower case.When operating in ANSI Standard mode,
symbols in the OLE package, including function, macro and CLOS class names, are all upper
case. The case in which they are written in a source file is not significant. Two symbols
that differ only in case cannot be used to name separate entities without escaping the
lowercase letters. These last two points mean that errors due to inconsistent case usage
disappear, but at the same time, the common C convention of using a capitalized symbol for
a structure or class name and lower case for an object, as in Party
and party
,
introduces a naming conflict.
When operating in :case-sensitive-lower mode, Allegro CLOS allows source files to
contain distinct symbols that differ only in the case of individual letters. In this mode,
all symbols that represent interfaces, OLE functions and OLE constants appear in lisp with
the same case configuration they have in C, e.g., IUnknown
, GetClassObject
,
DISP_E_UNKNOWNINTERFACE
. OLE types appear in the same case they have in C
unless the C type is all upper case, in which case the ACL OLE type name is all lower
case, e.g. pInterface
, bstr
.
Important: An ACL OLE application running in ANSI standard mode must direct the fasl loader to convert mixed-case symbol names to upper case, something the fasl loader does not do by default. The way to specify this directive is by evaluating
(convert-mixed-case-symbols t)
Loading the ACL OLE system into Allegro by evaluating (require :ole) or (require :ole-dev) automatically sets the correct convert-case-mode.
An ACL OLE file that has been compiled in :case-sensitive-lower mode can be
successfully loaded into a lisp running in ANSI standard mode, as long as the shift to all
upper case doesn't introduce any name clashes. The reverse is not true. An ACL OLE
application file compiled in ANSI standard mode will not successfully load into a
:case-sensitive-lower lisp, because there is no way to recover the mixture of upper and
lower case in symbols such as IUnknown
. For this reason, it is a good plan to
compile ACL OLE applications in :case-sensitive-lower mode, so the compiled application
can be loaded and run by a lisp running in either mode.
Once ACL OLE has been loaded into lisp, it is likely that subsequently changing the system's case mode will render ACL OLE inoperable. Attempting to call set-case-mode in this environment will raise a continuable error warning the user of the problem. Continuing from this error allows the system to change its case convention despite the danger to OLE.
An ACL OLE application file should include the following two forms to take advantage of this separation:
(eval-when (compile eval)
(require :ole-dev))
(eval-when (compile load eval)
(require :ole))
The minor modules include special CLOS classes and functions used to support automation clients and servers, a few other special OLE areas that are not needed in every application, and separate files for each client and server interface. ACL OLE provides three macros to include these modules:
These macros generate code to load the associated modules when they are needed.
The macros require-server-interfaces and require-client-interfaces are
generated as necessary by ACL OLE macros that refer to interfaces by name, such as def-ocx-class
,
and so are rarely coded explicitly. (The macro def-ocx-class
defines a
CLOS server class that supports a named set of interfaces. It generates the appropriate require-server-interfaces
forms, so no explicit requires are needed for those server interface modules.) The
most common situation in which these macros must be coded is when a program refers to
symbols or classes belonging to an interface that it neither defines itself nor names in
some other ACL OLE macro.
Programs that define part of an OLE server generally need to use require-modules to ensure the presence of the server-support functions. A typical automation server would include the form
(eval-when (compile load eval)
(ole:require-modules :automation-server :factory-server))
A server that did not provide an IDispatch interface would not need the :automation-server support, and could get by with just the :factory-server.
Currently there are no client-side modules that need to be loaded this way.
ACL OLE includes a library of interface definition modules grouped into three directories
The IUnknown interface, for example, has a base definition as .../defifc/iunknown.{cl,fasl}, while the associated client-side definitions appear in .../client/iunknown.{cl,fasl} and the server-side definitions are in .../server/iunknown.{cl,fasl}. The defifc/* files are only needed during compilation of the associated server/* and client/* files. The machinery for defining interfaces generates code to ensure the loading of all interface files on which a given interface or module depends. When an interface module is to be loaded, ACL OLE checks the current directory and the value of sys:*require-search-list*.
ACL OLE gives these OLE data types special treatment.
Many OLE API functions use codes to allow localization of labels and user-readable data. The ACL OLE interface uses two parameters to provide default values for these codes: ole:*ole-language* and ole:*ole-locale*. Currently, these are set to the machine-dependent-default values that are OLE's lowest-common-denominator.
Interfaces and Objects
ACL OLE distinguishes between those interfaces that lisp implements as a server and those that lisp uses as a client. CLOS objects represent interfaces, and different CLOS classes exist for the client and server views of the same interface. This is important because we often have to deal with an interface from both sides in the same program. While developer-defined interface classes can be given any names, the ACL OLE classes are named using the following convention: ACL OLE supports OLE interface IAbcde with client-side interfaces of class IAbcde-client and server-side interfaces of class IAbcde-server.
Example: When an ACL OLE application obtains an IUnknown interface from some external
object, it will be of type IUnknown-client
. An ACL OLE server application
will generate IUnknown-server
objects in response to requests for the
IUnknown interface.
An OLE object may reveal any number of interfaces to the outside world. The interfaces and the object are distinct entities, and in an ACL OLE application these will be represented by instances of different CLOS classes.
The ACL OLE object on the server
side will be an instance of a class
that inherits from the lisp-ole-object
class and from several mixin classes,
one for each OLE Interface the object supports. These mixin classes are named by the OLE
interface name, e.g., IClassFactory
or IOleObject
. The object
itself is completely under the server's control; only the interfaces are exported to the
rest of the world. As a server, the CLOS application must implement the methods of these
interfaces.
A developer will often do this by using def-ocx-class
to define the object
class, specifying the interface mixins. Here, for example, is the definition for ACL OLE's
class-factory
class .
(ole:def-ocx-class class-factory (:interfaces IClassFactory)
((registration-code :initform nil)
(locked :initform nil)
(children :initform nil)
(product-class :initarg :product-class)
(allow-aggregation :initform nil :initarg :allow-aggregation)
))
Here we are saying that a class-factory has the usual semantics for an OLE object
implemented in lisp, and that it exports two interfaces: IUnknown (supported by default)
and IClassFactory. These interfaces have been defined previously with def-ole-interface
and are implemented by IUnknown-server
and IClassFactory-server
instances, respectively. This class definition form arranges that class-factory
objects will respond to QueryInterface requests for the IUnknown and IClassFactory
interfaces, constructing and caching each interface the first time it is needed.
The registry is where most system information is kept in Windows. An ole server must
store information about itself in the registry
if it wants to be invoked automatically or allow certain automation clients (such as
Visual Basic) to create its objects. This section will describe the registry and how it is
manipulated from Lisp.
The registry is stored as a tree. Each node in the tree is called a key. Each key has a name, a collection of zero or more values, and a set of zero or more child keys. Each value stored in a key is also named (except for one value, which has no name, and is called the default value). The name of a registry key must consist of printable characters and no spaces.
Manipulating the registry from lisp consists of first getting a pointer to the particular key you want to modify. This is done by starting with an existing open registry key and traversing down the tree by using the names of successive keys that should be followed. Since you have to start somewhere in this process you can use one of the pre-opened registry keys to begin the registry scan. These pre-opened keys are
The following functions and macros are provided to open keys and to read and modify the registry. See the reference document for their definitions.
Automation allows one application to communicate with and control another. The server
application offers a set of objects to control. The client application controls the
objects offered by the server. The server can be a standalone application (often called an
'exe' or local server) or it can be a so-called in-proc server, i.e., one that is
implemented in a dll that is loaded into the client application. The client need not
know which method is being used, but it can set limits, refusing to use a local server,
for example.
An automation object has a set of properties and a set of methods. Properties can be read
or set (although the object server can ignore an attempt to set a property the server
considers read-only). Methods can be called on an object and a value returned from the
method. Properties and methods are named. The name is a case-insensitive string; ACL
OLE functions to access them can use strings or symbols.
A property and a method can have the same name because when that name is used it is clear
from the use whether the property or method is intended.
Dynamic naming
Automation is similar to lisp itself in that the binding of name to property or method is
done at runtime. Some automation objects support early-binding of names to properties and
methods using a type library. The lisp-ole interface does not support this yet.
Unique ids
In order for applications to talk about classes and interfaces and to be sure that they
are talking about the same ones, they use GUIDs. You'll often see a guid written this way
{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
where the x's are hex digits. In Lisp we represent guids as lisp-guid structures and
provide functions for converting between any of the common guid representations.
There is a Microsoft program to generate unique ids that are guaranteed to be
(almost-but-not-quite certainly) distinct from any other on Earth for all time. You'll
want to use this program if you plan on distributing your automation server. For just
experimenting you can choose any random sequence of digits and chances are it will be
different than anything else on your machine.
Classes
An OLE class describes a collection of objects. Each COM/OLE class has a unique guid. The
class is the key to getting communication started between the client and the server. The
client initiates the communication by asking for a pointer to a class object's class
factory. Once the class factory is returned, the client can ask the factory to create one
or more automation objects.
The Lisp remote-autotool Class
ACL OLE defines the ole:remote-autotool class to facilitate control of automation objects. An instance of ole:remote-autotool holds the Ole machinery that communicates with the server application.
Set-up
To establish a connection to an automation object you can use the function ask-for-autotool.
(setq autoinstance (ole:ask-for-autotool (ole:unique-guid "{dbce6200-e0a3-11cf-b565-00aa0064595a}") ole:CLSCTX_INPROC_SERVER))
is the form to use if you know the OLE class id. If the application that supports the class is registered, you can bypass the class id and use the registered application name:
(setq autoinstance (ole:ask-for-autotool "MSCAL.Calendar" ole:CLSCTX_INPROC_SERVER))
If you expect to create more than one instance of the same object type then it's more efficient to ask for the class factory and then ask the factory for the specific objects you want to create.
(setq factory
(ole:get-class-object
"{dbce6200-e0a3-11cf-b565-00aa0064595a}"
ole:CLSCTX_LOCAL_SERVER
ole:IID_IClassFactory))
(setq autoinstance (ole:ask-for-autotool factory))
If xdi is an IDispatch-client interface for an object that supports automation, you can build an autotool object for it like this:
(setq autoinstance (make-instance 'ole:remote-autotool :dispatch xdi))
Whichever route you take to acquire the remote-autotool instance, you can then
use the functions auto-getf, (setf auto-getf), and auto-method to read properties, set properties, and call
functions on the automation object.
For example,
(ole:auto-getf autoinstance :x)
will ask for the value of the x property of the automation object.
(setf (ole:auto-getf autoinstance :x) 555)
will set the x property value to 555.
(ole:auto-method autoinstance :foob 3 4)
will call the foob method on the automation object,
passing in two extra arguments, 3 and 4.
When you've completed using the remote object do (ole:release autoinstance) to free it on
the server side. After the call to ole:release, don't use the autoinstance object again
since it no longer refers to an object on the server.
Writing an automation server is simplified by using the ACL OLE automaton and factory interfaces. See ole/samples/sample04/server.cl for an example.
Suppose you, a lisp programmer, have some functionality you want to offer to clients via OLE/COM.
First figure out which interfaces you want to support in this object. You don't have to
declare them all immediately, you can add more later on. You must support IUnknown at
least. Each interface must be defined in your application. Many interfaces are already
defined. Check the ole/defifc directory to see which. If there's already a definition, you
probably want to use it. If the interface doesn't appear in ole/defifc, then you will need
to provide a definition using def-ole-interface
and def-server-interface
.
You can put the definitions in your application source code if the interface is unique to
that application. Alternatively, you can add it to the ACL OLE library if you expect to
use it in more than one application.
Next you define a CLOS class whose instances will represent objects allocated on behalf
of the client. This CLOS class should be defined with def-ocx-class
or be a
subclass of such a class. The def-ocx-class
form will name each interface
that this object will export to clients, except possibly IUnknown, which gets put in
automatically if you don't name it. The interface names are symbols like IStorage
,
IOleObject
, etc. Example:
(ole:def-ocx-class my-class (:interfaces IFoo) ....)
Put in all the interfaces you want to support after the :interfaces keyword. If you want to support two different interfaces with the same interface object, where one is based on the other, include a list of the related interfaces as one of the entries after the :interfaces keyword, as in
(ole:def-ocx-class my-class (base1 :interfaces
(IViewObject IViewObject2)
IOleObject)
((local-slot ..)
...))
Here, a request for either IViewObject or IViewObject2 will be satisfied with the same object, which will be of type IViewObject2-server. The last named interface in a set is the one that is used for any of them.
With the CLOS class my-class
defined you must now make sure that the
methods for each exported interface are defined over this type of data object. Suppose
your class supports the IFoo interface, an interface that has four methods: the three from
IUnknown, plus the method 'addem' that adds its two integer arguments together and returns
an integer result. The interface definition might look like this:
(def-ole-interface IFoo
(:iid "{12345678-1234-5678-1234-123456781234}")
(addem (in.arg1 integer) (in.arg2 integer)))
(def-server-interface IFoo)
When a client allocates an object of your class and gets a pointer to the IFoo
interface and then calls addem, control will eventually reach your server. When that
happens, the generic function addem
will be called with three arguments: the
first argument is the CLOS object of your class that the client has remotely allocated,
and the other two arguments are the integers to add. You could thus write your server
method in this way:
(defmethod addem ((obj my-class) x y) (+ x y))
In this particular case, as in most cases, the function of the addem method in the IFoo interface doesn't depend on the object itself, so we might just want to write that method for all classes that export that interface:
(defmethod addem ((obj IFoo) x y) (+ x y))
this works because all classes that export the IFoo interface are a subclass of IFoo. Or we could write it for the interface object itself and not bother to look for an object-specific function.
(defmethod addem ((ifc IFoo-server) x y) (+ x y))
ACL OLE uses this ability to write methods over an interface to define the three methods inherited from IUnknown: add-ref, release, and query-interface. Thus the server class writer generally doesn't have to worry about writing these methods. (And in fact, should not replace the primary methods, ever. :before, :after, and :around methods are OK.)
The def-ole-interface
macro defines each interface. With it you name the
interface (without the "-server" or "-client" as that is added by def-server-interface
and def-client-interface
). The def-ole-interface
macro allows
you to specify the IID and the methods for the interface, possibly specifying a base
interface to inherit methods from. You list the methods by name and give an argument map
(name and type) for each argument of each method. The macro expands into code that creates
a structure containing all this information. The argument-type encoding is the one
used by ff:def-foreign-type. An older form, approximating what appears in the C
header files, is also accepted, but is deprecated.
With def-server-interface
you specify the interface you're generating
linkage code for, as in
(def-server-interface IFoo)
The result is to define two classes: IFoo-server
is the server interface
class. IFoo
is the mixin class for all objects that support the IFoo
interface.
It may be confusing to have two classes representing an interface on the server side: IFoo-server
and IFoo
. The difference between the classes is this: Instances of IFoo-server
are interface objects, which have relatively simple and unchanging functionality. These
methods are called first when a client call comes in so you might want to write methods
that do argument transformation before passing the call to the object-specific code.
An object could reasonably be both IViewObject
and IOleObject
.
That would mean that it made both types of interfaces available to clients and responded
appropriately to methods on either interface. However, an IOleObject-server
object is definitely *not* an IViewObject-server
object. They have completely
different vtables attached to their proxies.
Thus if you want to write methods over your server objects that depend on them having
an IFoo interface, then you write those methods over IFoo. As an example, the IUnknown
reference counting methods would be best written over IUnknown
, since all
objects that support an IUnknown interface will inherit from IUnknown
.
The mixin class hierarchy for the IUnknown interface
ole:IUnknown ole::interface-mixin
The mixin class hierarchy for a random IFoo interface
IFoo ole:IUnknown ole::interface-mixin
The server class hierarchy for a random interface IFoo is
IFoo-server ole:IUnknown-server ole::lisp-ole-interface
The application class hierarchy for a random class my-class supporting
the IUnknown and IFoo interfaces is
my-class ole::lisp-ole-object IFoo ole:IUnknown
Now we'll look at what happens when you define a class like
(ole:def-ocx-class my-class (:interfaces IFoo) ....)
In this case my-class
inherits from IFoo
explicitly and from lisp-ole-object
and IUnknown
implicitly. The lisp-ole-object
class contributes
two instance slots:
ref-count
interfaces
The ref-count
is used to count the number of users of this object; we
don't track uses by each interface separately. The interfaces
slot holds data
that controls the allocation and caching of the interface objects for this instance of my-class
.
The data is built and stored in this slot automatically through some :around method magic;
newly allocated interface objects are cached for later reuse.
When an instance of my-class
is polled with QueryInterface, an interface
object is either found or is built and cached. Each interface object has two slots
The owner slot points to the instance of my-class that has this interface object on its interfaces list. The handle slot points to a proxy object. A proxy object is a :c foreign array made to look like a C++ (COM/OLE) object whose first slot points to a vtbl of functions for this interface. This proxy object is created when first needed. The second slot of the proxy contains information that helps us quickly find the associated lisp interface object when a method call comes in from the outside world.
Here is how it all works: When a client makes a call on an interface method, and control reaches the lisp server, lisp is passed the address of the proxy object as the first argument (this is the C++ 'this' pointer). Lisp then can use some internal machinery to locate the interface object, and calls the generic function associated with that method, usually a function with the same name as the OLE interface method. The generic function's arguments are the instance of the interface object associated with the proxy object, and the rest of the method arguments. If the interface was created in the usual way (with def-server-interface), then a method was automatically written that specializes on the first argument being an instance of this interface, and that method calls the same generic function, this time with the first argument being the instance of my-class found in the interface object's owner slot.
Here's an example using our IFoo interface with its addem(int x, int y) function. When the client calls ptr->addem(3,4) control reaches our (automatically generated) defun-c-callable function
vtbl.addem(proxy_address, 3, 4)
this function finds the interface object, an IFoo-server, from the proxy_address and calls the addem generic function:
(addem interface-obj 3 4)
This in turn invokes the specialized method
(addem (obj IFoo-server) x y)
which is automatically defined to do (slot-value obj 'owner) to find the instance with this interface and call the generic function
(addem owner-obj 3 4)
This selects the specialized method
(addem (obj my-class) x y)
That method, defined explicitly in the application code, computes and returns a value, which is then returned to the client.
Copyright (C) 1998-1999, Franz Inc., Berkeley, CA. All Rights Reserved.