Gem #109: Ada Plug-ins and Shared Libraries — Part 1

by Pascal Obry —EDF R&D

Let's get started...

This is the first part of a two-part Gem about Ada plug-ins. In this Gem, we discuss the way shared libraries are supported by GNAT and the interactions between shared libraries and the Ada run-time system. This is essential to fully understanding how to build Ada plug-ins, which are discussed in the second part.

In modern operating systems it is rare to have fully self-contained applications, that is, executables that require no external support from the operating system. Such applications occur mostly in the embedded world. In other domains, an application is built on top of other services that are accessible from libraries.

There are two basic kinds of libraries: static and dynamic. A static library is a simple container for object code that will be included in the final application's executable. This is done by the linker as the last step of creating an executable. That's why such libraries are called static (statically linked, with the library code embedded in the executable). Without static libraries we would need to link against many object files, so this simplifies linking. The other benefit of static libraries organized as archives of object files is to allow selective linking, so that only the necessary objects from the library are linked into the final executable.

A static library can be built with GNAT by using project files. For example:

library project MyLib is
    for Source_Dirs use ("src");
    for Object_Dir use "obj";
    for Library_Dir use "lib";
    for Library_Name use "mylib";
    for Library_Kind use "static";
end MyLib;

Dynamic libraries are quite a different story: they allow splitting the image of an executable into several pieces, some of which can be shared among different executables. Dynamic libraries offer two main advantages: 1) sharing code among executables, thus diminishing global memory usage, and 2) allowing modular maintainance of an application by having the capability of replacing a chunk of an application without having to regenerate or even interrupt the complete application. It is also important to note that dynamic libraries require direct support from the operating system.

Linking with a shared library is done in two steps. The first step occurs at link time, when the linker generates a table in the executable that contains the name of the shared library and all of the symbols that must be imported from the shared library. The second step occurs at load time using the dynamic linker.

The dynamic linker is part of the operating system and is in charge of finalizing the linking of the application. This step is quite complex, but let's summarize as follows. The dynamic linker:

1. Loads all shared libraries referenced by the executable.

2. Creates the executable-specific shared libraries' data sections (only code is shared between executables, not data).

3. Fills in the executable's corresponding shared library table with the actual addresses of the referenced symbols (variables and routines).

4. Calls the initialization routines (if any) defined in the just-loaded shared libraries.

At this point the application is ready to be run. Of course, shared libraries can reference other shared libraries; the dynamic linker is recursively doing the same job in this case. A very important point to understand is that whenever we have an Ada shared library used by an Ada application, both will be referencing the very same Ada run-time system. What is important in our context is that the elaboration is controlled by the executable and computed at bind time. This ensures proper Ada semantics as required by the standard.

A shared library can be built using a project file by setting the Library_Kind attribute to relocatable.

For plug-in support, the same piece of code may be loaded and unloaded several times and thus requires initialization to happen at load time and to be tied to the library itself; it cannot depend on the one-time global elaboration of the application.

GNAT has support for stand-alone shared libraries, and these are exactly the right kind of libraries to use in this context. A stand-alone library is a library that contains the necessary code to elaborate the Ada units that are included in the library and all the units it depends upon, recursively. Since many libraries may need to elaborate the same external units, the initialization code must be protected against multiple initializations of the same data objects.

Note that these libraries do not exactly conform to Ada semantics, since elaboration is supposed to happen only at program start and not in the middle when a plug-in is loaded. But such libraries are suitable for use with executables that are written in other languages or as plug-in libraries.

Declaring a library to be stand-alone is easy using GNAT Project files: it just requires adding an additional attribute in the library project file:

for Library_Interface use ("interface_unit1", "interface_unit2", ...);

This attribute declares the units that are visible from outside the library, and thus offers a hiding mechanism (for all units not in the interface), complementing the one offered by Ada: any unit not in the interface cannot be called from outside. This means, in particular, that changing the implementation of such units can be done without having to recompile, rebind, or relink the rest of the application.

In the second part of this series we will see how to set up an Ada plug-in framework based on stand-alone libraries.