TOC PREV NEXT INDEX DOC LIST MASTER INDEX



Shared Libraries and the Application Program Interface (API)

A common organization for large software systems is a collection of main programs, each of which references a common set of implementation units. Apex provides a facility for packaging these implementation units into a collection of "library views", which, taken together, form a "library." The object code for the units of a library can be bound up into a single shared object that is dynamically loaded by a main program that references it. Several concurrently running main programs can share the same copy of the shared library. Using shared libraries reduces the disk space and memory requirements of a set of main programs by keeping only one copy of the common library units. However, main programs can also be statically linked against library views, in which case the code for the referenced common units is included as part of each referencing main program.

A software vendor who wishes to distribute a library without distributing the source code for the library can do so by binding the library views into an API (Application Program Interface) view. An API view contains only the source code for those units specified by the author as part of the published interface for the library. An API view also contains the compilation artifacts necessary to compile and link programs against the published specification, but the complete source code cannot be derived from these artifacts.

The following topics are covered in this chapter:


Building a Simple API Step-by-Step

To build a simple API, follow these steps. This API could, for example, be used to hide classified source code.

1 . Create a subsystem for the API. In our example, this subsystem will be called hidden.ss

2 . Add a unique subsystem name switch to hidden.ss, for example, SUBSYSTEM_NAME: hidden.sciance.gdls.com. To do this, select hidden.ss from the directory viewer and do a Control > Show > Switches. Click on the Edit box and add the switch in alphabetical order.

Note that, unlike views, subsystems have only a few switches, so the list of switches will be relatively short.

3 . Create two views, one for the API source code and one for the API itself. For our example, these will be named api_source.wrk and api_rel_1.wrk.

4 . In the api_source.wrk view, copy or create the package containing the source code that your wish to hide. For simplicity, this package should not have any dependencies.

5 . Add the line, "pragma Api (hidden) as the last line in the package specification (after the `end').

Note that any other package specifications that are part of this API will need the same pragma.

6 . Code the API source view (api_source.wrk).

7 . Use the Apex shell (Tools > Shell) to build the API.

The command is build_api. It requires two parameters: the API destination view and the API source view. In our example, the command line is:

You can now import api_rel_1 and reference the hidden source code.


Library Views vs Non-Library Views

The difference between library views and non-library views is relevant only when linking main programs. If a main program depends on a unit in a library, it depends on the library itself. Consequently, every unit in the library must be compiled so that the code objects can be built. If any of these units fail to compile, the main program will not link, even if the erroneous units are not in the closure of the main program.

BUILD_POLICY switch

A view is declared to be a library view by setting the BUILD_POLICY switch. This switch, when it is used in Ada views, can have the following values.

A fundamental rule is that a given view may be a member of at most one library. The compiler will use the switch settings and imports to partition views into libraries if it can be done unambiguously. However, it is possible to set up switches and imports so that there is no unambiguous partition.

Consider the following example.

Figure 1 API Library Example - 1

In this case, A and B form one library, and C and D form another. Even though A imports both C and D, it does not claim either as member views. When collecting imports to include as member views, the compiler never includes any views whose BUILD_POLICY is LIBRARY, LAYER_LIBRARY, CLOSURE_LIBRARY, API, or (recursively) any members of those views.

But now consider a different example:

Figure 2 API Library Example - 2

Here there is no clear indication of whether C should be a member of library A or B. Since it cannot be a member of both, the compiler will issue an error message and fail.


Root View Switches

All of the object code for a library is bound up into a UNIX shared library and UNIX archive, or both. This behavior is controlled by the following switches, which should be set in the root view of a library:

Archive and shared library files must have a name of the form libNNNN.XX, where NNNN may be any name, but .XX is .a for archives, and either .so or .sl (depending on the operating system) for shared libraries. If the value specified for a library name switch does not conform to these rules, the name will be adjusted by prepending lib if necessary, and/or adding a suffix or replacing the one given.

The names chosen for archive files don't matter very much, since internally, Apex uses full pathnames for them. However, when a program is linked against a shared library object, the name of that object is embedded in the executable for the main program. The main program will not execute unless the shared library objects it was linked with (or copies of those objects) can be located. UNIX uses the following algorithm for locating the shared library objects. First, it checks for the full pathname the shared library had at link time. If no such object is found, it examines the environment variable LD_LIBRARY_PATH, which should be a list of directories separated by colons. It examines each directory on that list in order, searching for a shared library object with the appropriate simple name. If no such library can be found, the program will not successfully execute. Hence, if a main program depends upon shared libraries, and is to be executed in an environment other than the one in which it was linked, the programmer must ensure that when the executable for main program is copied, any shared libraries that it depends upon are also copied into one of the directories named by LD_LIBRARY_PATH. Moreover, the names of the shared libraries must be chosen so that the UNIX search-list algorithm locates the correct libraries.

When linking a main program against library views that contain both an archive and a shared library, the compiler chooses which to use based on the following switches.

ADA_LINK_MODE switch

The ADA_LINK_MODE switch may appear in the switch file for a view containing the main program, or it may be specified on the compiler command line. If the switch appears both places, the value on the command line overrides the value in the switch file. The switch may have the following values:

The default value of ADA_LINK_MODE is DEFAULT. The default value of LINK_CONTRIBUTION_DEFAULT_MODE is DYNAMIC_OR_STATIC.

When a main program is linked using library views, the compiler detects whether any units have been added, deleted, or recompiled since the last time the libraries were used, and rebuilds any code objects that are obsolete. In any given view, the compiler will only rebuild the code object selected by the ADA_LINK_MODE switch. When a code object is rebuilt, it is always rebuilt from the object files. Incremental updates of archives is not attempted, and incremental updates of shared libraries is impossible.

The compiler requires that library views must import only library views. If a library view contains a shared library, it is a good idea (though not required) that all the views it imports also contain shared libraries.

When the link contribution of a library view is static, only the units that are needed to resolve external references are included in the program being linked. However, when the link contribution is dynamic, all the units in the library will dynamically loaded when the program is run. If the shared library imports a library with a static contribution, units in the archive may be linked into the main program to satisfy unresolved references in the shared library, even if those units can never be reached when the main program is executed. And these extra units may, in turn, pull in more extra units.

However, the inclusion of these extra units does not affect the execution of the program; they are neither executed nor elaborated. The inclusion of extra units can be avoided by arranging that if a library has a dynamic link contribution, then all the libraries it imports also have a dynamic contribution.

If a unit in the closure of a main program contains:

when the main program is linked, "options" is added to the linker command line. The option string is scanned into tokens separated by spaces. If a token begins with the characters '+', '-', or '/' the token is added to the linker command line verbatim. If the token begins with anything else, it is presumed to name a file using a relative name. The full pathname of the directory containing the unit with the pragma is prepended to the filename before adding it to the linker command line.

When a program is linked with non-library views, only the Link_With pragmas in units in the closure of the main program are processed. However, when a main program depends on a library view (shared or archive), every pragma Link_With in the view is processed, since the linker options and extra files may be necessary to satisfy unresolved references in shared libraries.


API Builder

The API Builder allows a vendor to package and distribute a set of views as a library that clients can link into their programs without distributing the source code for the library. The acronym API stands for Application Program Interface.

The following steps are used to create an API.

First, determine which views will make up the source code to the API. Then determine which units in those views will be part of the published specification for the API. For each of these units, insert "pragma Api (name)" at the very end of the unit. The name must be an Ada identifier. The same name should be used on every unit in the API, although the name is not actually used by the current version of the API Builder. The name is present for compatibility with previous (and possibly future) versions of the API Builder.

The units that make up the specification of the API must be a closed set. That is, if a unit has a pragma Api, then every unit it withs must also have a pragma Api. If a child unit or subunit has a pragma Api, there must be a pragma Api on the parent unit. And if a body unit has a pragma Api, the corresponding spec unit must have a pragma Api. There are only a few situations in which it makes sense to put a pragma Api on a body unit, but it is legal to do so.

This closure restriction sounds reasonable in principle, but it often forces the API author to make some unpleasant choices. The problem is that if a unit U is withed by a package specification with a pragma Api, even if U is only referenced within the private part of the package, U must have a pragma Api and will be part of the published specification. Since this rule applies to U as well, the author can be forced to expose far more of the implementation than expected.

Possible solutions to this problem are:

1 . Complete private types T in API specs with declarations like:

2 . Use export sets to indicate which units the clients of the API are expected to reference. Although architectural control will prevent the client from referencing the implementation units, those names will be used up and unavailable to the client. For that reason, names like "types" and "utilities" are poor choices for those units.

3 . Write "sanitized" type completions in the private part of API specs, and use unchecked_conversion in the implementation to convert between the sanitized types and the actual types used by the implementation.

If the API is to be licensed, then each pragma Api should be followed by a pragma License. The format of this pragma is:

Where feature is a string literal, and version is a floating point literal. If version is omitted, it defaults to 1.000. A License pragma has no effect in a source view, but if an API view with a pragma License participates in a compilation, the compiler passes the string "feature:version" to the FLEXlm license manager. If the manager refuses to grant a license, the compilation will be terminated. The next step is to locate all the views that will be part of the API, plus all views imported, directly or indirectly by those views. For each of those views, the switch file in the containing subsystem (not the view!) must have the SUBSYSTEM_NAME switch set.

SUBSYSTEM_NAME switch

The SUBSYSTEM_NAME switch is string-valued switch that defaults to the null string. The intent is that every subsystem in the known universe have a unique value for the SUBSYSTEM_NAME switch. By convention, the subsystem switch has a value such as "asis.rational.com". The value of the switch should end in an Internet domain name. The portion of the name that precedes the domain name should be managed by a scheme prescribed by the named organization to guarantee uniqueness.

Of course, the compiler has no way enforcing this convention. The real requirement is that all the subsystems that are in the closure of a given compilation must have unique SUBSYSTEM_NAMEs. This rule extends to any views that have been packaged as APIs. The Internet domain-name convention assures that APIs produced by different vendors will not contain conflicting SUBSYSTEM_NAMEs. All Rational-supplied APIs use SUBSYSTEM_NAMEs that end in ".rational.com".

The Apex Ada compiler uses a name-mangling scheme to construct the external names of object. The mangled name depends, in part, on the value of the SUBSYSTEM_NAME switch. Consequently, library unit names in the unpublished portion of an API can be chosen without concern for duplicating names in client programs. Even if the simple library names are the same, the SUBSYSTEM_NAMEs (obtained from the switch file, or supplied by the compiler if none is present) will be different, and hence, the mangled names will be different. However, the SUBSYSTEM_NAME does not participate in the C++ name-mangling scheme, and C names are not mangled at all. Therefore, names in C code that will go into an API must be chosen to minimize the possibility of conflicting with names in client code. Note that the mangling schemes do preclude naming conflicts between C names and Ada names.

After these steps have been completed, the API Builder is run.

The syntax of the command is:

The api_view is the where the API is built. Any contents of that view are destroyed and replaced with the API specification. As a safety measure, the API Builder requires that api_view must be an already empty view or a view whose BUILD_POLICY switch has the value API.

source_views is the collection of Ada and C views that will make up the API. source_views may include one or more configuration files (with the suffix .cfg), in which case the views named by those files will be included.

The import closure of the source views must not contain two different views of the same subsystem. If there are any mutually importing views among the source views, all the mutually importing views of that set must be included. The source views must all have the same compiler key. None of them can be RCI views or API views. They all must be the same language dialect, Ada 83 or Ada 95.

The API builder first examines all the units in the source views, and attempts to compile any that are not compiled. Units in the closure of these views are compiled if necessary. If any of these compilations fail, the API builder does not update the API view.

Next, the API builder copies any unit in the source view containing pragma Api into the API view. The API builder does not create any subdirectories; units get copied directly into the API view no matter where they appeared in the directory hierarchy in the source view. The API builder also copies sufficient compiler artifacts so that clients can compile against the API just as if they had imported the source views, but only referenced the units containing pragma Api.

The view constructed by the API builder is considered by the compiler to be a read-only view. Do not edit, add, or delete Ada units from a view that has been built by the API builder. The compiler cannot be coaxed into performing any compilation in an API view. Any Ada units that are copied into the API view after the builder has run will be treated as ordinary text files by the compiler.

Because compilation cannot take place in API views, care should be taken not to delete any of the compilation artifacts, since they can only be recreated by rerunning the API builder. The Apex clean command will not modify an API view. If users have their own shell scripts for deleting compilation artifacts, they should be carefully written to avoid damaging API views.

When the API Builder is run, it records any export sets defined in the source views and uses them to enforce architectural control in the clients of the API. Any exports sets defined for the API view itself are ignored by the compiler. That is, if a unit U withs a unit V in an API view, the legality of that with determined using the exports sets defined in the source view from which V was copied when the API builder was run. If an API is created that uses export sets, it is a good idea to define a set of export sets in the API view that is the union of the exports sets in the source views. Although these sets will be ignored by the compiler, having them will minimize confusion among the users of the API.

The final steps in creating an API are to make any additions or adjustments necessary to the switch file produced by the API builder (see API Builder), copy in any documentation or readme files, and then copy the API to the place it will be referenced by the clients. A collection of related APIs will need specific import relations set up (see section yyy). After the APIs are installed for client use, and the imports are properly set up, they should be frozen. Because Apex requires that frozen views must import only other frozen views, the freezing of API views must wait until those views are copied to their final home.

If you take the set of source views used to construct an API, take their import closure, and then subtract the original source views, what remains is the import closure of the API. This set will never be empty; at minimum it will include a view of Rational's LRM subsystem, which must be imported by every view. When an API is installed in a client area, it must import a view of every subsystem in the API's import closure. The views imported by the API need not be identical to the views imported by the source views, but they must be compatible. The compiler performs a compatibility check whenever compiling units that reference API views.


Warning: The Apex clean command contains a special check that prevents it from touching an API. However, some developers may have private clean scripts that simply delete material from .Rational/Compilation directories without going through Apex. These scripts should not be used! If the contents of .Rational/Compilation are deleted in an API, it cannot be created by compilation, because not all the source code is present.

API Views

An API view is a library view, and follows all the rules and restrictions of library views. In particular, API views may only import other library views. Usually API views will import other API views, though one could deliver an API view along with the source code for a view that must be imported by the API. However, in that case, the imported view must be a library view. The compiler will check that the imported view is compatible with the API view.

Another library rule that applies to API views is that a given view may be a member of, at most, one library. The API author who is only producing a single API view need not worry about this restriction. However, creating multiple APIs that have source code in common requires a little more care. Suppose there are three views with the following import structure (see Figure 3) and that one wants to create APIs for A and B. It is certainly possible to use the API Builder to bind A and C into API_A, and B and C into API_B. Each of these APIs would work fine in isolation. However, if a client tried to write a program that used both API_A and API_B, the compiler would reject the program. The problem is that both API_A and API_B have a copy of C hidden inside them. Including both APIs gets two views (or two copies of the same view) of the same subsystem into a compilation, which is illegal.

Figure 3 API Views

The only way to ensure that the clients do not get into this kind of trouble is to follow the following rule: Never include a view of the same subsystem in more than one API. In this case, you could deliver a single API constructed from A, B, and C. Its published spec would contain the union of units with pragma API on them from A and B. (In this example, there are no such units in C).

The second choice is to create and deliver three APIs for A, B, and C, and arrange that the API views in the client area have the same import structure as these source views.

This option is a little confusing; let us explore it further. One might expect that it is necessary to go through the view C, find the units referenced by A and B and put pragma Api on them. This task is not necessary, assuming no published units from A or B them reference units from C. Simply run the API builder three times on A, B, and C, producing API_A, API_B, and API_C. Since C contains no units with pragma API, the spec of API_C will be empty. But that is not a problem, since the specs of API_A and API_B contain no references to units in C. In the client area, any view that imports API_A, API_B, or both, will get API_C in its import closure.

This scheme only works if the source view for A and B import the source view for C. There may be situations where this approach is impractical. Perhaps A, B, and C are large views (or collections of views) that are implemented by three different programming teams. It may be organizationally expedient to publish an internal specification for C, and have the group working on C to create APIs and deliver them for use by the groups developing A and B. However, when API_C is delivered to the clients of API_A and API_B, it can be built with the -specless option so that the internal specification is not exposed.

API Views with Dependencies on Apex Supplied Subsystems

The APIs have imports, the Imports/*Description.cfg files. Those imports have specific pathnames. Those pathnames will presumably be incorrect at the customer site.

Their API installation script, for the customer, will need to query the customer for the proper $APEX_BASE value. It can then edit the various .cfg files and substitute the customer's $APEX_BASE value everywhere their $APEX_BASE value appears.

The $APEX_BASE may be /rational/320b and their customer's $APEX_BASE might be /vendor/apex-latest and they would need to substitute /vendor/apex-latest everywhere that /rational/320b appeared in the Import .cfg files.

Then you will do an apex import -refresh on the installed API views. At that point if the two Apex versions are the same, the APIs should be functional.

Switch File

Since compilation is never performed in an API view, most compiler switches are irrelevant. When the API builder creates an API view, it creates a switch file containing the few switches that are relevant. Here are the switches that are relevant in API views. Because the requirements of switch files in API views are so different from those of source views, API views should not be used as models.


Rational Software Corporation 
http://www.rational.com
support@rational.com
techpubs@rational.com
Copyright © 1993-2004, Rational Software Corporation. All rights reserved.
TOC PREV NEXT INDEX DOC LIST MASTER INDEX TECHNOTES APEX TIPS