Gem #110: Ada Plug-ins and Shared Libraries — Part 2

by Pascal Obry —EDF R&D

Let's get started...

In the first part of this two-part series we saw that stand-alone shared libraries have the required properties to be used as dynamic plug-ins. In this Gem we explore how to load and unload code dynamically within an Ada application. In essence, the dynamically loaded code is compiled into a shared library, and when this shared library is modified it is reloaded automatically.

To accomplish this we need three modules:

  • Registry -- A module handles plug-in loading, unloading, and service registration
  • Computer -- A plug-in doing a simple computation using two integers and returning the result
  • Main -- The main application that uses the computer plug-in

Registry

Each plug-in will inherit from a common type named Any. The associated access type Ref is used to record all loaded plug-ins in a hashed map:

package Plugins is   
   type Any is abstract tagged null record;
   type Ref is access all Any'Class;
   --  A reference to any loaded plug-in
end Plugins;

Our Computer plug-in is described by:

package Plugins.Computer is
   
   Service_Name : constant String := "COMPUTER";
   --  Name of the service provided by this plug-in
   type Handle is abstract new Any with null record;
   type Ref is access all Handle'Class;
   
   function Call
     (H : not null access Handle; A, B : Integer) return Integer is abstract;   
end Plugins.Computer;

Note that this is only an abstract view that is shared by all the modules. This view describes all the routines supported by the plug-in. A concrete implementation of the computer plug-in will be given in the computer module.

The Registry spec is:

with Plugins;
package Registry is
   
   procedure Discover_Plugins;
   
   procedure Register
     (Service_Name : String; Handle : not null access Plugins.Any'Class);
   
   procedure Unregister (Service_Name : String);
   
   function Get (Service_Name : String) return access Plugins.Any'Class;
   
end Registry;

The Register, Unregister, and Get routines are trivial. The reference to the plug-in service is recorded in a hashed map by Register, removed by Unregister, and retrieved by Get. This part does not need further discussion.

The Discover_Plugins routine is the tricky one. Here is how it works. This routine scans the plug-ins directory for shared libraries prefixed by "libplugin_". If such a name is found, it is renamed by removing the "plugin" substring and loaded by the dynamic linker (see Shared_Lib.Load routine in the registry sources of the Gem's zip file). When the shared library is loaded, the elaboration code is used to register itself (that is, register the service name associated with the object reference) in the registry.

At this point the service is available and can be used by the main application.

Note that a map with all loaded shared libraries is kept. If a shared library is found to be loaded already, it is unloaded first. This calls the finalization code, which is used by the plug-in to unregister itself.

   procedure Discover_Plugins is
      
      function Plugin_Name (Name : String) return String is
         K : Integer := Strings.Fixed.Index (Name, "plugin_");
      begin
         return Name (Name'First .. K - 1) & Name (K + 7 .. Name'Last);
      end Plugin_Name;
      
      use Directories;
      use type Calendar.Time;
      
      S          : Search_Type;
      D          : Directory_Entry_Type;
      Only_Files : constant Filter_Type :=
                     (Ordinary_File => True, others => False);
      Any_Plugin : constant String :=
                     "libplugin_*." & Shared_Lib.File_Extension;
   begin
      Start_Search (S, "plugins/", Any_Plugin, Only_Files);
      while More_Entries (S) loop
         Get_Next_Entry (S, D);
         
         declare
            P     : Shared_Lib.Handle;
            Name  : constant String := Simple_Name (D);
            Fname : constant String := Full_Name (D);
            Pname : constant String := Plugin_Name (Fname);
         begin
            --  Proceed if plug-in file is older than 5 seconds (we do not want to try
            --  loading a plug-in not yet fully compiled/linked).
            if Modification_Time (D) < Calendar.Clock - 5.0 then
               Text_IO.Put_Line ("Plug-in " & Name);
               if Loaded_Plugins.Contains (Pname) then
                  Text_IO.Put_Line ("... already loaded, unload now");
                  P := Loaded_Plugins.Element (Pname);
                  Shared_Lib.Unload (P);
               end if;
               --  Rename plug-in (first removing any existing plug-in)
               if Exists (Pname) then
                  Delete_File (Pname);
               end if;
               Rename (Fname, Pname);
               --  Load it
               P := Shared_Lib.Load (Pname);
               Loaded_Plugins.Include (Pname, P);
            end if;
         end;
      end loop;
   end Discover_Plugins;

The Shared_Lib spec comes with two bodies, one for Windows and one for GNU/Linux. The proper body is selected automatically.

Note that the renaming of the plug-in is required on Windows, as it is not possible to write to a shared library which is in use.

Computer

Here is the specification of the Computer module that is an implementation of the abstract type described above:

with Plugins.Computer;
package Computer is
   type Handle is new Plugins.Computer.Handle with null record;
   overriding function Call 
     (H : not null access Handle; A, B : Integer) return Integer;
end Computer;

The body is straightforward. The Life_Controller is used to control the Computer object's life. The Initialize procedure (called when the plug-in is loaded) registers the service name and the Finalize procedure (called when the plug-in is unloaded) unregisters the service name.

with Ada.Finalization;
with Registry;
package body Computer is
   
   use Ada;
   
   type Life_Controller is new Finalization.Limited_Controlled with null record;
   overriding procedure Initialize (LC : in out Life_Controller);
   overriding procedure Finalize (LC : in out Life_Controller);
   
   H : aliased Handle;
   
   overriding function Call 
     (H : not null access Handle; A, B : Integer) return Integer is
   begin
      return A + B;
   end Call;
   
   overriding procedure Finalize (LC : in out Life_Controller) is
   begin
      Registry.Unregister (Plugins.Computer.Service_Name);
   end Finalize;
   
   overriding procedure Initialize (LC : in out Life_Controller) is
   begin
      Registry.Register (Plugins.Computer.Service_Name, H'Access);
   end Initialize;
   LC : Life_Controller;
   
end Computer;

Main

The main is the easy part. We just loop and once every second we discover plug-ins and call the Computer service if found.

with Ada.Text_IO;
with Plugins.Computer;
with Registry;
procedure Run is
   use Ada;
   use type Plugins.Ref;
   
   H      : Plugins.Ref;
   Result : Integer;
   
begin
   loop
      Text_IO.Put_Line ("loop...");
      Registry.Discover_Plugins;
      H := Plugins.Ref (Registry.Get (Plugins.Computer.Service_Name));
      if H /= null then
         Result := Plugins.Computer.Ref (H).Call (5, 7);
         Text_IO.Put_Line ("Result : " & Integer'Image (Result));
      end if;
      delay 1.0;
   end loop;
end Run;

To build and run the program, unpack the zip file attached to this Gem and execute the following commands:

$ gnat make -p -Pmain

$ gnat make -p -Pcomputer

$ ./run

Now, while the application is running, edit computer.adb and replace the addition in Call by a multiplication. Then recompile the computer plug-in:

$ gnat make -p -Pcomputer

After some time you'll see that the new plug-in code has been loaded automatically.

Note that an extended example illustrating dynamic loading and unloading is included as part of the GNAT examples.

plug-in.zip