If you're familiar with C++ classes but new to thinking about components, you may
find it instructive to know how the two compare. Although each has its own niche in
Macintosh software development, components and C++ classes have many features in
common.
In general, both components and C++ classes encourage a building-block approach to
solving complex problems. But whereas a component is separate from any application
that uses it, a class exists only within the application that uses it. Components are
intended to add systemwide functionality, while classes are intended to promote a
modular approach to developing a program.
We can also compare components and C++ classes in terms of how they're declared and
called, their use of data hiding and inheritance, and their implementation. But first,
let's briefly review what a class is and what a component is.
SOME BASIC DEFINITIONS
A class, in the programming language C++, is a description of a data structure and the
operations (methods) that can be performed on it. An instance of a class is known as an
object. Classes are provided in C++ to promote an "object-oriented programming
style." By grouping a data type and its methods together, classes enable programmers
to take a modular approach to developing a program.
A component, as described in the preceding article ("Techniques for Writing and
Debugging Components"), is a single routine that accepts as arguments a selector and a
parameter block. The selector specifies which of several (or many) operations to
perform, and the parameter block contains the arguments necessary for that
operation. Components are "registered" with the Component Manager and can be made
available to either the program that registered the component or to any program that's
executed, making it possible to add systemwide functionality. For instance, if Joe's
Graphics Corporation develops a new image compression technique, it can be sold to
users as a component. Users install the component simply by dragging an icon into a
folder, and that form of image compression is then automatically available to all
programs that make use of graphics.
DECLARING CLASSES AND COMPONENTS
A C++ class is declared in much the same way as a struct, with the addition of routines
that operate only on the structure described. Once the class is declared, instances can
be declared in exactly the same way as other variables. That is, to create an instance of
a class, you either declare a variable of that class or dynamically allocate (and later
deallocate) a variable of that class.
A component must be registered with the Component Manager. At that time, its type,
subtype, manufacturer, and name are specified. The type, subtype, and manufacturer
are long integers; the name is a string. Component instances can only be created
dynamically, using specific Component Manager routines. To create an instance of a
component that has been registered, a program must first find the component. If the
seeking program is the same one that registered the component, it already has the
component. If not, it can make Component Manager calls to search for all available
components with a given type, subtype, and manufacturer; any part of the description
can be a wild card.
Once a component has been found, it must be opened, and this operation produces a
reference to the component instance. Operations can be performed on the component
instance using this reference.
Table 1 compares how classes and components are declared and how instances of each
are created. (Note that for components, the code is idealized.)
CALLING ALL ROUTINES
Calling a routine that operates on a C++ object is slightly different from making a
standard routine call: the call more closely resembles a reference to an internal field
of a struct. The routine that gets called is identical to any other routine, except that
it's declared within the class definition rather than at the same brace level as the main
routine.
Calling a component routine is identical to calling any other routine. The first
argument is always the component instance, and other arguments may optionally
follow. The return type of every component routine is a long integer, and part of the
numerical range is reserved for error messages from either the component or the
component dispatch mechanism.
The Component Manager lets a program issue calls to a component that it has never
"met" before. This form of dynamic linking is crude, because no type checking is
performed.
Table 1 compares how classes and components are called.
DATA HIDING
A C++ class can have "private" fields and methods, which are accessible by class
methods but not by the caller. The programmer can see these private parts simply by
perusing the class declaration. If a change to the implementation of a class requires
that the private parts be changed, relinking with the implementation of the class won't
be sufficient: all clients must be recompiled, since the positions of public fields might
have changed. (One tricky way around this is to include a private field of type char *
that's really a pointer to the class's internal state data. The class constructor allocates
memory for whatever internal state it likes and coerces a pointer to it to live in that
char * field. This technique is useful for object-only software library distribution
and also protects proprietary algorithms from curious programmers.)
A component is responsible for allocating memory for its internal state (the
component's "globals") when it's opened and releasing that memory when it's closed.
There are both component globals and component instance globals. These correspond to
static and automatic variables in a C++ class and have similar utility. A component
might keep track of how many instances of itself have been opened and restrict that
number by failing on the open call.
INHERITANCE
It's often useful to build software on top of existing functionality or, alternatively, to
take existing functionality and alter it to perform a more specialized function. Both of
these things can be accomplished for C++ classes with inheritance. In the former case,
the new class will have methods that don't exist in the base class; in the latter, the new
class will have methods with the same name as methods in the base class but that take
precedence over the base methods.
Components and the Component Manager support both kinds of inheritance as well, as
discussed in the preceding article. All components of a given type must support the
same set of calls, although this is enforced only by convention. Components of a
particular type and subtype may optionally support other calls as well, and
components of a particular type, subtype, and manufacturer may support still more
calls. In the case where a component wants to use the services of another component
and perhaps override some of its functions with modifications, Component Manager
utilities let a component designate another component as its "parent." A simple
protocol ensures that the correct variant of a routine gets called. When a component
must call itself, it must issue the call to its child component, if any. When a
component wants to rely on the existing implementation of the parent component, it
must pass the call to its parent.
IMPLEMENTING CLASSES AND COMPONENTS
My discussion of implementation is based on the 68000 platform, since that's the only
one I've scrutinized with regard to compiled C++ and Component Manager calls.
The routines that can be used with a C++ class are declared, and optionally
implemented, within the class declaration. They behave like normal C routines, as
described earlier.
A call to a C++ class that has no parents or descendants is compiled as a direct
subroutine call, exactly as is a standard routine call. A call to a C++ class that has
parents or descendants is slightly more complicated. A table lookup is used at run time
to determine exactly which implementation of a routine gets called for the particular
object being operated on. Such a call takes perhaps a dozen assembly instructions.
A component consists of only a single routine. It's passed a selector and a parameter
block. The selector is used to decide which operation to actually perform, and the
parameter block contains all the arguments passed by the caller.
The component's parameter block is untyped -- the component routine has no way to
determine what kinds of arguments were originally passed, and herein lies the danger.
Some languages, such as LISP, have untyped arguments; in LISP, however, a routine
can determine how many arguments have been passed and what the argument types are.
A component interface is more like assembly language -- or C without prototypes! --
in that it can determine nothing about what has been passed to it.
You can't compile a C++ program containing a call to a nonexistent routine; the
compiler will balk. (Well, OK, this isn't strictly true: there are dynamically linking
systems for C++, and other languages, that let you call a C++ routine that hasn't been
linked with the rest of the compiled source code; the routine can be linked to later, at
run time. But no facility of this type is currently standard in the Macintosh Operating
System or supported under the standard Macintosh development tools.) In the case of
components, the compiler can't check for such illegal calls, since the particular
components that may be opened are decided at run time. Therefore, the caller must be
prepared to handle a "Routine Not Implemented" error if a call is made with an
unknown selector.
All calls to components pass through the Component Manager's dispatch mechanism.
The dispatcher must locate the component's entry point and globals from the component
reference, which is not simply a pointer but a packed record containing an index into a
table and some bits used to determine whether the component reference is still valid. If
a client makes a call to a component it no longer has open, the Component Manager has
a statistical likelihood of catching this call and returning an appropriate error.
The Component Manager has facilities to redispatch the parameter block to one of many
routines, and those routines are written to take the arguments as originally passed.
The Component Manager was originally written for use on the 68000 series of
processor; on computers with that processor, the parameter block doesn't have to be
recopied onto the stack for further dispatching. On other processors the parameters
might have to be recopied, however.
The Component Manager has been highly optimized and fast dispatching can reduce its
overhead still more, but in general its lookup-and-dispatch process still takes several
dozen instructions. If the component being called is using the Component Manager's
inheritance mechanism, further overhead is incurred by passing control to the parent
or child component. Overall, the Component Manager is quite efficient, but still not as
efficient as direct routine calls. Table 1 compares how classes and components are
implemented.
IN SUM
Components, as supported by the Component Manager, exhibit many of the features of
C++ classes. Both encourage a modular approach to solving problems. Both feature
inheritance and data hiding. Where they differ is in how they're declared and
implemented, how they do (or fail to do) type checking, and how expensive they are to
call. Each occupies its own distinct niche in Macintosh programming: classes as a way
to ease development of a single program, components as a way to add systemwide
functionality and give control and choice to the user.
Table 1A Comparison of Calls: Classes (Actual Code) Versus Components (Idealized
Code)
Declaring a Class
class MyClass {
/* Variables and methods for
the class */
}
Declaring a Component
myComponent = RegisterComponent(MyEntryRoutine,
myType, mySubType, myManufacturer, "A Component");
Creating a Class Instance
MyClass x;
Creating a Component Instance
myComponent= FindComponent(myType, mySubType, myManufacturer); myInstance = OpenComponent(myComponent);
Calling a Class
x.MyMethod(arg1, arg2);
Calling a Component
result = MyMethod(myInstance, arg1, arg2);
Implementing a Class
class MyClass {
void MyMethod(int arg1, int arg2) {
/* Some code for MyMethod */
}
}
Implementing a Component
long MyEntryRoutine(ComponentParams *params, char *globals) {
switch(params->selector) {
case kOpen:
case kClose:
return noErr;
. . . /* other required calls here */
case MyMethod:
/* Do my method. */
/* arg1 and arg2 are in params. */ return noErr;
default:
return routineNotImplementedErr;
}
}
DAVID VAN BRINK is a computer programmer. When he's not busy programming
computers, he can usually be found writing computer programs. Mostly, he does this
in the soothing fluorescent glow of his cubicle at Apple. He's presently writing
components (with great fervor) to support musical synthesizers for QuickTime. *
We welcome guest columns from readers who have something interesting or
useful to say. Send your column idea or draft to AppleLink DEVELOP or to Caroline Rose
at Apple Computer, Inc., 20525 Mariani Avenue, M/S 75-2B, Cupertino, CA
95014.*
Thanks to Casey King and Gary Woodcock for reviewing this column. *