Interfacing C variadic functions
Note
Variadic convention is supported by
GNAT Community Edition 2020
GCC 11
In C, variadic functions take a variable number of arguments and an ellipsis as the last parameter of the declaration. A typical and well-known example is:
int printf (const char* format, ...);
Usually, in Ada, we bind such a function with just the parameters we want to use:
procedure printf_double
(format : Interfaces.C.char_array;
value : Interfaces.C.double)
with Import,
Convention => C,
External_Name => "printf";
Then we call it as a normal Ada function:
printf_double (Interfaces.C.To_C ("Pi=%f"), Ada.Numerics.π);
Unfortunately, doing it this way doesn't always work because some ABIs use different calling conventions for variadic functions. For example, the AMD64 ABI specifies:
%rax
— with variable arguments passes information about the number of vector registers used;
%xmm0–%xmm1
— used to pass and return floating point arguments.
This means, if we write (in C):
printf("%d", 5);
The compiler will place 0 into %rax
, because we don't pass any float
argument. But in Ada, if we write:
procedure printf_int
(format : Interfaces.C.char_array;
value : Interfaces.C.int)
with Import,
Convention => C,
External_Name => "printf";
printf_int (Interfaces.C.To_C ("d=%d"), 5);
the compiler won't use the %rax
register at all. (You can't include
any float argument because there's no float parameter in the Ada
wrapper function declaration.) As result, you will get a crash, stack
corruption, or other undefined behavior.
To fix this, Ada 2022 provides a new family of calling convention
names — C_Variadic_N
:
The convention
C_Variadic_n
is the calling convention for a variadic C function taking n fixed parameters and then a variable number of additional parameters.
Therefore, the correct way to bind the printf
function is:
procedure printf_int
(format : Interfaces.C.char_array;
value : Interfaces.C.int)
with Import,
Convention => C_Variadic_1,
External_Name => "printf";
And the following call won't crash on any supported platform:
printf_int (Interfaces.C.To_C ("d=%d"), 5);
Without this convention, problems cause by this mismatch can be very hard to debug. So, this is a very useful extension to the Ada-to-C interfacing facility.
Here is the complete code snippet:
with Interfaces.C; procedure Main is procedure printf_int (format : Interfaces.C.char_array; value : Interfaces.C.int) with Import, Convention => C_Variadic_1, External_Name => "printf"; begin printf_int (Interfaces.C.To_C ("d=%d"), 5); end Main;