19 Java Byte Code and Mixing Languages

 

  <--Last Chapter Table of Contents Next Chapter-->  

 

19.1 Ada Meets Java

ACT's JGNAT compiler will compile Gnat source files into Java applications or applets. This section introduces JGNAT.

19.1.1 The Java Virtual Machine

Java is an interpreted language. It is compiled in an artificial machine language for a computer that doesn't exist. This computer is called the Java Virtual Machine (JVM) and the instructions are known as bytecode. The process is similar to the one used by the UCSD Pascal, a popular language from the 1980s that also used a virtual machine language (called P-Code).

When Java is compiled, the source code is converted to a .class file. This file contains the instructions for the JVM.

The JVM language is not directly related to Java. It is possible for other languages to create JVM executables. The JGNAT compiler converts Ada source files into .class files that can be executed by Java.

The official Java Virtual Machine Specification is available at Sun's Java website.

19.1.2 JGnat

Most of the Gnat tools have a corresponding JGnat version, including gnatmake. To compile an Ada program into a Java byte-code program, use jgnatmake:
  jgnatmake hello

Table: jgnatmake switches

JGnatmake Switch Description
-aConsider all files, even readonly ali files
-cCompile only, do not bind and link
-fForce recompilations of non predefined units
-iIn place. Replace existing ali file, or put it with source
-jnumUse nnn processes to compile
-kKeep going after compilation errors
-mMinimal recompilation
-MList object file dependences for Makefile
-nCheck objects up to date, output next file to compile if not
-o nameChoose an alternate executable name
-qBe quiet/terse
-sRecompile if compiler switches have changed
-vDisplay reasons for all (re)compilations
-zNo main subprogram (zero main)
--GCC=commandUse this jgnat command
--GNATBIND=commandUse this gnatbind command
--GNATLINK=commandUse this gnatlink command
-aLdirSkip missing library sources if ali in dir
-Adirlike -aLdir -aIdir
-aOdirSpecify library/object files search path
-aIdirSpecify source files search path
-IdirLike -aIdir -aOdir
-I-Don't look for sources & library files in the default directory
-LdirLook for program libraries also in dir
-nostdincDon't look for sources in the system default directory
-nostdlibDon't look for library files in the system default directory
-cargs optsopts are passed to the compiler
-bargs optsopts are passed to the binder
-largs optsopts are passed to the linker
-gGenerate debugging information
-IdirSpecify source files search path
-I-Do not look for sources in current directory
-O[0123]Control the optimization level
-gnataAssertions enabled. Pragma Assert/Debug to be activated
-gnatAAvoid processing gnat.adc, if present file will be ignored
-gnatbGenerate brief messages to stderr even if verbose mode set
-gnatcCheck syntax and semantics only (no code generation)
-gnatd?Compiler debug option ? (a-z,A-Z,0-9), see debug.adb
-gnatDDebug expanded generated code rather than source code
-gnateError messages generated immediately, not saved up till end
-gnatEDynamic elaboration checking mode enabled
-gnatfFull errors. Verbose details, all undefined references
-gnatFForce all import/export external names to all uppercase
-gnatgGNAT implementation mode (used for compiling GNAT units)
-gnatGOutput generated expanded code in source form
-gnathOutput this usage (help) information
-gnati?Identifier char set (?=1/2/3/4/8/p/f/n/w)
-gnatkLimit file names to nnn characters (k = krunch)
-gnatlOutput full source listing with embedded error messages
-gnatLUse longjmp/setjmp for exception handling
-gnatmnnnLimit number of detected errors to nnn (1-999)
-gnatnInlining of subprograms (apply pragma Inline across units)
-gnatoEnable overflow checking (off by default)
-gnatO nmSet name of output ali file (internal switch)
-gnatpSuppress all checks
-gnatPGenerate periodic calls to System.Polling.Poll
-gnatqDon't quit, try semantics, even if parse errors
-gnatRList representation information
-gnatsSyntax check only
-gnattTree output file to be generated
-gnatTnnnAll compiler tables start at nnn times usual starting size
-gnatuList units for this compilation
-gnatUEnable unique tag for error messages
-gnatvVerbose mode. Full error output with source lines to stdout
-gnatw?Warning mode. (?=s/e/l/u for suppress/error/elab/undefined)
-gnatWWide character encoding method (h/u/s/e/8/b)
-gnatxSuppress output of cross-reference information
-gnatXLanguage extensions permitted
-gnatyEnable all style checks
-gnatyxxxEnable selected style checks xxx = list of parameters:
  • 1-9 check indentation
  • b check no blanks at end of lines
  • c check comment format
  • e check end labels present
  • f check no form feeds/vertical tabs in source
  • h check no horizontal tabs in source
  • i check if-then layout
  • k check casing rules for keywords, identifiers
  • m check line length <= 79 characters
  • Mnnn check line length <= nnn characters
  • r check RM column layout
  • s check separate subprogram specs present
  • t check token separation rules
-gnatzDistribution stub generation (r/s for receiver/sender stubs)
-gnatZUse zero cost exception handling
-gnat83Enforce Ada 83 restrictions
 
jgnatmake will create two files: hello.class and ada_hello.class. To run the program under the Java interpreter, type
  java hello

Table: java switches

Java Interpreter Switch Description
-help Print usage info
-versionPrint version number
-ss sizeMaximum native stack size
-mx sizeMaximum heap size
-ms sizeInitial heap size
-as sizeHeap increment
-classpath pathSet classpath
-verifyVerify all bytecode
-verifyremoteVerify bytecode loaded from network
-noverifyDo not verify any bytecode
-Dproperty=valueSet a property
-verbosegcPrint message during garbage collection
-noclassgcDisable class garbage collection
-v, -verboseBe verbose
-verbosejitPrint message during JIT code generation
-verbosememPrint detailed memory allocation statistics
-debugTrace method calls
-noasyncgcDo not garbage collect asynchronously
-cs, -checksourceCheck source against class files
-oss sizeMaximum java stack size
-jarExecutable is a JAR

Limitations: Ada streams don't work with Jgnat.

 

19.2 ASIS

The Ada Semantic Interface Specification (ASIS) is a standard for building development tools for Ada 95. ASIS is implemented as a series of Ada packages and they are included with the Gnat compiler. Debuggers, source code browsers and code checkers can all be written using ASIS.

ASIS works like a database. Different ASIS child packages return different information. A program using ASIS issues queries to the ASIS packages and ASIS returns the query results.

Information and tutorials on ASIS is available from the ASIS Workgroup at http://info.acm.org/sigada/WG/asiswg/asiswg.html.

 

19.3 Assembly Language

This section discusses embedding assembly language into an Ada 95 program using Gnat. There is a tutorial on Ada assembly language programming is available at http://www.adapower.com/articles.

Optimizing your programs is more than just rewriting your Ada source code. Gnat performs many basic optimizations for you. For example, Gnat will take these statements

  x := 5;
  y := x+1;

and rewrite them as

  x := 5;
  y := 6;

to eliminate the addition operation.

In order to improve the performance of you source code, you'll have to consider issues that Gnat cannot know beforehand or issues that Gnat does not consider when it compiles:

The latest Pentium processors make great effort to reduce the average length of time it takes to execute an instruction, even when it makes programs difficult to optimize. In a sense, the processors take such drastic measures to improve bad source code that it's difficult to write good, efficient source code. Even in Ada, reversing two statements can measurably improve (or degrade) performance on a Pentium family processor.

However, there are other reasons to use assembly language besides optimization:

 

19.3.1 Pentium Family Processors

The latest Pentium processors are referred to as "IA-32" (32-bit Intel architecture) in the Intel literature. Although the instructions they execute are based on the 80386 processor, you can think of them as hardware emulators that pretend to follow 80386 instructions: internally, the instructions are broken down into different instructions called "microops".

Despite years of extensions, optimizations, and cache increases, the Pentium family is still just a 32-bit 80386 at its core. There are 8 general purpose registers--although they aren't really general purpose since some are more "general" than others. Only six are normally useful:

The remaining two represent the stack.

Instructions can address less than a register's entire contents. For example EAX can be refered to as AX (lower 16-bits) AH (the second (high) byte), and AL (the first (low) byte). The second four can address only the lower 16-bits by dropping the leading "E" (BP, SI, DI, SP).

There are also 6 segment registers (CS, DS, SS, ES, FS, GS), but you don't normally need to work with these.

An additional register, EFLAGS, contains the status bits for the processor. This represents the results of the last operation or modes that the processor is running in:

bit 0  - carry                    bit 11 - overflow
bit 1  - always 1                 bit 12 - I/O privilege
bit 2  - parity                   bit 13 - I/O privilege
bit 3  - always 0                 bit 14 - nested task
bit 4  - aux carry (BCD)          bit 15 - always 0
bit 5  - always 0                 bit 16 - resume flag
bit 6  - zero flag                bit 17 - virtual-8086 mode
bit 7  - sign                     bit 18 - alignment check
bit 8  - trap flag                bit 19 - virtual interrupt
bit 9  - interrupt enable         bit 20 - virtual interrupt pending
bit 10 - direction                bit 21 - ID flag (has CPUID instruction)

Bits 22-31 are zero.

19.3.2 Instruction Set

The actual instruction set for the Pentium family is too large to list here. The manual is available for download from Intel and is nearly 1000 pages long. They are available at Intel's Web Site. (The instruction set documentation on the Pentium II site covers all IA-32 processors, including Pentium III, 4, and so on.)

Floating point instructions use a separate set of registers and their own instructions usually prefaced by a "F".

The MMX, SSE and SSE2 are collectively referred to as SIMD instructions in the Intel documentation. "SIMD" is an acronym simply meaning an array operation. These instructions load, save and perform operations on part of an array. For example, using MMX instructions, you can load 32 16-bit integers, add them to a second set of 32 16-bit integers, and save the results with only 3 instructions. Since they work on fixed-size blocks, your arrays must be sized accordingly.

Some basic instructions for discussion purposes are:

   call   - call a subroutine
   clc    - clear the carry flag
   add    - add without carry
   adc    - add with carry
   and    - binary and
   inc    - increment
   jxx    - jump on condition where xx is
            - overflow - O / NO
            - carry - C, B, NAE, NC / NB /NAE
            - equal/zero - E / Z / NE / NZ
            - below and equal / above - BE / NA / A / NBE
            - negative - S / NS
            - parity - P / PE / NP / PO
            - < or >=  - L / NGE / GE / NL
            - <= or > - LE / NG / G / GLE
            - aux carry - U / NU
   mov    - load and save registers or transfer memory data
   nop    - no operation (do nothing), useful for experimenting
   or     - binary or
   pop    - pop/pull data from the stack
   push   - push data onto the stack
   pushfl - push EFLAGS onto the stack
   ret    - return from subroutine
   sal    - arithmetic shift left (multiply by 2)
   sar    - arithmetic shift right (divide by 2)
   sbb    - subtract with borrow
   stc    - set carry (set a borrow)
   sub    - subtract without borrow
   xor    - binary exclusive or

Most of these basic instructions can take a length suffix indicating the amount of data: b (byte), w (word), l (long). MOVW moves a 16-bit value. INCL increments a 32-bit value. This suffix is different in Linux than in the Intel documentation because Linux uses the AT&T syntax. For example, PUSHFD (push flags double) is PUSHFL (push flags long) in Linux.

Many of the IA-32 instructions are "CISC" instructions containing an instruction that does the equivalent of two or more simplier instructions at one time. For example, "CMOV" performs a test and then moves the data if the test succeeds--implicitly a jump plus a move. Since the lastest processors are heavily pipelined and paralleled, this type of combined instruction has less of an impact on performance than you might think. The processor sees both CMOV and a jump+MOV as the same sequence of microops internally.

 

19.3.3 Operands

Assembly language instructions take zero or more operands. Here are some examples of operands.

To tie in Ada variables, refer to the variable as %0...%n. The compiler will substitute in the proper operand to access the value of that variable.

In the AT&T assembly language syntax, the order of operands is reversed to that of Intel's literature. For example, to load hex F into EAX, the mov operands would be "$0XF, %eax" not "%eax, $0XF".

 

19.3.4 System.Machine_Code.Asm

To use assemble language, include the System.Machine_Code package in your Ada source file. This package contains a procedure called "asm" for inserting assembly language instructions into a program. This is very similar to the C language function of the same name.

C: The Ada Asm procedure is identical to the C asm function, except that Ada uses the syntax for a normal procedure:
  asm( "shrl $8, %0" : "=r" (answer) : "r" (operand) : "cc" );
becomes
  Asm( "shrl $8, %0", Unsigned_32'Asm_Input( "r", operand ), Unsigned_32'Asm_Output( "=r", answer ), "cc");

The first and only required parameter (named template =>) is the text, as a string, to be given to the assembler. Expect this to be quite literally saved into a temporary file for the GNU assembler to process. As a result, including any formatting, such as line feeds and tabs, so the assembler will read your instructions properly.

The shortest and safest example of asm is:

  asm( "nop" ); -- read instruction but do nothing

In order for Ada to use your assembly code to do something useful, it needs to know how to interface the Ada variables. The "inputs" and "outputs" parameters do this. These parameters use items created by the special 'Asm_Input and 'Asm_Output attributes.

  type'asm_input( constraint_string, variable )
  type'asm_output( constraint_string, variable )

The constraint string tell Ada how to load/save the variables:

These constraints implicitly let Ada know that these registers will be used by you and if it was using them, it will save them prior to executing your assembly code. You don't need to save them yourself.

In addition, there are general constraints that don't specify a particular register:

These aren't as useful as you might think. When using a general constraint, Gnat doesn't keep track of which registers it has assigned. "r", the constraint for any available register, will likely be the EAX accumulator. Using "r" for two inputs is the same as using "a" twice and will cause one value to overwrite the other.

All output constraints must use a "=" to indicate that it's for output.

For example,
    result : interfaces.unsigned_32;
    ...
    movl %%eax, %0  ; save result

would could be done as

    outputs => interfaces.unsigned_32'asm_outputs( "=a", result )
    -- save EAX accumulator into variable named "result"

Multiple input/output parameters are specified as "=> ( first, second, ... )".

When counting, the inputs are numbered first. That is, if you have one item in inputs and one item in outputs, the input is %0 and the output is %1.

 

19.3.5 Other Asm Flags

Another asm parameter, clobber, is a string with the names of the registers that need to be saved besides the ones implicilty referred to in the inputs and outputs. Clobber strings can be a register name, "cc" for processor flag or "memory" for a memory location.

The Asm procedure is treated as a normal Ada procedure. During optimization, Gnat may change the order in which the instructions in your program are executed to improve performance. For example, if your Asm procedure is inside a loop, Gnat may move the procedure outside of the loop if it thinks it is save to do so. This is safe to do for an Ada procedure, but an Asm procedure may suffer side-effects and not function correctly. Use the fourth Asm parameter, volatile, to indicate to Gnat that it is not safe to move your Asm procedure during optimzation.

For the same reason, you should not use two or more Asm procedures in one block of source code because Gnat may attempt to reorder them. Instead, place all your instructions into one Asm procedure to ensure the instructions will execute in the proper order.

 

19.3.6 A Complete Example

with Ada.Text_IO, Interfaces, System.Machine_Code;
use  Ada.Text_IO, Interfaces, System.Machine_Code;

procedure asm4 is
-- A demonstration of Pentium assembly language programming.
--
-- We'll use the Interface package's unsigned_32 integers for the 32-bit
-- values stored in registers.  Of course, we could have made our own types
-- to do the same thing...

function do_math( value1, value2 : unsigned_32 ) return unsigned_32 is
  -- Do some arbitrary math in assembly language.  We'll use
  -- ( value + 1 ) * 2 - value2 in this example.
  result : unsigned_32;
begin

  asm(

       "incl %%eax" & ASCII.LF & ASCII.HT &      -- increment by 1
       "sall %%eax" & ASCII.LF & ASCII.HT &      -- shift left ( * 2 )
       "subl %%ebx, %%eax",                      -- subtract value2 (ebx)

       -- EAX register := value1;
       -- EBX register := value2;

       inputs => (
           unsigned_32'asm_input( "a", value1 ),  -- value1 in EAX
           unsigned_32'asm_input( "b", value2 )   -- value2 in EBX
       ),

       -- result := EAX register;

       outputs => unsigned_32'asm_output( "=a", result ),

       -- The carry flag will be altered in EFLAGS by subl

       clobber => "cc"

  );
  return result;
end do_math;
pragma inline( do_math );

value1 : unsigned_32;
value2 : unsigned_32;
result : unsigned_32;

begin
  value1 := 5;
  value2 := 8;
  result := do_math( value1, value2 );
  put_line( "do_math will do ( value1 + 1 ) * 2 - value2" );
  put( "do_math(" & value1'img & "," & value2'img & ") = " );
  put_line( result'img );
end asm4;

do_math will do ( value1 + 1 ) * 2 - value2
do_math( 5, 8) =  4
 

19.4 Calling Ada from C

It is possible to combine Ada 95 with a main program written in C. Using Ada 95 classes, functions and procedures from another language is more difficult than the reverse process. While the GNAT compiler has a lot of support for other languages, the other languages do not supply the same level of support. Be prepared to do some manual chores in order to compile and link your program.

It is best to use the same GCC compiler for all the source files. Both the ACT and ALT versions of GCC have the C language enabled.

As discussed under types in this document, most C types have direct correspondence to Ada types. A C "int" is the same as an Ada "integer". For greater portability, the Interfaces.C package and its children contain the definitions of many standard C types. Arrays and records are directly equivalent to C arrays and structures. Special cases are noted below.

Before calling any Ada 95 subprograms, the C program should call the function adainit which performs the initializations and elaborations for the Ada 95 source code. Before the C program exits, it should call adafinal to perform any cleanup. These functions are created by gnatbind so you cannot create a C test program without creating at least one an Ada source file as well.

Suppose you want to call a single Ada procedure with no parameters. Your C main program would look something like this.

// main.c
//

#include 

extern void adainit( void );
extern void adafinal( void );

int main( int argc, char **argv) {

  puts("C main() started.");
  adainit();

  ada_subroutine();

  adafinal();
  puts("adafinal() returned.");

  return 0;
}

Create an Ada package containing the "ada_subroutine" procedure. The procedure should be exported to C using pragma export. Because C is a case sensitive language, pragma export will convert the procedure name to lower case characters. (There's another pragma that can change how the case conversion is performed.) Alternatively, you can explicitly supply a new C name in pragma export.

package Test_Subr is

   procedure Ada_Subroutine;
   pragma export(C, Ada_Subroutine);

end Test_Subr;

with Ada.Text_IO;
use Ada.Text_IO;

package body Test_Subr is

   procedure Ada_Subroutine is
   begin
      Put("Ada_Subroutine has been invoked from C.");
   end Ada_Subroutine;

end Test_Subr;

To build the project:

  1. Compile the C and Ada files separately.
  2. Because the project contains Ada source, it will have to be bound using gnatbind -n. The -n switch indicates there is no Ada main program.
  3. Compile file generated by gnatbind. (This contains adainit() and adafinal().)
  4. Link with gnatlink.

The following example uses the ALT GNAT 3.13p:

$ gnatgcc -c main.c
$ gnatgcc -c test_subr.adb
$ gnatbind -n test_subr
$ gnatgcc -c b~test_subr
$ gnatlink -o main main.o test_subr.ali
$ ./main
C main() started.
Ada_Subroutine has been invoked from C.
adafinal() returned.

Functions can likewise be exported.

function Times_2( i : integer ) return integer;
pragma export(C, Times_2);

Although the exported subprograms don't need to be prototyped (declaring the function headers), all subprograms should be prototyped in the same way that external C functions are prototyped. Prototyping ensures that the functions will be called with the proper parameters.

extern int times_2( int );

Declaring a function in Ada doesn't ensure that the parameters are strongly typed. The parameters are set up by C prior to calling the Ada function.

printf( "3 times 2 is %d\n", times_2( 3.0 ) );
will compile and return 6, just as if times_2 was a C function.

If Times_2 is overloaded, exporting it will cause an "already defined" error during linking. You will have to provide pragma export with alternate C names that won't conflict with each other.

function Times_2( i : integer ) return integer;
pragma export(C, Times_2, "int_times_2");

function Times_2( f : float ) return float;
pragma export( C, Times_2, "float_times_2" );

Variables can also be exported.

type a_test_record is record
   i : integer := 3;
   f : float := 1.5;
end record;
test_record : a_test_record;
pragma export(C, test_record );

Define the Ada record as a C structure.

struct a_test_record {
   int i;
   float j;
};
extern struct a_test_record test_record;

You can now access the fields of the Ada record from your C program.

printf( "test_record.i is %d\n", test_record.i );
printf( "test_record.j is %f\n", test_record.j );

test_record.i is 3 test_record.j is 1.500000

Likewise arrays can be exported. Remember that in C, array indices always start at zero.

type a_test_array is array(1..5) of character;
test_array : a_test_array := ('a','b','c','d','e');
pragma export(C, test_array );

extern char test_array[5];
...
printf( "test_array[0] is %c\n", test_array[0] );
printf( "test_array[4] is %c\n", test_array[4] );

test_array[0] is a test_array[4] is e

Certain variable types are more difficult to deal with. For example, Ada enumerated types must be declared with "pragma convention( C" to be compatible with C's enumerated type. This is the only case where Gnat stores a type differently for the benefit of another language.

type ada_enum_type is (red, green, blue);
pragma convention( C, ada_enum_type );

ada_enum : ada_enum_type := green;
pragma export( C, ada_enum );

enum ada_enum_type {red, green, blue};
extern enum ada_enum_type ada_enum;
...
if ( ada_enum == green )
   printf( "ada_enum is green\n" );
else
   printf( "ada_enum isn't green\n" );

ada_enum is green

The contents of arrays or records packed with pragma pack are not easily accessible from C. Ada represents these as raw bits.

Ada unconstrained arrays (including unconstrained strings) have no equivalent in C. The easiest work around for strings is to use C strings instead, as defined in the Interfaces.C package. For C to call a subprogram with unbounded Ada strings as parameters, the C program must also pass the string bounds as parameters. [If anyone knows how to do this, email me.--KB]

Ada access types are usually equivalent to C pointers.

 

19.5 Calling C++ from Ada

The Ada 95 standard doesn't support interfacing to C++, but Gnat provides extensions so that GNU C++ source code can be used with Ada.

If possible, the same GCC compiler should be used for all source files. If you are using the ACT version of GNAT 3.x or earlier, you should recompile GNAT to enable C++. The ALT version of GNAT 3.x (usually) has C++ enabled. If C++ is not enabled, you will receive a message from GCC about "cc1plus" (the C++ compiler) not being found. However, for the complete example below, I used two different version of GCC and had no problems.

For the most part, calling C++ from Ada is done the same was as calling C from Ada. Instead of using "C" in pragma import, use "CPP" (that is, C++) as the language convention. However, Gnat provides no support for C++ "name mangling": all C++ declarations should use extern "C" to stop the name mangling. (If you are an adventurer, use the dumpobj -t command to determine the symbol names used by C++ and include them explicitly in pragma import.)

If Ada and C++ use different GCC compilers, the linker may not be able to tell which version of libgcc to use. You can check which library is being used with the gnatlink -v -v (very verbose) switches.

Importing C++ objects into Ada is possible but difficult. C++ and Ada implement objects using different Object Oriented Programming models. C++ objects are not identical to tagged records. Gnat has special pragmas for importing C++ objects:

The Gnat C++ interface proposal also has a CPP_Destructor pragma, but this has not been implemented. [Perhaps it is not necessary? --KB]

There are two naming problems. First, name mangling is necessary with C++ classes. You will have to use objdump to determine the C++ method names. Second, if two C++ methods from two different classes have the same name (this is often the case when overridding), you'll have to declare the C classes in separate Ada packages. Otherwise, pragma import will report an error when attempting to import the same name twice.

Suppose you want to import a C++ car class named cpp_car.

type cpp_car is tagged record
   year       : integer;                     -- year of car
   weight     : integer;                     -- weight of car
   length     : integer;                     -- length of car
   car_vtable : Interfaces.CPP.Vtable_Ptr;   -- always the last field
end record;
pragma CPP_Class( cpp_car );                 -- this is a C++ class
pragma CPP_Vtable( cpp_car, car_vtable, entry_count => 3 ); -- car_vtable has 3 virtual methods

If a class has no vtable, it cannot be imported in Ada. CPP_Class will report an error.

Only the C++ classes you intend to use need to be imported. If cpp_car has a parent class called cpp_vehicle, it does not have to be declared in Ada if it will not be used. However, any fields in cpp_vehicle will have to be added to the beginning of the cpp_car tagged record or they will be missing.

C++ classes imported into Ada can't be assigned. This has to do with the differences in assignment semantics between the two languages. C++ classes used in Ada should always have a constructor because this is the only way to assign values to the object.

function Default_Constructor return cpp_car'class;
pragma CPP_Constructor( Default_Constructor );
-- the default constructor for cpp_car

The name of the constructor is not important. It simply gives the C++ constructor an Ada compatible name.

Other constructors can be imported using different parameters.

function Copy_Constructor( c : cpp_car'class ) return cpp_car'class;
pragma CPP_Constructor( Copy_Constructor );
-- construct a copy of a cpp_car object

Methods that are not virtual can be imported without a special pragma.

function get_year( c : cpp_car'class ) return integer;
pragma import( CPP, get_year );

C++ has no object parameter--it is implied. In Ada, the object must be declared and it can be declared in any position in the parameter list. When importing C++ objects, always put the object name in the position of the first parameter. This is the parameter used by C++ (even though it is not seen by the programmer).

One of the differences between C++ and Ada objects is that Ada has no equivalent of "virtual methods". In Ada, whether or not a method is virtual is determined by the way the class is declared. Also, Ada doesn't allow a class-wide type to be overridden by any children--a class-wide type is always class-wide with no hidden surprises further down the class tree. In C++, virtual functions must be explicitly declared as "virtual". Ada doesn't require all the methods in a C++ class to be imported.

pragma CPP_Virtual identifies which methods are virtual and the position in the vtable. In the simplest case, the first C++ virtual function is at position 1, the second is at position 2, and so on.

function get_total_weight( c : cpp_car ) return integer;
pragma import( CPP, get_total_weight );
pragma CPP_Virtual( get_total_weight, car_vtable, 1 );
-- first virtual function in class
-- child tagged records may override this function

function get_total_length( c : cpp_car ) return integer;
pragma import( CPP, get_total_length );
pragma CPP_Virtual( get_total_length, car_vtable, 2 );
-- second virtual function in class
-- child tagged records may override this function

When a virtual function is not overridden, it must be declared in Ada. Use CPP_Virtual to indicate which parent function to use.

type cpp_luxury_car is new cpp_car ...

function get_total_weight( c : cpp_luxury_car ) return integer;
pragma import( CPP, get_total_weight );
pragma CPP_Virtual( get_total_length, car_vtable, 3 );
-- overridden virtual function

function get_total_length( c : cpp_luxury_car ) return integer;
pragma import( CPP, get_total_length );
pragma CPP_Virtual( get_total_length, car_vtable, 2 );
-- not overridden virtual function
-- in C++, this function does not appear.  It is implied.
-- for a cpp_luxury_car, use cpp_car get_total_length in vtable at position 2

If necessary, Gnat allows the C++ class to be extended with Ada-specific tagged records. (A multi-language class may make a project unnecessarily complex and difficult to debug.)

Private and protected fields in a C++ object and be simulated using a combination of Ada's private keyword and the information hiding capabilities of Ada packages.

To build the project:

  1. Compile the C++ files with the c++ command
  2. Make the Ada project with gnatmake
  3. Bind the Ada project with gnatbind -x
  4. Link the project using gnatlink. Include the C++ library (-lstdc++) and direct Gnat to use the c++ linker (--link=c++). Use -L, if necessary, to specify the stdc++ directory

Here is a complete example using two simple C++ objects.

// c_class.h

  /* Unimaginative class declaration */

  class c_root_class {
    public:
      int i;
      c_root_class( void );
      int get_value ( void ) const;
      void set_value( int new_i );
      virtual int get_total_value( void ) const;
  };
  class c_extended_class: c_root_class {
    public:
      int j;
    c_extended_class( void );
    int get_j_value ( void ) const;
    void set_j_value( int new_j );
    virtual int get_total_value( void ) const;
  };


// c_class.cc
//
#include 
#include "c_class.h"

/* c_root_class: Method Bodies */

  c_root_class::c_root_class( void ) {
     i = 0;
  }

  int c_root_class::get_value( void ) const {
     return i;
  }

  void c_root_class::set_value( int new_i ) {
     i = new_i;
  }

  int c_root_class::get_total_value( void ) const {
     return i;
  }

/* c_extended_class: Method Bodies */

  c_extended_class::c_extended_class( void ) {
     j = 0;
  }
  int c_extended_class::get_j_value( void ) const {
     return j;
  }
  void c_extended_class::set_j_value( int new_j ) {
     j = new_j;
  }
  int c_extended_class::get_total_value( void ) const {
     return i+j;
  }

Now determine the C++ mangled names with objdump.

$ c++ -Wall -c c_class.cc
$ objdump -t c_class.o
c_class.o:     file format elf32-i386

SYMBOL TABLE:
00000000 l    df *ABS*	00000000 c_class.cc
00000000 l    d  .text	00000000 
00000000 l    d  .data	00000000 
00000000 l    d  .bss	00000000 
00000000 l       .text	00000000 gcc2_compiled.
00000000 l    d  .gnu.linkonce.d.__vt_16c_extended_class	00000000 
00000000 l    d  .gnu.linkonce.d.__vt_12c_root_class	00000000 
00000000 l    d  .rodata	00000000 
00000000 l    d  .gnu.linkonce.t.__tf12c_root_class	00000000 
00000000 l    d  .gnu.linkonce.t.__tf16c_extended_class	00000000 
00000000 l    d  .note	00000000 
00000000 l    d  .comment	00000000 
00000000 g     F .text	00000015 __12c_root_class
00000000  w    O .gnu.linkonce.d.__vt_12c_root_class	00000010 __vt_12c_root_class
00000018 g     F .text	0000000c get_value__C12c_root_class
00000024 g     F .text	0000000d set_value__12c_root_classi
00000034 g     F .text	0000000c get_total_value__C12c_root_class
00000040 g     F .text	00000029 __16c_extended_class
00000000  w    O .gnu.linkonce.d.__vt_16c_extended_class	00000010 __vt_16c_extended_class
0000006c g     F .text	0000000d get_j_value__C16c_extended_class
0000007c g     F .text	0000000e set_j_value__16c_extended_classi
0000008c g     F .text	00000011 get_total_value__C16c_extended_class
00000000  w    F .gnu.linkonce.t.__tf16c_extended_class	00000034 __tf16c_extended_class
00000000  w    F .gnu.linkonce.t.__tf12c_root_class	0000002b __tf12c_root_class
00000008       O *COM*	00000004 __ti12c_root_class
00000010       O *COM*	00000004 __ti16c_extended_class
00000000         *UND*	00000000 __rtti_user
00000000         *UND*	00000000 __rtti_class

Write the corresponding Ada packages containing the C++ class interface. Since virtual methods are used, we'll need to define each C++ class in a separate package to avoid problems with pragma import.

with Interfaces.CPP;

package c_root_class_package is

  type c_root_class is tagged record
      i : integer;
      vtable : Interfaces.CPP.VTable_Ptr; -- C++ vtable
  end record;
  pragma CPP_Class( c_root_class );
  pragma CPP_Vtable( c_root_class, vtable, entry_count => 2 );

  function default_constructor return c_root_class'class;
  pragma import( CPP, default_constructor, "__12c_root_class" );
  pragma CPP_Constructor( default_constructor );

  function get_value( cr : c_root_class'class ) return integer;
  pragma import( CPP, get_value, "get_value__C12c_root_class" );

  procedure set_value( cr : c_root_class'class; new_i : integer );
  pragma import( CPP, set_value, "set_value__12c_root_classi" );

  function get_total_value( cr : c_root_class ) return integer;
  pragma import( CPP, get_total_value, "get_total_value__C12c_root_class" );
  pragma CPP_Virtual( get_total_value, vtable, 1 );

end c_root_class_package;

with Interfaces.CPP;
with c_root_class_package; use c_root_class_package;

package c_extended_class_package is

  type c_extended_class is new c_root_class with record
    j : integer;
  end record;
  pragma CPP_Class( c_extended_class );

  function get_j_value ( ce : c_extended_class'class ) return integer;
  pragma import( CPP, get_j_value, "get_j_value__C16c_extended_class" );

  procedure set_j_value( ce : c_extended_class'class; new_j : integer );
  pragma import( CPP, set_j_value, "set_j_value__16c_extended_classi" );

  function extended_constructor return c_extended_class'class;
  pragma import( CPP, extended_constructor, "__16c_extended_class" );
  pragma CPP_Constructor( extended_constructor );

  function get_total_value( cr : c_extended_class ) return integer;
  pragma import( CPP, get_total_value, "get_total_value__C16c_extended_class" );
  pragma CPP_Virtual( get_total_value, vtable, 2 );

end c_extended_class_package;

The main program will declare two objects and test the methods.

with Interfaces.CPP;
with Ada.Text_IO; use Ada.Text_IO;
with c_root_class_package; use c_root_class_package;
with c_extended_class_package; use c_extended_class_package;

procedure main is

  object : c_root_class;
  extended_object : c_extended_class;

begin
  put_line( "Ada Using a C++ Class" );
  new_line;
  put_line( "With object (a c_root_class class object):" );
  put_line( "  object constructor should assign value 0, value is" & get_value( object )'img );
  set_value( object, 12 );
  put_line( "  set_value( object, 12 ), value is" & get_value( object )'img );
  put_line( "  object total value (virtual method) is" & get_total_value( object )'img );
  new_line;
  put_line( "With extended_object (a c_extended_class class object):" );
  put_line( "  extended object constructor should assign j value 0, value j is" & get_j_value( extended_object )'img );
  set_value( extended_object, 12 );
  put_line( "  set_value( extended_object, 12 ), value is" & get_value( extended_object )'img );
  set_j_value( extended_object, 15 );
  put_line( "  set_j_value( extended_object, 15 ), j value is" & get_j_value( extended_object )'img );
  put_line( "  extended object total value (virtual method) is" & get_total_value( extended_object )'img );
  new_line;

  put_line( "Ada finishes" );
end main;

Build and run the project.

$ gnatmake -c main.adb
$ gnatbind -x main.ali
$ gnatlink main c_class.o -lstdc++ -L/usr/lib/gcc-lib/i386-redhat-linux/2.96 --link=c++
$ ./main
Ada Using a C++ Class

With object (a c_root_class class object):
  object constructor should assign value 0, value is 0
  set_value( object, 12 ), value is 12
  object total value (virtual method) is 12

With extended_object (a c_extended_class class object):
  extended object constructor should assign j value 0, value j is 0
  set_value( extended_object, 12 ), value is 12
  set_j_value( extended_object, 15 ), j value is 15
  extended object total value (virtual method) is 27

Ada finishes
 

19.6 Calling Ada from C++

A C++ program can interface to Ada in the same way as C program.

If possible, the same GCC compiler should be used for all source files. If you are using the ACT version of GNAT 3.x or earlier, you should recompile GNAT to enable C++. The ALT version of GNAT 3.x (usually) has C++ enabled. If C++ is not enabled, you will receive a message from GCC about "cc1plus" (the C++ compiler) not being found. However, the example below was compiled with two different versions of GCC and there were no errors.

For the most part, calling Ada from C++ is done the same was as calling Ada from C. Instead of using "C" in pragma export, use "CPP" (that is, C++) as the language convention. However, Gnat provides no support for CPP "name mangling": all Ada extern declarations in a C++ file should use extern "C".

If Ada and C++ use different GCC compilers, the linker may not be able to tell which version of libgcc to use. You can check which library is being used with the gnatlink -v -v (very verbose) switches.

The following is the same sample C program used above in 19.4, converted to C++.

// main.cc
//
#include 

extern "C" {
  void adainit(void);
  void adafinal(void);

  void ada_subroutine( void );
}

int main(int argc, char **argv) {

  puts("C++ main() started.");
  adainit();

  ada_subroutine();

  adafinal();
  puts("adafinal() returned.");

  return 0;
}

The Ada package is the same except that convention CPP is used.


package Test_Subr is

   procedure Ada_Subroutine;
   pragma export(CPP, Ada_Subroutine );

end Test_Subr;

with Ada.Text_IO;
use Ada.Text_IO;

package body Test_Subr is

   procedure Ada_Subroutine is
   begin
      Put("Ada_Subroutine has been invoked from C++.");
   end Ada_Subroutine;

end Test_Subr;

The steps to build the project are similar to C:

  1. Compile the C++ and Ada files separately.
  2. Because the project contains Ada source, it will have to be bound using gnatbind -n. The -n switch indicates there is no Ada main program.
  3. Compile file generated by gnatbind. adafinal()).
  4. Link with gnatlink and use --link=c++ switch. This switch ensures that the C++ elaboration (such as calling constructors on global objects) is done correctly.
$ c++ -c test.cc
$ gnatgcc -c test_subr
$ gnatbind -n test_subr
$ gnatgcc -c b~test_subr
$ gnatlink -o main test.o test_subr.ali --link=c++
$ ./main
C++ main() started.
Ada_Subroutine has been invoked from C++.
adafinal() returned.

Tagged records cannot be exported directly to C++. Gnat does not understand C++ name mangling and it cannot give the tagged record subprograms names that C++ would recognize. Also, C++ does not understand Ada's Object Oriented Programming model--Ada tagged records are not identical to C++ object classes.

In order to use Ada tagged records from C++, you will have (dynamically) declare the objects in Ada and pass a "handle" (an ID number or a pointer) back to C++ to use in reference. The Ada source must have special subprograms to match the handle to a particular object and call the appropriate Ada subprogram for that object on behalf of C++.

 

19.7 Calling Ada from Java

It should be possible to call Ada using Java's C++ importing features.

 

  <--Last Chapter Table of Contents Next Chapter-->