Ian Sharpe - Technical Notes - Shared Libraries & Ada


Introduction

Shared libraries are binary files containing code that is intended to be linked with executing processes at run-time rather than during the link phase of executable development. This has a number of advantages:
Reduction in executable size
By placing common code in shared libraries, individual executables can be smaller. While a reduction in disk space is a worthy thing, a more important effect is a reduction in the total memory consumption when executing programs concurrently.
Finer version control
A suite of related programs can split related functionality into several shared libraries. Since these are not built in to any one program, it is possible to update the code of multiple executables by simply changing a single file.
Debugging/instrumentation
It is possible to create both 'production' and 'development' versions of the shared libraries and readily swap between the two. The development version may simply be the production version complied without optimisation or may contain extensive diagnostic outputs and profiling hooks.
Language independence
The shared library defines a callable interface. As long as a mechanism exists to call shared library routines (and ensure that the structure of the passed data is correct) the programming language of the caller and the library is unimportant. [Clearly this is rather idealistic]
Microsoft Windows, OS/2 and most forms of UNIX support two forms of shared library:
  1. static
  2. dynamic
The difference is in the way in which the shared library is loaded rather than in the library itself. In static linking, all of the references are known when the program is linked. For example, consider a simple shared library that contains two functions defined in C as:
  int   add_integer(int l, int r);
  float add_float(float l, float r);
A user of this library could call the above routines directly (by name) and the shared library linker would be able to resolve the names at 'link edit' time (i.e. when the executable was built). Thus apart from minor command line changes, using a statically-linked shared library is much the same as using a non-shared library.

Dynamic linking requires routines to be called to load the required shared library and lookup the address of symbols in that library. The user then has to call the routine at the specified address. Most systems also allow the library to be unloaded when no longer required. Two reasons to use dynamic shared libraries are

  1. when control of shared library loading/unloading is required, either to minimise resource usage or to select from several available libraries
  2. when the name of the routine to call is unknown at (executable) link-time, for example a data-driven state machine in which the state tables contain the names of routines to call and are loaded at run-time.

Static Shared Libraries in Ada

As indicated above, using statically-linked shared libraries is transparent as far as the code which calls library routines is concerned. Thus a simple test program to use the library specified in the Introduction is simply:
   procedure Test is

      function Add_Int(A, B: Integer) return Integer;
      pragma Import (C, Add_Int, "add_integer");
 
      A, B, C   : Integer;

   begin

      A := 1;
      B := 2;
      C := 0;

      C := Add_Int(A,B);

      if C = (A+B) then
         Text_Io.Put_Line("Add successful");
      else
         Text_Io.Put_Line("Add failed");
      end if;

   end Test;

Here the main program is in Ada, but the interface is assumed to be defined in terms of C.

Creating an Ada shared library has operating system, compiler and application specific aspects.

The simple test library can be trivially coded as:

    package Test_Lib is
       function Add(A, B: Integer) return Integer;
       function Add(C, D: Float) return Float;

    pragma Export_Function(Internal => Add,
			   External => add_integer,
			   Parameter_Types => (Integer, Integer),
			   Result_Type => integer);

    pragma Export_Function(Internal => Add,
			   External => add_float,
			   Parameter_Types => (Float, Float),
			   Result_Type => Float);
    end Test_Lib;

    package body Test_Lib is

       function Add(A, B: Integer) return Integer is
       begin
          return A + B;
       end Add;

       function Add(C, D: Float) return Float is
       begin
          return C + D;
       end Add;
    end Test_Lib;

The only noteworthy thing here is that the Add routines are explicitly exported with 'C-like' names. A 'non-shared library' executable can be built to test the basic system. For GNAT use the following:
  gcc -g -c Test_Lib.adb
  gnatmake -g Test.adb -largs Test_Lib.o
Note that the closure for Test no longer includes Test_Lib and so the Test_Lib object file must be explicitly passed to the linker. The resulting executable should be able to add two integers!

The details for compiling Test_Lib.adb as a shared library, and the subsequent binder/linker commands depend on the operating system being used. For OS/2 the following sequence works:

  gcc -g -Zmt -Zdll Test_Lib.adb Test_Lib.def -lc_dllrt
  emximp -o Test_Lib.lib Test_Lib.def
  emximp -o Test_Lib.a Test_Lib.lib
The first command creates the shared library (dll). The second two commands are required to create a COFF-style stub library so that references to the exported routines will be satisfied when Test.exe is built. A definition file (Test_Lib.def) is required to specify the routines being exported:
LIBRARY test_lib INITINSTANCE TERMINSTANCE
PROTMODE
DATA NONSHARED
EXPORTS
   add_integer
   add_float
Finally the executable itself can be created by:
  gcc -c -g Test.adb
  gnatbl Test.ali Test_Lib.a
The intermediate .lib file can then be deleted.

Dynamic Shared Libraries in Ada

Some of the details of dynamic shared library usage are unfortunately very operating-system specific. Most UNIX systems provide a C-defined interface like:
   void *dlopen(char *path, int mode);
   int   dlclose(void *handle);
   void *dlsym(void *handle, char *symbol);
   char *dlerror(void);
This can be mapped to an Ada package spec:
   package Dll is

      type Handle_Type is private;

      procedure Open(Handle : in out Handle_Type;
                     Name   : in     String;
                     Mode   : in     Integer := 0);
      procedure Close(Handle : in out handle_type);
      function Error(Handle : in Handle_Type) return String;

      generic
         type Item_Type is private;
      procedure Sym(Handle : in out Handle_Type;
                    Name   : in     String;
                    Item   :    out Item_Type);

      Dll_Exception : exception;

   private
      type Handle_Type is
      record
         Os_Handle : System.Address;
      end record;

   end Dll;
Here open, close and error map pretty much directly to the corresponding 'dl' functions.

In order to simplify the mapping of addresses to subprogram calls, the generic procedure Sym is used. This is instantiated with an access to the kind of symbol being accessed. Sym then allows the look-up to be made and returns an access to the appropriate object.

As an example, consider dynamically loading the simple library of the Introduction and use the routines to add two ints. [This is probably GNAT specific, or at any rate makes no effort to ensure that C-types map to Ada-types].

   procedure Test is
      type Shared_Function  is access function (L,R:Integer) return Integer;

      procedure Map_Shared_Function is new Dll.Sym(Item_Type => Shared_Function);

      Handle    : Dll.Handle_Type;
      Add_Int   : Shared_Function;
      A, B, C   : Integer;

   begin

      Dll.Open(Handle => Handle,
               Name   => "test.dll");

      Map_Shared_Function(Handle => Handle,
                          Name   => "add_integer",
                          Item   => Add_Int);

      A := 1;
      B := 2;
      C := 0;

      C := Add_Int(A,B);

      if C = (A+B) then
         Text_Io.Put_Line("Add successful");
      else
         Text_Io.Put_Line("Add failed");
      end if;

      Dll.Close(Handle);

   exception
      when Dll.Dll_Exception => Text_Io.Put_Line("dll exception: " & Dll.Error(Handle));

   end Test;
The procedure Test instantiates Dll.Sym so that subprograms with the signature
   function (l,r : integer) return integer;
can be mapped to a (dynamic) shared library.

The body of the test function simply opens the shared library (called test.dll), maps the address of add_integer to the Ada name Add_Int then uses the routine in the normal way. Finally the shared library in unloaded for completeness.

The body of Dll is also fairly unspectacular:

    package body Dll is

       -- Map UNIX-style dynamic-library functions to ada routines

       function Dlopen (Lib_Name : Interfaces.C.Strings.Chars_Ptr;
			Mode     : Interfaces.C.int) return System.Address;
       pragma Import(C, Dlopen, "dlopen");

       function Dlsym (Handle   : System.Address;
		       Sym_name : Interfaces.C.Strings.Chars_Ptr) return System.Address;
       pragma Import(C, Dlsym, "dlsym");

       function Dlclose (Handle : System.Address) return Interfaces.C.int;
       pragma Import(C, Dlclose, "dlclose");

       function Dlerror return Interfaces.C.Strings.Chars_Ptr;
       pragma Import (C, Dlerror, "dlerror");



       function Error(Handle : in Handle_Type) return String is
	  C_Str : Interfaces.C.Strings.Chars_Ptr;
       begin
	  C_Str := Dlerror;

	   if C_Str = Interfaces.C.Strings.Null_Ptr then
	      return "";
	   else
	      return Interfaces.C.Strings.Value(C_Str);
	  end if;
       end Error;


       procedure Open(Handle : in out Handle_Type;
		      Name   : in     String;
		      Mode   : in     Integer := 0) is
	  Raw_Address : System.Address;
	  C_Str       : Chars_Ptr;
       begin
	  C_Str := Interfaces.C.Strings.New_String(Name);

	  Raw_Address := Dlopen(Lib_Name => C_Str,
				Mode     => Interfaces.C.Int(Mode));

	  Interfaces.C.Strings.Free(C_Str);

	  Handle.Os_Handle := Raw_Address;

	  if Raw_Address = System.Null_Address then raise Dll_Exception; end if;

       end;

       procedure Close(Handle : in out handle_type) is
	  Os_Ret_Code : Interfaces.C.Int;
       begin
	  Os_Ret_Code := Dlclose(Handle.Os_Handle);

	  if Os_Ret_Code /= 0 then raise Dll_Exception; end if;

       end Close;


       procedure Sym(Handle : in out Handle_Type;
		     Name   : in     String;
		     Item   :    out Item_Type) is
	  function Address_To_Access is new
	    Unchecked_Conversion(Source => System.Address, Target => Item_Type);
	  Raw_Address : System.Address;
	  C_Str       : Chars_Ptr;
       begin
	  C_Str := Interfaces.C.Strings.New_String(Name);

	  Raw_Address := Dlsym(Handle   => Handle.Os_handle,
			       Sym_Name => C_Str);

	  Interfaces.C.Strings.Free(C_Str);

	  Item := Address_To_Access(Raw_Address);

	  if Raw_Address = System.Null_Address then raise Dll_Exception; end if;

       end;

    end Dll;


Original at http://www.sharpe-practice.co.uk/isharpe/technote/ada_sl.htm. $Id: ada_sl.htm,v 1.1 2002/01/18 12:36:01 i_sharpe Exp $