Static Comparison of Record Layouts

Viewed 161

I have two records, some fields of which need to be in the same positions within each record. Although this has been heavily commented in the code, it is possible that, in 10 years time, a programmer may change one of the records without changing the other and I would like to create a static check that this has not occurred.

I can create an "active" check in a procedure or function as follows:


procedure Main is

    type SimpleRecord1 is record
        FirstItem  : Integer;
        SecondItem : Boolean;
        ThirdItem  : Integer;
        DoNotCare  : Float;
    end record;

    type SimpleRecord2 is record
        Foo        : Integer;
        Bar        : Boolean;
        Baz        : Integer;
        Rfc3092    : Boolean;
    end record;

    MyRecord1 : SimpleRecord1;
    MyRecord2 : SimpleRecord2;
begin
    Pragma Assert ((MyRecord1.FirstItem'Position = MyRecord2.Foo'Position) and
                   (MyRecord1.SecondItem'Position = MyRecord2.Bar'Position) and
                   (MyRecord1.ThirdItem'Position = MyRecord2.Baz'Position));

    Put_Line ("The assert didn't fire");
          
end Main;

I am concerned that the first three vairiables have the same offsets within the two records. In the real code there are dozens of other variables within each record which are not the same between the records.

However, I would really like this to be a check, not on instances of the records (MyRecord1, MyRecord2), but on the records themselves (SimpleRecord1, SimpleRecord2). Then it could be placed in the .ads file where the records are defined.

SimpleRecord1.FirstItem'Position

is illegal. Is there a way to create a check without having to make instances and put the code into a function or procedure?

2 Answers

To make the last two comments (by Jere and Jim Rogers) more concrete, indeed the Ada way is to define the types of the list elements so that any kind of element can be placed in the same list, and accessed by the same kind of pointer, without any uncheckable conversions. In the OP's case, IMO the most appropriate method is to make all list elements be tagged records derived from the same abstract parent class where the parent contains the next, prev and priority components. For example like this:

type List_Element;
type List_Ptr is access List_Element'Class;
type List_Element is abstract tagged record
   Next, Prev : List_Ptr;
   Priority   : Boolean;
end record;

type Simple_Record_1 is new List_Element with record
   DoNotCare : Float;
end record;

type Simple_Record_2 is new List_Element with record
   Rfc3092 : Boolean;
end record;

The SW that handles the linked list deals with List_Ptr values that point to List_Element'Class objects but with only the common components Next, Prev and Priority visible. When there is a need to execute some processing that depends on the actual type of the list element, you can use either a dynamically dispatching call, or a membership test followed by a type conversion, to get from a List_Ptr to the underlying Simple_Record_1, for example.

I would do this - especially if you are going to go with Address Overlays and/or Unchecked_Conversion

-----------------------------------------------------------------------------

   type Header_Record is
      record
         First_Item  : Integer;
         Second_Item : Boolean;
         Third_item  : Integer;
      end record
     with Convention => C;
       
   for Header_Record use
      record
         First_Item  at 0 range  0 .. 31;
         Second_Item at 0 range 32 .. 47;
         Third_Item  at 0 range 48 .. 79;
      end record;
   
-----------------------------------------------------------------------------

   type Item_Record_1 is
      record
         Header    : Header_Record;
         DoNotCare : Float;
      end record
     with Convention => C;
   
   for Item_Record_1 use
      record
         Header    at 0 range  0 .. 79;
         DoNotCare at 0 range 80 .. 111;
      end record;

--------------------------------------------------------------------------------

   type Item_Record2 is
      record
         Header : Header_Record;
         Rfc3092 : Boolean;
      end record
     with Convention => C;
   
   for Item_Record2 use
      record
         Header  at 0 range  0 .. 79;
         Rfc3092 at 0 range 80 .. 95;
      end record;
   
-----------------------------------------------------------------------------

Here, we are specifying the exact bit layout. It's a bit of pain that you'll need to use one level of indirection ie

Item_Record1.Header.First_Item

However, this should work. Also, always remember to use

Convention => C

Since Ada record layouts and C struct layouts may vary considerably - which is to be expected, given Ada's rich semantics.

EDIT: In response to portability issues. Although, the OP did not specify that it must be portable, it's still no problem ...

   with Interfaces.C;

   subtype int is Interfaces.C.int;

   type Header_Record is
      record
         First_Item  : Interfaces.C.int;
         Second_Item : Interfaces.C.int; --bool?
         Third_item  : Interfaces.C.int;
      end record
     with Convention => C;
       
   -- Exact values very likely to be different, for demo only
   for Header_Record use
      record
         First_Item  at 0 range  0 .. int'Size;
         Second_Item at 0 range  int'Size + 1 .. int'Size * 2;
         Third_Item  at 0 range  int'Size * 2 + 1 .. int'Size * 3;
      end record;
Related