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:
unitunitname
; << declarations >> endunitname
;
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:
Only one external
declaration of an object may appear in a given source file. That is, you cannot define the same symbol twice as an external
object.
Only procedure
, static
, readonly
, and storage
variable objects can be external. var
, type
, const
, and parameter objects cannot be external.
external
objects must appear at the global declaration level. You cannot declare external
objects within a procedure or other nested structure.[99]
external
objects publish their name globally. Therefore, you must carefully choose the names of your external
objects so they do not conflict with other symbols.
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.