INFO: Multimedia Group System and API Design Guidelines
ID: Q67692
|
The information in this article applies to:
-
Microsoft Win32 Software Development Kit (SDK)
SUMMARY
The Microsoft Multimedia Systems Group is doing a large amount of
system design and implementation. This article comments on the areas of
system design.
MORE INFORMATION
Definitions
Term Definition
---- ----------
Module A module provides a set of functions and the interface to
access those functions. The interface is called the API.
Client A client uses a module. A client might be an application
or a dynamic-link library (DLL).
Prefix The initialization portion of the module that must be
called before any of the other functions can be accessed.
Postfix The closing portion of the module that must be called
after the client has finished using the functions of the
module.
Channel A channel is created by calling a module's prefix
function. It is used by the rest of the functions of the
module (including the postfix function).
Two notes about a channel:
- A module can allocate resources to a channel. For
example, a handle to a window is a channel, which has
memory, a window procedure, and many other resources
associated with it.
- The channel uniquely identifies the user of the
interface to the module. This allows the module to
perform functions uniquely on each open channel. For
example, each file handle represents a different file
that has been opened. Each file handle may have
different attributes (read, write, read/write, binary,
and so forth).
Separate Driver Interface from Module Interface
Separating the application from the hardware is one of the major tasks
of system software. Layers within the operating system are separated
from the hardware through the use of drivers. The system interface to
the application should hide the mechanisms of the drivers as much as
possible. This allows changing the mechanics of a driver in a later
version of the system software. This also preserves the formality of
the interface; applications are prevented from directly accessing the
hardware.
Each application should also be separated from the drivers within the
system. The driver API should not depend on where the driver is
located, what it is named, or what form it takes (DLL, VxD, and so forth).
A driver should shield the application writer from its internal
workings to enforce the principle of information hiding. If too much
information is available, the application writer may choose to
directly access the hardware, which jeopardizes the separation of
functionality provided by the driver mechanism and system API.
Every Module Should Have an Initialization Function
Requiring the application to call a prefix function before using a
system module and a postfix function after using a system module
provides three major benefits, as follows:
- Later versions of the driver can easily virtualize resources
because the driver defies a channel for all communication. The
driver can maintain separate resources for each communication
channel, which is required for virtualization.
Note that the complete virtualization does not need to be done in
the first implementation of the module. As long as the interface
requires the prefix and postfix calls, subsequent prefix calls can
fail with an appropriate error message. Future versions of the
system can properly handle multiple requests for the system
resource.
- The system module can allocate resources when the channel is
allocated (when the prefix function is called), rather than when
the system is initialized. Modules that are not called consume no
resources. Resource allocation for all modules is postponed as long
as possible.
- The system can resolve conflicts between clients through the
identification provided by the channels.
Interface Naming
During the process of designing a new module, use the interface naming
conventions from an existing module with similar functionality. For
example, an interface that deals with files would have a prefix
interface containing the word "open," and a postfix interface
containing the word "close." Extend naming conventions to similar
functional areas. A stream interface would also have an "open" and a
"close" function.
If the module has new functionality, then the functions can have
unique names, such as the MIDI driver's midiOutStop function. If the
module is similar to but not the same as another module, then use the
function name to distinguish between modules. For example, the
CreateWindow and CreateWindowEx functions in Windows create windows
but CreateWindowEx also allows an application to specify other
attributes.
The goal is to provide programmers familiar with existing modules a
basis by which to quickly learn the new interface.
Prefix function names with the module name (abbreviated, if
necessary). This allows the documentation to sort function names
alphabetically, while keeping related functions together. More
importantly, it allows easy identification of the module to which a
function belongs.
Definition Naming
Prefix the names of constants and data structures with the module name
(abbreviated, if necessary). For example, STRM_SEEK is a constant in
the STRM group. This allows easy identification of the module to which
a definition or data structure belongs.
As another example, MIDIOUTCAPS is the Device Capabilities structure
for the MIDI output module. Using the naming convention developed here
and symmetry (discussed in depth in the second part of this article),
the MIDI Input module Device Capabilities structure should be called
MIDIINCAPS.
Use additional prefixes as appropriate to identify the use of the
definition. For example, MOERR_NODRIVER is a definition in the MIDI
Output module to describe an error, that of no driver present.
Registering Drivers with Module
Most of the systems designed by the Multimedia group allow device
drivers to be installed by the original equipment manufacturer (OEM)
or even by the end user (given an appropriate setup program). There
are two main ways for these drivers to communicate with the main
module.
The first is to place an entry in the SYSTEM.INI file. When the parent
module loads, it loads the child driver and initiates communication
with the child.
The other method is for the child driver to call the parent to
register itself as a client. This second method presumes that there is
a suitable method available to load the child. Windows provides such a
mechanism.
Requiring a driver to register itself with the handler module provides
four benefits:
- Drivers can be installed by adding them to the "modules to load"
list. This is much easier than creating a line for the SYSTEM.INI file.
- The handler module is more general because it does not assume the
presence of certain drivers. This enhances system portability and
reduces interdependencies between drivers and handlers. This
advantage also applies to drivers loaded by a parent process.
- A driver can pass information about itself, such as its name and
entry points, to its parent during registration. This further
separates the parent module from the driver. As long as the format
of the interface data is fixed, independent changes may be made to
both parent and driver.
- Run-time installation of drivers is possible. The inherent nature
of registration makes installing new drivers while the system is
running much easier. This also simplifies implementing
virtualization.
Symmetry of Function Names
- Every Open function should have a Close function and every Get
function a Put function.
- Related functions in separate areas should work the same way. For
example, if the MIDI output has an Open, the MIDI input should also
have an Open. Additionally, the return values and parameters should
be as similar as possible. This eases the programmer's task of
learning the new APIs. This applies even if the current
implementation doesn't use API symmetry. See "Designing for
Implementation in Steps," below.
Symmetry in Naming Conventions
- Name defined constants and types for related areas should all be
named using the same conventions. For example, LPMIDICALLBACK and
LPWAVECALLBACK.
- If a naming convention already exists for a function type, adhere
to it. Example: use SEEK and TELL functions to move within a file
system.
- If any part of an existing convention is used, little deviation
from it is allowed. For example, a combination of SEEK and GET
functions to move within a file system would not be the product of
good design because it confuses an existing convention.
- If a convention does not already exist, create a new naming
convention to avoid confusing things. Example: KNOCK and ANSWER.
Design for Implementation in Steps
Most implementations of any size must be done in incremental steps of
functionality. More and more features are added to the modules until
the entire design is completely implemented. For large or complex
modules, this process may occur over several years. However, the
original design must anticipate the complete, final functionality, not
just the short-term goals. For example, even if allowing multiple
users of a module will not be implemented in the first phase, this
capability should be designed into the API. That way, the impact on
users of the module will be minimal once implementation is complete.
Avoid placing arbitrary limits on functionality due to details of the
current implementation. For example, even if only one user can have a
resource allocated today, this may not always be true. Specifically,
the Open function should return a handle to the resource that is then
passed to functions that manipulate the resource. In the future, when
multiple users of the resource is implemented, it will not be
necessary to change other functions or applications.
In a message-based system, functions should return a "message not
recognized" code for unexpected messages that is distinct from the "an
error occurred" code. Then, when a future version of the driver
contains extended functionality, an application can determine if the
installed version of the driver supports the new features. If not, the
application can take appropriate alternative action.
A project designed to be built in phases has well defined progress
milestones. This makes it much easier to track progress while the
module is under construction.
Building a module in phases also makes it easier to verify that the
module is built correctly. Testing receives increments of
functionality instead of the entire product toward the end of the
development cycle.
Error Reporting
An function call can fail for many reasons. It is best if the call can
return the specific cause of the error in addition to noting that the
call failed. Functions that return a handle, structure, or other data
cause particular problems because there is a limited set of values
that are always invalid.
Three approaches to error reporting are:
- Ignore it (not recommended).
- Provide a separate "what was that error?" call. This is more
complicated than it sounds because, in a multitasking system, there
can be multiple users of the module at the same "time." This makes
determining what was the last error for a particular application
difficult.
- Return the handle or structure in a parameter and return the error
code as the function return. This seems to be the best option, and
is the approach used by OS/2.
Now that the error code is available, what should be done with it? To
allow for internationalization and for additional error codes, the
application should not associate the error code with a message.
Instead, provide a function in each API that returns the text message
for a specified error code. This function might be named
GetTextErrorInformation, for example.
Client-Supplied Buffers
It is desirable for the client application to provide all buffers that
it will access. If a system module allocates and maintains buffers,
many implementation problems can arise when a buffer is made visible
to the client application. Three advantages of client-supplied buffers
are:
- If the system software runs at a different privilege level or on a
different CPU, or is otherwise separate from the client
application, the system software can easily access the buffer.
However, at the client's lower privilege level, or if the client
and operating system are on different CPUs, it may be extremely
difficult (if not impossible) to make a system-supplied buffer
available to the client.
- When the application supplies the buffers, the application has
complete control over how much memory the system module uses.
- The application is responsible for reporting an out-of-memory
error. This removes an error condition from the system call.
Additional query words:
3.00 3.10 3.50 4.00 95 win16sdk
Keywords : kbmm kbAPI kbSDKPlatform
Version : WINDOWS:
Platform : WINDOWS
Issue type : kbinfo
Last Reviewed: March 31, 1999