A record definition assigns different offsets to each field in the record according to the size of those fields. This behavior is quite similar to the allocation of memory offsets in a VAR or STATIC section. HLA provides a second type of structure declaration, the UNION, that does not assign different addresses to each object; instead, each field in a UNION declaration has the same offset - zero. The following example demonstrates the syntax for a UNION declaration:type unionType: union << fields (syntactically identical to record declarations) >> endunion;
You access the fields of a UNION exactly the same way you access the fields of a record: using dot notation and field names. The following is a concrete example of a UNION type declaration and a variable of the UNION type:type numeric: union i: int32; u: uns32; r: real64; endunion; . . . static number: numeric; . . . mov( 55, number.u ); . . . mov( -5, number.i ); . . . stdout.put( "Real value = ", number.r, nl );
The important thing to note about UNION objects is that all the fields of a UNION have the same offset in the structure. In the example above, the number.u, number.i, and number.r fields all have the same offset: zero. Therefore, the fields of a UNION overlap one another in memory; this is very similar to the way the 80x86 eight, sixteen, and thirty-two bit registers overlap one another. Usually, access to the fields of a UNION are mutually exclusive; that is, you do not manipulate separate fields of a particular UNION variable concurrently because writing to one field overwrite's the other fields. In the example above, any modification of number.u would also change number.i and number.r.
Programmers typically use UNIONs for two different reasons: to conserve memory or to create aliases. Memory conservation is the intended use of this data structure facility. To see how this works, let's compare the numeric UNION above with a corresponding record type:type numericRec: record i: int32; u: uns32; r: real64; endrecord;
If you declare a variable, say n, of type numericRec, you access the fields as n.i, n,u, and n.r; exactly as though you had declared the variable to be type numeric. The difference between the two is that numericRec variables allocate separate storage for each field of the record while numeric objects allocate the same storage for all fields. Therefore, @size(numericRec) is 16 since the record contains two double word fields and a quad word (real64) field. @size(numeric), however, is eight. This is because all the fields of a UNION occupy the same memory locations and the size of a UNION object is the size of the largest field of that object (see Figure 5.2).
Figure 5.2 Layout of a UNION versus a RECORD Variable
In addition to conserving memory, programmers often use UNIONs to create aliases in their code. As you may recall, an alias is a different name for the same memory object. Aliases are often a source of confusion in a program so you should use them sparingly; sometimes, however, using an alias can be quite convenient. For example, in some section of your program you might need to constantly use type coercion to refer to an object using a different type. Although you can use an HLA TEXT constant to simplify this process, another way to do this is to use a UNION variable with the fields representing the different types you want to use for the object. As an example, consider the following code:type CharOrUns: union c:char; u:uns32; endrecord; static v:CharOrUns;
With a declaration like the above, you can manipulate an uns32 object by accessing v.u. If, at some point, you need to treat the L.O. byte of this uns32 variable as a character, you can do so by simply accessing the v.c variable, e.g.,mov( eax, v.u ); stdout.put( "v, as a character, is `", v.c, "`" nl );
You can use UNIONs exactly the same way you use RECORDs in an HLA program. In particular, UNION declarations may appear as fields in RECORDs, RECORD declarations may appear as fields in UNIONs, array declarations may appear within UNIONs, you can create arrays of UNIONs, etc.
5.10 Anonymous Unions
Within a RECORD declaration you can place a UNION declaration without specifying a fieldname for the union object. The following example demonstrates the syntax for this:type HasAnonUnion: record r:real64; union u:uns32; i:int32; endunion; s:string; endrecord; static v: HasAnonUnion;
Whenever an anonymous union appears within an RECORD you can access the fields of the UNION as though they were direct fields of the RECORD. In the example above, for example, you would access v's u and i fields using the syntax "v.u" and "v.i", respectively. The u and i fields have the same offset in the record (eight, since they follow a real64 object). The fields of v have the following offsets from v's base address:v.r 0 v.u 8 v.i 8 v.s 12
@size(v) is 16 since the u and i fields only consume four bytes between them.
Warning: HLA gets confused if you attempt to create a record constant when that record has anonymous unions (HLA doesn't allow UNION constants). So don't create record constants of a record if that record contains anonymous unions as fields.
HLA also allows anonymous records within unions. Please see the HLA documentation for more details, though the syntax and usage is identical to anonymous unions within records.
5.11 Variant Types
One big use of UNIONs in programs is to create variant types. A variant variable can change its type dynamically while the program is running. A variant object can be an integer at one point in the program, switch to a string at a different part of the program, and then change to a real value at a later time. Many very high level language systems use a dynamic type system (i.e., variant objects) to reduce the overall complexity of the program; indeed, proponents of many very high level languages insist that the use of a dynamic typing system is one of the reasons you can write complex programs in so few lines. Of course, if you can create variant objects in a very high level language, you can certainly do it in assembly language. In this section we'll look at how we can use the UNION structure to create variant types.
At any one given instant during program execution a variant object has a specific type, but under program control the variable can switch to a different type. Therefore, when the program processes a variant object it must use an IF statement or SWITCH statement to execute a different sequence of instructions based on the object's current type. Very high level languages (VHLLs) do this transparently. In assembly language you will have to provide the code to test the type yourself. To achieve this, the variant type needs some additional information beyond the object's value. Specifically, the variant object needs a field that specifies the current type of the object. This field (often known as the tag field) is a small enumerated type or integer that specifies the type of the object at any given instant. The following code demonstrates how to create a variant type:type VariantType: record tag:uns32; // 0-uns32, 1-int32, 2-real64 union u:uns32; i:int32; r:real64; endunion; endrecord; static v:VariantType;
The program would test the v.tag field to determine the current type of the v object. Based on this test, the program would manipulate the v.i, v.u, or v.r field.
Of course, when operating on variant objects, the program's code must constantly be testing the tag field and executing a separate sequence of instructions for uns32, int32, or real64 values. If you use the variant fields often, it makes a lot of since to write procedures to handle these operations for you (e.g., vadd, vsub, vmul, and vdiv). Better yet, you might want to make a class out of your variant types. For details on this, see the chapter on Classes appearing later in this text.