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
Interfaces package hierarchy and the pragmas
Export allow you to make
inter-language calls while observing proper data representation for each
Let's start with the following C code:
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.
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
procedure Call (V : my_struct); pragma Import (C, Call, "call"); -- Third argument optional
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
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
c as an option, as in the following example of a project file named
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
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.
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
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:
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:
Arr is a constrained type (it doesn't have the
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:
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:
In Ada, a type can be modified so that parameters of this type can always be passed by copy.
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:
Note that in the above example, a
use clause on
Register_Interface could allow us to omit the prefix.
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
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:
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:
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:
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
Address clause associated to a variable, for example:
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 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.:
In Ada, the above can be represented through a Boolean array of enumerate values:
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:
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
xor operators. Further shift
operators can also be provided upon request through a
pragma. So the
above could also be literally translated to:
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:
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.
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:
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
This technique is called overlays for serialization. Now, any operation that we
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
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
B : Bit_Field (0 .. V'Size - 1) with Address => V'Address, Volatile;
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:
In this example, we first initialize
V with zero. Then, we use the
B and set the third element (
B (2)) to
This automatically sets bit #3 of
V to 1. Therefore, as expected,
the application displays the message
V = 4, which corresponds to
\(2^2 = 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
In C, we would rely on bit-shifting and masking to set that specific bit:
Ada has the concept of default initialization. For example, you may set the default value of record components:
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.
Default_Value aspect can be used to specify the
default value in other kinds of type declarations. For example:
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
Some types have an implicit default value. For example, access types have a
default value of
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:
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
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
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.
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:
This unwanted side-effect of the initialization by the
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_Valueaspect for array types, or
when we set use the
Initialize_Scalarspragma for a package.
Again, using the
Import aspect when declaring the overlay eliminates
We can use this pattern for objects of more complex data types like arrays or records. For example:
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,
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:
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:
In this example, the
Transmit procedure from
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,
Transmit for the object
R of record type
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.
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
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
This is the updated code for the
My_Recs package and the
In both versions of
To_Rec, we declare the record object
an overlay of the input bit-field. In the procedure version of
we then simply copy the data from
B_R to the output parameter
In the function version of
To_Rec, however, we need to declare a local
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:
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:
In this example,
As_Integer is an instantiation of
Unchecked_Conversion to convert between the
State enumeration and
Integer type. Note that, in order to ensure safe conversion, we're
State to have the same size as the
Integer type we
want to convert to.
This is the corresponding implementation using overlays:
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
Here, we instantiate
Unchecked_Conversion for the
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
This is how we can rewrite the implementation above using overlays:
Here, the conversion to the integer value is implicit, so we don't need to call a conversion function.
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
Unchecked_Conversion is a cleaner and safer approach.
On the other hand,
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
Unchecked_Conversion can only be instantiated for
constrained types. 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:
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