1. Getting Started

1.1. Introduction

GNAT Pro Common Code Generator (also known as GNAT Pro CCG) is a compiler based on the GNAT Pro technology that takes a subset of Ada source code as input and generates corresponding C source code with the same semantics, suitable for compilation by any target C compiler. In other words, a subset of C is used as a high-level and portable assembly to compile Ada source code.

Note that this manual comes as a complement to the GNAT, GPRbuild, and GNAT Studio documentation (User’s Guides and Reference Manuals).

1.2. How to Use GNAT Pro CCG

In order to use the GNAT Pro CCG compiler, you first need to create a project file (.gpr) as described in the CodePeer User’s Guide, section Setting Up a Project File or in the GPRbuild User’s Guide, section GNAT Project Manager.

In a nutshell, you need to at least specify the source directories where your Ada source files are located, and it is recommended, although not mandatory, to specify an object directory where the generated C code will be created. Here is an example of a project file:

project My_Project is
   for Source_Dirs use (".", "src1", "src2");  -- where source files are
   for Object_Dir use "obj";  --  where compilation artifacts will be
   for Main use ("main_unit.adb");  -- the name of the main file

   package Compiler is
      for Switches ("Ada") use
        ("-gnatp");  -- switches used for compiling Ada files
   end Compiler;
end My_Project;

You need to run using gprbuild and specify the special “c” target, either via the --target command line switch, or via the Target project file attribute:

$ gprbuild -p --target=c -Pmy_project

or:

project My_Project is
   for Target use "c";
   for Source_Dirs use ...;
   --  other project properties
end My_Project;
$ gprbuild -p -Pmy_project

gprbuild knows which files need to be (re)compiled and will generate a .c file for each Ada unit in your project under the object directory of your project and a bind file which takes care of initializing the runtime and invoking the main unit (for a commented example see http://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/example_of_binder_output.html).

Calling the target C compiler on the generated C files and performing the final link is left as the responsibility of the user, since it depends on the compiler environment used.

For example, assuming a GCC C compiler, you can do:

$ cd obj
$ gcc *.c -o main

2. Supported Constructs and Limitations

2.1. Supported Constructs

Here is a list of constructs supported and not supported by this technology:

  • Support for constructs that do not require runtime support:

    • packages (including child, separate, and generic packages)

    • subprograms (including separate, generic, nested, and overloaded subprograms)

    • most Ada types and subtypes, including:

      • scalar types (integer, enumeration)

      • floating point types

      • fixed point types

      • access types

      • constrained and unconstrained arrays

        Note that support for multidimensional unconstrained arrays requires a C99 compatible C compiler. If the target C compiler only supports earlier versions of the C standard, then only one-dimensional unconstrained arrays can be used.

        Array indices have to be within the bounds of the size_t C type, in other words you cannot use e.g. array indices of more than 2**31-1 on 32-bit targets.

      • record types

      • tagged types: Objects of tagged and class-wide types may be declared. Dispatching and class-wide subprograms are supported (including the object.operation notation).

      • private types

      • limited support for dynamically sized record types: records with a single (last) field whose size depends on a discriminant. Note that this feature builds on top of the C idiom “field[1];” where field is actually a larger array (allocated explicitly with sufficient memory). This idiom was replaced by flexible array declaration: “field[];” in C99.

  • Support for a minimal standard library only

  • delay until statement

  • Representation clauses that map to C bitfields only (records containing integer types only, of 1 to 64 bits)

  • Unsupported record representation clauses will generate a warning (unsupported representation clause, assuming confirming) and the generated code will ignore the unsupported representation clause, assuming the clause is simply confirming the default layout. This assumption (and this warning) need to be verified manually.

  • Support for packed arrays (e.g. arrays of booleans)

  • Support for assertions/preconditions/postconditions

  • Raising an exception (explicitly or implicitly via a runtime check or an assertion failure) is mapped to a call to a last chance handler subprogram. See Exception Handling for more details.

  • Support for most runtime checks: range, access, index, divide-by-zero checks.

  • Limited support for ‘Size:

    • Reading ‘Size is implemented via the C sizeof() built-in

    • Setting the Size or Object_Size of types and objects is ignored in most cases, the size of the underlying type is left to the underlying C compiler.

  • Support for ‘Valid on floating point requires the C99 isfinite() function.

  • No support for overflow checks

  • No support for subprograms in generic packages instantiated inside a subprogram, or for runtime pre/postconditions on subprograms in generic packages.

  • No support for the following constructs requiring runtime:

    • tasking

    • controlled types

    • interface types

    • exception handling

    • storage pools

    • functions returning unconstrained arrays or dynamically sized records

  • No support for ‘Image on floating point and fixed point types, only scalars

  • No support for the following attributes:

    • Alignment

    • Component_Size

    • Rounding

    • Bit

    • Bit_Position

    • First_Bit

    • Last_Bit

    • Position

    • Constrained

    • Mechanism_Code

    • Null_Parameter

    • Passed_By_Reference

  • No support for assembly insertion. If you need to include assembly code, you can do so by putting the assembly in a separate assembly file.

2.2. Minimal Runtime

GNAT Pro CCG comes with a minimal runtime including the following packages:

  • Ada

  • Ada.Assertions

  • Ada.Numerics

  • Ada.Numerics.Elementary_Functions

    This is a simplified version of this package which maps directly to the underlying <math.h> C library.

  • Ada.Numerics.Long_Elementary_Functions

    This is a simplified version of this package which maps directly to the underlying <math.h> C library.

  • Ada.Real_Time

    • Support for Ada.Real_Time.Clock requires the following two functions to be provided and tailored for the underlying target:

      unsigned long rts_monotonic_clock (void);
      /*  Returns time from the board/OS  */
      
      int rts_rt_resolution (void);
      /*  Returns resolution of the underlying clock used to implement  */
      /*  Monotonic_Clock.  */
      
    • Support for the delay until statement requires the following external function:

      void rts_delay_until (unsigned long t);
      /*  Delay until a given time as returned by rts_monotonic_clock  */
      
  • Ada.Text_IO and Text_IO

    This is a simplified version for doing simple output of characters and strings, and no input. If you want to output scalar types, you can use the ‘Image attribute to transform a scalar value into a string. This package depends on the C standard function putchar().

  • Ada.Unchecked_Conversion and Unchecked_Conversion

  • Ada.Unchecked_Deallocation and Unchecked_Deallocation

  • Interfaces

  • Interfaces.C

  • Interfaces.C.Extensions

  • System

  • System.Storage_Elements

  • GNAT

  • GNAT.Source_Info

  • standard.h

    This C header provides predefined Ada definitions as part of package Standard as well as some declarations needed by the generated code. It provides default settings suitable for most C99-compatible compilers (e.g. assumes that long long is supported by the compiler). Depending on your target C compiler, you might need to customize this file. You can either modify the file in the installation directory directly, or you can make a copy in one of your source directories, and then set the ADA_INCLUDE_PATH environment variable to point to this source directory, e.g:

    export ADA_INCLUDE_PATH=<directory containing standard.h>
    

    If your target C compiler does not provide any standard include and your Ada code does not depend on any C standard library, then you can compile the target C code with the -DCCG_NO_STD_INCLUDE which will disable the use of any include directive, unless your C compiler supports ISO C99 and sets the corresponding macro __STDC_VERSION__ to a value greater or equal to 199901L in which case, stdint.h and stdbool.h are referenced.

    If you want to override the definition of the GNAT_INLINE macro without modifying standard.h, you can do so when compiling the target C code by redefining this macro via e.g. -DGNAT_INLINE=inline.

This runtime can be found under the directory listed by the following command:

$ c-gnatls -v | grep adainclude

2.3. Use of GNAT runtime files

Some constructs (in particular the Image attribute and the “**” operator) require support from GNAT runtime units. These runtime units can be found under the adainclude directory, as given by the c-gnatls -v command. The corresponding .c and .h files for these units can be found under the adalib directory. You will need to include these files as part of your C build if and only if you use these Ada constructs. To do so, one option is to copy the needed C files to your C build directory and compile these files with your target C compiler, along with the C files generated by GNAT Pro CCG. The C linker will typically tell you that some symbols are missing, so if you can successfully and fully link your Ada application then it means that you do not need to link with GNAT runtime files.

See also Composite Assignment and Comparison for other external dependencies generated directly by GNAT Pro CCG.

2.4. Composite Assignment and Comparison

In order to implement support for composite (records and arrays) assignments, The compiler may generate calls to the standard C routines memcpy() and memmove() that need to be provided either by your target C compiler, or as part of your sources.

Similarly for composite comparison, calls to memcmp() may be generated.

If your target C compiler does not provide these routines then you have several options:

  • manually replace composite assignments or comparisons in the code, for example by using loops (in the case of arrays).

  • or provide your own implementation of these routines.

For example, here is a simple implementation of memcpy() that you can add as part of your project:

void *memcpy(void *dest, const void *src, size_t n)
{
  char *src_p = (char *)src;
  char *dest_p = (char *)dest;
  size_t i;

  if (n == 0)
    return dest;

  /* Copy contents of src[] to dest[] backwards. This loop in particular */
  /* properly handles the case of n == SIZE_MAX. */

  i = n - 1;
  do {
    dest_p[i] = src_p[i];
  } while (i-- > 0);

  return dest;
}

Similarly for memmove():

void *memmove(void *dest, const void *src, size_t n)
{
  char *src_p = (char *)src;
  char *dest_p = (char *)dest;
  size_t i;

  if (n == 0)
    return dest;

  /* This function must handle overlapping memory regions  */
  /* for the source and destination. If the dest buffer is */
  /* located in the middle of the src buffer then we use   */
  /* backward copying, and forward copying otherwise.      */

  if (dest > src && dest < src + n)
    {
      i = n - 1;
      do {
        dest_p[i] = src_p[i];
      } while (i-- > 0);
    }
  else
    {
      /* Copy in two parts to properly handle the case of n == SIZE_MAX */
      for (i = 0; i < n - 1; i++)
        dest_p[i] = src_p[i];
      /* i == n - 1 at this stage */
      dest_p[i] = src_p[i];
    }

  return dest;
}

And memcmp():

int memcmp(const void *s1, const void *s2, size_t n)
{
  char *s1_p = (char *)s1;
  char *s2_p = (char *)s2;
  size_t i;

  if (n == 0)
    return 0;

  for (i = 0; i < n - 1; i++)
    {
      if (s1_p[i] < s2_p[i])
        return -1;
      else if (s1_p[i] > s2_p[i])
        return 1;
    }

  /* i == n - 1 at this stage */
  if (s1_p[i] < s2_p[i])
    return -1;
  else if (s1_p[i] > s2_p[i])
    return 1;
  else
    return 0;
}

3. Configuration of the Compiler

3.1. Relevant Switches

Here are some switches which influence the generation of C code:

As well as some advanced switches:

  • -gnatd.4

    This switch forces the generation of a C file even in the case of errors (e.g. unsupported constructs) emitted by the code generator. The resulting file will likely be incomplete and uncompilable. Note that this switch has no effect in the case of illegal Ada code.

  • -gnatd.5

    This switch may optionally be used to stop the generation of C profiles for subprograms imported with a C convention. This switch can be useful in case the Ada profile for an imported C subprogram generates a conflict with the actual C declaration. When using this switch you will need to provide the C declaration by another means, for example by adding the needed #include directive in the standard.h file that comes with the runtime.

  • -gnatd.6

    By default the back-end avoids declaring types that are not referenced by the generated C code; these types may be internal types generated by the front-end or types defined in the sources passed to the compiler that are not used to declare other types or variables. This switch may be used to force the output of all the types.

3.2. Target Configuration File

In order to best describe your actual target, you may need to define a target configuration file describing e.g. the endianness of the target, the size of pointer and integer types, etc. In particular, the compiler is assuming by default a 32-bit little-endian target. So if your target is different, you will need to create a target configuration file by specifying this appropriately in the project file:

package Builder is
   for Global_Compilation_Switches ("Ada") use
     ("-gnateT=" & project'Project_Dir & "/target.atp");
end Builder;

where target.atp is a file stored here in the same directory as your project file, which contains the target parameterization. The format of this file is described in the GNAT User’s Guide as part of the -gnateT switch description.

Here is an example of a configuration file for a bare board PowerPC 750 processor configured as big-endian:

Bits_BE                       1
Bits_Per_Unit                 8
Bits_Per_Word                32
Bytes_BE                      1
Char_Size                     8
Double_Float_Alignment        0
Double_Scalar_Alignment       0
Double_Size                  64
Float_Size                   32
Float_Words_BE                1
Int_Size                     32
Long_Double_Size             64
Long_Long_Size               64
Long_Size                    32
Maximum_Alignment            16
Max_Unaligned_Field          64
Pointer_Size                 32
Short_Enums                   0
Short_Size                   16
Strict_Alignment              1
System_Allocator_Alignment    8
Wchar_T_Size                 32
Words_BE                      1

float          6  I  32  32
double        15  I  64  64
long double   15  I  64  64

3.3. Relevant Pragmas

Most Ada and GNAT pragmas are supported.

The following are particularly relevant in the context of GNAT Pro CCG. You will find more details about these pragmas in the GNAT Reference Manual.

  • pragma Restrictions (No_Dynamic_Sized_Objects)

    Ensure that the Ada code will not contain objects of dynamic size which require the use of alloca() or dynamic sized C objects, which are not supported by all C compilers. This pragma cannot be used when the sources have tagged types since dispatch tables require dynamically sized record types.

  • pragma Restrictions (No_Multiple_Elaboration)

    When this restriction is active, the compiler is allowed to suppress the elaboration counter normally associated with the unit, even if the unit has elaboration code. This counter is typically used to check for access before elaboration and to control multiple elaboration attempts. This is useful in the context of CCG to reduce the size of the generated code to a minimum and control more finely the global symbols in C files.

  • pragma Discard_Names

    Removes generation of internal strings in particular for enumeration types, generating simpler and smaller C code. This pragma also disables the support for ‘Image on enumeration types.

  • pragma Suppress_Exception_Locations

    Suppress messages associated with exceptions (in particular assertions, preconditions, postconditions and predicates), to reduce the memory footprint.

3.4. Dynamic Memory Handling

The use of dynamic memory (access types, aka pointers) is supported by the GNAT Pro CCG compiler, and will generate calls to the standard C functions malloc() (for memory allocation) and free() (for deallocation). If dynamic memory is used in the Ada sources, then malloc() and possibly free() need to be provided by the C compiler.

3.5. Exception Handling

If some runtime checks or assertions are enabled or if there are explicit raise statements in the source, then a call (conditional in case of a runtime check or assertion) to a subprogram with the following C profile is generated:

extern void __gnat_last_chance_handler (const char *file, int line);

This function needs to be provided as part of the user code if checks are enabled or raise statements are used. file if not NULL will represent a C string (nul terminated) with the name of the C file where the exception was raised (this is done via the C __FILE__ macro). The second parameter corresponds to the line number of the exception raise (C __LINE__ macro).

This function can perform e.g. logging activities and is then responsible for stopping the application or restarting it. If this function returns normally, then the execution becomes undefined.

Note that the call is performed via a macro called GNAT_LAST_CHANCE_HANDLER and defined in standard.h. If you define GNAT_LAST_CHANCE_HANDLER before including standard.h then you can override this macro and e.g. ignore the extra parameters, for example:

#define GNAT_LAST_CHANCE_HANDLER(FILE,LINE) my_last_chance_handler()

3.6. Enabling/Disabling Runtime Checks

By default, runtime checks are enabled when generating C code (except for overflow checks which are not supported).

If you want to suppress generation of runtime checks (e.g. because they have been proven by the SPARK toolset or for efficiency), you can use one of the following options:

  • -gnatp compiler switch, which disables all runtime checks

  • specify in a configuration file (named e.g. config.adc):

    pragma Suppress (All_Checks);
    

    In order to enable the use of config.adc you need to add in your project file:

    for Global_Configuration_Pragmas use "config.adc";
    
  • you can also selectively use pragma Suppress in source file, or in a configuration file to suppress only some checks, or checks for only certain packages or subprograms, e.g.:

    pragma Suppress (Range_Checks);  -- suppress range checks only
    

    or in a source file:

    procedure Proc1 is
       pragma Suppress (All_Checks);  -- suppress all checks for this procedure
    begin
       -- ...
    

See the GNAT documentation for more details.

3.7. Enabling/Disabling Runtime Assertions

By default, assertions (including pre- and postconditions) are disabled in the generated code.

There are several ways to enable assertions in the generated code:

  • The -gnata compiler switch will enable all assertions.

  • The pragma Assertion_Policy allows selective enabling/disabling of assertions.

See the GNAT documentation for more details.

3.8. Inserting Arbitrary C Fragments in the Generated Code

It is sometimes useful to insert extra code fragments that are compiler specific, such as #pragma directives.

To insert such code fragments in the generated C files, you can use the pragma Annotate as follows:

pragma Annotate (CCG, C_Pragma, "insert pragma contents");

which when encountered will generate in the C code, starting on a new line at column 1:

#pragma insert pragma contents

A more general syntax can also be used to insert arbitrary code:

pragma Annotate (CCG, Verbatim, "any valid C code");

which will insert in a new line the given any valid C code.

Note that this capability should be used with care and the resulting output should be manually reviewed for correctness, GNAT CCG does not make any attempt at verifying the contents of the C code provided, and this may generate invalid or incorrect C code.

GNAT CCG tries to handle and generate C code sequentially from the Ada code, so the order of insertion will in general follow the original Ada source order, but this is not always possible and in particular in the case of forward declarations of types or objects (including private definitions), this order will not be obeyed, and the location of the inserted C code is not guaranteed to be the expected one and should in any case always be manually reviewed.

4. Debugging & Code Generation Strategy

4.1. Debugging

Support for debugging the generated code comes with several options and associated limitations:

  • host debugging using a native Ada compiler.

    This option provides a powerful debugging experience, but does not allow debugging target-specific issues.

  • debugging the generated C code using a C debugger.

    This option requires no specific toolchain support, and allows debugging target-specific issues, but requires getting familiar with the generated C code. Using the -gnatL switch can help debugging the C code by having the Ada code also available as comments, see Relevant Switches.

  • debugging the Ada code on the target using a C debugger.

    This hybrid solution can be enabled via the -g compiler switch and using the corresponding debug switch for the target C compiler, and the corresponding target C debugger. This switch will generate #line directives in the generated C code, so that messages (errors or warnings) from the C compiler will be redirected to the Ada source code (depending on the C compiler capabilities), and debug information will also point to the Ada code, allowing step-by-step debugging at the Ada source level. This capability depends on the ability of the target C compiler to properly handle #line directives pointing to external non-C files. Some knowledge of the generated C code is also necessary in order to display values of Ada variables properly, in particular using the proper encoding for global variables and subprograms.

4.2. Code Generation Strategy

The code generation strategy used by GNAT Pro CCG is to generate two files for each compiled source file:

  • a .h file (named <source basename>.h) containing first a copy of standard.h, then all the needed declarations (types, object and function declarations) corresponding to the packages withed by the unit being compiled, then the declarations corresponding to the unit spec.

  • a .c file (named <source basename>.c) which includes the header file and contains all the statements and declarations of the unit body.

Each set of declarations coming from specific files (Ada spec files typically) is surrounded with the following preprocessor directives:

#ifndef <FILE>
#define <FILE>
/* C declarations for <FILE> */
#endif /* <FILE> */

For example, declarations from package1.ads will have:

#ifndef PACKAGE1_ADS
#define PACKAGE1_ADS
/* C declarations for package1.ads */
#endif /* PACKAGE1_ADS */

Each global entity (variable, constant, subprogram) will get an encoded name which is fully documented in exp_dbug.ads (part of the compiler sources). In a nutshell, entities in package Pack1 will generate a C symbol called pack1__<entity> (all lowercase), for example subprogram Pack1.Subprogram1 will generate a C function called pack1__subprogram1.

The compiler will generate C89 code as much as possible for maximum portability, unless C89 is too restricted in which case C99 constructs will be used (such as variable size arrays), or in rare cases GCC extensions (a warning will always be generated when using these extensions). GNAT CCG also relies on macros defined in standard.h and that need to be adapted to the target C compiler. The default macros provided are using GCC extensions when the C compiler used is GCC, and left empty for other C compilers. Finally, in order to support some record representation clauses, CCG will emit bitfield structs. Since these are not guaranteed to be portable, a warning is always emitted when generating such bitfields.

4.3. Calling Ada Code from C code

In order to call Ada subprograms and use corresponding data structures from C code, you can #include the relevant .h file(s) generated by the compiler.

For example, if you want to include the definitions of package Pack1 contained in pack1.ads, you need to do:

#include "pack1.h"

See Code Generation Strategy for more details on the contents of these header files.

4.4. Examples of Code Generation

If you consider the following package which uses built-in features of Ada such as exponentiation and functions returning an array:

package P is

   type Int is private;
   type Int_Array is array (1 .. 10) of Int;

   function Square (X : Int) return Int;
   function Square (X : Int_Array) return Int_Array;

private
   type Int is new Integer;
end P;
package body P is

   function Square (X : Int) return Int is
   begin
      return X ** 2;
   end Square;

   function Square (X : Int_Array) return Int_Array is
      Result : Int_Array;
   begin
      for J in X'Range loop
         Result (J) := X (J) ** 2;
      end loop;

      return Result;
   end Square;

end P;

Then the compiler will generate the following C code:

#ifndef P_ADS
#define P_ADS
typedef integer p__TintB;
typedef p__TintB p__int;
typedef p__int p__int_array[10];
extern p__int p__square(p__int x);
extern void p__square__3(p__int_array x, p__int_array RESULT);
#endif /* P_ADS */
#include "p.h"

p__int p__square(p__int x) {
  return ((p__int)(x * x));
}
void p__square__3(p__int_array x, p__int_array RESULT) {
  p__int_array result;
  {
    typedef integer p__square__3__T2b;
    p__square__3__T2b j;

    for (j = 1; j <= 10; j++) {
      result[j - 1] = (x)[j - 1] * (x)[j - 1];
    }
  }
  memcpy(RESULT, result, sizeof(p__int_array));
  return;
}