5.24 Units and the external Directive

Technically, the #include directive provides you with all the facilities you need to create modular programs. You can create several modules, each containing some specific routine, and include those modules, as necessary, in your assembly language programs using #include. However, HLA provides a better way: external and public symbols.

One major problem with the #include mechanism is that once you've debugged a routine, including it into a compilation still wastes time because HLA must recompile bug-free code every time you assemble the main program. A much better solution would be to preassemble the debugged modules and link the object code modules together. This is what the external directive allows you to do.

To use the external facilities, you must create at least two source files. One file contains a set of variables and procedures used by the second. The second file uses those variables and procedures without knowing how they're implemented. The only problem is that if you create two separate HLA programs, the linker will get confused when you try to combine them. This is because both HLA programs have their own main program. Which main program does the OS run when it loads the program into memory? To resolve this problem, HLA uses a different type of compilation module, the unit, to compile programs without a main program. The syntax for an HLA unit is actually simpler than that for an HLA program; it takes the following form:

unit unitname;

     << declarations >>

end unitname;

With one exception (the var section), anything that can go in the declaration section of an HLA program can go into the declaration section of an HLA unit. Notice that a unit does not have a begin clause and there are no program statements in the unit;[96] a unit contains only declarations.

In addition to the fact that a unit does not contain a main program section, there is one other difference between units and programs. Units cannot have a var section. This is because the var section declares automatic variables that are local to the main program's source code. Because there is no "main program" associated with a unit, var sections are illegal.[97]

To demonstrate, consider the two modules in Example 5-19 and Example 5-20.

Example 5-19. Example of a simple HLA unit

unit Number1;

static
    Var1:   uns32;
    Var2:   uns32;

    procedure Add1and2;
    begin Add1and2;

        push( eax );
        mov( Var2, eax );
        add( eax, Var1 );

    end Add1and2;

end Number1;

Example 5-20. Main program that references external objects

program main;
#include( "stdlib.hhf" );

begin main;

    mov( 2, Var2 );
    mov( 3, Var1 );
    Add1and2();
    stdout.put( "Var1=", Var1, nl );

end main;

The main program references Var1, Var2, and Add1and2, yet these symbols are external to this program (they appear in unit Number1). If you attempt to compile the main program as it stands, HLA will complain that these three symbols are undefined.

Therefore, you must declare them external with the external option. An external procedure declaration looks just like a forward declaration except you use the reserved word external rather than forward. To declare external static variables, simply follow those variables' declarations with the reserved word external. The program in Example 5-21 is a modification to the program in Example 5-20 that includes the external declarations.

Example 5-21. Modified main program with external declarations

program main;
#include( "stdlib.hhf" );

    procedure Add1and2; external;

static
    Var1: uns32; external;
    Var2: uns32; external;

begin main;

    mov( 2, Var2 );
    mov( 3, Var1 );
    Add1and2();
    stdout.put( "Var1=", Var1, nl );

end main;

If you attempt to compile this second version of main using the typical HLA compilation command HLA main2.hla, you will be somewhat disappointed. This program will actually compile without error. However, when HLA attempts to link this code it will report that the symbols Var1, Var2, and Add1and2 are undefined. This happens because you haven't compiled and linked in the associated unit with this main program. Before you try that and discover that it still doesn't work, you should know that all symbols in a unit, by default, are private to that unit. This means that those symbols are inaccessible in code outside that unit unless you explicitly declare those symbols as public symbols. To declare symbols as public, you simply put external declarations for those symbols in the unit before the actual symbol declarations. If an external declaration appears in the same source file as the actual declaration of a symbol, HLA assumes that the name is needed externally and makes that symbol a public (rather than private) symbol. The unit in Example 5-22 is a correction to the Number1 unit that properly declares the external objects.

Example 5-22. Correct Number1 unit with external declarations

unit Number1;

static
    Var1:   uns32; external;
    Var2:   uns32; external;

    procedure Add1and2; external;

static
    Var1:   uns32;
    Var2:   uns32;


    procedure Add1and2;
    begin Add1and2;

        push( eax );
        mov( Var2, eax );
        add( eax, Var1 );

    end Add1and2;

end Number1;

It may seem redundant declaring these symbols twice as occurs in Example 5-21 and Example 5-22, but you'll soon see that you don't normally write the code this way.

If you attempt to compile the main program or the Number1 unit using the typical HLA statement, that is,

HLA main2.hla
HLA unit2.hla

you'll quickly discover that the linker still returns errors. It returns an error on the compilation of main2.hla because you still haven't told HLA to link in the object code associated with unit2.hla. Likewise, the linker complains if you attempt to compile unit2.hla by itself because it can't find a main program. The simple solution is to compile both of these modules together with the following single command:

HLA main2.hla unit2.hla

This command will properly compile both modules and link together their object code.

Unfortunately, the command above defeats one of the major benefits of separate compilation. When you issue this command it will compile both main2 and unit2 prior to linking them together. Remember, a major reason for separate compilation is to reduce compilation time on large projects. While the above command is convenient, it doesn't achieve this goal.

To separately compile the two modules you must run HLA separately on them. Of course, you saw earlier that attempting to compile these modules separately produced linker errors. To get around this problem, you need to compile the modules without linking them. The -c (compile-only) HLA command-line option achieves this. To compile the two source files without running the linker, you would use the following commands:

HLA -c main2.hla
HLA -c unit2.hla

This produces two object code files, main2.obj and unit2.obj, that you can link together to produce a single executable. You could run the linker program directly, but an easier way is to use the HLA compiler to link the object modules together for you:

HLA main2.obj unit2.obj

Under Windows, this command produces an executable file named main2.exe;[98] under Linux, Mac OS X, and FreeBSD this command produces a file named main2. You could also type the following command to compile the main program and link it with a previously compiled unit2 object module:

HLA main2.hla unit2.obj

In general, HLA looks at the suffixes of the filenames following the HLA commands. If the filename doesn't have a suffix, HLA assumes it to be .HLA. If the filename has a suffix, then HLA will do the following with the file:

  • If the suffix is .HLA, HLA will compile the file with the HLA compiler.

  • If the suffix is .ASM, HLA will assemble the file with MASM (or some other default assembler such as FASM, NASM, or TASM under Windows) or Gas (Linux/Mac OS X/FreeBSD).

  • If the suffix is .OBJ or .LIB (Windows), or .o or .a (Linux/Mac OS X/FreeBSD), then HLA will link that module with the rest of the compilation.

Whenever you declare a symbol using the external directive, keep in mind several limitations of external objects:

This last point is especially important to keep in mind. HLA links your modules using a linker. At each step in this process, your choice of external names could create problems for you.

Consider the following HLA external/public declaration:

static
          extObj:          uns32; external;
          extObj:          uns32;
          localObject:     uns32;

When you compile a program containing these declarations, HLA automatically generates a "munged" name for the localObject variable that probably won't ever have any conflicts with system-global external symbols.[100] Whenever you declare an external symbol, however, HLA uses the object's name as the default external name. This can create some problems if you inadvertently use some global name as your variable name.

To get around the problem of conflicting external names, HLA supports an additional syntax for the external option that lets you explicitly specify the external name. The following example demonstrates this extended syntax:

static
     c: char; external( "var_c" );
     c: char;

If you follow the external keyword with a string constant enclosed by parentheses, HLA will continue to use the declared name (c in this example) as the identifier within your HLA source code. Externally (i.e., in the assembly code) HLA will substitute the name var_c whenever you reference c. This feature helps you avoid problems with the misuse of assembler reserved words, or other global symbols, in your HLA programs.

You should also note that this feature of the external option lets you create aliases. For example, you may want to refer to an object by the name StudentCount in one module while referring to the object as PersonCount in another module (you might do this because you have a general library module that deals with counting people and you want to use the object in a program that deals only with students). Using a declaration like the following lets you do this:

static
     StudentCount: uns32; external( "PersonCount" );

Of course, you've already seen some of the problems you might encounter when you start creating aliases. So you should use this capability sparingly in your programs. Perhaps a more reasonable use of this feature is to simplify certain OS APIs. For example, the Win32 API uses some really long names for certain procedure calls. You can use the external directive to provide a more meaningful name than the standard one the operating system specifies.

HLA's technique of using the same external declaration to define public as well as external symbols may seem somewhat counterintuitive. Why not use a public reserved word for public symbols and the external keyword for external definitions? Well, as counterintuitive as HLA's external declarations may seem, they are founded on decades of solid experience with the C/C++ programming language that uses a similar approach to public and external symbols.[101] Combined with a header file, HLA's external declarations make large-program maintenance a breeze.

An important benefit of the external directive (versus separate public and external directives) is that it lets you minimize duplication of effort in your source files. Suppose, for example, you want to create a module with a bunch of support routines and variables for use in several different programs (e.g., the HLA Standard Library). In addition to sharing some routines and some variables, suppose you want to share constants, types, and other items as well.

The #include file mechanism provides a perfect way to handle this. You simply create a #include file containing the constants, macros, and external definitions and include this file in the module that implements your routines and in the modules that use those routines (see Figure 5-10).

A typical header file contains only const, val, type, static, readonly, storage, and procedure prototypes (plus a few others we haven't look at yet, like macros). Objects in the static, readonly, and storage sections, as well as all procedure declarations, are always external objects. In particular, you should not put any var objects in a header file, nor should you put any nonexternal variables or procedure bodies in a header file. If you do, HLA will make duplicate copies of these objects in the different source files that include the header file. Not only will this make your programs larger, but it will cause them to fail under certain circumstances. For example, you generally put a variable in a header file so you can share the value of that variable among several different modules. However, if you fail to declare that symbol as external in the header file and just put a standard variable declaration there, each module that includes the source file will get its own separate variable—the modules will not share a common variable.

If you create a standard header file, containing const, val, and type declarations and external objects, you should always be sure to include that file in the declaration section of all modules that need the definitions in the header file. Generally, HLA programs include all their header files in the first few statements after the program or unit header.

This text adopts the HLA Standard Library convention of using an .hhf suffix for HLA header files (hhf stands for HLA header file).



[96] Of course, units may contain procedures and those procedures may have statements, but the unit itself does not have any executable instructions associated with it.

[97] Procedures in the unit may have their own var sections, but the procedure's declaration section is separate from the unit's declaration section.

[98] If you want to explicitly specify the name of the output file, HLA provides a command-line option to achieve this. You can get a menu of all legal command-line options by entering the command HLA -?.

[99] There are a few exceptions, but you cannot declare external procedures or variables except at the global level.

[100] Typically, HLA creates a name like 001A_localObject out of localObject. This is a legal MASM identifier, but it is not likely it will conflict with any other global symbols when HLA compiles the program with MASM.

[101] Actually, C/C++ is a little different. All global symbols in a module are assumed to be public unless explicitly declared private. HLA's approach (forcing the declaration of public items via external) is a little safer.