[Top] | [Contents] | [Index] | [ ? ] |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
In addition to creating distributed applications with Annex E, GNAT is capable of creating and using components that conform to Microsoft's Component Object Model (COM) and Distributed COM (DCOM) on Windows NT.
Microsoft introduced COM as the underlying model for the Object Linking and Embedding 2 (OLE2) services. COM provided OLE2 with the extensibility to meet the growing demand for interoperability of applications that users required and implementation flexibility developers required. It did this by providing a binary level, rather than a source level, object oriented specification for standard interfaces to executable objects allowing them to be used in a class wide fashion and implemented in the language best meeting the object's requirements. It also specified inter and remote process communication, to be provided by the operating system and optionally customized by the developer, and dynamic loading of the executable objects.
Microsoft has since advanced many new technologies built on top of COM including ADO, DirectX, Extended MAPI, and ActiveX (OCX) Controls. COM has remained stable and has only been slightly extended to include newer threading models, better security and more advance distribution. The fundamental principles have remained unchanged and it has proven itself as a powerful model for component based development.
The first use of COM to extend non-Microsoft technologies was a specification for OLE Automation Objects. Using this specification, developers could create custom components by creating COM objects that use a predefined set of interfaces and APIs. Developers could also create custom interfaces of their own and provide the interface specifications in a language neutral format called a Type Library. Client code could then be created in any language to make dynamic use of these binary components.
Microsoft then added a number of other specifications for creating custom components building on the OLE Automation Object specification such as the OLE Control (OCX) specification that creates a uniform way of implementing GUI components.
Technologies based on OLE Automation were later renamed for marketing purposes to ActiveX or ActiveX Controls. The ActiveX name was later expanded to include any COM object including those that do not conform to the OLE Automation specifications.
Distributed COM (DCOM) is the technology that allows COM objects to be transparently distributed across a network. Using settings in the registry of the client and server, when creating a COM object the operating system can create or use supplied proxies and stubs to access the COM object on a remote machine.
COM+ is Microsoft's latest extension to COM technology. The base services of COM+ are COM and DCOM and are identical with only the addition of another threading model. COM+ also designates the integration of Microsoft Transaction Server (MTS) and Microsoft Message Queueing (MSMQ).
In order to use COM or DCOM on Windows NT 4.0, service pack three or later must be installed. The additional services of COM+ (MTS and MSMQ) become available by adding the Microsoft Option Packs or upgrading to Windows 2000.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
COM objects are collections of public interfaces and private data. The internal representation of the COM object is irrelevant as clients of COM objects can only access interfaces. Win32 provides APIs for creating an instance of a COM object and requesting the first interface to the object. Clients are not required to specify the location of the COM objects as the operating system will resolve their location and take the needed steps to create the object, but the location or hints to the location may be specified if needed.
Every COM object must support at least one interface called
IUnknown
. Depending on the particular interfaces supported in addition
to IUnknown
, the object is referred to by different names in Microsoft
documentation. For example if the object supports the interface
IDispatch
it is called an OLE Automation object, if it supports the
various GUI related interfaces and persistence interfaces, it may be
called an OLE, OCX, or ActiveX control.
All interfaces and COM objects are identified using a GUID
, a globally
unique identifier. These IDs are created by the Win32 API function
CoCreateGuid
, or assigned by Microsoft. The GUIDs
referring to
interfaces are called IIDs
, interface identifiers, those referring to
the COM object as CLSIDs
, class IDs. If and object has registered a
textual name, called a ProgID
, for itself in the Windows registry, it is
possible to retrieve the CLSID
using the ProgID
with the
API function CLSIDFromProgID
.
An interface is a record containing an access to a collection of
function accesses. The collection is usually implemented as a record
with access to subprogram objects, and private data. The first parameter
of the function is always an access to the interface it belongs to and
usually returns an HRESULT
, an unsigned integer that returns success or
failure information. An interface is said to derive from another
interface if it contains a superset of another interface's collection of
function accesses in the same order.
The most important interface is called IUnknown
and all other
interfaces derive from it. It contains three functions
QueryInterface
, AddRef
, and Release
. These three
functions provide polymorphism and reference counting. The
QueryInterface
method is called on any interface to request the
COM object to return an access to another supported
interface. AddRef
and Release
provide reference counting
for the interface. An implicit AddRef
exists when an interface is
returned from a function such as QueryInterface
or through API
calls. Release
is called by the client when the interface is no
longer in use reducing the interface's internal reference count. When
the reference count reaches zero for all interfaces to the COM object,
the COM object is required to deallocate itself from memory.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
3.1 The Static Method 3.2 The Dynamic Method
COM objects can be used via two methods, the Static method, by calling methods of its interfaces, or the dynamic method if it supports IDispatch, as most do, by calling the Invoke method of the IDispatch interface using property and method IDs. The second method is the only one available to scripting languages (such as VBScript and JavaScript) and invocation speed is generally four to ten times slower then the first method. GNAT can access COM objects using both methods.
Complete examples of both methods are available with the GNAT examples. `COM_Static.adb' demonstrates the static method and `COM_Dynamic.adb' demonstrates the dynamic method.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Step 1 - Create Binding
The first step in using COM objects from GNAT is to create an Ada binding to the interfaces supported by the COM Object and to the needed GUIDs for the object and the interfaces. The author of the object provides this information in source format, or as a Type Library. There are a number of tools that can read Type Libraries and provide the needed information including COMScope (see http://www.adapower.com/com) and Microsoft's OLE/COM object viewer distributed with the Win32 platform development kit.
An aliased CLSID should be created for the GUID of the object to be created and IIDs for each interface.
For example:
-- CLSID of COM object to create -- {45F9F481-787C-11D3-821C-52544C1913DE} CLSID_GNATCOMClass : aliased CLSID := (16#45F9F481#, 16#787C#, 16#11D3#, (char'Val (16#82#), char'Val (16#1C#), char'Val (16#52#), char'Val (16#54#), char'Val (16#4C#), char'Val (16#19#), char'Val (16#13#), char'Val (16#DE#))); -- IID of the IGNATMessage interface -- {45F9F482-787C-11D3-821C-52544C1913DE} IID_IGNATMessage : aliased IID := (16#45F9F482#, 16#787C#, 16#11D3#, (char'Val (16#82#), char'Val (16#1C#), char'Val (16#52#), char'Val (16#54#), char'Val (16#4C#), char'Val (16#19#), char'Val (16#13#), char'Val (16#DE#))); |
Then create access methods for the members of the interface you are planning to create and any inherited methods. Note that the first parameter of every function should be a pointer back to the interface itself. This provides the C++ style this pointer to the COM methods being called.
For example:
-- Inherited methods from IUnknown type af_IGNATStat_QueryInterface is access function (This : access IGNATStat; riid : in REFIID; ppvObj : access Win32.PVOID) return Win32.Winerror.HRESULT; pragma Convention (StdCall, af_IGNATStat_QueryInterface); type af_IGNATStat_AddRef is access function (This : access IGNATStat) return Win32.ULONG; pragma Convention (StdCall, af_IGNATStat_AddRef); type af_IGNATStat_Release is access function (This : access IGNATStat) return Win32.ULONG; pragma Convention (StdCall, af_IGNATStat_Release); -- Methods of IGNATMessage type af_IGNATStat_Calls is access function (This : access IGNATStat; NumberOfTimes : access Interfaces.C.int) return Win32.Winerror.HRESULT; pragma Convention (StdCall, af_IGNATStat_Calls); -- Return number of times methods of the IGNATMessage interface -- were called |
Then construct the interface and the table of functions used to access the methods.
For example:
type IGNATStat is record lpVtbl : Pointer_To_IGNATStatVtbl; end record; pragma Convention (C_Pass_By_Copy, IGNATStat); type IGNATStatVtbl is record QueryInterface : af_IGNATStat_QueryInterface; AddRef : af_IGNATStat_AddRef; Release : af_IGNATStat_Release; Calls : af_IGNATStat_Calls; end record; pragma Convention (C_Pass_By_Copy, IGNATStatVtbl); |
Step 2 - Initialize COM
Initialize the COM libraries with a call to CoInitialize
:
hr : Win32.Winerror.HRESULT; hr := CoInitialize (System.Null_Address); |
Step 3 - Create Object
Create the object using CoCreateInstance
that will return and access to
the requested interface in the COM object.
For example:
-- The requested interfaces is IID_IGNATMessage hr := CoCreateInstance (CLSID_GNATCOMClass'Unchecked_Access, null, Win32.DWORD (CLSCTX_ALL), IID_IGNATMessage'Unchecked_Access, RetPointer'Unchecked_Access); GnatMessage_Ref := To_Pointer_To_IGNATMessage (RetPointer); |
Step 4 - QueryInterface
Use methods in the returned interface or request alternate interfaces
using QueryInterface
.
For example:
-- To call a method hr := GnatMessage_Ref.lpvtbl.Beep (GnatMessage_Ref); -- To access a different interfaces of the object -- in this case IID_IGNATStat hr := GnatMessage_Ref.lpvtbl.QueryInterface (GnatMessage_Ref, IID_IGNATStat'Unchecked_Access, RetPointer'Unchecked_Access); GnatStat_Ref := To_Pointer_To_IGNATStat (RetPointer); |
Step 5 - Release
When any interface is no longer needed a call to Release
should be made.
For example:
refcount : Win32.ULONG; refcount := GnatStat_Ref.lpvtbl.Release (GnatStat_Ref); |
The reference count that is returned can be bogus and should not be relied upon for any purpose.
Step 6 - Unitialize COM
Shutdown the COM libraries with a call to CoUninitialize
.
CoUninitialize; |
The client code should be compiled with the linker option -lole32
and
-loleaut32
to include the needed linker libraries. See the GNAT example
`com_static.adb' for a complete example of using COM objects.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Step 1 - Initialize COM
Initialize the COM libraries with a call to CoInitialize
:
hr : Win32.Winerror.HRESULT; hr := CoInitialize (System.Null_Address); |
Step 2 - Get CLSID from PROGID
Retrieve the CLSID
of the object using the Program ID (this may also be
done for the static method) and the API call CLSIDFromProgID
.
For example:
Class_ID : aliased CLSID; Hr := CLSIDFromProgID (To_LPCOLESTR (To_C (Wide_String'("GNATCOMLibrary.GNATCOMClass"))'Address), Class_ID'Unchecked_Access); |
Step 3 - Create Object
Create the object and request the IDispatch
interface.
For Example:
hr := CoCreateInstance (Class_ID'Unchecked_Access, null, Win32.DWORD (CLSCTX_ALL), IDispatch_ID'Unchecked_Access, RetPointer'Unchecked_Access); Dispatch_Ref := To_Pointer_To_Idispatch (RetPointer); |
Step 4 - Look up ID and Invoke
Look up the dispatch IDs for methods and properties using the
GetIDsOfNames
method of IDispatch
followed by calls to its invoke
method.
For example:
declare Method_Name : aliased Win32.ObjBase.LPOLESTR := To_LPOLESTR (To_C (Wide_String'("Beep"))'Address); ID : aliased Win32.OleAuto.DISPID; No_Arguments : aliased Win32.OleAuto.DISPPARAMS := (null, null, 0, 0); Result : aliased Win32.OleAuto.Variant; Excep_Info : aliased Win32.OleAuto.EXCEPINFO; Arg_Err : aliased Win32.UINT; begin Put_Line ("Look up ID of method to call"); Hr := Dispatch_Ref.lpvtbl.GetIDsOfNames (Dispatch_Ref, To_LPIID(System.Null_Address), Method_Name'Unchecked_Access, 1, 0, ID'Unchecked_Access); Hr := Dispatch_Ref.lpvtbl.Invoke (Dispatch_Ref, ID, To_LPIID (System.Null_Address), 0, DISPATCH_METHOD, No_Arguments'Unchecked_Access, Result'Unchecked_Access, Excep_Info'Unchecked_Access, Arg_Err'Unchecked_Access); end; |
Step 5 - Release
When the interface is no longer needed a call to Release
should be made.
For example:
refcount : Win32.ULONG; refcount := Dispatch_Ref.lpvtbl.Release (Dispatch_Ref); |
The reference count that is returned can be bogus and should not be relied upon for any purpose.
Step 6 - Unitialize COM
Shutdown the COM libraries with a call to CoUninitialize
.
CoUninitialize; |
The client code should be compiled with the linker option -lole32
and
-loleaut32
to include the needed linker libraries. See the GNAT example
`com_dynamic.adb' for a complete example of using COM objects dynamically.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The first step to building a COM object with GNAT is the construction of a type library. This can be done by compiling a specification created in Microsoft IDL (Interface Definition Language) and using Microsoft's MIDL compiler. The IDL specification must be constructed to contain a type library definition and it is highly recommended that it conform to the ole automation specifications. It is possible to create with GNAT COM objects that use any valid IDL that would work with other languages.
This documentation details an ideal IDL for easy implementation in Ada. See the GNAT example `gnatexample.idl' for the complete example.
Microsoft IDL is a superset of DCE RPC IDL which is very similar in syntax to C. IDL files for COM objects used with GNAT will generally take the form of interface definitions followed by a type library block that defines the composition of each COM object in the library.
IDL files should begin with the following two lines that import needed specifications:
import "unknwn.idl"; import "oaidl.idl"; |
IDL uses brackets to contain attributes and braces to define blocks. For example:
[ object, uuid(45F9F483-787C-11d3-821C-52544C1913DE), helpstring("Statistics on object interface"), oleautomation ] interface IGNATStat : IUnknown { [ helpstring("Return number of times methods were called") ] HRESULT Calls([out] int *NumberOfTimes); } |
Each interface block should have the attributes object, uuid,
helpstring, and oleautomation. If this interface is to also support
dynamic use through IDispatch
for script languages, it should also be
marked with the attribute dual. The Object attribute designates that
this is a COM interface and not an RPC interface. The uuid attribute
provides the univeral unique ID that this interface will be known as and
must be unique for each uuid attribute used. This number can be
generated using tools such as `uuidgen' or `guidgen' from the
Microsoft SDK, or by a call to the Win32 API function
CoCreateGuid
. The helpstring attribute allows type library
browsers and binding generators to display the help string that is
set. OleAutomation tells the MIDL compiler to insure that the interface
conforms to Ole Automation specifications insuring the type library
marshaling can be used for the object (see the section on creating COM
objects with GNAT).
Every interface must derive from either IUnknown
,
IDispatch
or any other child of IUnknown
in order to be
used with COM objects. Objects that are derived from IDispatch
should
include the attribute dual to the interface. Interfaces are defined
using the interface key word followed by the interface name then a colon
and the parent interface. The methods of the interface are then defined
in the interface block between brackets.
Each method of the interface may contain a helpstring attribute in the
same manner as the interface. If the interface is derived from
IDispatch
, you may optionally add the attribute ID to set its dispatch
ID, an ID used for dynamic invocation of the interface. If no ID is
given, MIDL will assign one automatically. For example:
[ id(1), helpstring("Audio Alert") ] HRESULT Beep(); |
Every method must return a type called an HRESULT
that returns the
success or failure of the method. Parameters if each method need to
include an attribute of in or out. Out parameters must be pointer
types. Allowable types are boolean
, unsigned char
,
double
, float
, int
, long
, short
,
BSTR
, CURRENCY
, DATE
, SCODE
, enum
,
IDispatch*
, IUnknown*
, SAFEARRAY
of any OLE
automation type, and pointers to any of the above types. For example:
[ id(2), helpstring("Display Message Box") ] HRESULT MessageBox([in] BSTR Message); |
Following all interfaces definitions, a library block should be created with the attributed uuid, helpstring, and version. With in the library block the first statement should be the following:
importlib("stdole32.tlb"); |
This imports the standard system library of OLE automation types. Following a line is added for each interface to import it in to the library. For example:
interface IGNATMessage; interface IGNATStat; |
Once the interfaces are now part of the library COM objects are defined that use the available interfaces using coclass blocks. Each block should have the attributes uuid and helpstring. With in the block each interface is listed and one interface should be chosen as the default interface. For example:
[ uuid (45F9F481-787C-11d3-821C-52544C1913DE), helpstring("GNAT Example Class") ] coclass GNATCOMClass { [default] interface IGNATMessage; interface IGNATStat; } |
After compiling the IDL file using MIDL, only the .tlb file will be used in creating the COM object with GNAT.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
5.1 The COM object implementation 5.2 InProc container implementation 5.3 LocalServer container implementation
COM objects are contained in DLLs, known as InProc Servers, or in EXEs, known as Local Servers. Containers may house more then one object and use clearly defined methods of exposing the objects to the operating system and publishing their existence in the system registry. The complete example is found in the `COMObject' directory.
The implementation of the COM objects are identical regardless of the container type, but the methods for publishing their existence and exposing of the factory object that creates the COM objects are different.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
5.1.1 Step 1 - The Resource File 5.1.2 Step 2 - Base package 5.1.3 Step 3 - Object Implementation 5.1.4 Step 4 - Class Factory Implementation
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The type library must be embedded in to every COM object in order to let the operating system know how to handle proxying and marshaling of the object when needed. It also provides information to binding generators, visual tools, and scripting languages about the object. This is done by creating and compiling with the GNU `windres' or Microsoft's `rc' followed by `res2coff' a resource script that contains the line:
1 TypeLib "gnatexample.tlb" |
Where `gnatexample.tlb' would be the name of your type library file. It is
also advisable to include a version info resource with a StringFileInfo
block containing the value pair OLESelfRegister
and a blank to indicate
that the COM object container will handle registration of the COM object
and type library when requested to do so from the command line for Local
Servers or using the utility `regsvr32.exe' for InProc Servers. For
example:
VS_VERSION_INFO VERSIONINFO FILEVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1 FILEFLAGSMASK 0x3fL FILEFLAGS 0x0L FILEOS 0x40004L FILETYPE 0x2L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "Your Company Here\0" VALUE "FileDescription", "gnatexample\0" VALUE "FileVersion", "1, 0, 0, 1\0" VALUE "InternalName", "gnatexample\0" VALUE "LegalCopyright", "Copyright©\0" VALUE "OriginalFilename", "gnatexample\0" VALUE "ProductName", "gnatexample\0" VALUE "ProductVersion", "1, 0, 0, 1\0" VALUE "OLESelfRegister", "" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1200 END |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The top level package for implementing the COM object will contain global reference counters, GUID definitions, and types.
There are two main global reference counters needed for COM objects a component count and a server lock count. The component count is used to identify when to shutdown the server and the server lock provides a means to lock the server in memory to enhance performance when objects will be created and deleted frequently.
There are three types of GUID definitions needed, library ID, class IDs, and interface IDs. These are the GUIDs defined in the IDL for the library, coclass'es, and interface's. For example:
-- LIBID of library GNATCOMLibrary -- {45F9F480-787C-11D3-821C-52544C1913DE} LIBID_GNATCOMLibrary : aliased Win32.Objbase.IID := (16#45F9F480#, 16#787C#, 16#11D3#, (char'Val (16#82#), char'Val (16#1C#), char'Val (16#52#), char'Val (16#54#), char'Val (16#4C#), char'Val (16#19#), char'Val (16#13#), char'Val (16#DE#))); |
The types needed for implementation are access functions for every method used by the interfaces of the COM object including those used in the interfaces' parents. Along with an interface type that will be the will be a an access parameter in the first position of every method. The interface type will contain the address of the instance data for the object and a reference count for the interface. This reference count allows for instances of the interface to automatically be deallocated from memory when the interface count reaches zero. For example:
-- Access function types for IGNATStat type af_IGNATStat_Calls is access function (This : access Interface; NumberOfTimes : access Interfaces.C.int) return HRESULT; pragma Convention (StdCall, af_IGNATStat_Calls); type Interface is record Vtbl : System.Address; Ref_Count : aliased Win32.LONG := 1; CoClass : System.Address; end record; pragma Convention (C, Interface); |
Additionally a boolean variable is added to the package indicating if
the object is in an InProc Server or a Local Server, and a procedure
called Can_Close
that is called every time an object is released, so
that in the event the object is contained in a Local Server, if all
objects are released and there are no server locks, the server can close
itself down. The implementation of Can_Close
is:
procedure Can_Close is bResult : Win32.BOOL; begin if Server_Lock_Count = 0 and then Component_Count = 0 and then InProcServer /= True then bResult := Win32.Winuser.PostThreadMessage (Win32.Winbase.GetCurrentThreadId, Win32.Winuser.WM_QUIT, 0, 0); end if; end Can_Close; |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The process of creating the actual COM object begins with creating
implementations of each method of every interface the object will be
using. Even though a COM object may support multiple interfaces and it
is not necessary, although certainly possible if needed, to repeat the
implementations of methods shared by each interface. For example it is
not necessary to define multiple instances of the AddRef
method. Interfaces are views of the object, and not objects themselves.
Each method must conform to the same signature as the access functions
they will be assigned to as they have been defined in the main
package. All objects will include implementations for IUnknown
and those that are dual interfaces, will also support IDispatch
.
Records need to be created for each interface that will contain a table of addresses of each method implementation. For example:
type IGNATStat_Vtbl_Record is record -- IUnknown QueryInterface : Af_IUnknown_QueryInterface := IUnknown_QueryInterface'Access; AddRef : Af_IUnknown_AddRef := IUnknown_AddRef'Access; Release : Af_IUnknown_Release := IUnknown_Release'Access; -- IGNATMessage Calls : Af_IGNATStat_Calls := IGNATStat_Calls'Access; end record; pragma Convention (C, IGNATStat_Vtbl_Record); type IGNATStat_Vtbl_Pointer is access all IGNATMessage_Vtbl_Record; |
An instance of each record must be present in memory so that the interface can be passed to clients of the COM object. This is done by creating an aliased object of the record type.
IGNATStat_Vtbl : aliased IGNATStat_Vtbl_Record; |
A record representing the instance data for the object is also created and a function to create a new COM object with that data is also needed. This record also contains a reference count for the COM object. Every time any interface reference is adjusted the object's reference count is also adjusted. When there are no interfaces in use the reference count will reach none and the instance data for the COM object can then be freed.
-- Internal data for the COM object. Notice that there is -- no Convention pragma on this type. This type is an Ada type. type GNATCOMClass_Object is record Ref_Count : aliased Win32.LONG := 1; Data : Controlled_Information; Count : Integer := 0; end record; type GNATCOMClass_Pointer is access all GNATCOMClass_Object; function New_Object return Interface_Pointer; |
The function that creates the COM object returns an access to the
objects IUnknown
interface to be used bye the objects class
factory. Typically the function will be like this one:
function New_Object return Interface_Pointer is New_Interface : aliased Interface_Pointer; New_Object : aliased GNATCOMClass_Pointer; begin New_Interface := new Interface; New_Object := new GNATCOMClass_Object; New_Interface.CoClass := New_Object.all'Address; New_Interface.Vtbl := IGNATMessage_Vtbl'Address; return New_Interface; end New_Object; |
COM objects that will support dual interfaces will most likely want to
take advantage of the Win32 APIs that implement IDispatch
using the
objects type library. In order for these APIs to function, they need
access to the type library. A sound way of implementing this is using a
controlled object that loads the type library when the object is created
and frees it when the object is destroyed. For example:
-- Controlled type for handling information that should be -- processed when the object is created or destroyed. type Controlled_Information is new Ada.Finalization.Controlled with record Type_Information : aliased Win32.OleAuto.LPTYPEINFO := null; end record; procedure Initialize (This : in out Controlled_Information); procedure Finalize (This : in out Controlled_Information); procedure Adjust (This : in out Controlled_Information); |
The implementation of this example uses the Win32 API LoadRegTypeLib
to
load the type library interface and then uses Adjust and Finalize to
handle the reference counting issues related to the
interface. Implementation of the IDispatch
methods then just call the
appropriate Win32 APIs passing the type information interface contained
in the controlled object.
Every object will need to implement the IUnknown
functions. The
Addref
and Release
methods will be the same for each object.
--------------------- -- IUnknown_AddRef -- --------------------- function IUnknown_AddRef (This : access Interface) return Win32.ULONG is lResult : Win32.Long; Object : GNATCOMClass_Pointer := To_Object_Pointer (This.CoClass); begin -- InterlockedIncrement is a thread protected Win32 API function -- to increment a long -- Interface reference increment lResult := Win32.Winbase.InterlockedIncrement (This.Ref_Count'Access); -- Object reference increment lResult := Win32.Winbase.InterlockedIncrement (Object.Ref_Count'Access); return Win32.ULONG (This.Ref_Count); end IUnknown_AddRef; ---------------------- -- IUnknown_Release -- ---------------------- function IUnknown_Release (This : access Interface) return Win32.ULONG is use type Interfaces.C.Long; lResult : Win32.Long; Object : GNATCOMClass_Pointer := To_Object_Pointer (This.CoClass); begin -- InterlockedDecrement is a thread protected Win32 API function -- to decrement a long lResult := Win32.Winbase.InterlockedDecrement (Object.Ref_Count'Access); if Win32.Winbase.InterlockedDecrement (This.Ref_Count'Access) /= 0 then return Win32.ULONG (This.Ref_Count); else -- Last reference to Interface so free it Free (This.all'Address); return 0; end if; end IUnknown_Release; |
The free procedure deallocates the interface instance from memory since the reference counter has reached zero. With in the free procedure the reference count of the object is also decremented. If it reaches zero, the the instance data is also freed destroying the COM object as it is no longer in use.
The implementation of QueryInterface is similar from one object to the
next, but needs to be customized to support the interfaces exposed by
the COM object. In the example below, an elsif for each supported
interface needs to be added after the support for IUnknown
. The same
pattern is followed, but the assignment of the table of access to
functions is changed to the appropriate one for the requested interface.
function IUnknown_QueryInterface (This : access Interface; riid : in Win32.Objbase.REFIID; ppvObject : access Win32.PVOID) return HRESULT is use type Win32.Rpcdce.Guid; New_Interface : aliased Interface_Pointer; lResult : Win32.LONG; Result : Win32.ULONG; Object : GNATCOMClass_Pointer := To_Object_Pointer (This.CoClass); begin if riid.all = IID_IUnknown then -- Since IUnknown is the parent of every interface, just -- return back a pointer to this interface with an additional -- reference count and the client will use it as if it was -- IUnknown ppvObject.all := This.all'Address; Result := IUnknown_AddRef (This); elsif Riid.all = IID_IDispatch then New_Interface := new Interface; New_Interface.CoClass := This.CoClass; lResult := Win32.Winbase.InterlockedIncrement (Object.Ref_Count'Access); New_Interface.Vtbl := IGNATMessage_Vtbl'Address; ppvObject.all := New_Interface.all'Address; elsif Riid.all = IID_IGNATMessage then New_Interface := new Interface; New_Interface.CoClass := This.CoClass; lResult := Win32.Winbase.InterlockedIncrement (Object.Ref_Count'Access); New_Interface.Vtbl := IGNATMessage_Vtbl'Address; ppvObject.all := New_Interface.all'Address; elsif Riid.all = IID_IGNATStat then New_Interface := new Interface; New_Interface.CoClass := This.CoClass; lResult := Win32.Winbase.InterlockedIncrement (Object.Ref_Count'Access); New_Interface.Vtbl := IGNATStat_Vtbl'Address; ppvObject.all := New_Interface.all'Address; else ppvObject.all := System.Null_Address; return Win32.Winerror.E_NOINTERFACE; end if; return Win32.Winerror.S_OK; end Iunknown_QueryInterface; |
Once IUnknown
methods have been implemented the only additional work is
to implement each interface method. In order to have access to the
object instance data, a conversion function is used on the CoClass
member of the interface type like this one:
function To_Object_Pointer is new Ada.Unchecked_Conversion (System.Address, GNATCOMClass_Pointer); |
A simple method implementation may look something like this:
function IGNATStat_Calls (This : access Interface; NumberOfTimes : access Interfaces.C.int) return HRESULT is Object : GNATCOMClass_Pointer := To_Object_Pointer (This.CoClass); begin Message_Box("Calls", Integer'Image (Object.Count)); NumberOfTimes.all := Interfaces.C.int (Object.Count); return Win32.Winerror.S_OK; end IGNATStat_Calls; |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The implementation of the class factory for any COM object will be
identical to the implementation found in the example files
`gnatexample-factory.ad?'. The only difference will be in the function
IClassFactory_CreateInstance
where the function used to create the COM
object will be the one created in Step 3.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Step 1 - The export file
Every InProc container (DLL housing a COM object), needs to export four functions in its export (.def) file.
The def file must start exactly like this, but may contain additional exports following these four:
EXPORTS DllGetClassObject=DllGetClassObject@12 @2 DllCanUnloadNow=DllCanUnloadNow@0 @3 DllRegisterServer=DllRegisterServer@0 @4 DllUnregisterServer=DllUnregisterServer@0 @5 |
Step 2 - The dll implementation
In addition to the four exports mentioned every DLL must contain an
exported DllMain
procedure. The implementation for the
DllMain
will be the same for all objects, unless additional
functionality is needed on the containers startup or shutdown:
function DllMain (hinstDLL : Win32.Windef.HINSTANCE; fdwReason : Win32.DWORD; lpvReserved : Win32.LPVOID) return Win32.BOOL is procedure Adainit; pragma Import (C, Adainit); procedure Adafinal; pragma Import (C, Adafinal); DLL_PROCESS_DETACH : constant := 0; DLL_PROCESS_ATTACH : constant := 1; begin case fdwReason is when DLL_PROCESS_ATTACH => AdaInit; hInstance := hinstDll; return 1; when DLL_PROCESS_DETACH => AdaFinal; return 1; when others => return 1; end case; end DllMain; |
The Instance handle assigned to the Dll is save in a variable hInstance
for use by the exported COM Dll functions.
The DllGetClassObject
is used by COM to request from the Dll a pointer
to the factory object for a particular type of COM object. The
implementation needs to check that it does in fact support creation of
those objects and then needs to return a pointer to the class factory
that will produces objects of the requested type. See the implementation
in the `gnatexample-dll.adb' file for an example.
The DllCanUnloadNow
function is called periodically by the operating
system to determine if the Dll can be unloaded or needs to remain in
memory. The Dll needs to remain in memory and returns S_FALSE
if there
are any server locks or if there are any created objects still being
used. For example:
function DllCanUnloadNow return HRESULT is use type Interfaces.C.Long; begin if Server_Lock_Count = 0 and then Component_Count = 0 then return Win32.Winerror.S_OK; else return Win32.Winerror.S_FALSE; end if; end DllCanUnloadNow; |
The functions DllRegisterServer
and DllUnregisterServer
are called by the application regsvr32.exe
(or an install
program) to request that the Dll add or remove all the relevant COM
entries of the registry for its supported objects and type libraries.
The first step in DllRegisterServer
is to register the type
libraries. This is done by first obtaining the full path file name of
the Dll. Using this path, the Win32 APIs LoadTypeLib
and
RegisterTypeLib
are used to load and register the type
library. Then the following entries are registered to the NT registry
under HKEY_CLASSES_ROOT
(using a simple helper procedure in the
example file):
Register ("CLSID\" & CLSID, "", Library_Description); -- CLSID = CLASS ID of the COM Object (CoClass GUID) -- Library_Description = Description of object to appear in COM -- browsers Register ("CLSID\" & CLSID, "AppID", CLSID); -- This entry links together the COM information (under \CLSID) -- with the remoting information for DCOM (under \APPID) Register ("CLSID\" & CLSID & "\InProcServer32", "", C.To_Ada (DllPath)); -- Path to InProcServer 32 bit implementation of the COM object -- (a DLL) Register ("CLSID\" & CLSID & "\InProcServer32", "ThreadingModel", "Apartment"); -- Tells the operating system that this object is aware of -- threading issues, but the OS should synchronize access to -- the object. -- This does not affect the type of clients that can use this -- object,ie. a client usingCoInitializeEx(CLSCTX_MULTI_THREADED) -- can create this object and the OS will handle synchronization -- of calls made by each thread trying to access the object. Register ("CLSID\" & CLSID & "\ProgID", "", Object_Name & "." & Library_Version); -- Links the CLSID to the PROGID -- The ProgID is the human readable name of the COM object used -- by VB and other languages instead of the GUID. -- The PROGID also contains a dot followed by the version number. Register ("CLSID\" & CLSID & "\VersionIndependentProgID", "", Object_Name); -- Links the CLSID to the PROGID with out the version number Register (Object_Name, "", Library_Description); -- Creates the version independant PROGID entry Register (Object_Name & "\CLSID", "", CLSID); -- Links the PROGID to the CLSID of the object Register (Object_Name & "\CurVer", "", Object_Name & "." & Library_Version); -- Links the version independant PROGID to the version -- dependant PROGID Register (Object_Name & "." & Library_Version, "", Object_Name); -- Creates the PROGID entry Register (Object_Name & "." & Library_Version & "\CLSID", "", CLSID); -- Links the PROGID to the CLSID Register ("AppID\" & CLSID, "", Library_Description); -- Creates the AppID entry where remoting infromation about the -- COM object is stored. Register ("AppID\" & CLSID, "DllSurrogate", ""); -- Tells the opearting system that if a request is made to run -- the COM object as a DCOM object or as a separate process -- (LocalServer) to provide this for the dll using a built in NT -- application DLLHOST.EXE |
The DllUnregisterServer
function first removes the registered type
library using the Win32 API function UnregisterTypeLib
, it then
removes all the entries in the NT registery created by
DllRegisterServer
.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
LocalServers (Exe's containing COM objects) must handle three command
line parameters, /Embedding
, /RegServer
, and /UnregServer
.
The LocalServer is called with /Embedding
when the operating system
needs to create COM objects that are contained in the server. It may
also be used form the command line to pre-load the container in memory
so that future requests of clients to create COM objects will be
quicker. When embedding, the Win32 API CoInitialize
is called to load
the COM environment. Then a class factory for each COM object type that
can be created is registered with the operating system using the
CoRegisterClassObject
API call. In order for the server to wait for the
COM objects to be used and to later shutdown it starts up a Win32
message loop. Upon receiving notice that the server is no longer needed
(all the COM objects it created were destroyed and there are no server
locks), the messag loop exits and the API function
CoUnregisterClassObject
is called for each factory that was
registered. The application then completes after calling the
CoUnitialize
API function to shutdown the COM environment.
When started with /RegServer
or /UnregServer
then same
process used in the InProc implementation is used to register or
unregister the type libraries and NT registry entries.
See the `gnatexample-exe.adb' file for a complete example.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Using COM objects in a distributed fashion is more of a configuration issue then a programming issue. For both the client code and the server code there are no changes that need to be done. Remote machines need to have their registry updated to include the various COM entries and the type library must be registered. A simple application that does this is in `remote.adb'.
It registers the type library and inserts the same registry entries as
the COM containers, but does not insert a server type and uses the
key/value pair RemoteServerName/ServerName to specify the IP address or
name of the remote server. It is possible to override the specified
server name by using CoCreateInstanceEx
instead of
CoCreateInstance
in the client code.
When using DCOM, you must also configure permissions to remote machines to access the COM objects. This is done using the Windows NT utility dcomcnfg.exe.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Depending on the type of COM object and the client type there are different techniques used to debug a COM object.
The easiest form of COM object to debug is the Local Server (EXE) type.
The COM object is compiled with the -g option to gnatmake and run in gdb
as any other application. Break points can then be set and when the client
compiled with gdb compatible debugging information or not is run the break
points will be hit in the local server. It is also possible to use the gdb
command attach
to connect to an already running local server.
The program ID that is used with the attach
command can be located
using the task manager.
Since Inproc Server COM objects are DLLs, they run in the same address space as the executable using them. If the client of the COM object is compiled with gdb debug information it is possible to debug normally and step through the client directly in to the COM object. Break points though can not be set in the COM object until after the object is created in the client code.
If the client does not contain gdb debug information (such as when the
client is a Microsoft Visual C++ or Visual Basic client), an alternate
techniques must used. The executable of the client can either be run
with in gdb or attached to using the gdb attach command. Since it is not
possible to set a break point in the client code it may not be possible
to easily determine when the COM object has been created in order to
stop execution of the client application and to set break points. One
technique to assist in determining this point is to place a MessageBox
or similar blocking function in the objects creation code. Once the
message box displays a ctrl-c sequence can be sent to gdb to pause
execution of the application and break points can then be set in to COM
object. A continue
statement can then be sent to gdb and the
dialog box's OK button can be pushed to continue program execution and
debugging.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Jump to: | A C D G H I L M O P Q R S T U W |
---|
Jump to: | A C D G H I L M O P Q R S T U W |
---|
[Top] | [Contents] | [Index] | [ ? ] |
[Top] | [Contents] | [Index] | [ ? ] |
1. Brief Introduction to COM, Distributed COM, ActiveX, and COM+
2. What are COM Objects and Interfaces?
3. Using COM objects with GNAT
4. Creating Type Libraries for COM objects
5. Creating COM objects with GNAT
6. Debugging COM objects with gdb
Index
[Top] | [Contents] | [Index] | [ ? ] |
Button | Name | Go to | From 1.2.3 go to |
---|---|---|---|
[ < ] | Back | previous section in reading order | 1.2.2 |
[ > ] | Forward | next section in reading order | 1.2.4 |
[ << ] | FastBack | previous or up-and-previous section | 1.1 |
[ Up ] | Up | up section | 1.2 |
[ >> ] | FastForward | next or up-and-next section | 1.3 |
[Top] | Top | cover (top) of document | |
[Contents] | Contents | table of contents | |
[Index] | Index | concept index | |
[ ? ] | About | this page |