A Cross-Platform Plug-in Toolkit

XPIN, the cross-platform plug-in toolkit Ramin presents here, helps you create portable applications that (currently) run on Windows and the Macintosh. Ramin's toolkit consists of a caller API and a plug-in skeleton.


June 01, 1993
URL:http://www.drdobbs.com/open-source/a-cross-platform-plug-in-toolkit/184409021

Figure 1


Figure 2


Copyright © 1993, Dr. Dobb's Journal

JUN93: A CROSS-PLATFORM PLUG-IN TOOLKIT

A CROSS-PLATFORM PLUG-IN TOOLKIT

Creating dynamically extendable applications

This article contains the following executables: XPIN.ARC XPIN.SIT

Ramin Firoozye

Ramin heads rp&s Inc. in San Francisco, California and can be reached on the Internet at [email protected] or at 70751,252 on CompuServe.


Plug-ins, which have been popularized in recent years on the Macintosh by programs like Adobe Photoshop, may well represent the ultimate embodiment of modular software--they allow you to update or add functionality without the need to patch your application. This is possible because a plug-in, much like a resource, is loaded by the host application at run time. Photoshop uses this approach to provide additional support for special effects and import/export filters.

The plug-in concept is not specific to the Macintosh. But as useful as plug-ins are, they introduce platform-specific elements into your application. Because my work often takes me from the Macintosh to the PC, I developed the Cross-Platform Plug-in Toolkit (XPIN) presented in this article. The toolkit consists of two parts: a caller API and a plug-in skeleton. The caller API is comprised of a set of routines that make an application plug-in aware. The plug-in skeleton is a set of declarations that can be used to build from simple to fully functional plug-ins. The XPIN toolkit takes care of all the underlying bookkeeping necessary for looking up, managing, and invoking plug-ins. The complete source code for XPIN is available electronically; see "Availability," page 7.

The toolkit currently has been tested against a number of C compilers on both the Windows (Borland and Microsoft) and Macintosh platforms (Think and MPW C), and ports to Windows NT and SunOS are under way.

Plugging In

A plug-in is a small, self-contained program that cannot be invoked by itself. A plug-in aware program (or caller) acts as a host for a plug-in. At run time, the caller looks for all plug-in files in a given directory and registers their presence. The caller can then invoke a plug-in at any time during execution. However, the caller has no idea what it may encounter at run time. The only predefined constants are the directory to search and the file type to locate. When found, the plug-ins become integral parts of the main program. Since the act of locating the plug-ins occurs at run time, no special registry or patching mechanism is required: Just copy the plug-in file into the directory and restart the program.

I had two general goals in creating the XPIN toolkit. The first was source-level compatibility across multiple platforms. The caller API would be identical across Macintosh and Windows, as would the plug-in skeleton. You take the same plug-in skeleton from one system to another and recompile it without any changes. The contents of the plug-in, however, are as portable as you make them. The toolkit provides some compile-time portability mechanisms to help isolate system dependencies. The second goal was simplicity in the interface. The caller API consists of six C-language functions that look and behave identically across all platforms.

As an example, Figure 1 shows what the menu bar for a sample caller program looks like when executed. Everything up to this point looks incredibly ordinary! Figure 2 shows the same program. This time, however, two plug-in files have been copied into the application's directory. Note that the application has detected the presence of the plug-ins, obtained their labels (an internal value that has nothing to do with the filename), and added them to the menu bar. The user can now invoke each of these plug-ins as if they were integral parts of the original code.

Adding this functionality to the sample caller program requires about 30 extra lines of code (see Example 1), primarily having to do with handling the menu. This source code was directly copied from a Macintosh to a PC and recompiled. The platform-specific functions of the plug-in were isolated using the appropriate #ifdefs, but were built without further code modification.

Example 1: Adding functionality to the sample caller program.

  #include "XPIN.h"
  #ifdef OS_MAC
  #include <Dialogs.h>
  #endif
  XPIN(xblk)
  {
  int localVarsGoHere;
  DescribeXPIN(xblk, "Dialog", "Description of Dialog", 1, 0);
  #ifdef OS_WIN
      MessageBox(NULL, "This is a sample dialog box...",
                       "Dialog Plug-in Message",
          MB_ICONSTOP | MB_YESNO);
  #elif OS_MAC
          ParamText("\pThis is a sample dialog box...",
                  NIL, NIL, NIL);
          Alert(128, NIL);
  #endif
      XSETSTATw(xblk, 0);     // Return 0 as status
  EndXPIN();
  }

Plug-in Identification

The toolkit defines six functions for use by the caller; see Table 1 . To distinguish plug-in files from other files, a "type" is assigned to each plug-in. I decided to let the operating system help identify files of this type. This was faster and simpler. On a Macintosh every file has a creator and a type associated with it. These values allow the Macintosh Finder to automatically find and launch a program when one of its data files is double-clicked. On the Macintosh, all plug-ins of one category must have a given finder type, for example, all Photoshop import filters of type 8BAM. There are routines in the Macintosh Toolbox that allow a directory to be scanned for all files of a given type, effectively performing a wildcard search. A happy side effect of this mechanism is that the Macintosh Finder assigns individual icons to each file type. Thus, plug-ins can have icons that visually distinguish them from other files.

Table 1: Caller functions.

  Function   Description
  -------------------------------------------------------------------------

  XPINInit   Initializes internal data and searches the toolkit for
             plug-ins.  Those found are asked to provide a label and a
             description.  These are cached in the caller data space for
             quick access.

  XPINCount  Returns the number of plug-ins found.

  XPINLabel  Returns label associated with a given plug-in.

  XPINDesc   Returns the description string associated with a given
             plug-in.  This could be used for an "About" message or a
             simple help screen.

  XPINCall   Calls a single plug-in.  A general-purpose parameter block is
              used to pass arguments to the plug-in.

  XPINDone   Called before the program exits.  The internal data structures
             are deallocated.  Each plug-in is also sent a "done" message
             to allow it to deallocate any space it may have allocated.

No such intrinsic type can be associated with a file on the PC. You do, however, have the three-letter file extension. Many DOS programs (including the command processor) use this file extension to locate special-use files (for example, COMMAND.COM allows files of type .EXE, .COM, and .BAT to be executed by simply typing the filename). Moreover, there are corresponding DOS functions that return files based on filename wildcards.

Since it is possible for a non-plug-in file to be accidentally assigned a plug-in type value, the toolkit validates all files it locates to make sure they are legitimate plug-ins. The toolkit effectively ignores all "bad" plug-ins.

The toolkit does not apply any architectural limits to the types and numbers of plug-ins a single program can handle. To stop runaway searches, the toolkit stops the directory search after 100 files, a constant defined in a header file. This limit could be easily updated or removed altogether. An application program can also support multiple plug-in types. Each type can be located in a different directory (or they can all be located together). The overhead for the plug-in structure is small. The main internal tracking structure takes about 120 bytes of space. Each plug-in takes about 300 additional bytes. The number of internal structures is limited by available memory. The plug-in structure is allocated off the heap and released when XPINDone is called.

The Pathname Dilemma

The way directory pathnames are identified under each operating system is a problem when writing cross-platform software. For example, the directory-name separator under DOS is the backslash character. The Macintosh uses the colon, and UNIX uses the forward slash. There are other syntactic and semantic differences in pathnames that go beyond the scope of this article. Although more polished solutions are possible, XPIN requires you to make sure the right pathname was passed to the toolkit on each platform. This seems like a cop-out, but it turns out that there are two "blessed" locations under both operating systems: the application's home directory and the environment's system directory. On the Mac-OS, this is the System Folder; under Windows, it's the directory in which Windows is installed. So the toolkit supports two "aliases" as a replacement for the plug-in search path. The $HOME and $SYSTEM names are aliases that are replaced with absolute paths at run time. They can be used on their own (for maximum portability) or incorporated into an absolute path (for example, $HOME\PLUGIN\ or $HOME:PlugIn:). The other advantage of using aliases is that $HOME allows the user to freely move the entire application with plug-ins from one directory to the other, and the $SYSTEM alias simplifies booting off different volumes. The application and its plug-ins can be located easily, since the aliases are resolved at run time.

Passing Parameters

The toolkit takes care of much the background work, like locating and invoking the plug-ins. However, you must consider other issues. An application needs to be able to exchange information between itself and the plug-in. In the case of plug-ins, the compiler does not have the luxury of knowing the parameter types being passed. In a static program, the compiler and linker know the type and number of parameters being exchanged between a program and a function. The compiler often takes care of all typecasting and parameter conversion. Parameters are usually passed to a function using the stack. The compiler generates the instructions necessary to pass the right number of parameters onto the stack and to restore the stack after the function returns. The function being called expects the stack to be in a well-defined state. The only exception to this rule is the varargs mechanism (first popularized under UNIX). Despite its flexibility, I chose not to use this mechanism because of implementation discrepancies across platforms.

At run time, the compiler has no idea what it may encounter in a dynamic situation like the one faced with plug-ins. The solution to this problem is the parameter block (called the XBlock in the toolkit). The number and type of parameters between the caller and the plug-in function are predefined. However, one of the parameters is a pointer to an XBlock structure. The caller is free to load anything it wants into the XBlock as long as the plug-in is designed to expect the proper values in the right order.

The alternative to using a parameter block was to devise a dynamic runtime parsing mechanism that supported a stream of variable-length arguments, each identified by their data type and size. Despite its flexibility, I decided against this "parameter-stream" approach due to its overhead. I will probably revisit the issue at some future time; for now, however, the XBlock mechanism appears to be sufficient. The XBlock is a fixed-size block of arguments. (The number of slots can be changed in a header file.) Each slot can be either a byte, word, long, or pointer (a far pointer under Windows). The values in the slots can be easily accessed through a series of XSET# and XGET# macros (where # is b, w, l, or p). Example 2 shows the definition of the XBlock structure.

Example 2: Definition of the XBlock structure.

  #define XBLOCK_MAXARGS 5
  union   Arg {
          UPtr     p;    // Pointer
          Ubyte    b;    // Byte
          Uword    w;    // Word
          Ulong    l;    // Longword
          };
  typedef union Arg Arg;
  struct XBlock {
      Uword  action;                 // action code (XOP)
      Arg    args [XBLOCK_MAXARGS];  // array of args
      Arg    status;                 // result sent back from XPIN
  };

The XBlock also has space for an action code and a status value. You can define the action code. It is most useful for defining behavior common to all plug-ins. Internally, the toolkit reserves two action codes to get the plug-ins to return their label and description fields (XOP_INIT) and to perform cleanup action (XOP_DONE). Other popular attempts at plug-ins either require fixed parameters (Adobe Photoshop) or the use of parameter blocks (HyperCard).

Coming up with a flexible specification for the XBlock is one of the crucial tasks of application programmers who want to create plug-in aware programs. If not correctly specified, ill-defined plug-ins will instantly throw the main program into spasms. It behooves you to define how the application intends to pass parameters to the plug-in (and to properly document it). The published specification can be used by enterprising third-parties to write undoubtedly amazing plug-ins.

What I've been calling "plug-ins" are, in reality, stand-alone code resources under the Macintosh environment and dynamic link libraries (DLLs) under Windows. I've just simplified their interfaces. Under the hood, however, they have to obey the rules and restrictions of their host operating environment. These are not debilitating restrictions. The main thing to consider is that using plug-ins requires an understanding of how programs use memory, particularly in the form of the stack and the heap. (It was illuminating to discover that Macintosh CODE resources and Windows DLLs have similar but opposite memory requirements.)

Windows plug-ins (DLLs) have no stack of their own. When loaded, they share the stack of the calling application. This means that the plug-in should not assume the presence of a large amount of stack space. What's left is the space the application started with minus the space used before invoking the plug-in. If the plug-in accidentally exceeds the stack space, the heap will almost certainly become corrupted. Allocating space on the heap, however, carries much less risk. The well-behaved plug-in should check the status returns from all memory-request calls and gracefully handle errors. You should also be aware that Windows keeps only one copy of a DLL in memory at any given time. This can cause problems with static variables declared inside a DLL.

Macintosh CODE resources, on the other hand, can have a stack. However, they take care in how they access global variables (A5-globals) and the heap (which is accessed as an offset from A5). Both the Apple MPW and the Symantec Think development environments provide ways to access globals. If you think this might not apply to you, I should mention that QuickDraw uses A5-globals. If you are not careful, the first time you use a QuickDraw function inside the plug-in will lead to the gallows. The plug-in skeleton code takes care of access to A5-globals but you should be aware of the limitation the Mac OS applies to stand-alone CODE resources.

One final rule about memory management and plug-ins: Leave the world the way you found it. If a plug-in allocates any memory, it should deallocate it before returning. Unless previously arranged, allocated memory cannot be retained by plug-ins. A better solution is for the calling program to allocate enough memory and pass its address to plug-ins through the XBlock. The plug-ins can be assured of a safe, clean area to operate in. To have persistent memory, the plug-in can return a value to the caller through the XBlock that indicates that the block of memory should be preserved by the caller.

Presentation

How should plug-ins be shown to the user? There seems to be some sort of a secret agreement that plug-ins should be added and invoked through menus. In fact, there's no need to limit plug-ins to the menu bar. The menu just happens to be a user-interface element that can be dynamically changed at run time. It is, of course, up to you to associate plug-ins with the appropriate user-interface elements in their applications.

The toolkit associates a string label and a description string with each plug-in. These are maintained internally by each plug-in and have nothing to do with filenames. The plug-in returns these values to the caller when the toolkit first finds and initializes all plug-ins. The developer is free to use the label in any way. Internally, each plug-in is identified by an index (starting at 0). It is up to you to devise a way to associate the menu (or whatever) items with the index for each plug-in. For example, the first element of the plug-in menu can be associated with the 0th plug-in index. When the user selects the first item in the menu bar, the application program is required to translate that command to a call to the plug-in toolkit with 0 as the plug-in index. The second menu may be associated with index 1, and so forth.

This isn't as hard as it may sound. A simple lookup table or a clever menu-numbering scheme will do nicely. In the sample caller application provided with the toolkit, all plug-in menu items start at a base value (1000) and go up sequentially. When the application gets a menu command with an ID above the base value, it simply subtracts 1000 to get the 0-based plug-in index and makes the call to the toolkit with that index.

Missing Features

I deliberately left a number of features out of this version of the XPIN toolkit, the most important being callbacks. A callback is a function inside the main application that can be called by a plug-in. Callbacks allow plug-ins and the application to maintain a two-way dialog and exchange information freely. I kept callbacks out because of serious cross-platform incompatibilities they would introduce into the toolkit, especially in the use of stacks and globals. A future version may add this feature if an appropriate scheme can be found. Designing generic callbacks required making assumptions that varied between different applications and unnecessarily complicated the design. An application that requires the plug-in to call it back can pass the address of the callback entry point inside the XBlock. I leave it to the developer to devise a callback mechanism that works with the individual application.

As mentioned before, each plug-in has a label and a description associated with it. This brings up the other feature that was left out: icons. I would have liked to have associated icons with each plug-in. These could be used, for example, in button strips or toolbars. There is a certain attraction to a plug-in that carries all its baggage in a single package. However, icons are among those system-dependent entities that require special treatment. Again, simplicity was used as the driving goal, and icons were dropped. It is possible, however, for an application to associate icons with each plug-in index, just as it was possible to associate a label with a menu item.

Cross-platform Support

To be able to write portable code, you need to know the environment and platform on which you're running. And on each platform, there are a number of popular development tools, each with their own idiosyncrasies. Determining which system and compiler are being used can either be done when compiling or at run time. For performance reasons, I chose the compile-time option. C provides #ifdefs to allow the compiler to strip out code that does not meet certain conditions. To help break the code into common and platform-independent code, the toolkit provides the XCONFIG.H #include file. Including this file on top of every source file will enable the proper #defines that can be used by subsequent #ifdefs.

The toolkit takes advantage of certain predefined settings and combinations of values to help define a common set of constants. This eliminates the need for a large amount of hand editing of #include files or special "configuration" programs often needed when supporting cross-platform programming. The values set by XCONFIG.H (a file that's available electronically, see "Availability" on page 7) are shown in Table 2. I found these settings to be sufficient for most cases. In fact, I used these settings for the toolkit itself. The entire finished source code for the XPIN-toolkit library can be copied between the PC and Macintosh systems and recompiled without modification. (It was surprising how much of the toolkit worked on both systems and did not require the use of conditional compilation.)

Table 2: Environment-settings flags.

  OS_SUPPORTED      Platform
                    supported
  ---------------------------------

  OS_MAC            Macintosh
  OS_WIN            Windows
  OS_NT             Windows/NT
  OS_UNIX           UNIX

  COMPILER_MSC      Microsoft C/C++
  COMPILER_BORLAND  Borland C++
  COMPILER_THINK    Think-C
  COMPILER_MPW      MPW C/C++
  COMPILER_GNU      GNU C/C++

  LANGUAGE_C        C language
  LANGUAGE_CC       C++ language

Conclusion

Writing portable programs requires planning and re-examination of your assumptions about a given platform or environment. The XPIN plug-in toolkit should introduce a larger audience in the developer community to the benefits of plug-ins. At the same time, it should remove a technical barrier to developing robust cross-platform programs. Publishing plug-in interfaces is a good way to encourage other programmers to extend your application and make it more attractive to end users.

References

Inside Macintosh, volumes I-VI. Apple Computer.

Klein, Mike. Windows Programmer's Guide To DLLs and Memory Management. Carmel, IN: Sams, 1992.

Macintosh Technical Note #256: Stand-Alone Code. Apple Computer, August 1990.

Rollin, Keith. "Another Take on Globals in Standalone Code." develop (December, 1992).

Think C Reference Manual, Version 5.0, Symantec.


Copyright © 1993, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.