As we will explore in this book, objects as expressed in OLE most definitely support the notions of encapsulation, polymorphism, and reusability; again, inheritance is really just a means to the latter two. OLE objects are just as powerful as any other type of object expressed in any programming language, and might be more so. While inheritance and programming languages are excellent ways to achieve polymorphism and reusability of objects or components within a large monolithic application, OLE is about integration between binary components, and it is therefore targeted at a different set of problems. OLE is designed to be independent of programming languages, hardware architectures, and other implementation techniques. So there really is no comparison with, and no basis for pitting OLE against, object-oriented programming languages and methodologies; in fact, such languages and methods are very helpful and complementary to OLE in solving customer problems.
The nature of an OLE object, then, is not expressed in how it is implemented internally but rather in how it is exposed externally. As a basis for illustrating the exact structures involved, let's assume we have some software object, written in whatever language (code) that has some properties (data or content) and some methods (functionality), as illustrated in Figure 1-2. Access to these properties and methods within the object and its surrounding server code is determined by the programming language in use.
Figure 1-2.
Any object can be seen as a set of properties (data members or content) and methods (member functions).
The accessibility of the object's members in its native programming language is not of interest to OLE. What does concern OLE is how to share the object's capabilities with the outside world, which does not need to correspond at all to the object's internal structure. The external appearance of the object, that is, how clients of this object will access its functionality and content, is what OLE helps you define and implement.
In OLE, you factor an object's features into one or more groups of semantically related functions, where each group is called an interface. Again, an object can provide multiple interfaces to its client, and this capability is one of OLE's key innovations. Microsoft has already defined many interfaces representing many common features, and many of these interfaces will likely never require revision. For example, the group of functions that describes structured data exchange is easily specified and can thus be a "standard" OLE-defined interface. You are also free to define "custom" interfaces for your own needs without every having to ask Microsoft to approve the design—custom interfaces fit into the OLE architecture the way any standard interface does. We'll explore both standard and custom interfaces throughout this book.
Regardless of who defines an object's interfaces, all access to an object happens through member functions of those interfaces, meaning that OLE doesn't allow direct access to an object's internal variables. The primary reason is that OLE works on a binary component level where you would require complex protocols to control access. (In contrast, programming languages handle such control easily through source code constructs.) In addition, accessing object variables directly usually involves pointer manipulation in the client's address space, which makes it very difficult to make such access transparent across process or machine boundaries unless you stipulate language structures and compiler code generation. On the other hand, because a client gives control to the object through a function call, it is quite easy to transparently intercept that call with a "proxy" object and have it forward the call to another process or machine where the real object is running, and do so in a way independent of languages and compilers.
In the binary standard for an interface, the object provides the implementation of each member function in the interface and creates an array of pointers to those functions, called the vtable.3 This vtable is shared among all instances of the object class, so to differentiate each instance, the object code allocates according to the object's internal implementation a second structure that contains its private data. The specifications for an OLE interface stipulate that the first 4 bytes in this data structure must be a 32-bit pointer to the vtable, after which comes whatever data the object wants (depending on the programming language). An interface pointer is a pointer to the top of this instance structure: thus a pointer to the pointer to the vtable. It is through this interface pointer that a client accesses the object's implementation of the interface—that is, calls the interface member functions but cannot access the object's private data. This interface structure is depicted in Figure 1-3.
Figure 1-3.
The binary standard for an OLE interface means that an object creates a vtable that contains pointers to the implementations of the interface member functions and allocates a structure in which the first 32 bits are a pointer to that vtable. The client's pointer to the interface is a pointer to the pointer to the vtable.
If you are familiar with the internals of C++, you'll recognize this structure as exactly that which many C++ compilers typically generate for a C++ object instance. This is entirely intentional on OLE's part, making it very convenient to write OLE objects using C++. In short, if a C++ object class is derived from an OLE interface definition, you can typecast the object's this pointer into an interface pointer. Chapter 2 will describe various techniques for doing so. This interface design, however, is merely convenient for C++ programmers. You can easily generate this same interface structure from straight C code, as we'll also see in Chapter 2, and even from assembly language. Since you can express it in assembly language, and because any other programming language can be reduced to an assembly equivalent, you can create an interface from any other language as long as your tools know about OLE and give you a language device through which you can implement or use an interface.
Again I'll point out that because different compilers and languages will store an object's instance data differently in the instance structure, the client cannot directly access that data through an interface pointer. In fact, the client never has any sort of pointer to the object, only to interfaces, because the notion of an object is so variable, whereas the notion of an interface is a binary standard. Besides, direct access—client code manipulating data based on a memory offset—works only when the client and the object share the same address space. In OLE, this is not always the case, so OLE's definition of an interface pointer type restricts the use of a pointer at compile time. The definition of the type depends, of course, on the programming language but always allows you to call functions by name through an interface pointer, such as pInterface->MemberFunction(…), and provides type checking on the function's arguments. This is much more convenient than trying to call functions through an array offset with no type checking.
What is highly inconvenient is having to draw the entire binary structure whenever you want to illustrate an object. By convention, interfaces are drawn as plug-in jacks extending from the object, as illustrated in Figure 1-4. When a client wants to use the object through an interface, it must plug into that interface. The electronics analogy is that for a client to use a jack (interface), it must have a plug that fits (code that knows how to use the members of that interface).
Figure 1-4.
Interfaces are drawn as plug-in jacks extending from the object.
This representation of an object and its interfaces emphasizes an important point: an interface is not an object. An interface is merely a channel for the object, and only one of the many channels an object might support. In a "Prolegomenon to Object Metaphysics," we might think of how the philosopher Immanuel Kant, asking how we know something is real, would differentiate objects from interfaces.4 In such a Kantian analysis, objects are noumena—things-in-themselves that are in principle incapable of being known or experienced directly. Interfaces are, on the other hand, phenomena—manifestations of objects for sensing experience (your code). Since we can't know objects directly, we tend to refer to them and reify them as their interfaces, but strictly speaking, an object remains conceptual as far as clients of that object are concerned. The client knows only the general "type" of an object as a collection of interfaces that define that type.
As a further reinforcement of this idea, all interfaces are conventionally named with a capital I prefix, as in IUnknown, IDataObject, and so on. The symbolic name of the interface describes the feature of functionality defined in that interface. In addition, you identify an interface at run time not by its textual name but by a binary 128-bit globally unique identifier (globally unique in the real and literal sense). Contrast this to a C++ class, which is only identified at compile time by a text name that is unique only to the compilation.
You should notice that hiding the object behind its interfaces is exactly the fundamental notion of encapsulation. OLE also supports the idea of polymorphism between interfaces. All that is needed are two interfaces that share a common subset of functions—a base interface—in their vtables, as shown in Figure 1-5. C++ inheritance works well to define such relationships.
The interface named Iunknown has three functions and is the ubiquitous base interface for every other interface in OLE. All interfaces are polymorphic with IUnknown, as also illustrated in Figure 1-5. This interface represents two fundamental OLE object features. The first is the ability to control an object's lifetime by using reference counting, which happens through the member functions AddRef and Release. The second feature is navigation between multiple interfaces on an object through a function called QueryInterface, as we'll see shortly.
So at least at the interface level, polymorphism is clearly supported in OLE. What is left in the set of fundamental object notions that we've defined here is reusability, and it should be apparent to you that the client of any object can itself be an object. Such a client object can implement any of its
Figure 1-5.
Interfaces are polymorphic through a base interface when they both have the same base interface functions at the top of their vtables.
interfaces by using the implementation of another object's interfaces internally, which is reusability by containment. The client object contains internally the object being reused, and external clients of the containing object are unaware of such reuse, as it should be. Containment is by far the most common method of reusability in OLE and requires no special support in either object. In some special circumstances, one object might want to directly expose another object's interface pointer as its own, and this requires a special relationship known as aggregation. We'll explore reusability through both means in Chapter 2, but for now we can realize that OLE objects support all three fundamental object notions: encapsulation, polymorphism, and reusability. And as we're now ready to discuss, OLE's idea of multiple interfaces is an important and powerful innovation.
3 For "virtual function table" because the design of an OLE interface is modeled after the structure of C++ objects that have virtual functions. |
4 My thanks to Mark Ryland (Microsoft) for this philosophical diversion. |