Plug-ins can be an effective way to minimize platform-specific code in the core of an application. This section describes places where plug-ins are appropriate. The specifics of writing plug-ins for Mac OS X are described in depth in “Dynamic Libraries and Plug-ins.”
Effectively managing plug-ins can be tricky when dealing with multiple platforms. You may choose to have a plug-in for each platform, or for each service, or both. The choice of methodology depends largely on the amount of code that you need to put into an external module.
If you have only a few platform-specific bits, it is probably sufficient to have a single per-platform plug-in. A good place for such a design is in an application that needs, for example, to support a single feature in both Mac OS 9 and Mac OS X. By isolating this block of code into an external module, the appropriate piece can be loaded according to the platform in which the application is launched.
Another approach is to have a separate plug-in for each distinct service from the operating system. This is particularly effective if the number of services is significant. Separating services into separate modules makes debugging each service easier from a version control point of view.
There are several right ways to design a plug-in architecture and many, many more wrong ways. The most important features of a plug-in architecture are extensibility, simplicity, extensibility, robustness, and extensibility, with emphasis on extensibility.
Ensuring that an architecture is robust is the responsibility of the implementors, and is beyond the scope of any design document. You should consider extensibility and simplicity early and often during the design process.
In general, the simpler the plug-in API, the more likely people will use it rather than grafting hacks into other parts of the code. Of course, this becomes a problem if you fail to expose features that are needed for a given plug-in. To solve this problem, design your API based around message passing concepts rather than function calls. That way, you can easily add additional types of messages to the API without modifying the API itself.
Of course, if your plug-in API only needs to handle one particular type of communication and you are relatively certain that this will not change, a plug-in architecture based on functions is somewhat easier to use. As with all designs, there are tradeoffs.
Extensibility in API design is a tricky issue. When you are creating a plug-in architecture in general, you can rarely envision the sorts of plug-ins that might eventually be useful. Someone might want plug-ins to add additional functionality that no one had even thought of, much less invented, when you created the architecture
You can also use plug-ins to abstract the interface presented by platform-specific services into a more generic form to simplify your application. These designs are more straightforward than architectures intended for adding new functionality; creating plug-in architectures for adding features are beyond the scope of this document.
The first step in designing a plug-in API for service abstraction is to choose the level of abstraction at which the application ends and the plug-in begins. If you choose a level that is too close to the application, the plug-ins will contain redundant code. If you choose a level that is too far removed, you will eventually need to interface your application with an environment that does not meet your initial expectations. As a result, the interface code for that module will become excessively complex. In either case, a rewrite is probably in order, which wastes time and resources that could have been saved had you chosen the right level of abstraction to begin with.
Choosing the level of abstraction for a plug-in interface can range from straightforward to downright impossible, depending on the application. In general, you should choose to split out the smallest amount of code possible such that the following conditions are met:
No assumptions are made about the data structures of the underlying service.
Return values are standardized in a way that meets the needs of your application.
The calling convention can be implemented on any system that provides the support necessary for your application to function—that is, no special features of the underlying service should be implied at the plug-in API level or in your application.
The data types passed to and from the plug-in API are one of two kinds: either base types in the programming language of choice, or composites of those base types that are structurally relevant to the application as a whole rather than to the underlying service.
Designing a plug-in API in this fashion may, however, result in substantial duplication of code. In such environments, a nested plug-in approach may be more practical.
The concept of nesting plug-ins is straightforward. You should consider using nested plug-ins when you find numerous opportunities for dividing an application from its plug-ins—that is, when dealing with a general class of underlying services divided into a number of subclasses that contain multiple specific variants with common characteristics.
Nested modules are convenient for:
Authentication—a generic authentication layer with a generic plug-in for UNIX-based systems and specific plug-ins below to handle platform-specific variations
Databases—at the top level, you might have ODBC, JDBC, STET, and SQL, with submodules for other, more specific SQL implementations
Printing—for example, you might have a plug-in that handles PostScript printers with narrower plug-ins that override generic assumptions for a particular printer model
In short, whenever you have a group of underlying services that are substantially similar in behavior, but where you want to be able to also support services with dramatically different behavior, a nested plug-in approach is an effective design (unless the differences in behavior at the lowest level are very minimal).
A good example of plug-in API design is database access. As an example, consider a project that uses a database to obtain song playlist information.
Such a program requires various specific pieces of information from the database. For a given song, it might need the title, the artist, the total length, the intro (talkover) time, the time at which the next song should begin playing (trigger time), and the time at which the song should be faded out, if applicable. In addition, a comments field could be included for composer or other genre-specific information. This is referred to as the internal data level.
To facilitate support for arbitrary databases, you add the first layer of abstraction at the internal data level. The core code calls functions with names like getsongname and getsongtrigger. Any arbitrary database, regardless of the query language used, can easily return a string or an integer (or at worst, be coerced into doing so). This is considered the minimum level of functionality needed to support this application, and thus forms the first plug-in split.
However, a surprising number of databases use a common syntax, SQL, for making requests. Although the syntax is the same, certain details (data types) are different, and the libraries used for accessing them are also different. For these reasons, supporting multiple SQL servers is a desirable goal, because you can use most common databases as storage.
Because the SQL instructions themselves are so similar between databases, the second split comes somewhat naturally. The code to actually execute a query can be placed in an implementation-specific module, and the code to generate the query—for a song’s name, for example—can be placed in a generic SQL language module. The API for the implementation-specific module could, for example, include:
getdbc—Returns a pointer to a database connection object. The SQL core code treats this as an opaque type, but this is necessary to allow many database implementations to be used in a multithreaded environment. This information should not leave the SQL core unless the larger design requires connections to multiple databases simultaneously.
opendb—Opens a database connection.
closedb—Closes a database connection.
dbquerystring—Executes an SQL query and returns the first result as a string.
dblistsongs—Returns an array of song IDs that match an SQL query.
It might strike you as odd that this design doesn’t specify a generic SQL query call. This is largely a space–time tradeoff. Implementing a generic query would allow the GUI, for example, to request all of the information about a call in a single request. However, doing so would add a great deal of code for a relatively small time benefit. Since the GUI does not need to refresh frequently, and since the number of queries per second, therefore, tends to be relatively small, it makes sense to design the API to be as simple as possible. If low latency and high bandwidth are required for the specific application, a generic routine is desirable.
If you are familiar with SQL, though, you are probably aware that the similarities among SQL implementations end at inserting new tables into the database. Because this is such a small piece of the overall picture for this type of application (one or two lines of code total), it could easily be “special-cased” in the core code or (preferably) in the generic SQL code. This is one of those cases where you must decide whether the extra 1% of functional abstraction is worth the extra effort involved. In many cases, it is not.
If creating tables is more significant, you could create them in one of two ways: by adding a function in the implementation-specific plug-in, or by adding a function in the generic SQL plug-in, using a table for the data types (which could, if desired, reside in the implementation-specific plug-in).
To demonstrate the effectiveness of this design, MySQL database support was added to an application that used this exact design in about an hour. With the exception of table creation, no modifications to the generic SQL support routines were required.
In summary, proper plug-in design is much like proper abstraction. Code that can be reused should be reused, but the API to the plug-ins should not assume any knowledge of the underlying system.
Last updated: 2008-04-08