Object-Oriented Software Development Made Simple with COM+ Runtime Services
|COM+ is designed to radically simplify the creation and use of software components. COM+ provides a runtime and services that are readily used from any programming language or tool, and enables extensive interoperability between components regardless of how they were implemented.|
The information contained herein is based on prerelease software and is published without the endorsement of Microsoft Corporation. Microsoft Corporation makes no warranty as to the accuracy or completeness of the information. Since anything is subject to change, readers are advised that they use this information at their own risk.
This article assumes you're familiar with COM
Mary Kirtland is a Program Manager on the Microsoft COM team.
She is currently working on a book about COM+ to be published in 1998 by Microsoft Press, and can't remember the last time she had dinner at home.|
Building component-based software
will soon be much simplified thanks to
an upcoming extension to COM currently called COM+. COM+ provides a runtime and services that are readily used from any programming language or tool, and enables extensive interoperability between components regardless of how they were implemented.
In this article, I'll give you an overview of object-oriented development issues and discuss how the introduction of COM+ will resolve many of them. Future articles will delve into the COM+ programming model and the services provided by COM+. Note that at press time, the delivery mechanisms and dates for COM+ had not been announced, and COM+ was not yet in alpha release, so some details may change.
The basic notion of object-oriented software is that a user problem can be expressed in terms of objects and the actions that can be performed on those objects. At a high level, objects are things the user understands. For example, in an airline reservation system, passenger, flight, and seat are types of objects a travel agent would understand. As design and implementation progresses, additional types of objects that make more sense to a developer will be defined. Conceptually, you're always thinking about objects.
A class is a type of object that's defined in terms of its state and behavior. State is defined by a set of properties. The values of those properties make up the state of an object, but the set of properties is the same for all objects of a particular class. Behavior is defined by public methods that clients can call on the object to modify its state. The only way for clients to interact with an object is through its properties and methods. Note that the properties and methods do not imply anything about how the class is implemented and, ideally, clients should never need to know anything about the implementation.
As you create classes, you may discover that certain groups of methods are used by more than one class. You can build on this common behavior by defining interfaces. An interface is just a definition of a related set of properties and methods with no implementation. Interfaces are implemented by classes and are a powerful tool for reuse. Say you have two classes, A and B, that implement the IAddress interface. Because you are interacting with objects via the public interfaces, a client that has an IAddress pointer doesn't need to know whether it is talking to an instance of class A or class B, or even that A and B exist! Furthermore, A and B can be completely unaware of each other. The ability to treat multiple classes of objects as if they were the same type is known as polymorphism.
One important relationship between classes is the inheritance relationship used to construct class hierarchies. In this relationship, one class inherits both interface and implementation from another. Other relationships between classes are used to define composite object types. Finally, there is the purely physical matter of organizing classes into deployable units.
Object-oriented programming languages such as C++ and Java provide facilities that let you implement classes and interfaces. Languages vary in the degree to which they enforce restricting access to objects to public interfaces and to which interfaces are a distinct concept. However, the basics are there for you to use. Although object-oriented programming languages offer many benefits, they are not sufficient for developing large-scale, distributed applications.
Programming languages are typically focused on creating single-process, single-language applications. However, it may not be practical to write all the code for an application in a single language. For example, you might want to write the user interface with a RAD tool, but need to write code that processes instrument data using C++ to take advantage of a library of mathematical functions. You'll need to learn some interprocess or intermachine communication mechanisms to get the application working across multiple machines. And most languages encourage use of implementation inheritance, which can cause problems when base classes need to be modified.
System-level object models such as COM address these problems. COM provides a simple, powerful model for building software systems from interacting objects. COM defines a binary standard for objects and interobject communication. All communication with an object must occur through interfaces, and all communication must look like simple method callseven if the destination object is located in another process or on another machine. Implementations of COM provide the basic services required to locate, activate, and access components and objects, irrespective of their location. Additional services are built on this base by defining interfaces and implementing components that expose those interfaces. Because COM defines a binary standard for what objects and interfaces look like in memory, COM is language-neutral. Clients don't care (and normally don't know) what language was used to write the components they use.
Taken together, COM and object-oriented programming languages provide a powerful means of simplifying the development process. You can construct systems of interacting COM objects and write COM components using object-oriented languages. But while the COM programming model may seem simple, writing components and component-based applications is often harder than it
ought to be.
One problem is the many incompatibilities between what tools and languages consider an object and what COM considers an object. At a conceptual level, this makes learning about component-based development more difficult. It also makes development of components and system services that can truly be used from any language or tool very difficult. For example, many tools and languages support only a subset of COM features, so generally accessible components are restricted to the common subset supported by the tools (such as Automation) or must expose functionality in multiple ways. Ideally, COM would present a consistent vision of objects that is readily accessible to and compatible with the notions of objects defined by modern object-based languages and tools. It would also ensure that new APIs and services could be easily called from any compiled or interpreted language without extra development, testing, or documentation effort. And all objects, regardless of origin, should interoperate.
If you look at a typical component or application written in C++, large portions of the code have nothing to do with the problem being solved. There's code to initialize services, code to hook into the operating system, code to push system information aroundlots of code. Most of this code is the same from application to application and from component to component. Tools such as Visual Basic® derive much of their usefulness from hiding this common code in a tool-specific runtime and letting the developer focus on the code that solves a business problem. Numerous frameworks and application generators have been developed to bring similar services to C++ developers.
There are disadvantages to tool-specific runtimes and frameworks: you are limited to the features supported by that tool. Generally, there is a significant delay before
a new operating system feature is accessible from most tools. If the operating system provides the runtime, however, it should be possible to make new features immediately available to all tools. In addition, if you are using components written with multiple tools, you'll probably have many runtimes loaded into memory, unnecessarily increasing the memory footprint of your applications. A system-supplied runtime should eventually be the only runtime required. And with tool-specific runtimes, you need to make sure that the runtimes are installed everywhere you use components that rely on them. A system-supplied runtime would always be installed automatically.
In addition to simplifying component development,
many tools have introduced innovative features to make developing COM-based applications easier. Some of these features more properly belong at the system level. For example, the Active Template Library (ATL) introduced a script-based mechanism for simplifying component registration. Visual Basic introduced easy-to-use data access through data-bound controls. These are both features
that many components need.
Microsoft® Transaction Server (MTS) introduced many features to make writing scalable, distributed applications easier. In addition to its support for transactions, MTS provides a runtime environment with thread and object management services. Developers can write components assuming that only one client at a time will access their objectsMTS does the rest. This greatly simplifies component development.
The role-based security services in MTS offer both declarative and programmatic access. Role-based security is interesting (as I'll explain a little later), but declarative access to services is a concept that could be used for many other services. Instead of writing code to pass data to initialization functions of system services, wouldn't it be easier to just set an attribute value and let the system figure out how to get the value to the service? Another advantage to declarative access is that it presents the opportunity to defer complete initialization of attribute values until the application is deployed. For example, with role-based security, you merely say what role is required by a particular component. The administrator defines the actual user accounts for each role later.
MTS also shows one limitation of the current COM architecture: there is no standard mechanism for adding external services to COM. Of course, you can always define interfaces and write components. But what if you want to hook into the object activation process, or watch method calls? MTS munges the registry so that it is invoked when an object is created, then creates a wrapper that sits between clients and the actual object. This works well for MTS, but what will happen if a second service comes along that wants to do the same thing?
COM is a pretty successful object model, with widespread tools support and a thriving third-party market for components. And yet, as I explained, there are areas for improvement. COM+ is intended to address these areas. The primary goals of COM+ are:
- To make it easier to build COM components
- To address key issues in developing and deploying COM-based apps
- To provide new services for COM developers
- To provide a standard extensibility mechanism for incorporating new innovations
Figure 1 The Evolution of COM
The first two points are addressed by the COM+ runtime. As shown in Figure 1, the COM+ runtime evolved from work done at Microsoft to make writing COM objects easier with specific languages. The final two points are addressed by services built on the COM+ runtime, which evolve from and extend earlier work in COM and MTS. Together, the runtime and services are known as COM+.
Making it Easier
Today, a developer of COM-based objects (or the creator of a COM development toolset) must worry about many issues seemingly unrelated to the actual functionality of your components (see Figure 2). Every component must provide an implementation of IUnknown to provide reference counting and QueryInterface services. Clients of an object must correctly use reference counting to manage the object's lifetime and must use QueryInterface to gain access to the object's features. Every component also requires a class factory that knows how to create objects of a particular type, along with packaging code needed to get the component properly initialized in the system at runtime. Furthermore, components need to provide registration information. New interfaces must be described through IDL, which generates proxy/stub DLLs and type libraries. If a component needs to be accessed from scripting languages, it must implement Automation support via IDispatch and other interfaces. Components that want to fire events and clients that want to receive events must implement the IConnectionPoint interfaces. You even have to consider whether the toolset you're using honors the COM binary standard for object layout in memory.
Figure 2 Writing Components
Much of the code for implementing IUnknown, IDispatch, events, class factories, and component packaging is nearly identical for all components. Thus, the first goal of COM+ is to supply a runtime that provides a default implementation to handle these issues for the most common scenarios (see Figure 2). Developers merely implement classes to handle the application logic and provide information describing class characteristicsthe COM+ runtime does the rest. No special code is required to register the class, describe its functionality to clients, provide a class factory to create objects, manage object lifetime, or anything else that is not directly related to the business logic encapsulated by the component. If the runtime implementation doesn't suit your needs, you can always use traditional COM techniques to provide a custom implementation.
If you use tools like Visual Basic, you may be wondering what the big deal is; Visual Basic already handles all
these issues for the component developer and component client. The advantage of a system-supplied runtime is
that the same runtime can be used by everything, irrespective of the development language. This provides performance benefits and more consistent component behavior. But the true benefits of COM+ for Visual Basic-based development are the services and extensibility mechanisms I'll describe later.
A second goal of COM+ is to address key issues in developing and deploying COM-based applications. Many of the difficulties you're likely to experience when creating components are related to defining new interfaces through IDL. With COM+, interfaces are defined using standard programming languages; you don't need to master IDL or manage a separate interface definition file. Tools use the COM+ runtime to output metadata describing the interfaces. The metadata is sufficient to generate proxies and stubs automatically, which helps simplify the process of building and installing components.
COM+ also defines a simpler, more robust model for registering, installing, and versioning components. This model builds on the best of the Win32® code download services, MTS packages, the Windows NT® class store, and COM administrative tools, and combines them into a consistent, easy-to-use service.
A package is a single, deployable unit of code containing one or more classes. A package can also represent a concept, such as a user or a machine. Information about packages is stored in a well-known location on each machine. All of the information can be maintained by the developer's tools or system administrators. There is no need to write code to create registration information.
The registration service provides a mechanism for overriding the version of a class associated with a package. This helps solve one of the more vexing issues of component applications. For example, say application A uses version 1.0 of component C and works just fine. Application B ships version 2.0 of component C and works just fine, but application A stops working. With the COM+ runtime, both versions of component C can be used, version 1.0 for application A and version 2.0 for application B.
Developers can easily take advantage of these services. For new components, you'll just write the classes, provide class attributes, and let the COM+ runtime do the rest. Most of the work in migrating components will actually
be deleting code. The resulting components are COM components and will work with any tool that can use COM components today, but these new components are easier to write and install than traditional COM components.
New Runtime Services
COM+ also offers many new, optional services through its runtime. These services are optional because they may potentially impact the way both components and their clients are written. You should carefully consider whether each service provides any benefit to your applications.
First, COM+ lets you expose classes directly, not just interfaces on the classes. Methods are exposed directly on the class. Clients don't need to worry about interface pointers, reference counting, or QueryInterface. Instead, objects are accessed through object references. The objects are still reference-counted COM objects, but the runtime handles all the gory details. Sounds great, right? Well,
yes, but tools need to understand what object references are in order to use this, and existing client code would need
COM+ also supports implementation inheritance. If you elect to expose classes directly, you also have the option of making your classes subclassableor of subclassing existing classes yourself. COM+ doesn't solve all the problems associated with implementation inheritance: there's still the fragile base class issue, and versioning is a nightmare. Figuring out when your subclass is supposed to call the base class methods it's overriding isn't much better (you probably won't have the source for the base class). And this doesn't even begin to consider the impact on clients. But in certain circumstances, implementation inheritance is usefuland COM+ will let you use it if you need to.
COM+ helps prevent memory leaks through garbage collection. As you might imagine, garbage collection could have drastic consequences for both your class and its clients. You don't need to worry about reference counting and you can get away with circular references without needing some mechanism to break the circle. But this means your objects might not have a predictable lifetime. Depending on how garbage collection is implemented, objects may not get destroyed when the reference count goes to zero; they get destroyed when the garbage collector decides to destroy them. This has huge implications if your object hangs on to expensive or limited resources. In addition, certain language features you've come to take for granted may not be compatible with garbage collection (for example, the C runtime library). The details of how this feature will be implemented haven't been determined yet.
The most difficult aspect of COM for most developers
to master is securing access to components. Because of this, the COM+ runtime provides a security model that combines and extends the security models provided by COM, MTS, the Microsoft Java Virtual Machine (VM), and Authenticode. It provides multilevel user and code-
access security while simplifying the development and deployment of components that must be secured. Access checks are based on two key abstractions: roles and privileges. A role is an abstract group of users. Privileges provide access to resources.
To use access security, you define roles played by users of your components and interfaces by declaring attributes. You don't need to assign specific user accounts to rolesthat's done by administrators during deployment. You also specify the privileges required by your component. Administrators assign privileges to user accounts. At runtime, if the active user account does not participate in the role you specify for your component or does not have the privileges you require, access to the component is denied. The beauty and power of this approach is that security has been abstracted to a level at which developers and administrators can easily establish secure access to components and applications, without requiring expert knowledge of the underlying operating system security provider.
Code-trust security is also provided based on Authenticode and zones. Authenticode technology is a code-signing mechanism insuring that the code you get is the same as the code made available by the provider, and that you know the identity of the provider. It is used for code download and installation. Zones are a concept introduced with Microsoft Internet Explorer 4.0. A zone is a group of code sources that is granted the same privileges. Trust security works in conjunction with access security. When access checks are performed, not only must the user be a member of the required roles and have the required privileges, the component itself must be from a zone with the required privileges.
In COM+ all of these services are provided to COM objects regardless of the language those objects are implemented in. It's incredibly easy to expose existing classes as COM objects, so you don't need to rewrite your code. Plus, you can pick and choose the services you want to use. COM+ lets you choose the language and services that work best for your component or application.
Services for Distributed Applications
The features discussed so far make it much easier to write components than before. But I haven't addressed the difficulties inherent in writing component-based distributed applications. This is where COM+ really shines. COM+ introduces a general extensibility mechanism called interception. COM+ extensions are called interceptors, and they can receive and process events related to instance creation, calls, returns, errors, and instance deletion. This enables very powerful services to be provided as interceptors. COM+ itself uses interceptors to provide data access and distributed services.
The cool thing about interception is how you access these services in your components: you simply set some attributes on your classes. Developers can enter these attributes from an integrated development environment (IDE) by using language syntax or property editors. Some attributes can even be edited externally by system administrators. The interceptors are used to interpret those attributes and automatically enable the appropriate services. In addition to this declarative method of accessing services, you can also set attribute values programmatically. The same general process applies to all services: no new APIs to learn, no complex code to obscure your application logic, just a few attributes to select.
Services that MTS provides today will become part of COM+. For example, an interceptor is installed to process the transaction class attribute. This interceptor provides the same functionality as today's MTS, but with less work on your part. In addition, you can specify object state independent of clientsas you can today using MTSmerely by specifying a value for the state class attribute.
Similarly, other services provided by COM today will be provided as interceptors in COM+, giving you declarative access to the functionality needed for high-performance distributed applications. For example, to indicate the concurrency model supported
by a class, you would simply specify the threading attribute. The interceptor that supports this attribute would ensure that client access to individual objects is correctly synchronized.
COM+ will also include innovative, easy-to-use services based on features pioneered by COM-enabled tools such as Visual Basic. A key service is declarative data binding. This is very similar to the data binding currently offered by Visual Basic in forms. In Visual Basic, you drop data source and data-bound controls on a form, bind the data source control to a database, and bind the data-bound controls to specific database fields. With COM+, classes are decorated with a class attribute of the database to connect to. A class contains one or more data source fields that also have some of their properties defined, such as the SQL statement to execute. Data-bound fields are associated with a data source field and columns of its query results. Whenever the data source field is updated, all bound fields are updated with values that match the associated columns. Data binding is enabled by a binding engine, which is a runtime interceptor for all instances of classes that have data binding support. This engine ensures that when an instance is activated, the proper data source connection is obtained and associated with the instance.
In addition to the vast array of useful features for component writers and application developers, COM+ is an enabling technology. Interception opens the doors to a whole new category of COM extensions. And COM+ focuses entirely on core COM features, leaving encapsulation of higher-level features, such as controls, to application frameworks.
Now that I've shown you the new features COM+ offers, let's take a quick look at its architecture. Figure 3 shows the basic COM+ components and how they relate to other parts of the system. Developers generally do not call COM+ services directly from their component or application source code. Instead, those services are automatically invoked as needed by developer tools.
Figure 3 COM+ Architecture
The first set of services provided by the COM+ runtime is interfaces for creating and consuming class, interface, and library definitions. As shown in Figure 4, you define a class by writing source code. The language you use will define a mechanism for specifying that the class should be exposed via COM. The language compiler or interpreter uses the COM+ runtime compiler services to create metadata that describes the class, along with the usual binary form of the class code. The compiler services are also used to import metadata describing other classes, interfaces, or libraries used in the source code. Calls to other runtime services will be inserted into the binary class code by the compiler or interpreter based on keywords and attributes specified in the source code. The binary class code and metadata are used by a linker to generate redistributable class packages, which are typically Windows® PE-format DLLs.
Figure 4 Creating a Component
The COM+ runtime also provides a set of interfaces for registering and installing classes and packages. Information about packages and classes is stored in a well-known location on each machine. Packages are identified by GUIDs, but also have user-friendly display names and may be arranged in a hierarchy. In addition to specifying a local path to a package, a URL indicating where the package can be obtained may also be specified. If a package is not installed locally or provided with a code download URL, the COM+ runtime will query the class store for the package.
Classes are also identified by GUIDs, and are associated with a package and version information. When classes are built, they are assigned unique class version identifiers. When a client is built, its metadata includes a version context and information about the classes it depends onincluding the versions it was built with. By default, the loader will use the latest version of a class. However, if that doesn't work, an administrator can specify an override so that the client uses a particular version of one or more classes (typically, the versions the client was built with).
At application runtime, the COM+ runtime interfaces are used to load classes and create objects (see Figure 5). When a client application is built, startup code is automatically linked in with the application to initialize the runtime. The initialization function calls the runtime loader to load classes defined within the application and any referenced classes. The loader builds a global class table (GCT) and creates an in-memory representation of each class. The in-memory representation is interesting because it is how COM+ hooks in default implementations of standard COM interfaces. When a client wants to create a new object, the request is passed to the COM+ runtime. The runtime looks in the GCT to find the in-memory representation of the class and figures out how much memory must be allocated for the object. It then allocates the memory and initializes it to look like a COM object, before passing a reference to the object back to the client.
Figure 5 Loading Classes and Creating Objects
Finally, the COM+ runtime provides interception services. Because COM+ is involved in loading classes and creating objects, it can insert itself into just about any interesting aspect of a class or object's lifetime. It does this using thunks (see Figure 5). When interesting events occur to a class or object, the thunk intercepts the request and informs one or more interceptors. I'll take a closer look at interception in a future article.|
Key Benefits of the Architecture
Restricting interaction with the COM+ runtime to a narrow, well-defined set of interfaces makes it extremely flexible. With component technologies like COM and Java, which define specific in-memory and on-disk layouts, it's difficult to introduce innovative new features with broad tool support while continuing to maintain backward compatibility. The COM+ runtime encapsulates all knowledge of component in-memory and on-disk representations behind its interfaces. Not only can representations be modified without affecting tools or components, but multiple representations can be supported by the COM+ runtime simultaneously.
The ability to support multiple on-disk representations is important. Today, components are packaged as DLLs or EXEs, or as .class files for Java. In the future, additional component packaging mechanisms are likely to be introduced. By hiding the details of how a component is stored on disk, the COM+ runtime can readily support interoperability between many kinds of components.
Flexibility in memory representation is also important. COM specifies a binary in-memory representation (referred to as the vtable layout). Compilers and tools are required to lay objects out using this format. This tends to be restrictive for cases where there is a need to change the in-memory representation of objects over time.
More natural language mappings are a result of this architecture. Because interfaces are defined by calling the COM+ runtime interfaces, no text-based format for interface definitions is implied. In particular, a separate interface definition language is not needed. Developers can define interfaces within their chosen language. The language parser can detect that an interface is being defined and make the appropriate calls to the COM+ runtime.
Because these interfaces are part of a runtime, object behavior can be defined on the fly. The interceptor architecture lets the system and installed extensions modify behavior of some or all objects of a given class when certain events occur in each object's lifetime. It's even possible to declare new classes at runtimean essential feature for today's scripting languages and interpreted environments.
One of the primary motivators behind COM+ is to narrow the gap between objects as viewed by languages or tools and objects as viewed by COM. COM+ does this while maintaining interoperability with existing object definitions by virtualizing objects. The COM+ runtime maintains an internal representation of the object and associates an object reference with it. Tools create, manipulate, and destroy objects exclusively through the COM+ runtime object interfaces. Tools can present information about objects to developers using the natural object semantics of their language. In essence, an object becomes whatever it needs to be to work with its clients and other objects.
Evolution, Not Revolution
In recent years, component-based development has become a widely accepted method for creating solutions that meet increasingly complex and ever-changing user needs. By selecting components that model real-life objects within the users' problem domain rather than components tied to a specific solution, developers can often use those components in multiple solutions. In addition, since components and client applications communicate through well-defined interfaces, the component implementation can be easily modifiedwhether to fix problems or to address changing business needswithout impacting clients.
COM+ builds on and enhances COM. Existing COM components, including ActiveX controls, can be used side-by-side with components that use COM+ services. Components and clients can be moved forward to use COM+ services, when appropriate, with minimal changes to classes that implement your business logic. Because COM+ provides additional infrastructure services, code to manage this infrastructure can be eliminated from individual components and clients.
COM+ does not force everyone to learn a new language, a new API set, or a new way of writing code. You can use the languages, high-level RAD tools, APIs, and frameworks you are most comfortable with. One of the key innovations of COM+ is to provide a natural mapping from language-specific object models to the system object model via its compiler and object runtime services.
COM+ builds on lessons learned during the adoption of COM and other component-based technologies. It is an evolutionary step to easier, more productive development for the next generation of component-based applications. In a future article, I'll take a look at the COM+ programming model, how it builds on and simplifies the COM programming model, and how it maps to language-specific programming models. See you then!
From the November 1997 issue of Microsoft Systems Journal.
Get it at your local newsstand, or better yet, subscribe.