next up previous contents
Next: 4 Inheritance Up: IMPL_repdoc Previous: 2 Motivation and Assessment   Contents

Subsections

3 Shallow and Deep Classes

We will use the term ``classes'' generically to refer not only to class structures in C++ but also to derived types in F90 and structs in C. Two distinct schemes were developed for handling classes that will traverse language boundaries. The distinction is based on whether memory allocation for the class occurs on the calling language side or on the implementation language side. Shallow classes do not have any memory allocated on the implementation language side - memory is passed to them from the calling language. Their memory can be drawn from the stack in the local procedure, so there is no need for the calling language to ``create'' and ``destroy'' them. Deep classes allocate memory on the implementation language side, and therefore must be ``created'' and ``destroyed'' by the calling language.

Shallow classes have all function calls dispatched at compile time, whereas deep classes implemented in C++ may actually have virtual function tables and runtime dispatching of calls. Either shallow or deep classes may work on or may contain pointers to arrays that are allocated by the calling language. If this is the case, the expectation is that procedures in the implementation language will not be responsible for deallocating or altering the memory associated with the ``foreign language'' array.

All classes are assumed to be ``opaque'' from the calling language side; that is, accessor functions are required to retrieve data or attributes embedded in a class. All data members in our examples are accessed from the implementation language side. This restriction can be lifted in the case of FORTRAN calling C with the introduction of the F200X BIND(C) attribute for derived types [7]. We note that for the compilers we have used simple F90 derived types are memory identical to C structs or C++ classes, and it would be possible to directly access class data using either the calling or implementation language. However, it does not appear necessary or desirable at this point to utilize this capability since it may degrade the robustness of the implementation considerably in unpredictable ways.

The shallow class approach is warranted for very simple classes within the ESMF. A Date class, for example, does not need to contain internal pointers and would, if it was created within a single-language application, normally be handled as a local stack variable. Deep classes are needed to represent more complex classes within the framework, such as a Route class that stores data transfer paths between two components. Such classes are likely to allocate storage within the construction or other methods of the class.

3.1 C++ Implementation

Code for the example is in lightweight_example/C++.

Shallow Object Example

The following example illustrates shallow objects with a particle that has an associated position and velocity. The example is an adaption of the particle object in [2].

In C++:

#include <iostream.h>
#include <ftn.h>
#include <stdio.h>
class particle {
public:
  virtual ~particle() = 0;
  particle_init(double *position_, double *velocity_) {
    int i;   
    for (i = 0; i < 3; i++) {
      position[i] = position_[i];
      velocity[i] = velocity_[i];
    }
  }
  void move(double &timeStep) {
    position[0] += velocity[0] * timeStep;
    position[1] += velocity[1] * timeStep;
    position[2] += velocity[2] * timeStep;
  }
  void print() {
    int i;
    for (i=0; i < 3; i++)
      cout << "position[" << i << "] = " << position[i] << endl;
  }
private:
  double position[3], velocity[3];
};

This section of code defines the particle class. Functions for integrating movement and printing out particle position are included in the example.

This C++ code doubles as the implementation and the C++ interface. However, to use this class in F90 we must build an analagous F90 module:

	module particleMod

	type particle
	sequence
	real(r8) position(3)         ! These variables exist to create a struct
	real(r8) velocity(3)         ! with the same size as the corresponding
	end type                     ! C++ class.  They are not used directly.

	contains

	subroutine particleCreate(this, position, velocity)
	type(particle), intent(inout) :: this
	real(r8), intent(in) :: position(:), velocity(:)
	call c_particle_create(this, position, velocity)
	end subroutine

	subroutine particleMove(this, timeStep)
	type(particle) :: this
	real(r8) :: timeStep
	
	call c_particle_move(this, timeStep)

	end subroutine

	subroutine particlePrint(this)
	type(particle) :: this
	
	call c_particle_print(this)

	end subroutine

        end module

This interface contains a datatype that is the same size as the particle class. This assures that when the object is created in F90, it will have the correct amount of memory available to be used by the C++ class. F90 interface functions/subroutines are included here to call the appropriate C++ class member functions. These F90 routines do not perform any real work (the implentation is in C++), but they map the F90 call to the C++ member function. An intermediate level of C code is required since C++ name-mangles its functions (the macro FTN is a cpp macro that maps a C function to the appropriate F77 linkage; i.e. it adds underscores as needed):

// Interface Layer for F90 
extern "C"
{
  void FTN(c_particle_create)(particle *pa, double *position, double *velocity) {
    pa->particle_init(position, velocity);
  }
  void FTN(c_particle_move)(particle *pa, double *timeStep) {
    pa->move(*timeStep);
  }
  void FTN(c_particle_print)(particle *pa) {
    pa->print();
  }
}

The extern ``C'' in the interface tells the C++ compiler to give the interface functions C linkage, so they are immediately callable from Fortran.

When a section of code defines a particle on the F90 stack, the compiler allocates space for it and the C++ routines use that memory to operate on. The F90 structure address is passed to the C interface function, where the type of the address space is cast to a type particle*.

  void FTN(c_particle_move)(particle *pa, double *timeStep) {
    pa->move(*timeStep);
  }

The resulting address is used to call the member function. This function lookup actually occurs at compile time, and the pointer is merely sent in as the this pointer.

With this simple class, there are no virtual function tables or complex structures associated with the class. Hence, a function lookups are automatic. Consider:

#include <iostream.h>
class example {
        public:
        void print() 
        {
                cout << "hello world\n";
        }
};

main()
{
        ((example*)(NULL))->print();
}

This program works and prints ``hello world''. Thus when we pass our chunk of memory allocated from F90, the correct class member function is dispatched. In the compilers we have worked with, C++ doesn't store any any information about how to find functions within the class memory itself (for shallow classes). All function lookups in these classes are done at compile time and the image of the C++ class in memory is exactly like a C struct with the same data members. Once inheritance and more complicated techniques are involved, this does not work and we must move to our second ``deep'' scheme, discussed later. Our F90 main would look like this:

	program particleEX
	Use particleMod

	call useparticle()

	contains
	
	subroutine useparticle()
	type(particle) :: mypart
	real(r8) :: position(3) = (/ 1.01, 1.0, 1.0 /)
	real(r8) :: velocity(3) = (/ 0.5, 0.0, 0.1 /)
	integer :: i
	real(r8) :: timeStep

	call particleCreate(mypart, position, velocity)
	timeStep = 1.0

	do i=1, 10
	   call particleMove(mypart, timeStep)
	enddo
	call particlePrint(mypart)
	
	end subroutine
	
	end program

The major convenience of this strategy is that we have allocated and deallocated memory for our particle on the F90 stack automatically. The user has the convenience of not having to explicity call a destroy routine.

3.2 F90 Implementation

Code for the example is in lightweight_example/F90.

Shallow Object Example

We again use the particle example, implemented in F90 this time, and wrapped in C++.

	module particleMod

	type particle
	sequence
	real(r8) position(3)
	real(r8) velocity(3)
	real(r8) mass
	end type

	contains

	subroutine particleCreate(this, position, velocity)
	type(particle), intent(inout) :: this
	real(r8), intent(in) :: position(:), velocity(:)
	integer :: i
	do i=1, 3
	   this%position(i) = position(i)
	   this%velocity(i) = velocity(i)
	enddo
	end subroutine

	subroutine particleMove(this, timeStep)
	type(particle) :: this
	real(r8) :: timeStep
	integer :: i
	do i=1, 3
	   this%position(i) = this%position(i) + 
     &	   this%velocity(i) * timeStep
	enddo

Next, the module must be made callable by C/C++ code. It is not feasible to portably link directly to module functions, since module routines have unpredictable linkage, and since F90 in general requires call headers to be on the stack when a function is called. Therefore we had to go through F77 functions, which are simpler both in linkage and calling. This way the module functions are called from FORTRAN code, and the language deals with the linkage and call stack issues.

	subroutine f_particlecreate(this, position, velocity)
	Use particleMod
	type(particle) :: this
	real(r8) :: position(3)
	real(r8) :: velocity(*)
	call particleCreate(this, position, velocity(1:3))
	end

	subroutine f_particleprint(this)
	Use particleMod
	type(particle) :: this
	call particlePrint(this)
	end

	subroutine f_particlemove(this, timeStep)
	Use particleMod
	type(particle) :: this
	real(r8) :: timeStep
	call particleMove(this, timeStep)
	end

These functions are now callable from C/C++ on the compilers we have tried.

The array parameters position and velocity are not used as assumed shape parameters (i.e. real(r8) :: position(:) because this requires that a call header be placed on the stack describing the dimensional characteristics of the array. The format of this call header is not public or standardized between different compilers, so it is difficult to use. (See Section 6.2 for strategies for working with Fortran arrays.) This example employes the two simpler methods of array passing that require no call headers, namely the explicit-shape method (position(3)) and the old F77 array passing method (velocity(*)).

Now we have functions that we can call from C/C++, so we create a C++ class that maps member functions to these F90 interfaces (via C):

class particle;

extern "C" {
  // These prototype the F77 interface functions.
  void FTN(f_particlecreate)(particle *pa, double *position,
		      double *velocity);
  void FTN(f_particlemove)(particle *pa, double *timeStep);
  void FTN(f_particleprint)(particle *pa);
}

class particle {
public:
  particle(double *position_, double *velocity_) {
    FTN(f_particlecreate)(this, position_, velocity_);
  }
  void move(double &timeStep) {
    FTN(f_particlemove)(this, &timeStep);
  }
  void print() {
    FTN(f_particleprint)(this);
  }
private:
  double position[3], velocity[3];
  double mass;
};

With this class definition we have essentially turned the C++ shallow class example inside out, and now the C++ member functions are merely stubs to call F90 code, as the F90 module procedures in our earlier example were stubs to call C++ code. In these shallow classes we still have corresponding data structures on both sides of the language interface.

Our C++ driver uses this class as if it were any ordinary C++ class:

int main(int argc, char *argv[]) {
  int i;
  double tstep;
  double position[] = { 1.1, 1.2, 1.3};
  double velocity[] = { 0.5, 0.0, 0.1};
  particle part1(position,
		 velocity);
  tstep = 1.0;
  for (i = 0; i < 10; i++) {
      part1.move(tstep);
    }
  part1.print();
}

Deep Object Example

The other language interoperability strategy involves objects of more complexity, such as classes with inheritance, or classes that construct and destruct resources. These deep classes must have legitimate constructors and destructors and we expect the memory created/initialized by the constructors to be referenced when using the class. Deep classes may be associated with data related to OO constructs such as virtual function tables, or may allocate memory during construction or operations. Examples of deep objects are provided in the Sections 4 and 5.


next up previous contents
Next: 4 Inheritance Up: IMPL_repdoc Previous: 2 Motivation and Assessment   Contents
esmf_support@list.woc.noaa.gov