October 90 - WRITING A DEVICE DRIVER IN C++ (WHAT? IN C++?)
WRITING A DEVICE DRIVER IN C++ (WHAT? IN C++?)
TIM ENWALL
Most developers write device drivers in assembly language, rarely considering a
higher level, object-based language such as C++ for such a job. This article describes
some of the advantages of higher level languages over assembly and warns of some of
the gotchas you may encounter if you write a driver in C++. An example of a device
driver written in C++ follows a brief discussion of drivers in general.
When you think of writing a device driver, your first reaction may be, "But I haven't
brushed up on assembly language in some time." After taking a deep breath, you think
of another approach: "Why can't I use a high-level language?" You can. One such
language is C++.
In comparison with standard C, C++ offers some definite advantages, including ease of
maintenance, portability, and reusability. You can encapsulate data and functions into
classes, giving future coders an easier job of maintaining and enhancing what you've
done. And you can take advantage of most (but not all) of the powerful features of C++
when you write stand-alone code.
You will run into a few gotchas, including the fact that polymorphism is available only
if you do some extra work (for a definition of polymorphism, seedevelop,Issue 2, page
180). Because the virtual tables (vTables) reside in the jump-table segment, a
stand- alone code resource can't get at the vTables directly (more on this topic later).
You also have to deal with factors such as how parameters are passed to methods, how
methods are called, how you return to the Device Manager, how you compile and link
the DRVR resource, and how the DRVR resource is installed when the machine starts
up. We'll tackle some of these obstacles as we work through the sample device driver
presented later in this article.
WHY C++?
When someone suggests writing a device driver in anything other than assembly
language, the common reaction is, "But you're talking to a device! Why would you
want to use C++?
For communication with devices, assembly language admittedly gets the job done in
minimal time, with maximum efficiency. But if you're writing something where code
maintenance, portability, and high-level language functionality are just as important
as speed and efficiency, a higher level language is preferable. Not all device drivers
actually communicate with physical devices. Many device drivers have more esoteric
functions, such as interapplication communication, as in the sample driver in this
article. (In fact, DAs are of resource type DRVR and behave exactly the same way
device drivers behave. DAs are even created the same way.) For these kinds of device
drivers, C++ is a great language to use because you can take advantage of all the
features of a high- level language, plus most of the object-based features of C++.
Finally, device drivers have some nice features that make them appealing for general
usage:
• They can remain in the system heap, providing a common interface for
any application to easily call and use.
• They get periodic time (if other applications are not hogging the CPU).
Good examples of nondevice drivers are the .MPP (AppleTalk ®) driver and the .IPC
(A/ROSETMinterprocess communication) driver. Both these drivers provide pretty
high-level functionality, but neither directly manipulates a device as such (except for
the very low-level AppleTalk manipulations of communication ports). Of course, if
you were writing code to communicate quickly and efficiently to a modem, for example,
assembly language might be the better choice, depending on your need for efficiency
and timing. For the purposes of this article, any reference to a device driver includes
both types of drivers.
Clearly, higher level languages have a place, but what about object-based languages?
Object-based languages provide a great framework for encapsulation of data and
functions and hence increase the ease of maintenance and portability (if used
elegantly). One question still remains: Why C++?
Notables such as Bjarne Stroustrup and Stanley Lippman have pointed out some of the
advantages C++ offers over conventional high-level languages. C++ offers great
extensions, such as operator and function overloading, to standard C. C++ is much
more strongly type checked than C, so it saves us programmers from ourselves. C++
classes offer a way to encapsulate data--and functions that operate on the
data--within one unit. You can make different elements and functions "private" to
objects of only one class or "public" to objects of every type. The private and public
nature of data and member functions allows you to accomplish real encapsulation.
COMPARING C++ AND ASSEMBLY LANGUAGE
C++ ÎAssembly Language
Pros Portable ÎFast
Reusable ÎEfficient
Easy to maintain ÎCompact
Object-based design ÎDirect access to CPU
High-level language features
Data encapsulation
Cons Three separate source files Not portable
multiple compiles ÎHard to maintain
Speed inefficient ÎLacking high-level language
Polymorphism difficult features such as loops
in stand-alone code Îand IF-THEN-ELSE
SOME LIMITATIONS
As noted, one valuable feature of C++, polymorphism, is not readily available when
you write a device driver in C++. Other limitations involve working with assembly
language, possible speed sacrifices, work-arounds for intersegment calls, and mangled
procedure names.
POLYMORPHISM
Because a device driver is a stand-alone code resource, there is no "global" space or
jump table. C++'s virtual function tables (vTables), which are the means to the
polymorphism end, live in an application's global space. The loss of virtual tables is a
limitation of stand-alone code, not a limitation of C++. Patrick Beard's article,
"Polymorphic Code Resources in C++" (this issue), shows one way to work around
this limitation. The work-around takes some extra work and is dependent on the
current implementation of CFront, which may make future compatibility a problem.
In the interests of clarity and compatibility, I have chosen not to use polymorphism
for the example in this article.
ASSEMBLY-LANGUAGE MUCK
Another difficulty is that we have to get our hands assembly-language dirty. The
Device Manager is going to call the device driver with a few registers pointing to
certain structures, and we'll have to put those on the stack so the C++ routines can get
to them. Specifically, A0 points to the parameter block that is being passed, and A1
has a handle to the Device Control Entry for the driver. Having to do some assembler
work is a limitation of the operating system; the toolbox doesn't push the parameters
onto the stack (now if there were glue to do that--).
These registers must somehow make their way onto the stack as parameters to our
routines because procedures take their parameters off the stack. When we've finished,
we also have to deal with jumping to jIODone or plain RTSing, depending on the
circumstances. For the simple driver shown in the example, we will in reality almost
always jump via jIODone when finished with our routines. But, for drivers that
wish to allow more than one operation at a time, the Prime,Control, and Status
calls must return via an RTS to signal the Device Manager that the request has not
been completed. The driver's routines should jump to jIODone only when the request
is complete.
We must also decide whether or not to call a C++ method directly from the assembly
language "glue." If we call the method directly, we have to put the "this" pointer on the
stack because it's passed implicitly to all object methods. We also have to use the
"mangled" name generated by the compiler and used by the linker. (If you haven't had
the opportunity to see mangled names, you'll find they're a joy to figure out without
the help of our friend Mr. Unmangle.) So, if we choose to call extern C functions, as
the example does, we run into yet another level of "indirection" before we get to the
real meat of the matter.
SPEED
Some might say we sacrifice speed as well as efficiency--and they're correct. In
general, compilers can't generate optimally speed-efficient code. They can come close,
but nothing even approaches how the human mind tackles some tricky machine-level
issues. Thus, we're at the mercy of the compiler--the loss of speed is the result of the
compiler's inefficiency.
You'll probably find the sample driver presented in this article pretty inefficient. But
the trade-off is acceptable because speed isn't important in this case, and you can use
all the features of an object-based language. In fact, in most instances you can limit
assembly language to a few routines, which must be tightly coded, and use C++ for the
rest.
MANGLED IDENTIFIERS
If you're familiar with C++, you've undoubtedly seen the visions of unreadability
created by CFront. But, if you're still unfamiliar with C++ in practice, here's an
explanation. CFront is simply a preprocessor that creates C code, which is passed to
the C compiler. So CFront has to somehow take a function of the form
TDriver::iacOpen(ParmBlkPtr aParmBlkPtr)
and create a C function name the C compiler can understand. The problem is that when
the linker complains, it will use the mangled name, which is hard to decipher.
Here's how it looks:
from MPW Shell document
unmangle iacOpen__7TDriverFP13ParamBlockRec
Unmangled symbol: TDriver::iacOpen(ParamBlockRec*)
It's clear why these names are referred to as mangled and unmangled. Fortunately, the
unmangle tool provided with MPW allows you to derive the unmangled name from the
mangled.
A SAMPLE C++ DRIVER
The sample driver that follows illustrates some of the issues involved in writing a
device driver in general, and specifically in C++. The code is in the folder labeled C++