3.7 System representation of types

In C a number of features are provided to allow the programmer a great deal of flexibility in specifying the structure of data, so for example a status word might be defined as follows.

#pragma pack(1)

typedef struct {
   unsigned status : 4;
   unsigned unused : 3;
   unsigned parity : 1;
} device_status_word;

The initial pragma (common across most C compilers, though preprocessor pragmas are not subject to standardisation) packs all data structures on single byte boundaries, important for this application. The structure then uses the C bit-field syntax to define a structure which takes up a single byte like this:

Unfortunately this is not guaranteed. The actual implementation of bit-fields in C is compiler dependant and can take any form the compiler decides. For example bits may be placed in a different order, instead of from left to right as in the diagram above the compiler may place them right to left. Imagine writing a value of this type to a file using one compiler and reading it with another compiler which has used a different order for the bit-fields.

Also in C you could only use the types int, signed or unsigned to specify bit-fields, C++ expands this to allow char, short, long or enum however it does not attempt to specify the implementation, in fact it states (section 9.6 of [11])

People often try to use bit-fields to save space. Such efforts are often naïve and can lead to waste of space instead.

As you might expect with Ada's background in embedded and systems programming there are ways in which you can force a type into specific system representations (section 13 of the reference manual) such as the C example above. We will start with some of the more basic representation clauses, and finish with an example matching the above. All of the examples in this section are combined into the source example System_Representation.adb.

This first example shows the most common form of system representation clause, the size attribute. We will ask the compiler to give us modular type representing the range 0 to 255 and then forcing it to be 8 bits in size.

declare
   type BYTE is mod 256;
   for BYTE'Size use 8;

   BYTE_Size : Integer := BYTE'Size;

   Storage : Integer := System.Storage_Unit;
   WORD_Size : Integer := System.Word_Size;

We can also, as you can see use this attribute to query the size of an object. We can also use the two values in package System to query the size of a storage unit and the size of the machine word. In most cases the Storage_Unit is a byte and so its value is 8.

Again this is useful for system programming, it gives us the safety of enumeration range checking, so we can only put the correct value into a variable, but does allow us to define what the values are if they are being used in a call which expects specific values.

type Activity is (Reading, Writing, Idle);
for Activity use (Reading => 1, Writing => 2, Idle => 4);

We might find out that the activity data, represented by Activity above, is always to be found in a certain memory location, so we may write:

Status_Address : constant System.Address := 
   System.Storage_Elements.To_Address(16#0340#);

Device_Status : Activity;
for Device_Status'Address use Status_Address;

The value used to specify the address in this case must be a constant value of type System.Address. The most common way to specify an address is to convert from an Integer literal as we have in this case.

The address literal uses Ada's version of the C++ 0x340 notation, where the general form is base#value# where the base is a value between 2 and 16 and the value may include underscores for clarity, so bit masks are very easy to define, for example:

Is_Available : constant BYTE := 2#1000_0000#;
Not_Available: constant BYTE := 2#0000_0000#;

This last example is the most complex, it is declares an Ada record which will match the intention of the C structure device_status_word above. Firstly we will see a normal record declaration, where we declare the components Status, Unused and Parity (with more appropriate types than those allowed by C). We then move onto the representation clauses, the alignment clause is a direct equivalent to the #pragma pack 1 used by C. The second clause specifies the bit order to assume (specified by the enumerationBit_Order in the package System). Finally we specify the exact layout of the components, the value between the at and the range is the offset in storage units of the element and the range is the bits taken within the storage unit.

type Bit_Flag is mod 2**1;

type Device_Status_Word is 
   record 
      Status : Activity;
      Unused : Integer range 0 .. 7;
      Parity : Bit_Flag;
   end record;

for Device_Status_Word'Alignment use 1;
for Device_Status_Word'Bit_Order use Low_Order_First;
for Device_Status_Word use
   record 
      Status at 0 range 0 .. 3;
      Unused at 0 range 4 .. 6;
      Parity at 0 range 7 .. 7;
   end record;

The example above although a little long winded is easy to read, easy to understand and because of careful statement in the language reference manual we know as programmers that we will get a single byte structure with all components where we want them. This representation is absolute and holds for any compiler on any machine. We can then write the structure to a file, pass it over the network and we know that whoever receives it will be able to make sense of it.

As well as the record representation clauses a set of three attributes can be applied specifically to record components, Position, First_Bit and Last_Bit. These can be used to accomplish the same as the above clause.

for Device_Status_Word'Alignment use 1;
for Device_Status_Word'Bit_Order use Low_Order_First;

for Device_Status_Word.Status'Position use 0;
for Device_Status_Word.Status'First_Bit use 0;
for Device_Status_Word.Status'Last_Bit use 3;
for Device_Status_Word.Unused'Position use 0;
for Device_Status_Word.Unused'First_Bit use 4;
for Device_Status_Word.Unused'Last_Bit use 6;
for Device_Status_Word.Parity'Position use 0;
for Device_Status_Word.Parity'First_Bit use 7;
for Device_Status_Word.Parity'Last_Bit use 7;

In C it is very easy to work with values which are considered to represent a set of discrete bits, for example:

#define ISBITSET(value, bit) value && (1 << (bit))

{
   int bit_set;

   get_register(&bit_set);

   if (ISBITSET(bit_set, 0)) 
   {
      do_something();
   }
   if (ISBITSET(bit_set, 1)) 
   {
   do_somthingelse();
   }
   ..
}

The macro tests if the bit specified is set (non-zero) in the value passed. The example then calls some function to set the value of bit_set and then proceeds to perform actions based on the values of different bits.

In Ada this is usually accomplished with a packed array of Boolean's. The pragma Pack is used to represent the object in the smallest possible storage and for an array of boolean values this is one single bit each.

declare
   type Bit_Set is array (Integer range 0 .. Integer'Size - 1)
      of Boolean;
   pragma Pack(Bit_Set);

   Register : Bit_set := (others => False);
begin

   Get_Register(Register);

   if Register(0) then
      Do_Something;
   end if;

   if Register(1) then
      Do_Something_Else;
   end if;
   ..
end ;

The advantages of this approach are that we can have actual types for our bit set's rather than having to rely on integer values. I consider that the readability of the code is much greater in the Ada example and can be enhanced even further by using an enumeration type to specify the array bounds.

declare
   type Register_Bits is (Read_Enabled, Write_Enabled,
                          Flush_Enabled, Restart_Enabled,
                          Async_Write_Enabled,
                          Error_Packet_Enabled,
                          Unused_Bit_0,
                          Unused_Bit_1);

   type Register is array (Register_Bits) of Boolean;
   pragma Pack(Register);

   A_Register : Register:= (others => False);
begin

   Get_Register(A_Register);

   if A_Register(Read_Enabled) then

      Do_Something;
   end if;
   ..
end ;

One further not so obvious feature of the Ada approach is that bit masks can still be used, for example consider this C code:

{
   int required_capability = 15; /* 0,1,2,3 = set */
   int bit_set;

   get_register(&bit_set);

   if ((bit_set && required_capability) == required_capability) 
   {
      do_something();
   }
   ..
}

We have declared a mask which has set all the bits which specify capabilities required to be set, the use of the logical and tests if the bits are set in the register. To accomplish this in Ada we declare an array with the values we require set to True, we then and the two arrays together (such logical operations are defined for array types).

declare
   ..
   Required_Capability : Register := (Read_Enabled => True,
                                      Write_Enabled => True,
                                      Flush_Enabled => True, 
                                      Restart_Enabled => True,
                                      others => False);
   A_Register : Register := (others => False);
begin

   Get_Register(A_Register);

   if (A_Register and Required_Capability) = Required_Capability then

      Do_Something;
   end if;
   ..
end ;

Previous PageContents PageNext Page

Copyright © 1996 Simon Johnston &
Addison Wesley Longman