Modifying an existing application while it executes, without rebuilding from scratch, is a powerful way to enhance and correct large deployed systems such as operating systems. This dynamic extension is relatively straightforward with interpreted languages or virtual-machine based languages such as Java, in which new code is loaded upon demand. In languages typically implemented with static executable images this capability can be offered through DLLs or shared objects, depending on the underlying operating system). However, in practice it is impractical to make full use of this capability because the protocol for invoking subprograms in a shared library is very low-level and error-prone. You simply provide to an operating system routine a string containing the name of the intended function. The operating system routine returns the address of the function which is in turn used to call the function, regardless of whether it accepts the expected number of parameters or returns a value of the expected type.
Object-oriented programming with plug-ins makes this approach practical by using dynamic dispatching to invoke dynamically loaded functionality with a more robust, high-level protocol. In an OO paradigm, a plug-in contains instances of new classes that extend the set of classes in the original application. Calls to routines in the plug-in (represented by a shared library) are done implicitly through dynamic dispatching which is much simpler, transparent to the programmer, typesafe, and more robust.
In this article, we show how a statically-typed, statically-built, object-oriented language such as Ada can make full use of the notion of dynamic plug-ins. To that end, we build an extensible application and illustrate adding new functionality to it at run-time via plug-ins. Selected new features of Ada 2005 are used in the implementation and are highlighted. In particular, we use the "distinguished receiver" syntax familiar to users of C++ and Java. We also use one of the new "containers" data structures packages. The main program and plug-ins are built with the GNAT Ada 2005 implementation developed by AdaCore.
About Ada 2005
The Ada language was originally designed in the 1980s to meet the requirements of large, mission-critical, long-lived systems only characteristic--at the time--of applications developed for the U.S. Department of Defense. Today those characteristics are typical in commercial systems as well.
Under the auspices of the International Organization for Standardization (ISO) Ada has continuously evolved since its inception, incorporating facilities that exploit new developments in hardware and software technology. Over the past few years, new features have been proposed and considered. Although these revisions have not yet been formally approved by ISO, they have been developed to the point of broad consensus that is expected to lead to acceptance. The newest instance of the language is informally referred to as "Ada 2005" to distinguish it from its predecessors. Officially the name will always be "Ada" and we will use the two names interchangeably.
The demonstration application is a crude simulation of instruments on an automobile dashboard, with the actual instruments in separate plug-ins. Instruments are thus loaded at run-time such that the simulation can take newly created instruments into account without having to be restarted.
A common code base is shared by both the application and any plug-ins to ensure type consistency. In the GNAT model, this base is represented by a dynamic library that is initialized along with the main program. The code base consists of the package representing the dashboard along with the package declaring the abstract Instrument class. This abstract class is the base class for the extensions defined by the plug-ins, as Figure 1 illustrates.
The code base also includes the object representing the discovered instruments logically present on the dash board. Essentially this object is a registry of access values designating instrument objects created by the plug-ins during plug-in elaboration. For example, a fragment of the package body for package Dash_Board follows. Note the instantiation of one of the new Ada 2005 data structure packages on line 2. The package Ada.Containers.Vectors provides an extensible array abstraction, including operations to append and remove from arbitrary index locations as well as to the logical end of the vector (line 8 in the code below). In the instantiation, we specify subtype Positive as the index subtype for the internal array. More importantly, the element type contained within this vector is specified as Any_Instrument, an access-to-class-wide type designating type Instrument'Class so that the vector can (indirectly) contain any type of instrument, regardless of when the instrument subclass is created.
1. package Instruments is 2. new Ada.Containers.Vectors (Positive, Any_Instrument); 3. 4. Registry : Instruments.Vector; 5. 6. procedure Register (Device : Any_Instrument) is 7. begin 8. Registry.Append (Device); 9. end Register;
All plug-ins call this Register procedure for the objects they allocate locally, passing access values designating those objects. You should understand that the single Registry object is shared among the main program and all the loaded plug-ins. Figure 2 illustrates the relationship of the Registry content to the instances within the plug-ins.
The main program simply iterates over the content of the Registry, accessing the designated instrument subclass instances to update and display their states. The number of objects displayed changes over time as new plug-ins register new instances with the Registry. We discuss how the update and display operations are invoked shortly.
Dynamic Plug-In Linking
When the main program runs, the code base is dynamically linked to it so that the base becomes part of the application. Similarly, plug-ins link to the code base when they are loaded into the main program at runtime. This dynamic linkage is essential so that only one of any shared objects actually exists (in this case, the Registry object). This linkage is possible because of the way the main program, base, and plug-ins are built.
Building the main program involves first creating the base shared library and then creating the main program that is linked against that library. We use GNAT "project files" for this purpose. Project files encapsulate switch settings and source file information, among other things, and make building applications convenient. In particular, building libraries (that is, plug-ins) is trivial with project files. Our "main" project file references a "base" project file shared by all the plug-ins and the client main program. This base project file is as follows:
1. project Base is 2. for Source_Files use ("dash_board.adb", "indash.adb"); 3. for Object_Dir use "obase"; 4. for Library_Dir use "lbase"; 5. for Library_Name use "base"; 6. for Library_Kind use "dynamic"; 7. package Compiler is 8. for Default_Switches ("ada") use 9. ("-g", "-gnat05", "-gnatwcfkmruv"); 10. end Compiler; 11. end Base;
The syntax is intentionally close to that of Ada packages and attribute definition clauses, with extensions. This particular project file is used to create a library because it contains attributes specifying library information. For example, it specifies a dynamically loadable library (line 6) named "base" (line 5) to be placed in subdirectory "lbase" (line 4). It contains two source files (line 2) and compiles the resulting object files into a subdirectory named "obase" (line 3), using compilation switches that enable debugging, Ada 2005 features, and various warnings (line 9).
The project describing the client main program references both the "base" project and a project describing the Windows Win32 Ada binding, such that those source files and switch settings are available during the build:
1. with "win32ada"; 2. with "base"; 3. 4. project Main is 5. 6. for Source_Files use 7. ("demo.adb", "plugins.adb", "hashed_strings.ads", "h.adb"); 8. for Main use ("demo"); 9. 10. package Compiler renames Base.Compiler; 11. 12. for Source_Dirs use ("."); 13. for Object_Dir use "."; 14. 15. end Main;
Using the project file involves merely naming it as an argument to the tools. We use the gnatmake tool that performs all processing required to build the requested unit:
C:\Source\demo>gnatmake -P main
Thus, gnatmake generates a main program executable named "demo" (line 8) using the sources in the current directory (line 12) and those named in particular on line 7. The compiler will use the same switches defined by project "base" (line 10) and will put the object files and executable in the current directory (line 13).
The project files for the plug-ins create dynamic libraries like the base library and so are similar to the base project file.
Dynamic Plug-In Loading
Plug-ins are loaded as they are discovered by the main program in the directories local to where the main program executes. We simply search for all files with an extension indicating a DLL or shared object. (This is an uncontrolled, and therefore, unrealistic approach, but it suffices.) Loading is accomplished using the interface provided by the particular operating system in use. For Windows this is the LoadLibrary routine. LoadLibrary takes the full path name of the DLL, loads it into memory, and returns a handle that can be used in subsequent API calls. If the returned handle is null the load failed and we raise an exception. In the UNIX version of the facility we use the lopen routine in a similar manner. These differences are hidden behind a single interface with two different implementations in the form of two different package bodies. The UNIX version is as follows:
1. function Load (Path : String) return Plugin is 2. 3. function dlopen (Lib_Name : String; Mode : Interfaces.C.int) 4. return System.Address; 5. pragma Import(C, dlopen, "dlopen"); 6. 7. RTLD_LAZY : constant := 1; 8. C_Path : constant String := Path & ASCII.Nul; 9. Result : Plugin; 10. begin 11. Result.Ref := new Implementation; 12. Result.Ref.all := Implementation (dlopen (C_Path, RTLD_LAZY)); 13. if Result.Ref.all = Implementation (System.Null_Address) then 14. Free (Result.Ref); 15. raise Not_Found with Value (dlerror); 16. end if; 17. return Result; 18. end Load;
The dlopen routine is imported from the C implementation provided by UNIX, on line 5. Note the use of the type int provided by the standard package Interfaces.C on line 3. This package has been available since Ada 95 was defined and provides Ada type definitions that correspond to the predefined types in C. Note also the syntax for raising an exception with a string message on line 15 that is new with Ada 2005. The string raised with the exception in this case is provided by the function Value defined in package Interfaces.C.Strings. The input to Value comes from the function dlerror, also imported from UNIX as follows:
1. function dlerror return Interfaces.C.Strings.Chars_Ptr; 2. pragma Import (C, dlerror, "dlerror");
Package Interfaces.C.Strings defines Ada types and routines specific to manipulating C string values.
Dynamic Dispatching to Plug-In Operations
In an older version of our Windows plug-in facility, a plug-in (DLL) required manual initialization once loaded. (This limitation has since been removed.) The initialization procedure was named by the catenation of the plug-in name and init. For example, a DLL named "clock" would have be initialized by calling a routine named clockinit in that DLL.
This initialization was an example of the low-level protocol that is less robust than dynamic dispatching: given the name of a function, the operating system returns an address to use to call that function. No function signature verification takes place.
Our plug-in abstraction provides a routine for making these low-level calls. The implementation of this routine illustrates the issue:
1. function Call (P : Plugin; Unit_Name : String) return Interfaces.C.int is 2. Name : aliased constant String := Unit_Name & ASCII.Nul; 3. F : FARPROC; 4. Result : Interfaces.C.int; 5. use type FARPROC; 6. begin 7. -- get a pointer to the function within the plugin 8. F := GetProcAddress (HINSTANCE (P.Ref.all), As_LPCSTR (Name'Address)); 9. if F = null then 10. raise Not_Found with Unit_Name; 11. end if; 12. -- now we call the function via the pointer and capture the result 13. Result := F.all; 14. return Result; 15. end Call;
The unit name is used to get an address for the corresponding routine within the DLL (line 8), which is then used to call the routine (line 13). The signature of the routine ("FARPROC") is assumed to be correct but clearly there is no guarantee. ("FARPROC" is a Windows-defined pointer to subprogram type designating a function that returns a value of type Interfaces.C.int.)
In contrast, the procedures Dash_Board.Display and Dash_Board.Update, called by the main program, illustrate high-level dynamic dispatching calls. Both procedures iterate over the shared Registry of instrument object pointers maintained by package Dash_Board, making dispatching calls to each instrument. The body of Display follows. (Procedure Update is essentially identical.)
Note the use of the "distinguished receiver" method invocation syntax on line 5. This is the "object.operation (parameters)" notation used by popular object-oriented languages such as Java, Eiffel, and C++ that has been added to Ada 2005.
1. procedure Display is 2. C : Cursor := First (Registry); 3. begin 4. while C /= No_Element loop 5. Element (C).Display; -- dispatches 6. Next (C); 7. end loop; 8. Ada.Text_IO.New_Line; 9. end Display;
On line 5 we call the function Element from the Ada.Containers.Vectors instantiation to get the access-to-class-wide value at the current cursor C. This access value designates an instrument object to which we can apply the Display operation, thereby dynamically dispatching to a Display routine specific to the type of the instrument. The call therefore dispatches from the main program to an instance allocated within the plug-in, and it does so in a typesafe manner because the invoked operation's signature is specified by a declaration in code that is common to both the plug-in and the client main program. The fact that the object is of a type not necessarily in existence when the main program was compiled is completely transparent to both the programmer and the client main program.
We have demonstrated that the concept of dynamically linked and loaded "plug-ins" invoked via dynamic dispatching provides a high-level, robust mechanism compared to the typical approach using only string names to identify the operation to invoke. Selected new Ada 2005 features and constructs have been demonstrated as well. In particular we used the Ada.Containers.Vectors generic and the distinguished receiver syntax.
You may be surprised that such dynamic behavior is possible with Ada, given that the language has not been in the public eye as much as other languages. Ada is, in fact, a powerful object-oriented programming language that continues to be improved and enhanced with features that raise the bar in programming language design.
The source code for this demonstration is available from the authors at comar @adacore.com and [email protected], respectively.