C to Ada Translation Patterns
Naming conventions and casing considerations
One question that may arise relatively soon when converting from C to Ada is the style of source code presentation. The Ada language doesn't impose any particular style and for many reasons, it may seem attractive to keep a C-like style — for example, camel casing — to the Ada program.
However, the code in the Ada language standard, most third-party code,
and the libraries provided by GNAT follow a specific style for
identifiers and reserved words. Using a different style for the rest of
the program leads to inconsistencies, thereby decreasing readability and
confusing automatic style checkers. For those reasons, it's usually
advisable to adopt the Ada style — in which each identifier starts
with an upper case letter, followed by lower case letters (or digits),
with an underscore separating two "distinct" words within the
identifier. Acronyms within identifiers are in upper case. For example,
there is a language-defined package named Ada.Text_IO
. Reserved words
are all lower case.
Following this scheme doesn't preclude adding additional, project-specific rules.
Manually interfacing C and Ada
Before even considering translating code from C to Ada, it's worthwhile to evaluate the possibility of keeping a portion of the C code intact, and only translating selected modules to Ada. This is a necessary evil when introducing Ada to an existing large C codebase, where re-writing the entire code upfront is not practical nor cost-effective.
Fortunately, Ada has a dedicated set of features for interfacing with other
languages. The Interfaces
package hierarchy and the pragmas
Convention
, Import
, and Export
allow you to make
inter-language calls while observing proper data representation for each
language.
Let's start with the following C code:
[C]
#include <stdio.h> struct my_struct { int A, B; }; void call (struct my_struct *p) { printf ("%d", p->A); }
To call that function from Ada, the Ada compiler requires a description of the
data structure to pass as well as a description of the function itself. To
capture how the C struct my_struct
is represented, we can use the
following record along with a pragma Convention
. The pragma directs the
compiler to lay out the data in memory the way a C compiler would.
[Ada]
with Ada.Text_IO; use Ada.Text_IO; with Interfaces.C; procedure Use_My_Struct is type my_struct is record A : Interfaces.C.int; B : Interfaces.C.int; end record; pragma Convention (C, my_struct); V : my_struct := (A => 1, B => 2); begin Put_Line ("V = (" & Interfaces.C.int'Image (V.A) & Interfaces.C.int'Image (V.B) & ")"); end Use_My_Struct;
Describing a foreign subprogram call to Ada code is called binding and it is
performed in two stages. First, an Ada subprogram specification equivalent to
the C function is coded. A C function returning a value maps to an Ada
function, and a void function maps to an Ada procedure. Then, rather than
implementing the subprogram using Ada code, we use a pragma Import
:
procedure Call (V : my_struct);
pragma Import (C, Call, "call"); -- Third argument optional
The Import
pragma specifies that whenever Call
is invoked by Ada
code, it should invoke the Call
function with the C calling convention.
And that's all that's necessary. Here's an example of a call to Call
:
[Ada]
with Interfaces.C; procedure Use_My_Struct is type my_struct is record A : Interfaces.C.int; B : Interfaces.C.int; end record; pragma Convention (C, my_struct); procedure Call (V : my_struct); pragma Import (C, Call, "call"); -- Third argument optional V : my_struct := (A => 1, B => 2); begin Call (V); end Use_My_Struct;
Building and Debugging mixed language code
The easiest way to build an application using mixed C / Ada code is to create
a simple project file for gprbuild and specify C as an additional
language. By default, when using gprbuild we only compile Ada source
files. To compile C code files as well, we use the Languages
attribute and
specify c
as an option, as in the following example of a project file named
default.gpr:
project Default is
for Languages use ("ada", "c");
for Main use ("main.adb");
end Default;
Then, we use this project file to build the application by simply calling
gprbuild
. Alternatively, we can specify the project file on the
command-line with the -P
option — for example,
gprbuild -P default.gpr
. In both cases, gprbuild
compiles all C
source-code file found in the directory and links the corresponding object
files to build the executable.
In order to include debug information, you can use gprbuild -cargs -g
. This
option adds debug information based on both C and Ada code to the executable.
Alternatively, you can specify a Builder
package in the project file and
include global compilation switches for each language using the
Global_Compilation_Switches
attribute. For example:
project Default is
for Languages use ("ada", "c");
for Main use ("main.adb");
package Builder is
for Global_Compilation_Switches ("Ada") use ("-g");
for Global_Compilation_Switches ("C") use ("-g");
end Builder;
end Default;
In this case, you can simply run gprbuild -P default.gpr
to build the
executable.
To debug the executable, you can use programs such as gdb or ddd, which are suitable for debugging both C and Ada source-code. If you prefer a complete IDE, you may want to look into GNAT Studio, which supports building and debugging an application within a single environment, and remotely running applications loaded to various embedded devices. You can find more information about gprbuild and GNAT Studio in the Introduction to GNAT Toolchain course.
Automatic interfacing
It may be useful to start interfacing Ada and C by using automatic binding
generators. These can be done either by invoking gcc
-fdump-ada-spec
option (to generate an Ada binding to a C header file) or
-gnatceg
option (to generate a C binding to an Ada specification file). For
example:
gcc -c -fdump-ada-spec my_header.h
gcc -c -gnatceg spec.ads
The level of interfacing is very low level and typically requires either massaging (changing the generated files) or wrapping (calling the generated files from a higher level interface). For example, numbers bound from C to Ada are only standard numbers where user-defined types may be desirable. C uses a lot of by-pointer parameters which may be better replaced by other parameter modes, etc.
However, the automatic binding generator helps having a starting point which ensures compatibility of the Ada and the C code.
Using Arrays in C interfaces
It is relatively straightforward to pass an array from Ada to C. In particular, with the GNAT compiler, passing an array is equivalent to passing a pointer to its first element. Of course, as there's no notion of boundaries in C, the length of the array needs to be passed explicitly. For example:
[C]
void p (int * a, int length);
[Ada]
procedure Main is type Arr is array (Integer range <>) of Integer; procedure P (V : Arr; Length : Integer); pragma Import (C, P); X : Arr (5 .. 15); begin P (X, X'Length); end Main;
The other way around — that is, retrieving an array that has been creating on the C side — is more difficult. Because C doesn't explicitly carry boundaries, they need to be recreated in some way.
The first option is to actually create an Ada array without boundaries. This is
the most flexible, but also the least safe option. It involves creating an
array with indices over the full range of Integer
without ever creating
it from Ada, but instead retrieving it as an access from C. For example:
[C]
int * f ();
[Ada]
procedure Main is type Arr is array (Integer) of Integer; type Arr_A is access all Arr; function F return Arr_A; pragma Import (C, F); begin null; end Main;
Note that Arr
is a constrained type (it doesn't have the range <>
notation for indices). For that reason, as it would be for C, it's possible to
iterate over the whole range of integer, beyond the memory actually allocated
for the array.
A somewhat safer way is to overlay an Ada array over the C one. This requires having access to the length of the array. This time, let's consider two cases, one with an array and its size accessible through functions, another one on global variables. This time, as we're using an overlay, the function will be directly mapped to an Ada function returning an address:
[C]
int * f_arr (void); int f_size (void); int * g_arr; int g_size;
[Ada]
with System; package Fg is type Arr is array (Integer range <>) of Integer; function F_Arr return System.Address; pragma Import (C, F_Arr, "f_arr"); function F_Size return Integer; pragma Import (C, F_Size, "f_size"); F : Arr (0 .. F_Size - 1) with Address => F_Arr; G_Size : Integer; pragma Import (C, G_Size, "g_size"); G_Arr : Arr (0 .. G_Size - 1); pragma Import (C, G_Arr, "g_arr"); end Fg;with Fg; procedure Main is begin null; end Main;
With all solutions though, importing an array from C is a relatively unsafe pattern, as there's only so much information on the array as there would be on the C side in the first place. These are good places for careful peer reviews.
By-value vs. by-reference types
When interfacing Ada and C, the rules of parameter passing are a bit different with regards to what's a reference and what's a copy. Scalar types and pointers are passed by value, whereas record and arrays are (almost) always passed by reference. However, there may be cases where the C interface also passes values and not pointers to objects. Here's a slightly modified version of a previous example to illustrate this point:
[C]
#include <stdio.h> struct my_struct { int A, B; }; void call (struct my_struct p) { printf ("%d", p.A); }
In Ada, a type can be modified so that parameters of this type can always be passed by copy.
[Ada]
with Interfaces.C; procedure Main is type my_struct is record A : Interfaces.C.int; B : Interfaces.C.int; end record with Convention => C_Pass_By_Copy; procedure Call (V : my_struct); pragma Import (C, Call, "call"); begin null; end Main;
Note that this cannot be done at the subprogram declaration level, so if there is a mix of by-copy and by-reference calls, two different types need to be used on the Ada side.
Naming and prefixes
Because of the absence of namespaces, any global name in C tends to be very long. And because of the absence of overloading, they can even encode type names in their type.
In Ada, the package is a namespace — two entities declared in two different packages are clearly identified and can always be specifically designated. The C names are usually a good indication of the names of the future packages and should be stripped — it is possible to use the full name if useful. For example, here's how the following declaration and call could be translated:
[C]
void registerInterface_Initialize (int size);#include "reg_interface.h" int main(int argc, const char * argv[]) { registerInterface_Initialize(15); return 0; }
[Ada]
package Register_Interface is procedure Initialize (Size : Integer) with Import => True, Convention => C, External_Name => "registerInterface_Initialize"; end Register_Interface;with Register_Interface; procedure Main is begin Register_Interface.Initialize (15); end Main;
Note that in the above example, a use
clause on
Register_Interface
could allow us to omit the prefix.
Pointers
The first thing to ask when translating pointers from C to Ada is: are they needed in the first place? In Ada, pointers (or access types) should only be used with complex structures that cannot be allocated at run-time — think of a linked list or a graph for example. There are many other situations that would need a pointer in C, but do not in Ada, in particular:
Arrays, even when dynamically allocated
Results of functions
Passing large structures as parameters
Access to registers
... others
This is not to say that pointers aren't used in these cases but, more often than not, the pointer is hidden from the user and automatically handled by the code generated by the compiler; thus avoiding possible mistakes from being made. Generally speaking, when looking at C code, it's good practice to start by analyzing how many pointers are used and to translate as many as possible into pointerless Ada structures.
Here are a few examples of such patterns — additional examples can be found throughout this document.
Dynamically allocated arrays can be directly allocated on the stack:
[C]
#include <stdlib.h> int main() { int *a = malloc(sizeof(int) * 10); return 0; }
[Ada]
procedure Main is type Arr is array (Integer range <>) of Integer; A : Arr (0 .. 9); begin null; end Main;
It's even possible to create a such an array within a structure, provided that the size of the array is known when instantiating this object, using a type discriminant:
[C]
#include <stdlib.h> typedef struct { int * a; } S; int main(int argc, const char * argv[]) { S v; v.a = malloc(sizeof(int) * 10); return 0; }
[Ada]
procedure Main is type Arr is array (Integer range <>) of Integer; type S (Last : Integer) is record A : Arr (0 .. Last); end record; V : S (9); begin null; end Main;
With regards to parameter passing, usage mode (input / output) should be preferred to implementation mode (by copy or by reference). The Ada compiler will automatically pass a reference when needed. This works also for smaller objects, so that the compiler will copy in an out when needed. One of the advantages of this approach is that it clarifies the nature of the object: in particular, it differentiates between arrays and scalars. For example:
[C]
void p (int * a, int * b);
[Ada]
package Array_Types is type Arr is array (Integer range <>) of Integer; procedure P (A : in out Integer; B : in out Arr); end Array_Types;
Most of the time, access to registers end up in some specific structures
being mapped onto a specific location in memory. In Ada, this can be achieved
through an Address
clause associated to a variable, for example:
[C]
int main(int argc, const char * argv[]) { int * r = (int *)0xFFFF00A0; return 0; }
[Ada]
with System; procedure Test is R : Integer with Address => System'To_Address (16#FFFF00A0#); begin null; end Test;
These are some of the most common misuse of pointers in Ada. Previous sections of the document deal with specifically using access types if absolutely necessary.
Bitwise Operations
Bitwise operations such as masks and shifts in Ada should be relatively rarely needed, and, when translating C code, it's good practice to consider alternatives. In a lot of cases, these operations are used to insert several pieces of data into a larger structure. In Ada, this can be done by describing the structure layout at the type level through representation clauses, and then accessing this structure as any other.
Consider the case of using a C primitive type as a container for single bit boolean flags. In C, this would be done through masks, e.g.:
[C]
#define FLAG_1 0b0001 #define FLAG_2 0b0010 #define FLAG_3 0b0100 #define FLAG_4 0b1000 int main(int argc, const char * argv[]) { int value = 0; value |= FLAG_2 | FLAG_4; return 0; }
In Ada, the above can be represented through a Boolean array of enumerate values:
[Ada]
procedure Main is type Values is (Flag_1, Flag_2, Flag_3, Flag_4); type Value_Array is array (Values) of Boolean with Pack; Value : Value_Array := (Flag_2 => True, Flag_4 => True, others => False); begin null; end Main;
Note the Pack
directive for the array, which requests that the array
takes as little space as possible.
It is also possible to map records on memory when additional control over the representation is needed or more complex data are used:
[C]
int main(int argc, const char * argv[]) { int value = 0; value = (2 << 1) | 1; return 0; }
[Ada]
procedure Main is type Value_Rec is record V1 : Boolean; V2 : Integer range 0 .. 3; end record; for Value_Rec use record V1 at 0 range 0 .. 0; V2 at 0 range 1 .. 2; end record; Value : Value_Rec := (V1 => True, V2 => 2); begin null; end Main;
The benefit of using Ada structure instead of bitwise operations is threefold:
The code is simpler to read / write and less error-prone
Individual fields are named
The compiler can run consistency checks (for example, check that the value indeed fit in the expected size).
Note that, in cases where bitwise operators are needed, Ada provides modular
types with and
, or
and xor
operators. Further shift
operators can also be provided upon request through a pragma
. So the
above could also be literally translated to:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Value_Type is mod 2 ** 32; pragma Provide_Shift_Operators (Value_Type); Value : Value_Type; begin Value := Shift_Left (2, 1) or 1; Put_Line ("Value = " & Value_Type'Image (Value)); end Main;---- run info:
Mapping Structures to Bit-Fields
In the previous section, we've seen how to perform bitwise operations. In this section, we look at how to interpret a data type as a bit-field and perform low-level operations on it.
In general, you can create a bit-field from any arbitrary data type. First, we declare a bit-field type like this:
[Ada]
type Bit_Field is array (Natural range <>) of Boolean with Pack;
As we've seen previously, the Pack
aspect declared at the end of the
type declaration indicates that the compiler should optimize for size. We must
use this aspect to be able to interpret data types as a bit-field.
Then, we can use the Size
and the Address
attributes of an
object of any type to declare a bit-field for this object. We've discussed the
Size
attribute earlier in this course.
The Address
attribute indicates the address in memory of that object.
For example, assuming we've declare a variable V
, we can declare an
actual bit-field object by referring to the Address
attribute of
V
and using it in the declaration of the bit-field, as shown here:
[Ada]
B : Bit_Field (0 .. V'Size - 1) with Address => V'Address;
Note that, in this declaration, we're using the Address
attribute of
V
for the Address
aspect of B
.
This technique is called overlays for serialization. Now, any operation that we
perform on B
will have a direct impact on V
, since both are using
the same memory location.
The approach that we use in this section relies on the Address
aspect.
Another approach would be to use unchecked conversions, which we'll
discuss in the next section.
We should add the Volatile
aspect to the declaration to cover the case
when both objects can still be changed independently — they need to be
volatile, otherwise one change might be missed. This is the updated
declaration:
[Ada]
B : Bit_Field (0 .. V'Size - 1) with Address => V'Address, Volatile;
Using the Volatile
aspect is important at high level of optimizations.
You can find further details about this aspect in the section about the
Volatile and Atomic aspects.
Another important aspect that should be added is Import
. When used in
the context of object declarations, it'll avoid default initialization which
could overwrite the existing content while creating the overlay — see an
example in the admonition below. The declaration now becomes:
B : Bit_Field (0 .. V'Size - 1)
with
Address => V'Address, Import, Volatile;
Let's look at a simple example:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; procedure Simple_Bitfield is type Bit_Field is array (Natural range <>) of Boolean with Pack; V : Integer := 0; B : Bit_Field (0 .. V'Size - 1) with Address => V'Address, Import, Volatile; begin B (2) := True; Put_Line ("V = " & Integer'Image (V)); end Simple_Bitfield;---- run info:
In this example, we first initialize V
with zero. Then, we use the
bit-field B
and set the third element (B (2)
) to True
.
This automatically sets bit #3 of V
to 1. Therefore, as expected,
the application displays the message V = 4
, which corresponds to
22 = 4.
Note that, in the declaration of the bit-field type above, we could also have used a positive range. For example:
type Bit_Field is array (Positive range <>) of Boolean with Pack;
B : Bit_Field (1 .. V'Size)
with Address => V'Address, Import, Volatile;
The only difference in this case is that the first bit is B (1)
instead
of B (0)
.
In C, we would rely on bit-shifting and masking to set that specific bit:
[C]
#include <stdio.h> int main(int argc, const char * argv[]) { int v = 0; v = v | (1 << 2); printf("v = %d\n", v); return 0; }
Important
Ada has the concept of default initialization. For example, you may set the default value of record components:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Rec is record X : Integer := 10; Y : Integer := 11; end record; R : Rec; begin Put_Line ("R.X = " & Integer'Image (R.X)); Put_Line ("R.Y = " & Integer'Image (R.Y)); end Main;
In the code above, we don't explicitly initialize the components of
R
, so they still have the default values 10 and 11, which are
displayed by the application.
Likewise, the Default_Value
aspect can be used to specify the
default value in other kinds of type declarations. For example:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Percentage is range 0 .. 100 with Default_Value => 10; P : Percentage; begin Put_Line ("P = " & Percentage'Image (P)); end Main;
When declaring an object whose type has a default value, the object will
automatically be initialized with the default value. In the example above,
P
is automatically initialized with 10, which is the default value
of the Percentage
type.
Some types have an implicit default value. For example, access types have a
default value of null
.
As we've just seen, when declaring objects for types with associated
default values, automatic initialization will happen. This can also happens
when creating an overlay with the Address
aspect. The default value
is then used to overwrite the content at the memory location indicated by
the address. However, in most situations, this isn't the behavior we
expect, since overlays are usually created to analyze and manipulate
existing values. Let's look at an example where this happens:
[Ada]
package P is type Unsigned_8 is mod 2 ** 8 with Default_Value => 0; type Byte_Field is array (Natural range <>) of Unsigned_8; procedure Display_Bytes_Increment (V : in out Integer); end P;with Ada.Text_IO; use Ada.Text_IO; package body P is procedure Display_Bytes_Increment (V : in out Integer) is BF : Byte_Field (1 .. V'Size / 8) with Address => V'Address, Volatile; begin for B of BF loop Put_Line ("Byte = " & Unsigned_8'Image (B)); end loop; Put_Line ("Now incrementing..."); V := V + 1; end Display_Bytes_Increment; end P;with Ada.Text_IO; use Ada.Text_IO; with P; use P; procedure Main is V : Integer := 10; begin Put_Line ("V = " & Integer'Image (V)); Display_Bytes_Increment (V); Put_Line ("V = " & Integer'Image (V)); end Main;---- build info:---- run info:
In this example, we expect Display_Bytes_Increment
to display each
byte of the V
parameter and then increment it by one. Initially,
V
is set to 10, and the call to Display_Bytes_Increment
should change it to 11. However, due to the default value associated to the
Unsigned_8
type — which is set to 0 — the value of
V
is overwritten in the declaration of BF
(in
Display_Bytes_Increment
). Therefore, the value of V
is 1
after the call to Display_Bytes_Increment
. Of course, this is not
the behavior that we originally intended.
Using the Import
aspect solves this problem. This aspect tells the
compiler to not apply default initialization in the declaration because the
object is imported. Let's look at the corrected example:
[Ada]
package P is type Unsigned_8 is mod 2 ** 8 with Default_Value => 0; type Byte_Field is array (Natural range <>) of Unsigned_8; procedure Display_Bytes_Increment (V : in out Integer); end P;with Ada.Text_IO; use Ada.Text_IO; package body P is procedure Display_Bytes_Increment (V : in out Integer) is BF : Byte_Field (1 .. V'Size / 8) with Address => V'Address, Import, Volatile; begin for B of BF loop Put_Line ("Byte = " & Unsigned_8'Image (B)); end loop; Put_Line ("Now incrementing..."); V := V + 1; end Display_Bytes_Increment; end P;with Ada.Text_IO; use Ada.Text_IO; with P; use P; procedure Main is V : Integer := 10; begin Put_Line ("V = " & Integer'Image (V)); Display_Bytes_Increment (V); Put_Line ("V = " & Integer'Image (V)); end Main;---- run info:
This unwanted side-effect of the initialization by the Default_Value
aspect that we've just seen can also happen in these cases:
when we set a default value for components of a record type declaration,
when we use the
Default_Component_Value
aspect for array types, orwhen we set use the
Initialize_Scalars
pragma for a package.
Again, using the Import
aspect when declaring the overlay eliminates
this side-effect.
We can use this pattern for objects of more complex data types like arrays or records. For example:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; procedure Int_Array_Bitfield is type Bit_Field is array (Natural range <>) of Boolean with Pack; A : array (1 .. 2) of Integer := (others => 0); B : Bit_Field (0 .. A'Size - 1) with Address => A'Address, Import, Volatile; begin B (2) := True; for I in A'Range loop Put_Line ("A (" & Integer'Image (I) & ")= " & Integer'Image (A (I))); end loop; end Int_Array_Bitfield;---- run info:
In the Ada example above, we're using the bit-field to set bit #3 of the first
element of the array (A (1)
). We could set bit #4 of the second element
by using the size of the data type (in this case, Integer'Size
):
[Ada]
B (Integer'Size + 3) := True;
In C, we would select the specific array position and, again, rely on bit-shifting and masking to set that specific bit:
[C]
#include <stdio.h> int main(int argc, const char * argv[]) { int i; int a[2] = {0, 0}; a[0] = a[0] | (1 << 2); for (i = 0; i < 2; i++) { printf("a[%d] = %d\n", i, a[i]); } return 0; }
Since we can use this pattern for any arbitrary data type, this allows us to easily create a subprogram to serialize data types and, for example, transmit complex data structures as a bitstream. For example:
[Ada]
package Serializer is type Bit_Field is array (Natural range <>) of Boolean with Pack; procedure Transmit (B : Bit_Field); end Serializer;with Ada.Text_IO; use Ada.Text_IO; package body Serializer is procedure Transmit (B : Bit_Field) is procedure Show_Bit (V : Boolean) is begin case V is when False => Put ("0"); when True => Put ("1"); end case; end Show_Bit; begin Put ("Bits: "); for E of B loop Show_Bit (E); end loop; New_Line; end Transmit; end Serializer;package My_Recs is type Rec is record V : Integer; S : String (1 .. 3); end record; end My_Recs;with Serializer; use Serializer; with My_Recs; use My_Recs; procedure Main is R : Rec := (5, "abc"); B : Bit_Field (0 .. R'Size - 1) with Address => R'Address, Import, Volatile; begin Transmit (B); end Main;---- build info:---- run info:
In this example, the Transmit
procedure from Serializer
package
displays the individual bits of a bit-field. We could have used this strategy
to actually transmit the information as a bitstream. In the main application,
we call Transmit
for the object R
of record type Rec
.
Since Transmit
has the bit-field type as a parameter, we can use it
for any type, as long as we have a corresponding bit-field representation.
In C, we interpret the input pointer as an array of bytes, and then use
shifting and masking to access the bits of that byte. Here, we use the
char
type because it has a size of one byte in most platforms.
[C]
typedef struct { int v; char s[4]; } rec;void transmit (void *bits, int len);#include "serializer.h" #include <stdio.h> #include <assert.h> void transmit (void *bits, int len) { int i, j; char *c = (char *)bits; assert(sizeof(char) == 1); printf("Bits: "); for (i = 0; i < len / (sizeof(char) * 8); i++) { for (j = 0; j < sizeof(char) * 8; j++) { printf("%d", c[i] >> j & 1); } } printf("\n"); }#include <stdio.h> #include "my_recs.h" #include "serializer.h" int main(int argc, const char * argv[]) { rec r = {5, "abc"}; transmit(&r, sizeof(r) * 8); return 0; }
Similarly, we can write a subprogram that converts a bit-field — which
may have been received as a bitstream — to a specific type. We can add a
To_Rec
subprogram to the My_Recs
package to convert a bit-field
to the Rec
type. This can be used to convert a bitstream that we
received into the actual data type representation.
As you know, we may write the To_Rec
subprogram as a procedure or as a
function. Since we need to use slightly different strategies for the
implementation, the following example has both versions of To_Rec
.
This is the updated code for the My_Recs
package and the Main
procedure:
[Ada]
package Serializer is type Bit_Field is array (Natural range <>) of Boolean with Pack; procedure Transmit (B : Bit_Field); end Serializer;with Ada.Text_IO; use Ada.Text_IO; package body Serializer is procedure Transmit (B : Bit_Field) is procedure Show_Bit (V : Boolean) is begin case V is when False => Put ("0"); when True => Put ("1"); end case; end Show_Bit; begin Put ("Bits: "); for E of B loop Show_Bit (E); end loop; New_Line; end Transmit; end Serializer;with Serializer; use Serializer; package My_Recs is type Rec is record V : Integer; S : String (1 .. 3); end record; procedure To_Rec (B : Bit_Field; R : out Rec); function To_Rec (B : Bit_Field) return Rec; procedure Display (R : Rec); end My_Recs;with Ada.Text_IO; use Ada.Text_IO; package body My_Recs is procedure To_Rec (B : Bit_Field; R : out Rec) is B_R : Rec with Address => B'Address, Import, Volatile; begin -- Assigning data from overlayed record B_R to output parameter R. R := B_R; end To_Rec; function To_Rec (B : Bit_Field) return Rec is R : Rec; B_R : Rec with Address => B'Address, Import, Volatile; begin -- Assigning data from overlayed record B_R to local record R. R := B_R; return R; end To_Rec; procedure Display (R : Rec) is begin Put ("(" & Integer'Image (R.V) & ", " & (R.S) & ")"); end Display; end My_Recs;with Ada.Text_IO; use Ada.Text_IO; with Serializer; use Serializer; with My_Recs; use My_Recs; procedure Main is R1 : Rec := (5, "abc"); R2 : Rec := (0, "zzz"); B1 : Bit_Field (0 .. R1'Size - 1) with Address => R1'Address, Import, Volatile; begin Put ("R2 = "); Display (R2); New_Line; -- Getting Rec type using data from B1, which is a bit-field -- representation of R1. To_Rec (B1, R2); -- We could use the function version of To_Rec: -- R2 := To_Rec (B1); Put_Line ("New bitstream received!"); Put ("R2 = "); Display (R2); New_Line; end Main;---- build info:---- run info:
In both versions of To_Rec
, we declare the record object B_R
as
an overlay of the input bit-field. In the procedure version of To_Rec
,
we then simply copy the data from B_R
to the output parameter R
.
In the function version of To_Rec
, however, we need to declare a local
record object R
, which we return after the assignment.
In C, we can interpret the input pointer as an array of bytes, and copy the individual bytes. For example:
[C]
typedef struct { int v; char s[3]; } rec; void to_r (void *bits, int len, rec *r); void display_r (rec *r);#include "my_recs.h" #include <stdio.h> #include <assert.h> void to_r (void *bits, int len, rec *r) { int i; char *c1 = (char *)bits; char *c2 = (char *)r; assert(len == sizeof(rec) * 8); for (i = 0; i < len / (sizeof(char) * 8); i++) { c2[i] = c1[i]; } } void display_r (rec *r) { printf("{%d, %c%c%c}", r->v, r->s[0], r->s[1], r->s[2]); }#include <stdio.h> #include "my_recs.h" int main(int argc, const char * argv[]) { rec r1 = {5, "abc"}; rec r2 = {0, "zzz"}; printf("r2 = "); display_r (&r2); printf("\n"); to_r(&r1, sizeof(r1) * 8, &r2); printf("New bitstream received!\n"); printf("r2 = "); display_r (&r2); printf("\n"); return 0; }
Here, to_r
casts both pointer parameters to pointers to char
to get
a byte-aligned pointer. Then, it simply copies the data byte-by-byte.
Overlays vs. Unchecked Conversions
Unchecked conversions are another way of converting between unrelated data
types. This conversion is done by instantiating the generic
Unchecked_Conversions
function for the types you want to convert. Let's
look at a simple example:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; with Ada.Unchecked_Conversion; procedure Simple_Unchecked_Conversion is type State is (Off, State_1, State_2) with Size => Integer'Size; for State use (Off => 0, State_1 => 32, State_2 => 64); function As_Integer is new Ada.Unchecked_Conversion (Source => State, Target => Integer); I : Integer; begin I := As_Integer (State_2); Put_Line ("I = " & Integer'Image (I)); end Simple_Unchecked_Conversion;
In this example, As_Integer
is an instantiation of
Unchecked_Conversion
to convert between the State
enumeration and
the Integer
type. Note that, in order to ensure safe conversion, we're
declaring State
to have the same size as the Integer
type we
want to convert to.
This is the corresponding implementation using overlays:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; procedure Simple_Overlay is type State is (Off, State_1, State_2) with Size => Integer'Size; for State use (Off => 0, State_1 => 32, State_2 => 64); S : State; I : Integer with Address => S'Address, Import, Volatile; begin S := State_2; Put_Line ("I = " & Integer'Image (I)); end Simple_Overlay;---- run info:
Let's look at another example of converting between different numeric formats.
In this case, we want to convert between a 16-bit fixed-point and a 16-bit
integer data type. This is how we can do it using Unchecked_Conversion
:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; with Ada.Unchecked_Conversion; procedure Fixed_Int_Unchecked_Conversion is Delta_16 : constant := 1.0 / 2.0 ** (16 - 1); Max_16 : constant := 2 ** 15; type Fixed_16 is delta Delta_16 range -1.0 .. 1.0 - Delta_16 with Size => 16; type Int_16 is range -Max_16 .. Max_16 - 1 with Size => 16; function As_Int_16 is new Ada.Unchecked_Conversion (Source => Fixed_16, Target => Int_16); function As_Fixed_16 is new Ada.Unchecked_Conversion (Source => Int_16, Target => Fixed_16); I : Int_16 := 0; F : Fixed_16 := 0.0; begin F := Fixed_16'Last; I := As_Int_16 (F); Put_Line ("F = " & Fixed_16'Image (F)); Put_Line ("I = " & Int_16'Image (I)); end Fixed_Int_Unchecked_Conversion;
Here, we instantiate Unchecked_Conversion
for the Int_16
and
Fixed_16
types, and we call the instantiated functions explicitly. In
this case, we call As_Int_16
to get the integer value corresponding to
Fixed_16'Last
.
This is how we can rewrite the implementation above using overlays:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; procedure Fixed_Int_Overlay is Delta_16 : constant := 1.0 / 2.0 ** (16 - 1); Max_16 : constant := 2 ** 15; type Fixed_16 is delta Delta_16 range -1.0 .. 1.0 - Delta_16 with Size => 16; type Int_16 is range -Max_16 .. Max_16 - 1 with Size => 16; I : Int_16 := 0; F : Fixed_16 with Address => I'Address, Import, Volatile; begin F := Fixed_16'Last; Put_Line ("F = " & Fixed_16'Image (F)); Put_Line ("I = " & Int_16'Image (I)); end Fixed_Int_Overlay;---- run info:
Here, the conversion to the integer value is implicit, so we don't need to call a conversion function.
Using Unchecked_Conversion
has the advantage of making it clear that a
conversion is happening, since the conversion is written explicitly in the
code. With overlays, that conversion is automatic and therefore implicit. In
that sense, using an unchecked conversion is a cleaner and safer approach.
On the other hand, an unchecked conversion requires a copy, so it's less
efficient than overlays, where no copy is performed — because one change
in the source object is automatically reflected in the target object (and
vice-versa). In the end, the choice between unchecked conversions and overlays
depends on the level of performance that you want to achieve.
Also note that an unchecked conversion only has defined behavior when instantiated for constrained types. For example, we shouldn't use this kind of conversion:
Ada.Unchecked_Conversion (Source => String,
Target => Integer);
Although this compiles, the behavior will only be well-defined in those cases
when Source'Size = Target'Size
. Therefore, instead of using an
unconstrained type for Source
, we should use a subtype that matches this
expectation:
subtype Integer_String is String (1 .. Integer'Size / Character'Size);
function As_Integer is new
Ada.Unchecked_Conversion (Source => Integer_String,
Target => Integer);
Similarly, in order to rewrite the examples using bit-fields that we've
seen in the previous section, we cannot simply instantiate
Unchecked_Conversion
with the Target
indicating the
unconstrained bit-field, such as:
Ada.Unchecked_Conversion (Source => Integer,
Target => Bit_Field);
Instead, we have to declare a subtype for the specific range we're interested in. This is how we can rewrite one of the previous examples:
[Ada]
with Ada.Text_IO; use Ada.Text_IO; with Ada.Unchecked_Conversion; procedure Simple_Bitfield_Conversion is type Bit_Field is array (Natural range <>) of Boolean with Pack; V : Integer := 4; -- Declaring subtype that takes the size of V into account. -- subtype Integer_Bit_Field is Bit_Field (0 .. V'Size - 1); -- NOTE: we could also use the Integer type in the declaration: -- -- subtype Integer_Bit_Field is Bit_Field (0 .. Integer'Size - 1); -- -- Using the Integer_Bit_Field subtype as the target function As_Bit_Field is new Ada.Unchecked_Conversion (Source => Integer, Target => Integer_Bit_Field); B : Integer_Bit_Field; begin B := As_Bit_Field (V); Put_Line ("V = " & Integer'Image (V)); end Simple_Bitfield_Conversion;
In this example, we first declare the subtype Integer_Bit_Field
as a
bit-field with a length that fits the V
variable we want to convert to.
Then, we can use that subtype in the instantiation of
Unchecked_Conversion
.