Interfacing with C
Ada allows us to interface with code in many languages, including C and C++. This section discusses how to interface with C.
Multi-language project
By default, when using gprbuild we only compile Ada source files.
To compile C files as well, we need to modify the project file used by
gprbuild. We use the Languages
entry, as in the following
example:
project Multilang is
for Languages use ("ada", "c");
for Source_Dirs use ("src");
for Main use ("main.adb");
for Object_Dir use "obj";
end Multilang;
Type convention
To interface with data types declared in a C application, you specify
the Convention
aspect on the corresponding Ada type
declaration. In the following example, we interface with the
C_Enum
enumeration declared in a C source file:
To interface with C's built-in types, we use the Interfaces.C
package, which contains most of the type definitions we need. For example:
Here, we're interfacing with a C struct (C_Struct
) and using the
corresponding data types in C (int
, long
, unsigned
and
double
). This is the declaration in C:
Foreign subprograms
Calling C subprograms in Ada
We use a similar approach when interfacing with subprograms written in C. Consider the following declaration in the C header file:
Here's the corresponding C definition:
We can interface this code in Ada using the Import
aspect. For example:
If you want, you can use a different subprogram name in the Ada code. For
example, we could call the C function Get_Value
:
Calling Ada subprograms in C
You can also call Ada subprograms from C applications. You do this with
the Export
aspect. For example:
This is the corresponding body that implements that function:
On the C side, we do the same as we would if the function were written
in C: simply declare it using the extern
keyword. For example:
Foreign variables
Using C global variables in Ada
To use global variables from C code, we use the same method as
subprograms: we specify the Import
and Convention
aspects for each variable we want to import.
Let's reuse an example from the previous section. We'll add a global
variable (func_cnt
) to count the number of times the function
(my_func
) is called:
The variable is declared in the C file and incremented in my_func
:
In the Ada application, we just reference the foreign variable:
As we see by running the application, the value of the counter is the
number of times my_func
was called.
We can use the External_Name
aspect to give a different name
for the variable in the Ada application in the same we do for
subprograms.
Using Ada variables in C
You can also use variables declared in Ada files in C applications. In
the same way as we did for subprograms, you do this with the
Export
aspect.
Let's reuse a past example and add a counter, as in the previous example, but this time have the counter incremented in Ada code:
The variable is then increment in My_Func
:
In the C application, we just need to declare the variable and use it:
Again, by running the application, we see that the value from the counter
is the number of times that my_func
was called.
Generating bindings
In the examples above, we manually added aspects to our Ada code to
correspond to the C source-code we're interfacing with. This is called
creating a binding. We can automate this process by using the Ada spec
dump compiler option: -fdump-ada-spec
. We illustrate this by
revisiting our previous example.
This was our C header file:
To create Ada bindings, we'll call the compiler like this:
gcc -c -fdump-ada-spec -C ./test.h
The result is an Ada spec file called test_h.ads
:
Now we simply refer to this test_h
package in our Ada application:
You can specify the name of the parent unit for the bindings you're
creating as the operand to fdump-ada-spec
:
gcc -c -fdump-ada-spec -fada-spec-parent=Ext_C_Code -C ./test.h
This creates the file ext_c_code-test_h.ads
:
Adapting bindings
The compiler does the best it can when creating bindings for a C header
file. However, sometimes it has to guess about the translation and the
generated bindings don't always match our expectations. For example,
this can happen when creating bindings for functions that have
pointers as arguments. In this case, the compiler may use
System.Address
as the type of one or more pointers. Although
this approach works fine (as we'll see later), this is usually not how
a human would interpret the C header file. The following example
illustrates this issue.
Let's start with this C header file:
And the corresponding C implementation:
Next, we'll create our bindings:
gcc -c -fdump-ada-spec -C ./test.h
This creates the following specification in test_h.ads
:
As we can see, the binding generator completely ignores the
declaration struct test
and all references to the test
struct
are replaced by addresses (System.Address
). Nevertheless, these
bindings are good enough to allow us to create a test application in
Ada:
We can successfully bind our C code with Ada using the automatically-generated bindings, but they aren't ideal. Instead, we would prefer Ada bindings that match our (human) interpretation of the C header file. This requires manual analysis of the header file. The good news is that we can use the automatic generated bindings as a starting point and adapt them to our needs. For example, we can:
Define a
Test
type based onSystem.Address
and use it in all relevant functions.Remove the
test_
prefix in all operations on theTest
type.
This is the resulting specification:
And this is the corresponding Ada body:
Now we can use the Test
type and its operations in a clean, readable
way.