Lisp implementations of COM interfaces are created by defining an appropriate class and then defining COM methods for all the interfaces implemented by this class.
The class can inherit from standard-i-unknown to obtain an implementation of the i-unknown interface. This superclass provides reference counting and an implementation of the query-interface method that generates COM interface pointers for the interfaces specified in the class definition. It also supports aggregation.
There are two important things to note about COM classes and methods:
To implement a COM interface in Lisp, you need the following:
Since COM objects can be accessed from outside the Lisp world, possibly from a different application, their lifetimes are controlled more carefully than those of normal Lisp objects. The diagram below shows the lifecycle of a typical COM object.
The lifecycle of a COM object
Each COM object goes through the following stages.
In the first stage, the object is created by a call to make-instance, either by a class factory (see 1.9.3 Class factories) or explicitly by the application. The normal CLOS initialization mechanisms such as initialize-instance can be used to initialize the object. During this stage, the object is known only to Lisp and can be garbage collected if the next stage is not reached.
At some point, the server makes the first COM interface pointer for the object by invoking the COM method query-interface
, either automatically in the class factory or explicitly using by using macros such as query-object-interface or call-com-object. When this happens, the object's reference count will become 1 and the object will be stored in the COM runtime system. In addition, the generic function com-object-initialize is called to allow class-specific COM initialization to be done.
In this stage, the object is used via its COM interface pointers by a client or directly by Lisp code in the server. Several COM interface pointers might be created and each one contributes to the overall reference count of the object.
This stage is entered when the reference count is decremented to zero, which is triggered by all the COM interface pointers being released by their clients. The generic function com-object-destructor is called to allow class-specific COM cleanups and the object is removed from the COM runtime system. From now on, the object is not known to COM world.
The final stage of an object's lifecycle is the normal Lisp garbage collection process, which removes the object from memory when there are no more references to it.
The LispWorks COM runtime provides an implementation of the class factory protocol, which will construct COM objects on demand. The class factory implementation supports aggregation when passed an outer unknown pointer.
Class factories are described by objects created with make-factory-entry and must be registered with the COM runtime using register-class-factory-entry. The function start-factories should be called when the application initializes to start all the registered class factories.
When using the Automation API described in 3 Using Automation and 4 Automation Reference Entries, class factories are created and registered automatically by the define-automation-component macro if appropriate.
If the class does not define all the COM methods for the interfaces it implements, then some of those methods may be inherited from superclasses (see 1.9.5 Inheritance). If there is no direct or inherited definition of a method, then a default method that returns E_NOTIMPL
will be provided automatically. The default method also fills all out arguments with null bytes and ignores all in and in-out arguments except those needed to compute the size of arrays for filling out arguments.
A COM object class will inherit COM method implementations from its superclasses if no direct method is defined. However, unlike Lisp methods where an effective method is computed from the set of applicable methods for each generic function, COM methods are always inherited in groups via their defining interface. This is because the interface is used to call a COM method, not the COM object.
Specifically, each method is inherited from the first class in the class precedence list that implements the interface where the method is declared. No attempt is made to search further down the class precedence list if this class is using the unimplemented method definition described in 1.9.4 Unimplemented methods.
The inheritance rules may lead to unexpected results in the case of multiple inheritance. For example, consider the following IDL:
// IDL definition of IFoo import "unknwn.idl"; [ uuid(7D9EB760-E4E5-11D5-BF02-000347024BE1) ] interface IFoo : IUnknown { HRESULT meth1(); HRESULT meth2(); HRESULT meth3(); }
and these three (partial) implementations of the interface i-foo
.
meth2:
(define-com-implementation foo-impl-1 () () (:interfaces i-foo)) (define-com-method meth1 ((this foo-impl-1)) s_ok) (define-com-method meth3 ((this foo-impl-1)) s_ok)
meth2
:
(define-com-implementation foo-impl-2 () () (:interfaces i-foo)) (define-com-method meth2 ((this foo-impl-2)) s_ok)
(define-com-implementation foo-impl-12 (foo-impl-1 foo-impl-2) () (:interfaces i-foo))
In step 3, the class foo-impl-12
implements the interface i-foo
, but inherits all the i-foo
method definitions from foo-impl-1
, which is the first class in the class precedence list that implements that interface. These method definitions include the "unimplemented" definition of meth2
in foo-impl-1
, which hides the definition in the other superclass foo-impl-2
. As a result, when the following form is evaluated with p-foo
created from an instance of foo-impl-12
:
(let ((object (make-instance 'foo-impl-12))) (with-temp-interface (p-foo) (nth-value 1 (query-object-interface foo-impl-12 object 'i-foo)) (with-com-interface (call-p-foo i-foo) p-foo (values (call-p-foo meth1) (call-p-foo meth2) (call-p-foo meth3)))))
the three values are S_OK
, E_NOTIMPL
and S_OK
.
Here is a further extension to the example in 1.9.5.1 An example of multiple inheritance, with an additional interface i-foo-ex
.that inherits from i-foo
as in the following IDL:
[ uuid(7D9EB761-E4E5-11D5-BF02-000347024BE1) ] interface IFooEx : IFoo { HRESULT meth4(); }
This interface has the following additional implementations:
i-foo-ex
:
(define-com-implementation foo-ex-impl-1 () () (:interfaces i-foo-ex)) (define-com-method meth1 ((this foo-ex-impl-1)) s_ok) (define-com-method meth2 ((this foo-ex-impl-1)) s_ok) (define-com-method meth3 ((this foo-ex-impl-1)) s_ok) (define-com-method meth4 ((this foo-ex-impl-1)) s_ok)
(define-com-implementation foo-ex-impl-2 (foo-impl-12 foo-ex-impl-1) () (:interfaces i-foo-ex))
In step 2, the class foo-ex-impl-2
implements the interface i-foo-ex
and is a subclass of foo-ex-impl-1
, which implements i-foo
. When the following form is evaluated with p-foo-ex
created from an instance of foo-ex-impl-2
:
(let ((object (make-instance 'foo-ex-impl-2))) (with-temp-interface (p-foo-ex) (nth-value 1 (query-object-interface foo-ex-impl-2 object 'i-foo-ex)) (with-com-interface (call-p-foo i-foo-ex) p-foo-ex (values (call-p-foo meth1) (call-p-foo meth2) (call-p-foo meth3) (call-p-foo meth4)))))
the four values are S_OK
, E_NOTIMPL
, S_OK
and S_OK
.
Note that, even though foo-ex-impl-2
only explicitly implements i-foo-ex
, the methods meth1
, meth2
and meth3
were declared in its parent interface i-foo
. This means that their definitions (including the "unimplemented" definition of meth2
) are inherited from foo-impl
(via foo-impl-12
), because foo-impl-12
is before foo-ex-impl-2
in the class precedence list of foo-ex-impl-2
. Only meth4
, which is declared in i-foo-ex
, is inherited from foo-ex-impl-1
.
All IDL definitions map onto FLI definitions, mirroring the mapping that midl.exe
does for C/C++. However, IDL provides some additional type information that C/C++ lacks (for instance the string
attribute), so there are some additional conversions that Lisp performs when it can. For a complete example of data conversion, see the file:
(example-edit-file "com/manual/args/args-impl")
The COM API uses the information from the IDL to convert data between FLI types and Lisp types where appropriate for arguments and return values of COM method definitions. In particular:
Each argument is the IDL has a corresponding argument in the
define-com-method form. In addition, each argument has a pass-style which specifies whether additional conversions are performed.
If the pass-style of a parameter is :foreign
, then the value will be exactly what the FLI would provide, i.e. foreign pointers for strings and for all out or in-out parameters (which are always pointers in the IDL).
If the pass-style of a parameter is :lisp
, then the conversions described in the following sections will be done.
If there is a parameter marked with the vararg
attribute then the value must be an array.
string
attribute will be converted to a Lisp string. The string should not be destructively modified by the body.BSTR
will be converted to a Lisp string. The string should not be destructively modified by the body.VARIANT*
will be converted to a Lisp object according to the VT code in the variant (see Automation types, VT codes and their corresponding Lisp types).SAFEARRAY(type)
or SAFEARRAY(type)*
will be converted to a Lisp array. The elements of type type are converted as in Automation types, VT codes and their corresponding Lisp types.VARIANT_BOOL
will be converted to nil
(for zero) or t
(for any other value). Note that a parameter of type BOOL
will be converted to an integer because type libraries provide no way to distinguish this case from the primitive integer type.size_is
attribute will be converted to a temporary Lisp array. The Lisp array might have dynamic extent.size_is
attribute will be converted to a Lisp array of the appropriate size allocated for the dynamic extent of the body forms. After the body has been evaluated, the contents of the array will be copied into the foreign array that the caller has supplied.nil
initially and the body should use setq to set it to the value to be returned.
In the latter case, the value will be converted to a foreign object after the body has been evaluated. The following conversions are done:
string
attribute, a Lisp string will be converted to a foreign string using CoTaskMemAlloc()
.BSTR*
, a Lisp string will be converted to a foreign string using SysAllocString()
.VARIANT*
, the value can be any Lisp value, with the VT code being set according to the Lisp type (see Automation types, VT codes and their corresponding Lisp types). If exact control is required, use the pass-style :foreign
and the function set-variant.SAFEARRAY(type)*
, the value can be either a foreign pointer to an appropriate SAFEARRAY
or a Lisp array. In the latter case, a new SAFEARRAY
is created which contains the elements of the Lisp array converted as in Automation types, VT codes and their corresponding Lisp types.VARIANT_BOOL*
, the value can be a generalized boolean.size_is
attribute will be converted to a Lisp array of the appropriate size allocated for the dynamic extent of the body forms. The initial contents of the Lisp array will be taken from the foreign array which was passed by the caller. After the body has been evaluated, the contents of the Lisp array will be copied back into the foreign array.string
attribute, the parameter will be the converted to a Lisp string. To return a different string, the parameter should be set to another (non eq) Lisp string, which will cause the original foreign string to be freed with CoTaskMemFree()
and a new foreign string allocated with CoTaskMemAlloc()
. The initial string should not be destructively modified by the body.BSTR*
, the parameter will be the converted to a Lisp string. To return a different string, the parameter should be set to another (non eq) Lisp string, which will cause the original foreign string to be freed with SysFreeString()
and a new foreign string allocated with SysAllocString()
.VARIANT*
, the parameter will be converted to a Lisp object (see Automation types, VT codes and their corresponding Lisp types). To return a different value, the parameter should be set to another (non eq) value, which will be placed back into the VARIANT
with the VT code being set according to the Lisp type (see Automation types, VT codes and their corresponding Lisp types). If exact control of the VT code is required, use the pass-style :foreign
and the function set-variant.SAFEARRAY(type)*
, the parameter will be converted to a Lisp array. The elements of type type are converted as in Automation types, VT codes and their corresponding Lisp types. To return a different value, the parameter should be set to another (non eq) value, which can be either a foreign pointer to an appropriate SAFEARRAY
or a Lisp array. In the latter case, a new SAFEARRAY
is created which contains the elements of the Lisp array converted as in Automation types, VT codes and their corresponding Lisp types.VARIANT_BOOL*
, the parameter will be nil
or t
according to the initial value (zero or non zero). To return a different value, set the parameter to a new value, which can be a generalized boolean.
COM/Automation User Guide and Reference Manual - 01 Dec 2021 19:38:38