by Bryan Morgan
Perhaps the most heavily discussed feature of Microsoft's Internet strategy is a group of technologies collectively referred to as ActiveX. Microsoft introduced this related set of existing and future technologies to allow developers and users to extend their current tools for use across the Internet. For example, although developers once wrote OLE controls for use in building Windows applications, with a few changes these controls can now be downloaded from a Web server and can be displayed and manipulated within a Web browser. Like most other technologies, ActiveX has a well-documented set of advantages and disadvantages. Many developers who have examined ActiveX have concluded that its disadvantages outweigh its advantages, and therefore they have sworn off using it. Other developers with a lot of time and money already invested in ActiveX (through COM and OLE2) plan to make extensive use of its components.
As has been mentioned throughout this book, Visual J++ and the Microsoft Java Virtual Machine (JVM) allow developers to call ActiveX objects (and, in a larger sense, any COM object). COM objects can even be written in Java, compiled by Visual J++, and then called and used within any other Windows application. This versatility opens an entirely new world to Java programmers and raises the Java language to first-class status in the Windows programming world.
The purpose of this chapter is to provide an overview of both COM in general and the capabilities and tools provided with Visual J++. This chapter thoroughly introduces the component object model (COM) and explains its standard capabilities and uses. (The rest of this section of the book discusses Java and the COM in great detail.) In addition, the relative advantages and disadvantages of the COM (and therefore ActiveX) are discussed in detail. (Determining whether the advantages of the COM outweigh the disadvantages is left to the reader.)
Although the classes and tools that come with Visual J++ insulate the user from delving into the COM "plumbing" beneath the surface, good programmers usually understand the foundations of technologies they are working with. Experienced developers know that although GUI environments and class libraries shelter the developer from having to deal with many low-level technical issues, any large development project will invariably require them to get under the hood once in awhile. By the end of this chapter, you will know enough about the COM to visualize what is going on "behind the scenes" in the Microsoft JVM.
COM provides a mechanism for software objects, written in any language and running on any platform, to intercommunicate and exchange information. However, because of the heavy marketing behind Microsoft's OLE technologies, until recently most developers almost completely ignored COM.
COM was passed over because it offers no concrete "features" that developers can use within applications, such as GUI components or structured storage mechanisms. OLE 2.0, meanwhile, introduced a number of capabilities that Windows application developers could reuse immediately. On the other hand, COM only defines the hows and whys of how objects can communicate and be used together. In no way does COM actually specify what these objects are supposed to do. (That task is left to the developer!)
To borrow from object-oriented terminology, COM is the superclass, or parent, of OLE. OLE defines a large number of objects that possess certain behaviors such as structured storage, drag-and-drop of objects, and monikers. The underlying COM controls the way in which these objects are created and used within an application.
COM is both parts: specification and implementation. The COM specification describes, in detail, how component objects are to be created and referenced as well as what characteristics these objects should have. This specification was designed to apply generally to any object-oriented system running on any platform. (In other words, the specification is not a Windows-only specification.) Therefore, if you were an enterprising sort of developer with a lot of time and resources available, Microsoft would be perfectly happy to provide you with assistance in turning the COM specification into an actual implementation for, say, the Apple Macintosh.
The COM implementation is only available for the Windows platform. To verify that your Windows system is indeed "COM aware," do a quick search for the COMPOBJ.DLL file. Within this DLL is a group of functions commonly referred to as the COM Applications Programming Interface, or the COM API. In general, each of these functions begins with the letters Co (such as CoCreateInstance(), CoInitialize(), or CoRegisterClassObject()). Keep in mind, however, that at the current time, this implementation of COM exists only for the Windows platform. Heavy work is underway to introduce versions for the UNIX and Macintosh platforms by early 1997.
COM objects are registered with the system and are given a globally
unique identifier (GUID, or CLSID as it is commonly called). Microsoft
chose to use the distributed computing environment (DCE) UUID
(universally unique ID) algorithm to generate IDs for COM objects.
This algorithm uses a combination of a machine's network address,
the current date, and the current time to generate a 16-byte identification
number. (For developers who are still paranoid about the "uniqueness"
of this ID, Microsoft maintains a registry of official CLSIDs
and will even generate one for you.) This ID is a 128-bit number
and is guaranteed to be unique. Identifying objects in this way
frees the programmer from hard-coding library and function ordinals
into the source code. Later in this chapter, you will see how
to use the JAVAREG tool to generate these CLSIDs for COM objects
created in Java.
| Note |
If you are currently using a computer with a Windows operating system, you can examine the Windows Registry to determine the CLSIDs of COM objects. Under Windows 95, run the REGEDIT.EXE program. Under Windows NT, run REGEDT32.EXE. Doing a search for CLSID will locate all of the currently registered objects on your system (or under Windows 95, locate the MY COMPUTER\HKEY_CLASSES_ROOT\CLSID key). As an example, the ActiveMovie control object's CLSID is {05589FA1-C356-11CE-BF01-00AA0055595A}. A Microsoft Word 6 object's CLSID is {00020901-0000-0000-C000-000000000046}. |
Programmers familiar with dynamic link libraries (DLLs) in Windows should recall that a function's ordinal number within both a library and the library name is indeed hard-coded into the calling application. If the library is ever modified or if a DLL with a duplicate name is copied onto your system, your application will no longer work correctly. COM provides a layer of indirection and additional capabilities to ensure that this problem never occurs. COM objects are referenced by their CLSID numbers and can be queried to determine what features they support. If an older version of a COM object exists on the system (or the COM object does not exist at all!), the application can exit gracefully or prompt the user without crashing. In addition, through the use of distributed COM (DCOM) that is currently available under Windows NT 4.0, COM objects can be located on remote machines as well. Using DCOM, COM objects are communicated with via a remote procedure call (RPC) mechanism. (DCOM is discussed in more detail in Chapter 24, "Distributed Component Object Model.")
To understand how COM objects interoperate, you first need to
understand the concept of an interface. As is the case
with the Java language, an interface in COM is simply a related
group of methods. Interfaces can inherit from other interfaces
to produce added capabilities while reusing existing code. All
COM objects must implement at least one interface: IUnknown.
| Note |
By convention, all COM interface names start with the letter I, for example, IUnknown, IClassFactory, and IOleObject. This naming convention allows developers to quickly find interfaces within source code. |
The IUnknown interface is discussed in more detail later in the chapter. For now, you just need to know that this interface defines three methods (QueryInterface(), AddRef(), and Release()) that must be implemented by every COM object. These methods are responsible for the following:
A COM object can be accessed only via the set of interfaces that it implements. Because ActiveX controls are COM objects, all ActiveX controls, at a minimum, implement the IUnknown interface. To get at other capabilities supported by the ActiveX control, the IUnknown.QueryInterface() method is called to retrieve other interfaces within the control. Consequently, no method within an object can ever be called without first retrieving a proper interface from the object that contains the method. Conversely, objects can be queried to determine if they support certain interfaces. (As is the case with Java, if a COM object implements an interface, it agrees to implement all the methods within that interface.)
Once an interface to an object has been obtained, the interface's methods can be called like any other object method. Each COM object is responsible for tracking the number of users it currently has. As each user of the object finishes using that object's interfaces, the user count is decremented. When the count goes to zero, the COM object is then responsible for freeing itself from memory. This method of memory management removes the burden from the user of the object and forces the object to be responsible for creating and freeing itself from memory. If two objects are dependent on each other (and therefore their reference count never goes to zero), each object will terminate itself when the application using these objects shuts down.
Once again, it is imperative to stress that using COM does not necessarily imply the use of OLE or ActiveX within an application. Because many users associate COM with inserting spreadsheets into word processing documents (or other common OLE tasks such as Automation), many developers have become confused over what COM actually is. In short, COM in no way specifies any user interface standards or operating system components. It is simply a specification that defines how objects are registered on a system and how these objects can be incorporated together to build applications. Each COM object is referenced through the use of interfaces and can be written in virtually any programming language. OLE/ActiveX derives from COM in that each ActiveX technology is essentially an implementation of a set of COM interfaces. For instance, OLE controls implement the IUnknown interface as well as a set of interfaces that allow the object to be added to a toolbar or manipulated at design time. As explained in the next section, the Java language is ideally suited for the creation and use of COM objects.
When Java was initially introduced in 1995, many pundits predicted the imminent fall of Microsoft from its position of market dominance. Others pointed to Microsoft's reluctance to license Java as a sign that Microsoft was indeed threatened by the technology and hoped to simply kill it by refusing to support it. Perhaps the first sign that Java would indeed be successful was when Microsoft agreed to add Java support to the Internet Explorer browser. Additional negotiations eventually led Microsoft to agree to provide the Windows platform reference implementation of the JVM.
Although one can argue that the "networked" nature of
Java (and its related component architecture, Java Beans) threatens
companies such as Microsoft, which rely largely on the success
of the desktop computing market, Microsoft is interested in Java
for reasons other than its ability to run code within a Web browser.
Microsoft's interest is primarily a result of the synergy between
the Java programming language and COM. Because of the excellent
fit between Java and COM, Microsoft is presenting Java as a top-notch
Windows development language.
| Note |
Although Internet Explorer 3.0 does support the use of Java applets, Microsoft is clearly pushing its own ActiveX controls as the tools for supplying active Web content. Because ActiveX controls have complete access to the underlying operating system, they do have some advantages over Java applets. However, these access privileges bring up a number of security and portability concerns that worry many developers. This section of the book does not attempt to answer or defend any of the praise/criticism directed at COM or ActiveX. Instead, the capabilities of Visual J++ will be presented in an objective manner so that you can decide which product is most suitable for your development project. |
The two biggest features that allow Java to be used almost seamlessly with COM are
As mentioned earlier, COM objects are defined by which interfaces they implement. In fact, each object is accessed using these interfaces. Although other programming languages are forced to use more difficult pointer notation to access interface tables within an object, the Java language supports the notion of interfaces directly. The following example illustrates the creation of a new COM object in C++ versus that same object being created in Java.
In C++:
ISurfaceArea pArea;
CoCreateInstance(CLSID_SURFACEAREA, NULL, CLSCTX_SERVER, IID_ISurfaceArea,
(void**)&pArea );
x=pArea->Calculate();
.
.
.
In Java:
ISurfaceArea Area = (ISurfaceArea)new SurfaceArea(); x=Area.Calculate();
In the case of the C++ example, the COM API function CoCreateInstance() is used to retrieve the ISurfaceArea interface. This interface is treated as a pointer to another pointer within C++. This syntax becomes fairly cumbersome over the course of an entire program and results in COM API functions being called to instantiate each COM interface.
For programmers who have ever written a line of Java code or a line of COM code, the Java statement should be somewhat exciting. For Java programmers, the standard new operator is used to call the SurfaceArea() constructor. This line of code is completely indistinguishable from any other Java construction statement. No special syntax or function calls are required thanks to the magic of the Microsoft JVM and the Visual J++ Type Library Wizard (discussed later in this chapter). For COM programmers, this method of retrieving a COM interface is much cleaner and more elegant than traditional methods in C or C++.
Programmers who write in C/C++ must be careful to free all memory that has been allocated when it is no longer used. If memory is not deallocated appropriately, the program could eventually exhaust all of the system memory resources. Meanwhile, recall that programmers can use a variety of garbage collection strategies to dynamically free Java objects at runtime once these objects are no longer being used. This memory management scheme removes the burden from the individual programmer and places the burden on the system garbage collector.
Coincidentally, COM objects are responsible for freeing themselves from memory when they are no longer being used. This similarity enables the Java garbage collector to monitor an object's usage status. When an object is no longer in use, the COM object will free itself from memory as it is supposed to. The difference between Java and C++, for instance, is that this reference counting is completely transparent to the Java programmer. Although the C++ programmer is forced to call the AddRef and Release methods in the IUnknown interface, the Java programmer must simply call the object's constructor. The Microsoft JVM will release the object at the correct time thanks to the Java runtime garbage collection capabilities.
Table 16.1 compares COM and Java, pointing out relevant similarities
and differences.
| COM | Java |
| Uses interfaces to define object protocols. | Uses interfaces to define object protocols. |
| Binary objects stored in files that can be accessed across a network. | Bytecode objects stored in files that can be accessed across a network. |
| Each object must support the IUnknown interface at a minimum. | Objects may implement 0 or more interfaces. |
| Objects can be written in any language. | Objects can be written in the Java language, accessed via native methods, or can be referenced via COM. |
| Reference counting is used for runtime memory management. | Garbage collection is used for runtime memory management. |
| Interfaces are grouped within classes. | Interfaces are grouped within classes. |
| Objects are located and created dynamically at runtime. | Objects are located and created dynamically at runtime. |
| Objects can be queried to retrieve implemented interfaces. | Objects can be typecast to retrieve implemented interfaces. |
| Currently platform dependent (Windows only). | Platform independent (requires existence of JVM). |
| Type library files contain class and interface definitions. | Class files contain class and interface definitions. |
As you can see from examining Table 16.1, Java and COM have many things in common. In fact, COM objects appear to be identical to Java objects within Java source code. Although a number of additional events must be accomplished outside of the source code (such as registering a COM object, importing a type library), the designers of the Microsoft JVM and Visual J++ deserve some praise for providing Java developers with a truly clean interface to the COM.
In addition to sharing some key features with Java, COM is also an object-oriented framework that supports object reuse. You will recall (from Chapter 3 "Object-Oriented Programming with Java") that three key components of an object-oriented programming language are
COM fully supports encapsulation through the use of interfaces. All functionality provided by a class is accessed through one of the class's implemented interfaces. All data variables within a class are not visible to the calling application. Instead, this data must be accessed through the use of interface access methods. (You may also remember from Chapter 3that this method was the preferred means of data access in object-oriented design.) The interface access method of data hiding is the equivalent of making all data members private to Java and C++ programmers.
COM objects are also polymorphic. For example, assume that two or more interfaces within a class are derived from a single interface, IUnknown. (In fact, all interfaces in COM are required to derive from IUnknown!) Once this derivation has been set up, all member functions available in IUnknown are also members of all of its derived classes. A parallel can also be drawn for different objects that implement duplicate sets of interfaces. All of these objects are then polymorphic; that is, each object will contain a known set of member methods.
COM has received some criticism from object-oriented purists over the years for not fully supporting inheritance. Although it is true that inside an object a COM interface can inherit from another interface using language inheritance (such as is supported in C++ and Java), binary COM objects cannot inherit from one another. In other words, COM does not support object inheritance. (Other object models such as CORBA and OpenDoc do support object inheritance.)
Microsoft's reasoning behind this decision is perfectly credible. In its view, COM objects are for use by end users and developers. Because a COM object could have been developed by vendor X and then distributed as part of the operating system, an extremely unstable environment could result when vendor Y decides to override one of the methods within vendor X's object. Unless vendor Y has specific knowledge of the internal workings of vendor X's object (through source code or personal contact), he or she can never be completely sure that overriding vendor X's functionality will cause no problems. On a large-scale basis when hundreds or even thousands of objects are installed on a server operating system, such uncertain behavior could quickly lead to an unreliable environment. In summary, although the designers of COM fully support the notion of inheritance while building an object (using languages such as C++ or Java), their design decision was that the end user of these objects is not really concerned with inheriting from it or overriding certain aspects of the object.
COM objects do support the concept of object/code reuse through the notion of aggregation. This topic is discussed later in the chapter. The following section discusses some of the terminology associated with COM that may be unfamiliar to novice COM/Java developers.
All Java programs currently consist of one or more class files
(denoted by the .class file extension). In the near future,
programmers will also be able to create binary executable files
(using later releases of Visual J++ and other compilers). Java
programs are divided into two primary categories: applets
and applications.
| Note |
By now, you should be very familiar with the differences between these two types of Java programs. For more information, see Chapter 8 "Developing Applets and Applications." |
The Java applet runs within a Web browser and is therefore contained by that Web browser. The applet serves its contents to the browser and is displayed to the user via some hidden hooks implemented within the browser. Java applications are similar to applets except for the fact that they run without the aid of a browser. If applets are servers within a container object, then Java applications can be thought of as standalone servers.
The COM uses similar terminology to describe different types of
objects. These objects are outlined in Table 16.2.
| Name | Description |
| Container | An object that can contain other COM objects. An example of a COM container is Internet Explorer 3.0. |
| Server | An object that implements interfaces and exposes them to other objects. |
| In-Process server | An object server that is implemented within a DLL. |
| EXE server | An object server that is implemented in an EXE. |
Object handler An in-process server that provides a partial implementation of an object. Most often used for redistribution purposes that do not require editing features.
Another noteworthy feature is that an object can be both a container and a server. For instance, a Microsoft Word document object can contain other COM objects, making Microsoft Word a Container application. Microsoft Word also exposes a number of COM objects for use in other applications (spell checker, mail merge, art, and so on); therefore, Word is also an EXE server. Many ActiveX/OLE controls come in two varieties: in-process servers and object handlers. The in-process server version is for use at design time to set properties and add method handlers. For distribution purposes, a smaller, more efficient version is provided that implements no design-time interfaces.
This section discusses the responsibilities of COM objects and the different ways in which they can be used. Keep in mind that many of these details are hidden from the Visual J++ developer thanks to the Visual J++ class library and the Windows JVM. Nevertheless, you should understand what is going on beneath the surface to truly appreciate how COM works.
Each application that uses or creates a new COM object must, at a minimum, initialize and uninitialize the component object library implementation before attempting to access any COM functionality. The COM API provides two methods for doing so: CoInitialize() and CoUninitialize().
CoInitialize() accepts a pointer to an IMalloc interface, although this value can be null if you decide to use the default memory allocator supplied by the COM implementation. Calling CoInitialize() will ensure that the COMPOBJ.DLL library is loaded along with any additional libraries required to access COM objects. Although calling this method with a NULL parameter is perfectly valid, an IMalloc interface can be passed should you wish to override the default memory allocation implementation. The IMalloc interface defines the following methods:
These methods correspond to normal memory allocation functions found in the Windows API, but in this case they are encapsulated within a COM interface.
CoUninitialize() unloads all COM DLLs that were loaded during the course of an application's execution. In addition to other internal functions, this method also calls the CoFreeAllLibraries() COM API function in order to unload all COM DLLs from memory.
COM objects, like Java classes, are created and linked at runtime by a calling application. The COM API provides the CoCreateInstance() function in order to create an object at runtime. This function accepts a CLSID as input and uses this CLSID to locate the COM object on the local or remote machine. The C calling syntax for the CoCreateInstance() function is as follows:
HRESULT CoCreateInstance(REFIID clsid, LPUNKNOWN pUnknown, DWORD context, REFIID interface_id, LPVOID FAR *iface);
Taken individually, these parameters represent the following information:
When the CoCreateInstance() function is called, the Registry is searched to determine which object will be created. When this object is located, CoCreateInstance() internally retrieves an IClassFactory interface from the object. An IClassFactory interface is functionally equivalent to a Java class constructor. This interface supplies two methods, CreateInstance() and LockServer(), which can be used to dynamically create an object and then force that object to stay in memory.
The CoCreateInstance() function works fine if you are certain of two things:
Assuming that you know these two things and can retrieve an interface from an object using the CoCreateInstance() method, how can you access other interfaces within that object without first shutting down the object and continually calling CoCreateInstance() each time you want another interface? The answer lies within the IUnknown interface!
Earlier in the chapter, I mentioned that all COM objects,
at a minimum, must implement the IUnknown interface.
This interface defines three key methods that supply the fundamental
operations common to all COM objects. These three methods are
explained in Table 16.3.
| Method Name | Purpose |
| QueryInterface | Accepts a requested interface and returns that interface if it exists within the object |
| AddRef | Increments the object's reference count (for memory management purposes) |
| Release | Decrements the object's reference count and frees the object when the count equals zero |
The key method in this group is the QueryInterface() method. Because all COM interfaces inherit from the IUnknown interface, once any interface pointer is obtained via a call to CoCreateInstance(), the QueryInterface() method can be called to determine what other interfaces the object supports. (In fact, the IID_IUnknown interface ID itself can be passed directly to CoCreateInstance() if the user wants to return an IUnknown interface directly.) The QueryInterface() method actually does double duty because, if an interface does exist that is the type asked for by the caller, that interface will be returned directly to the calling program. (The AddRef() method will also be automatically called to increment the object's reference count.)
By using the QueryInterface() method, applications can gracefully handle unsupported features in COM objects. In addition, as the system is updated with more capable objects, applications can be written to immediately take advantage of them. COM's use of interfaces as the means of access to an object's capabilities prevents any attempts to access nonexistent features within an object.
The C calling syntax for the QueryInterface() method is as follows:
HRESULT QueryInterface(REFIID interface_id, LPVOID FAR *iface);
As you can see, the QueryInterface() method accepts an
interface ID (IID) and a "pointer to a pointer" that
will contain the interface pointer if it exists. Like the CoCreateInstance()
method, QueryInterface() returns an HRESULT
value. Table 16.4 lists some of the valid values for an HRESULT
return value.
| HRESULT Value | Meaning |
| E_UNEXPECTED | Unexpected failure |
| E_NOTIMPL | Not implemented |
| E_OUTOFMEMORY | Ran out of memory |
| E_INVALIDARG | One or more arguments are invalid |
| E_NOINTERFACE | No such interface supported |
| E_POINTER | Invalid pointer |
| E_HANDLE | Invalid handle |
| E_ABORT | Operation aborted |
| E_FAIL | Unspecified error |
| E_ACCESSDENIED | General access denied error |
| E_NOTIMPL | Not implemented |
| DISP_E_UNKNOWNINTERFACE | Unknown interface |
| DISP_E_MEMBERNOTFOUND | Member not found |
| DISP_E_PARAMNOTFOUND | Parameter not found |
| DISP_E_TYPEMISMATCH | Type mismatch |
| DISP_E_UNKNOWNNAME | Unknown name |
| DISP_E_NONAMEDARGS | No named arguments |
| DISP_E_BADVARTYPE | Bad variable type |
| DISP_E_EXCEPTION | Exception occurred |
| DISP_E_OVERFLOW | Out of present range |
| DISP_E_BADINDEX | Invalid index |
| DISP_E_UNKNOWNLCID | Memory is locked |
| DISP_E_ARRAYISLOCKED | Memory is locked |
| DISP_E_BADPARAMCOUNT | Invalid number of parameters |
| DISP_E_PARAMNOTOPTIONAL | Parameter not optional |
| DISP_E_BADCALLEE | Invalid callee |
| DISP_E_NOTACOLLECTION | Does not support a collection |
As you will see in Chapter 17, "Encapsulating COM with the Visual J++ Class Library," these HRESULT values are provided for the Visual J++ programmer in the com.ms.com.ComFailException Java class.
To summarize, each application must at some level go through the following C++ language steps to initialize and use COM objects:
HRESULT retValue = CoInitialize(NULL);
if (retValue == S_OK) then
{
CoCreateInstance(CLSID_SOMEAPP, NULL, CLSCTX_LOCAL_SERVER, IID_IUnknown,
(void**)&pIUnknown);
retValue = pIUnknown->QueryInterface(IID_SOMEINTERFACE,
(void**)&pISomeInterface);
if (retValue == S_OK) then
{
pISomeInterface->Method1();
pISomeInterface->Method2();
/* When finished with the interface, call its Release() method! */
pISomeInterface->Release();
}
else
{
/* Perform some default operation */
DisableMenuItem(InterfaceMenu);
}
CoUninitialize();
}
COM does not have a way to return a list of the interfaces supported by an object. The only real reason to return such a list is to display some sort of interface list to the user (which is not an extremely useful function). Otherwise, if you know that a specific interface is needed, simply calling QueryInterface() for that interface is as easy as scrolling through a list, locating the interface, and then using it. Having such a list would still force you to provide a fallback case because the object could change dynamically over time. Therefore, even if an interface did not exist one day, someone may have installed new software on the machine the next day. Suddenly, the interface now exists! Therefore, your programs should always look for an interface and then provide some default error-handling capability if the interface does not exist.
Although the QueryInterface() method can return information about an object at runtime, the IUnknown interface supplies the AddRef() and Release() methods to track the number of users an object currently has. At its simplest, the AddRef() method could simply increment an internal counter each time the method was called. The Release() method would decrement the internal counter when called and could free the object when the counter returned to zero. To make sure that reference counting occurs properly for an object, the following two steps must always occur:
Keep in mind that reference counting applies globally to an entire object, not just to an individual interface. Therefore, you may have cases in which calling AddRef() and Release() is not necessary. These cases occur when an interface is created, used, and freed within the lifetime of another interface, as shown in the following sample code:
void *Interface1; void *Interface2; CreateTheInterface(&Interface1); /* Do something with the interface */ Interface1->Method1(); Interface1->Method2(); Interface2 = Interface1; /* Do something with Interface2 */ Interface2->Method5(); Interface2->Method1(); Interface2 = NULL; Interface1->Release();
In this sample code, Interface2 lives and dies during the lifetime of Interface1. The programmer doesn't have to call the AddRef() and Release() methods of Interface2 because before the beginning and after the end of its lifetime, the reference count stays the same.
A general rule of thumb is that all methods that return an interface pointer should call AddRef() for that interface. All methods that accept an interface as an input parameter or retrieve an interface from some method output should call Release() when they are using that interface. Failing to call the Release() method of an object has important consequences because the object will never be freed as long as the application is running.
Once an object has been created and installed on a file system, implementing inheritance from that object using a new object is not possible. However, within the object, using a language such as C++, interfaces can be inherited from or overridden. Using any object-oriented language that supports inheritance, this kind of functionality is possible when building the object. Nevertheless, once the DLL or EXE has been compiled and built and the object has been installed on a system, that object cannot be inherited from in a binary sense because of the unknown nature of objects within a system. Most developers do not feel comfortable blindly overriding methods contained in objects produced by others, especially if they have no idea of what is really going on inside that object. Therefore, object inheritance is not a supported feature of COM.
However, COM does allow objects to reuse the capabilities of other objects through techniques known as containment and aggregation. Using these techniques, an object does not override another object's interface. Instead, the second object directly uses the first object's interface, and the object that "owns" the interface maintains its own local memory and reference count.
Containment refers to the process whereby one object completely contains another object. When the contained object's interface methods are called, the container object can choose to call the default method or can override that method by calling one of its own. The container object maintains an internal pointer to the contained object's interface. When the container is freed, it will in turn free the contained object. The container object will implement stubs for each method in the contained object. Some of these stubs will override the contained object's methods, whereas others will be passed directly through to the container object. Figures 16.1 and 16.2 illustrate the Book and Dictionary COM objects.
Figure 16.1 : The Book object.
Figure 16.2 : The Dictionary object.
The ways in which these illustrations are drawn serves to illustrate a COM standard. Objects are always drawn as rectangles with an IUnknown "node" showing at the top of the object illustration. The other interfaces are drawn on the left side of the rectangle. Figures 16.3 and 16.4 show how these nodes can connect objects via their exposed interfaces.
Figure 16.3 : Reusing an object through containment.
Figure 16.4 : Reusing an object through aggregation.
In these illustrations, two objects are being used: the Book object and the Dictionary object. (In object-oriented terms, we could say that Dictionary inherits from Book.) The Book object implements the IUnknown and IBook interface. This IBook interface could supply useful functions such as Read(), Buy(), and TurnPage(). The Dictionary object supplies its own IUnknown interface, but in addition supplies an IReference interface. This interface supplies functions such as LookUp(), FindSynonym(), and CheckSpelling().
The Book object is implemented so that it is completely independent of the Dictionary object. If no Dictionary object is present or if no Dictionary object is passing data to the Book object, the Book object is completely self-contained and will function normally. However, the Dictionary object will implement containment and therefore will create an instance of a Book object when it is created. Recall that this instance can be created via a call such as
CoCreateInstance(CLSID_Book, NULL, CLSCTX_LOCAL_SERVER, IID_IBook, (void**)&pIUnknown);
The IBook interface is stored internally within the Book object. When the Book object is itself destroyed at runtime, the IBook.Release() method will be called to free the Book object as well. Because the Dictionary object plans to always store an internal Book object, it can expose the IBook interface as its own (see Figure 16.3).
If the Dictionary.IBook interface wants to reuse a function exposed by the Book.IBook interface, it can call that function directly. Otherwise, the Dictionary object can override the function by supplying one of its own.
Aggregation is different from containment in that the container object (or aggregate object, as it is called) does not choose to override any of the methods within the contained object. Instead, it directly exposes the interfaces of the contained object as its own (see Figure 16.4).
In Figure 16.4, the Dictionary object exposes the Book.IBook interface directly as one of its own. In no way does the Dictionary object attempt to implement any of the IBook functions on its own. This programming device eliminates the need for stub functions (that call the IBook functions directly) but complicates the issue in another sense. Suppose that a user of the Dictionary object retrieves what it thinks is the Dictionary.IBook interface. (In reality, this interface actually belongs to the Book object, but through aggregation it appears to belong to Dictionary.) Using the IBook interface, the following call is made:
retValue = pIUnknown->QueryInterface(IID_DICTIONARY, (void**)&pIDictionary);
In reality, the Book object is being referenced here, and it has no knowledge of the Dictionary object! How can this possibly work? Aggregation works thanks to the second parameter passed to the CoCreateInstance() function that defines the controlling unknown of the aggregate. (So far we have assumed that this parameter would be NULL.) By passing in this controlling unknown, the Dictionary object (the aggregate object) and the Book object (the contained object) agree to work together. If an unknown interface is requested from the Dictionary object, it is passed directly to the Book object for creation. Likewise, a QueryInterface() call to one of the Book object's interfaces can return a Dictionary interface.
Keep in mind that aggregation and containment do not allow binary COM objects to inherit from one another at the system level. In other words, an object cannot simply install itself into the Windows Registry and inform Windows that it is overriding features within another object. Instead, aggregation and containment occur within an object's source code and force an object to have knowledge of another object at compile time in order to be successful.
The primary facets of COM development have now been covered. The discussion started by showing the many similarities between Java and COM (refer to Table 16.1). An overview of the Microsoft Component Object Model (COM) followed this comparison. Here is a review of the main points:
It is now time to get down to the details that should truly interest the Visual J++ developer. Vague references have been made to operations such as "registering" a COM object with the operating system or "publishing" an object's interfaces. Just as the Java language lends itself to virtually seamless use of COM objects side by side with Java objects, Visual J++ provides several useful tools that greatly ease the Java/COM integration process.
The purpose of COM and other object models (such as SOM and CORBA) is to provide a method of reuse for binary objects installed on local or remote operating systems. This world of distributed objects offers many advantages, but it does not necessarily make the developer's life any easier. In terms of advantages, applications are easier to partition (and thus can be scaled when performance reasons call for it). Application components can be upgraded in a transparent manner, and, perhaps most interestingly, these application components can be written in virtually any language. Therefore, if DCOM is successfully ported to the Macintosh and UNIX operating systems, a Windows/Mac/UNIX application could essentially be distributed across a network of computers running the most popular operating systems. In the past, one of the trickiest portions of this design from a software development viewpoint was to develop separate Windows/Mac/UNIX client graphical user interfaces. With Visual J++, even that task should be relatively straightforward in the future!
For simplicity's sake, one part of the puzzle has thus far been ignored. Assume that some of the objects you plan to use in your application will be developed by many different vendors using languages such as C, C++, and Java. How do you begin to determine the contents of the interfaces supplied by these various vendors? To drop down to an even more detailed question, how will your application physically link to these objects? In Java, native methods can be used to tie a Java method to a C language method and standard programming methods exist for linking in a DLL's function. However, linking in methods is quite different from linking in entire interfaces.
Fortunately, this problem has been well researched, and in fact, an entire language has been developed to assist developers in describing the contents of an object. This language, named the Interface Definition Language, or IDL, was originally developed by the Open Software Foundation for use in describing remote procedure call (RPC) interfaces. IDL is also currently used by the CORBA object model. Microsoft has added extensions to standard IDL to produce a description language known as the Object Description Language (ODL). Developers who have used OLE Automation may be familiar with the compiled version of an ODL file: the type library.
Type libraries are used to expose items such as data types, objects, or (of course!) interfaces within binary objects. In essence, these libraries are compiled versions of an ODL source file. These source files were traditionally compiled using a tool such as MKTYPLIB. Once compiled, a type library can exist as a resource within a DLL, OCX, or EXE file just like other resources such as menus, icons, and bitmaps. In addition, a type library can exist as a standalone TBL file that is distributed with a product. A quick search of my hard drive reveals a variety of type library files, including libraries used to access objects within Microsoft Access, PowerPoint, and the Data Access Objects.
Visual J++ developers have a variety of tools at their disposal to retrieve information from the type libraries of existing objects. In keeping with the Visual J++ goal of making COM integration as seamless as possible, a tool known as the Java Type Library Wizard automatically extracts type library information from COM objects. Then Java class files are generated and can be imported like any other Java class. (As already noted, COM objects appear identical to Java objects from a language viewpoint.) Unfortunately, creating COM objects in Java using Visual J++ requires a little more effort. The language does not currently contain a "reverse" Type Library Wizard that can produce a type library from a Java object. Instead, the ODL code must be written manually and compiled using the MKTYPLIB or MIDL tools provided by Microsoft. Once this type library has been created, a Java COM object can be registered using the JAVAREG tool provided with Visual J++. The OLE2 Object Viewer included with Visual J++ allows developers to view the contents of a type library quickly, using an intuitive graphical user interface.
The Java Type Library Wizard is an extremely handy tool for developers
who want to use existing COM objects or to implement existing
COM interfaces. However, if you decide to build an application
using COM as a foundation, at some point you will have to define
and create your own COM objects. The next section discusses the
process of physically creating an ODL file and building a type
library using MKTYPLIB. Before you can undertake that
process, however, you must retrieve a globally unique identifier
(GUID) for each interface being defined and for each COM object
being built. Visual J++ provides the GUIDGEN.EXE tool
to help you.
| Note |
If this tool does not appear in your Visual J++ program group, go to the .\BIN directory of your Visual J++ installation (\MSDEV\BIN by default). The GUIDGEN.EXE executable and help file are located in this directory. |
Running the GUIDGEN tool will automatically produce a unique ID that can be used for each interface (see Figure 16.5).
Figure 16.5 : GUIDGEN -- The GUID generator tool.
Several options are available within the GUIDGEN application. For Java development, your concern is in creating a new GUID ID to be associated with a Java class within the Windows Registry. Therefore, click on the fourth option, Registry Format, to retrieve a new GUID. A sample GUID generated using this tool on my current machine was
{1A6D6E81-1421-11d0-B4EE-444553540000}
Of course, because it is globally unique, when this tool is run on your machine, a different ID will be generated. Once this ID has been generated, it can identify objects or interfaces within an ODL file.
The name of the MKTYPLIB tool suggests its function: to make type libraries. Many Java developers may never need to actually build an object description using ODL. Keep in mind that ODL is used to define new interfaces. Therefore, if you are only implementing existing COM interfaces, using the Java Type Library Wizard to generate class files for the specified interface is sufficient. Once these class files exist, write the Java class implementing the interfaces in Java and then use the JAVAREG tool to register this Java class and receive a CLSID (see the section on JAVAREG later in the chapter). Assuming that you definitely want to define a new COM interface and implement it in Java, you must take the following steps:
Chapter 21, "Building COM Objects with Java," covers in detail the process of creating COM objects using the Java language. The following section briefly describes the ODL and its syntax.
This section covers the basic syntax of ODL. (For a more thorough description, consult the Visual J++ documentation.) The basic structure of each library defined within an ODL source file follows:
library LibName
{
importlib "xxx.tlb"
[attributes] elementname typename {
memberdescriptions
};
[attributes] elementname typename {
memberdescriptions
};
}
Each ODL source file begins with a library statement that relays pertinent information to the type library builder. All statements that apply to a type library must be defined with the library block. In addition, all statements within the library block are used to build the type library. MKTYPLIB considers all statements outside of the library block (except for the actual library attributes) to be IDL file statements. The following example illustrates the use of the library statement to create a new type library named Book:
[
uuid(1A6D6E81-1421-11d0-B4EE-444553540000),
helpstring(""),
lcid(0x0009),
version(0.1)
]
library Book
{
importlib("stdole.tlb");
[
uuid(1A6D6E82-1421-11d0-B4EE-444553540000),
helpstring("The Book Interface!"),
oleautomation,
dual
]
interface IBook : IUnknown
{
void CheckOut([in, string] unsigned char * BookTitle);
//IUnknown methods omitted here
};
[uuid 1A6D6E83-1421-11d0-B4EE-444553540000]
coclass TheBook
{
interface IBook;
}
}
Valid attributes that can be used with the library statement are the following: helpstring, helpcontext, lcid, restricted, hidden, control, and uuid. Table 16.5 describes some common ODL attributes.
The importlib statement imports an exterior type library's contents for compilation purposes. It is similar to the C/C++ language include statement or the Java language import statement.
The interface statement defines a new interface within the library block. An interface is defined as inheriting from a base interface by the use of the colon (:) operator. For example, in the preceding code, interface IBook inherits from the IUnknown interface. The attributes dual, helpstring, helpcontext, hidden, odl, oleautomation, uuid, and version are accepted before the interface keyword. The following attributes are accepted on a function in an interface: helpstring, helpcontext, string, propget, propput, propputref, bindable, defaultbind, displaybind, and vararg. The uuid attribute is required before the interface definition.
The coclass statement defines a new class within the
library block. The helpstring, helpcontext,
licensed, version, control, hidden,
and appobject attributes are accepted, but
not required, before a coclass definition. Meanwhile,
the uuid attribute is required before the coclass
definition.
| Attribute | Description |
| bindable | Property supports data binding |
| control | Indicates a control item (for display purposes) |
| defaultbind | Indicates the default bindable property for the object |
| displaybind | Indicates that the property should be displayed |
| dual | Interface exposes properties through IDispatch and VTBL |
| helpcontext | Sets the help file context ID |
| helpstring | Sets the help string to be displayed |
| hidden | Indicates the item should not be displayed in an object browser |
| lcid | Parameter is a locale ID |
| licensed | Class is licensed |
| odl | Identifies an interface as an ODL interface |
| oleautomation | Interface is compatible with OLE Automation |
| propget | Specifies a property-accessor function |
| propput | Specifies a property-setting function |
| propputref | Specifies a property set-by-reference function |
| restricted | Hides item from use by a macro programmer |
| string | Specifies a string |
| uuid | Specifies the GUID of the item |
| vararg | Indicates a variable number of arguments |
| version | Specifies an object's version number |
Once the ODL file has been built to define your new COM object and its interfaces, you can use the MKTYPLIB tool to compile the ODL definition into a COM type library.
MKTYPLIB is the name of the tool used to "compile"
an ODL class definition into a binary type library.
| Note |
If this tool does not appear in your Visual J++ program group, go to the .\BIN directory of your Visual J++ installation (\MSDEV\BIN by default). The MKTYPLIB.EXE executable is located in this directory. |
The basic syntax required to use MKTYPLIB is the following:
MKTYPLIB [options] ODLFileName
Table 16.6 lists the MKTYPLIB options as described in
the Visual J++ documentation.
| Option | Description |
| /? or /help | Displays command line Help. In this case, ODLfile does not need to be specified. |
| /align:alignment | Sets the default alignment for types in the library. An alignment value of 1 indicates natural alignment; n indicates alignment on byte n. |
| /cpp_cmd cpppath | Specifies cpppath as the command to run the C preprocessor. By default, MkTypLib invokes CL. |
| /cpp_opt "options" | Specifies options for the C preprocessor. The default is /C /E /D__MkTypLib__. |
| /D define[=value] | Defines the name define for the C preprocessor. The value is its optional value. No space is allowed between the equal sign (=) and the value. |
| /h filename | Specifies filename as the name for a stripped version of the input file. This file can be used as a C or C++ header file. |
| /I includedir | Specifies includedir as the directory where include files are located for the C preprocessor. |
| /nocpp | Suppresses invocation of the C preprocessor. Specify this option if you do not have the C compiler installed on your machine. (This option means that you cannot use preprocessor directives in your ODL file.) |
| /nologo | Disables the display of the copyright banner. |
| /o outputfile | Redirects output (for example, error messages) to the specified outputfile. |
| /tlb filename | Specifies filename as the name of the output TBL file. If not specified, it will be the same name as the ODLfile, with the extension .tlb. |
| /win16 /win32 /mac /mips /alpha /ppc /ppc32 | Specifies the output type library to be produced. The default is the current operating system. |
| /w0 | Disables warnings. |
Two points need to be made about these options and MKTYPLIB, in general.
First of all, note that some of the options apply specifically to the use of MKTYPLIB in conjunction with a C or C++ compiler. If you are building new type libraries for Java COM objects, you should always use MKTYPLIB with the /nocpp flag (unless a valid C/C++ compiler is installed on your system). Also, avoid the use of all preprocessor flags.
The second point is that the MKTYPLIB tool will be useful only if you are actually creating new COM objects. You should use the Java Type Library Wizard to extract information from an existing type library if you want to use an existing COM object in your Java code.
Once a COM class has been created in Java and its type library has been created as well, you can use the JAVAREG tool to place this object's identification information in the Windows Registry. It is where the linkage takes place between the Java class file name and the CLSID stored in the Registry. The syntax to be used with the JAVAREG tool is as follows:
javareg /register /class:JAVACLASSNAME /clsid:{CLSID}
Note that in this syntax, the physical name of the Java class
file should be substituted for the JAVACLASSNAME symbol,
and the CLSID of that class should be substituted for
the CLSID symbol.
| Note |
If this tool does not appear in your Visual J++ program group, go to the .\BIN directory of your Visual J++ installation (\MSDEV\BIN by default). The JAVAREG.EXE executable is located in this directory. |
To view all of the options available for the JAVAREG tool, run the program from the command line using the following syntax:
javareg ?
Once run, JAVAREG will add the appropriate keys to the Windows Registry under the HKEY_CLASSES_ROOT\CLSID node (or My Computer\HKEY_CLASSES_ROOT\CLSID under Windows 95).
One additional tool that is included with Visual J++ is the OLE2
Object Viewer. This tool is extremely useful for examining the
contents of all OLE (and therefore COM) objects currently installed
on the local system. Although many of the options available within
this tool apply specifically to OLE 2.0 concepts, you can also
use this tool to view the type libraries of any COM object.
| Note |
If this tool does not appear in your Visual J++ program group, go to the .\BIN directory of your Visual J++ installation (\MSDEV\BIN by default). The OLE2VW32.EXE executable and help file (OLE2VIEW.HLP) are located in this directory. |
Figure 16.6 shows the OLE2 Object Viewer tool and the registered COM objects currently available on a system.
Figure 16.6 : The OLE2 Object Viewer tool.
To examine the contents of a specific type library, double-click an object in the left frame within OLE2VW32U. The contents of that object's type library will be displayed in a dialog box resembling Figure 16.7.
Figure 16.7 : Viewing the contents of a COM type library.
Figure 16.7 displays the contents of the type library for the Beeper COM DLL included with the Visual J++ sample applications. In fact, examining the figure closely reveals the definition of the QueryInterface() method itself! Selecting the To FileÉ option will save the contents of the type library to a text file for closer examination. Listing 16.1 shows the equivalent type library information for this object as generated by the OLE2 Object Viewer.
Listing 16.1. Type library contents for the Beeper COM object.
'===============================================================
' Type Library: BeeperLib, Library Version 1.000
' GUID: {6384D582-0FDB-11CF-8700-00AA0053006D}
' LCID: 0X00000000
' Documentation: Beeper 1.0 Type Library
' Help: (o vial)Tx ie *tt'====================================================
'===============================================================
' Type Info: Beeper, TypeInfo Version 0.000
' GUID: {6384D586-0FDB-11CF-8700-00AA0053006D}
' LCID: 0X00000000
' TypeKind: coclass
' Documentation: Beeper Object
'-------------------------------
'===============================================================
' Type Info: IBeeper, TypeInfo Version 0.000
' GUID: {6384D584-0FDB-11CF-8700-00AA0053006D}
' LCID: 0X00000000
' TypeKind: dispinterface
' Documentation: IBeeper Interface
'-------------------------------
' Function: QueryInterface
'
Declare Sub QueryInterface (ByRef riid As Variant, ByRef ppvObj As Variant)
' Function: AddRef
'
Declare Function AddRef () As ULONG
' Function: Release
'
Declare Function Release () As ULONG
' Function: GetTypeInfoCount
'
Declare Sub GetTypeInfoCount (ByRef pctinfo As Variant)
' Function: GetTypeInfo
'
Declare Sub GetTypeInfo (ByVal itinfo As UINT, ByVal lcid As ULONG,
ByRef pptinfo As Variant)
' Function: GetIDsOfNames
'
Declare Sub GetIDsOfNames (ByRef riid As Variant, ByRef rgszNames As Variant,
ByVal cNames As UINT, ByVal lcid As ULONG, ByRef rgdispid As Variant)
' Function: Invoke
'
Declare Sub Invoke (ByVal dispidMember As Long, ByRef riid As Variant,
ByVal lcid As ULONG, ByVal wFlags As USHORT, ByRef pdispparams As Variant,
ByRef pvarResult As Variant, ByRef pexcepinfo As Variant,
ByRef puArgErr As Variant)
' Function: Beep
' Documentation: Play the current sound
'
Declare Sub Beep ()
' Function: Count
' Documentation: Returns number of strings in collection.
'
Declare Function Count () As Long
' Function: Item
' Documentation: Given an index, returns a string in the collection
'
Declare Function Item (ByVal Index As Long) As String
' Function: _NewEnum
'
Declare Function _NewEnum () As LPUNKNOWN
Visual Basic developers should be familiar with the syntax of the function declarations in the output listed in Listing 16.1. Because the OLE2 Object Viewer was provided initially to help Visual Basic programmers implement OLE Automation (controlling OLE objects remotely from another application), the output of the OLE2 Object Viewer gives VB-syntax function declarations for each interface function with the Beeper object. Java developers shouldn't feel left out, though. As you will see in Chapter 20, "Calling COM Objects from Java," the Java Type Library Wizard also generates a summary information file containing the function declarations within a COM object. However, these function declarations use Java syntax instead of Visual Basic!
Microsoft's COM was overshadowed by its child technology, OLE, for several years as Microsoft marketed the prebuilt capabilities available in the OLE 2.0 suite of objects. Although OLE provides a set of implemented interfaces that Microsoft defined to handle a number of common computing concepts (for example, visual editing, structured storage, in-place activation), COM is the flexible object model that underlies all of OLE. Because of Java's popularity, the introduction of distributed COM, and the Internet-aware, streamlined OLE now known as ActiveX, COM has once again become the focus of Microsoft's marketing plan. Visual J++ includes a set of tools to aid the Java developer in integrating and extending COM using the Java language.
Because of the many similarities between Java and COM, integration between the two is nearly seamless for the Java developer. Instead of calling COM API methods directly and passing interface pointers and CLSIDs around an application, COM objects appear to be completely identical to Java objects within Java source code. To create a new COM object for use within an application, the new keyword can be used to call the object's constructor. Microsoft can thank the many similarities between component object models and Java (support for multiple interfaces, dynamic creation and linking, and runtime garbage collection) for this ease of integration.
The following chapters in this part of Visual J++ Unleashed explain how to combine Java and COM. The examples show both COM objects built in Java and Java programs calling COM objects. First, however, the items that make the combination of Java and COM possible will be addressed. These items are the Microsoft JVM and the com.ms.com package included with Visual J++.