Gem #64: Handling Multiple-Unit Source Files

Let's get started…

In lots of legacy systems that were initially developed with technologies other than GNAT, a single source file might include several Ada units (or both spec and body of a single unit).

Where possible, it is best to follow GNAT's traditional approach and split these files into several files. The main advantage this provides is to limit the amount of recompilation that gnatmake and gprbuild will do. When several units are found in the same file, modifying any of them will force recompilation of all files that depend on any of the units found in the file. In particular, changing the body of a unit when the spec is in the same source file will force recompilation of all units that depend on it. This is contrary to the whole concept of separate specifications.

This is also the recommended method in the Ada Quality and Style manual, so this should really be the preferred approach where possible.

Such splitting of source files can be done through the gnatchop command line tool. If you want to keep the original (multiple-unit) source files and keep modifying these, we recommend using the "-r" switch, which will force GNAT's error messages to reference the original (unsplit) file.

If however you do not intend to keep them, you should not use "-r".

Most likely the "-w" switch will also come in handy, to force overwriting of previously split files. Also, since you are recreating the source files that gnatmake sees, you should use the "-m" switch for gnatmake so that it doesn't pay attention to timestamps. Otherwise you would end up recompiling all files all the time.

Calling gnatchop cannot be done automatically by gnatmake. Instead, you should have a script or a Makefile that first calls gnatchop, and then spawns gnatmake to do the actual compilation.

However, in some cases it isn't possible to split the source files. Most often, this is because the files are under configuration control and you want to preserve history. In other cases, this is because you still want to compile with your older compiler.

GNAT provides a workaround in cases where you cannot split your source files and cannot or do not want to change your build methods to use gnatchop. Let us emphasize that this is really considered as only a workaround, and is not fully supported by all the tools. For instance, the "-gnatD" and "-gnatR" switches are currently not compatible with the pragmas described below. More important perhaps, GPS will have some known limitations when using multiple-unit source files (cross-references will not work in a lot of cases, and compiling the current file will fail if it contains multiple units)

You have to let GNAT know about your source-file organization, which can be done in several ways.

Method 1: If you are using project files, you can add or create a package Naming inside your project file, and use a syntax like:

 package Naming is
   for Specification ("unit1") use "file1.ada" at 1;
   for Implementation ("unit1") use "file1.ada" at 2;
 end Naming;

The index after the at starts at 1 and indicates the position of the unit inside the source file.

You can either create this Naming package manually by editing the project file, or else by using the gnatname command-line tool. This tool will traverse all the directories you pass it via its "-d" switch, check each file in them, and determine what unit or units they contain. It will then create a new project file called my_project_naming.gpr, and modify the project you specified on the command line in a manner similar to:

 with "my_project_naming";
 project My_Project is
    package Naming renames My_Project_Naming.Naming;
 end My_Project;

GPS was recently modified to properly support such project files, and properly preserve the existing "at" information when you edit a project, as well as to allow you to create new naming exceptions directly from the graphical interface.

Method 2: If you are not using project files yet, you can specify the same information by using pragmas in a configuration file. Through its "-c" switch, gnatname is also able to generate this file of pragmas. The general form of the configuration file is:

 pragma Source_File_Name
   (Unit1, Spec_File_Name => "file1.ada", Index => 1);
 pragma Source_File_Name
   (Unit1, Body_File_Name => "file1.ada", Index => 2);

where the information is similar to what was specified in the project file example above.

There is a difference between using these project attributes or pragmas, and using "gnatchop -r" as we described in the first part: while there is a single command to spawn, and no duplication of source files with the pragmas, you still have the issue that gnatmake will recompile everything that depends on any unit in the file. When using gnatchop, however, that problem does not exist, because gnatmake sees single-unit source files. Also, the pragma-based method is not fully supported by all the tools yet, so the preferred approach is really to split your files (and preferably replace the multiple-unit source files with the resulting single-unit files).