All Manuals > COM/Automation User Guide and Reference Manual > 1 Using COM

1.9 Implementing COM interfaces in Lisp

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:

The relationship between an Lisp object and its COM interface pointers

1.9.1 Steps required to implement COM interfaces

To implement a COM interface in Lisp, you need the following:

  1. Some COM interface definitions, converted to Lisp as specified in 1.2.2 Generating FLI definitions from COM definitions.
  2. A COM object class defined with the macro define-com-implementation, specifying the interface(s) to implement.
  3. Implementations of the methods using define-com-method.
  4. If the objects are to be created by another process, a description of the class factories created with make-factory-entry and registered with register-class-factory-entry.
  5. Initialization code to call co-initialize. It should also call start-factories in a thread that will be processing Windows messages (for instance a CAPI thread) if you have registered class factories.

1.9.2 The lifecycle of a COM object

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.

  1. CLOS object initialization.

    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.

  2. COM initialization.

    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.

  3. COM usage.

    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.

  4. COM destruction.

    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.

  5. Garbage collection.

    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.

1.9.3 Class factories

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.

1.9.4 Unimplemented methods

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.

1.9.5 Inheritance

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.

1.9.5.1 An example of multiple inheritance

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.

  1. An implementation with no definition of 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)
    
  2. An implementation with no definition except meth2:
    (define-com-implementation foo-impl-2 ()
      ()
      (:interfaces i-foo))
     
    (define-com-method meth2 ((this foo-impl-2))
      s_ok)
    
  3. A combined implementation, inheriting from steps 1 and 2.
    (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.

1.9.5.2 A second example of multiple inheritance

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:

  1. An implementation defining all the methods in 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)
    
  2. A combined implementation, inheriting from step 3 from 1.9.5.1 An example of multiple inheritance and step 1 above.
    (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.

1.9.6 Data conversion in define-com-method

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")
1.9.6.1 FLI types

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.

1.9.6.2 In parameters

For in parameters:

1.9.6.3 Out parameters

For out parameters:

In the latter case, the value will be converted to a foreign object after the body has been evaluated. The following conversions are done:

1.9.6.4 In-out parameters

For in-out parameters:


COM/Automation User Guide and Reference Manual - 01 Dec 2021 19:38:38