Structs and Classes

All problems in computer science can be solved by another level of indirection. — David Wheeler

While structs and classes are very similar in C++, Shroud wraps them much differently. Shroud treats structs as they are in C and creates a corresponding derived type for the struct. Shroud wraps classes by creating a shadow class which holds a pointer to the instance then uses Fortran type bound procedures to implement methods.

Class

Classes are wrapped by creating a shadow class for the C++ class. A pointer to an instances is saved along with a memory management flag.

Using the tutorial as an example, a simple class is defined in the C++ header as:

class Class1
{
public:
    void Method1() {};
};

And is wrapped in the YAML as:

declarations:
- decl: class Class1
  declarations:
  - decl: int Method1()
  - decl: Class1()
    format:
      function_suffix: _default
  - decl: Class1(int flag)
    format:
      function_suffix: _flag
  - decl: ~Class1()        +name(delete)

While C++ will provide a default constructor and destructor, they must be listed explicitly to wrap them. They are not assumed since they may be private. The default name of the constructor is ctor. The name can be specified with the name attribute. If the constructor is overloaded, each constructor must be given the same name attribute.

The function_suffix is added to distinguish overloaded constructors. The default is to have a sequential numeric suffix. The function_suffix must not be explicitly set to blank since the name is used by the Fortran generic interface.

If no constructor is wrapped, then some other factory method should be available to create instances of the class. There is no way to create it directly from Fortran.

When the constructor is wrapped the destructor should also be wrapper or some other method should be wrapped to release the memory.

C

Each class in the YAML file will create a struct in the C wrapper. All of these structs are identical but are named after the class. This is intended to provide some level of type safety by making it harder to accidently use the wrong class with a method. Shroud refers to this as a capsule.

// helper capsule_CLA_Class1
struct s_CLA_Class1 {
    void *addr;     /* address of C++ memory */
    int idtor;      /* index of destructor */
};
typedef struct s_CLA_Class1 CLA_Class1;

The C wrapper will extract the address of the instance then call the method.

All constructors are very similar. They call the C++ constructor then saves the pointer to the instance. The idtor field is the index of the destructor maintained by Shroud to destroy the instance.

CLA_Class1 * CLA_Class1_ctor_flag(int flag, CLA_Class1 * SHC_rv)
{
    // splicer begin class.Class1.method.ctor_flag
    classes::Class1 *SHCXX_rv = new classes::Class1(flag);
    SHC_rv->addr = static_cast<void *>(SHCXX_rv);
    SHC_rv->idtor = 1;
    return SHC_rv;
    // splicer end class.Class1.method.ctor_flag
}

Finally the wrapper for the destructor. The addr field is cleared to avoid a dangling pointer.

void CLA_Class1_delete(CLA_Class1 * self)
{
    classes::Class1 *SH_this = static_cast<classes::Class1 *>
        (self->addr);
    // splicer begin class.Class1.method.delete
    delete SH_this;
    self->addr = nullptr;
    // splicer end class.Class1.method.delete
}

A function which returns a class, including constructors, is passed a pointer to a F_capsule_data_type. The argument’s members are filled in by the function. The function will return a type(C_PTR) which contains the address of the F_capsule_data_type argument. The prototype for the C wrapper function allows it to be used in expressions similar to the way that strcpy returns its destination argument. The option C_shadow_result can be set to False to change the function to return void instead. The Fortran wrapper API will be uneffected.

C++ functions which return const pointers will not create a const C wrapper. This is because the C wapper will return a pointer to the capsule and not the instance.

Fortran

The Fortran wrapper uses the object-oriented features added in Fortran 2003. There is one derived type for each library which is used for the capsule. This derived type uses bind(C) since it is passed to the C wrapper. Each class uses the same capsule derived type since it is considered an implementation detail and the user should not access it. Then each class creates an additional derived type as the shadow class which contains a capsule and has type-bound procedures associated with it.

! helper capsule_data_helper
type, bind(C) :: CLA_SHROUD_capsule_data
    type(C_PTR) :: addr = C_NULL_PTR  ! address of C++ memory
    integer(C_INT) :: idtor = 0       ! index of destructor
end type CLA_SHROUD_capsule_data

The capsule is added to the Fortran shadow class. This derived type can contain type-bound procedures and may not use the bind(C) attribute.

type class1
    type(SHROUD_CLA_capsule_data) :: cxxmem
contains
    procedure :: delete => class1_delete
    procedure :: method1 => class1_method1
end type class1

The wrapper for the method passes the object as the first argument. The argument uses the format field F_this to name the variable and defaults to obj. It can be renamed if it conflicts with another argument.

function class1_method1(obj) &
        result(SHT_rv)
    use iso_c_binding, only : C_INT
    class(class1) :: obj
    integer(C_INT) :: SHT_rv
    ! splicer begin class.Class1.method.method1
    SHT_rv = c_class1_method1(obj%cxxmem)
    ! splicer end class.Class1.method.method1
end function class1_method1

A generic interface with the same name as the class is created to call the constructors for the class. The constructor will initialize the Fortran shadow class.

interface class1
    module procedure class1_ctor_default
    module procedure class1_ctor_flag
end interface class1

The Fortran wrapped class can be used very similar to its C++ counterpart.

use classes_mod
type(class1) var     ! Create Fortran variable.
integer(C_INT) i
var = class1()       ! Allocate C++ class instance.
i = var%method1()
call var%delete

Some additional type-bound procedures are created to allow the user to get and set the address of the C++ memory directly. This can be used when the address of the instance is created in some other manner and it needs to be used in Fortran. There is no way to free this memory and it must be released outside of Fortran.

For example, a C++ function creates an instance then passes the address of it to Fortran function worker. The shadow class is initialized with the address and can then be used in an object-oriented fashion:

subroutine worker(addr) bind(C)
use classes_mod
type(C_PTR), intent(IN) :: addr
type(class1) var
integer(C_INT) i

call var%set_instance(addr)
i = var%method1()

Two instances of the class can be compared using the associated method.

type(class1) var1, var2
var1 = get_class(1)    ! A library function to fetch an instance
var2 = get_class(2)
if (var1%associated(var2) then
    print *, "Identical instances"
endif

These functions names are controlled by format fields F_name_associated, F_name_instance_get and F_name_instance_set. If the names are blank, the functions will not be created.

The .eq. operator is also defined.

A full example is at Constructor and Destructor.

Python

An PyObject is created for each C++ class. It constains the same values as the capsule.

typedef struct {
PyObject_HEAD
    classes::Class1 * obj;
    int idtor;
    // splicer begin class.Class1.C_object
    // splicer end class.Class1.C_object
} PY_Class1;

The idtor argument is used to release memory and described at Memory Management. The splicer allows additional fields to be added by the developer which may be used in function wrappers.

Additional fields can be added to the splicer for custom behavior.

Chained functions

C++ allows function calls to be chained by returning the this argument. Several functions can be called in succession on the same object.

auto var = Class1()->returnThis()

The return_this field indicates that the function may be chained so the wrapper can generate appropriate code.

  - decl: Class1 * returnThis();
    return_this: True

C

The C wrapper returns void instead of a pointer to the this argument.

Fortran

Fortran does not permit his behavior. The function is treated as a subroutine.

subroutine class1_return_this(obj)
    class(class1) :: obj
    ! splicer begin class.Class1.method.return_this
    call c_class1_return_this(obj%cxxmem)
    ! splicer end class.Class1.method.return_this
end subroutine class1_return_this

The chaining must be done as a sequence of calls.

use classes_mod
type(class1) var

var = class1()
call var%return_this()

Class static methods

To wrap the method:

class Singleton {
    static Singleton& getReference();
};

Use the YAML input:

- decl: class Singleton
  declarations:
  - decl: static Singleton& getReference()

Fortran

Class static methods are supported using the NOPASS keyword in Fortran.

type singleton
    type(CLA_SHROUD_capsule_data) :: cxxmem
    ! splicer begin class.Singleton.component_part
    ! splicer end class.Singleton.component_part
contains
    procedure, nopass :: get_reference => singleton_get_reference
    ! splicer begin class.Singleton.type_bound_procedure_part
    ! splicer end class.Singleton.type_bound_procedure_part
end type singleton

Called from Fortran as:

type(singleton) obj0
obj0 = obj0%get_reference()

Note that obj0 is not assigned a value before the function get_reference is called.

Class Inheritance

Class inheritance is supported.

- decl: class Shape
  declarations:
  - decl: Shape()
  - decl: int get_ivar() const

- decl: "class Circle : public Shape"
  declarations:
  - decl: Circle()

Fortran

Inheritance is implemented using the EXTENDS Fortran keyword. Only single inheritance is supported.

type shape
    type(CLA_SHROUD_capsule_data) :: cxxmem
contains
    procedure :: get_ivar => shape_get_ivar
end type shape

type, extends(shape) :: circle
end type circle

Python

Python uses the PyTypeObject.tp_base field.

Forward Declaration

A class may be forward declared by omitting declarations. All other fields, such as format and options must be provided on the initial decl of a Class. This will define the type and allow it to be used in following declarations. The class’s declarations can be added later:

declarations:
- decl: class Class1
  options:
     foo: True

- decl: class Class2
  declarations:
  - decl: void accept1(Class1 & arg1)

- decl: class Class1
  declarations:
  - decl: void accept2(Class2 & arg2)

Member Variables

For each member variable of a C++ class a C and Fortran wrapper function will be created to get or set the value. The Python wrapper will create a descriptor. It is not necessary to list all members of the class, only the one which are to be exposed in the wrapper. private members cannot be wrapped.

class Class1
{
public:
   int m_flag;
   int m_test;
};

It is added to the YAML file as:

- decl: class Class1
  declarations:
  - decl: int m_flag +readonly;
  - decl: int m_test +name(test);

The readonly attribute will not create the setter function or descriptor. Python will report:

>>> obj = tutorial.Class1()
>>> obj.m_flag =1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'm_flag' of 'tutorial.Class1' objects is not writable

The name attribute will change the name of generated functions and descriptors. This is helpful when using a naming convention like m_test and you do not want m_ to be used in the wrappers.

The names of these functions are controlled by the options SH_class_getter_template and SH_class_setter_template. They are added as additional methods on the class.

For wrapping details see Getter and Setter.

The getter and setter for a member which is a pointer to a native type can use a Fortran pointer if the member is given the dimension attribute.

- decl: class PointerData
  declarations:
  - decl: int nitems;
  - decl: int *items  +dimension(nitems);

Notice that the dimension uses another field in the class. This will create a getter which can be called from Fortran. Likewise, the setter will require an argument of the same rank as the dimension attribute.

type(PointerData) var
integer(C_INT) :: nitems
integer(C_INT), pointer :: items(:)
integer(C_INT) :: updated(10)

var = PointerData()
nitems = var%get_nitems()
items => var%get_items()

call var%set_items(updated)
var%nitems = size(updated)    ! keep nitems and items consistent

The user must be consistent in the use of the getter and setter. For example, item is nitems long, but if the setter assigns an array which is shorter, the next call to the getter will still create a Fortran pointer which is nitems long.

Another point to note is that the variable updated should have the TARGET attribute since we’re saving its address in var.

Struct

Shroud supports both structs and classes. But it treats them much differently. Whereas in C++ a struct and class are essentially the same thing, Shroud treats structs as a C style struct. They do not have associated methods. This allows them to be mapped to a Fortran derived type with the bind(C) attribute and a Python NumPy array.

For wrapping purposes, a struct is a C++ class without a vtable. It will contain POD types. Unlike classes where all member variables do not need to be wrapped, a struct should be fully defined. This is necessary to allow an array of structs to be created in the wrapper language then passed to C++.

A struct is defined in the yaml file as:

- decl: struct Cstruct1
  declarations:
  - decl: int ifield;
  - decl: double dfield;

It can also be defined as one decl entry:

- decl: struct Cstruct1 {
          int ifield;
          double dfield;
        };

The struct statement can can also be used to declare a variable of a previously defined structure. This is required for C but is optional for C++ where a struct statement defines a type. To distinguish a variable declaration from a struct declaration, the trailing semicolon is required.

- decl: struct Cstruct1 var;

Fortran

This is translated directly into a Fortran derived type with the bind(C) attribute.

type, bind(C) :: cstruct1
    integer(C_INT) :: ifield
    real(C_DOUBLE) :: dfield
end type cstruct1

All creation and access of members can be done using Fortran.

type(cstruct1) st(2)

st(1)%ifield = 1_C_INT
st(1)%dfield = 1.5_C_DOUBLE
st(2)%ifield = 2_C_INT
st(2)%dfield = 2.6_C_DOUBLE

Python

Python can treat a struct in several different ways by setting option PY_struct_arg. First, treat it the same as a class. An extension type is created with descriptors for the field methods. Second, as a numpy descriptor. This allows an array of structs to be used easily. Finally, as a tuple of Python objects.

When treated as a class, a constructor is created which will create an instance of the class. This is similar to the default constructor for structs in C++ but will also work with a C struct.

import cstruct
a = cstruct.Cstruct1(1, 2.5)
a = cstruct.Cstruct1()

When treated as a NumPy array no memory will be copied since the NumPy array contains a pointer to the C++ memory.

import cstruct
dt = cstruct.Cstruct1_dtype
a = np.array([(1, 1.5), (2, 2.6)], dtype=dt)

The descriptor is created in the wrapper NumPy Struct Descriptor.

Member Variables

Generally, getter and setter functions are not required since Fortran can directly access the member fields. But when the member is a pointer it is more convient to have a getter and setter which works with Fortran pointers.

struct PointerData
{
   int nitems;
   int *items;
};

The generated getter and setter are not type-bound functions and must be passed the struct variable:

type(PointerData) var
integer(C_INT) :: nitems
integer(C_INT) :: items(10)
integer(C_INT), pointer :: out(:)

var%nitems = 10
call pointerdata_set_items(var, items)

out => pointerdata_get_items(var)

The names of these functions are controlled by the options SH_struct_getter_template and SH_struct_setter_template. They are added to the same scope as the struct.

Option F_struct_getter_setter can be set to false to avoid creating the getter and setter functions.

Object-oriented C

Object oriented programing is a model and not a language feature. This model has been used for years in C by creating a struct for the object, then functions for the methods. C++ will implicitly pass the this argument. C methods explicitly pass the struct as an argument. Fortran and Python both pass an explicit object then wrap it in syntacatic sugar to allow a self.method() syntax to be used. Shroud allows a struct and collection of functions to be treated as a class.

First, define a struct and set the wrap_struct_as options to class.

- decl: struct Cstruct_as_class {
          int x1;
          int y1;
        };
  options:
    wrap_struct_as: class

Create a constructor function. The class_ctor options associates this with the struct.

- decl: Cstruct_as_class *Create_Cstruct_as_class(void)
  options:
    class_ctor: Cstruct_as_class

Then add methods. The class_method option associates this with the struct. The format field F_name_function is used to name the method. The default method name is the same as the function name. But since this name will be used in the context of the object, it can be much shorter. The pass attribute marks this as the ‘object’.

- decl: int Cstruct_as_class_sum(const Cstruct_as_class *point +pass)
  options:
    wrap_python: False
    class_method: Cstruct_as_class
  format:
    F_name_function: sum

Fortran

A shadow class is created for the struct. This is the same as wrapping a C++ class. Getters and setters are created for the member variable. And the sum method is added.

type cstruct_as_class
    type(STR_SHROUD_capsule_data) :: cxxmem
    ! splicer begin class.Cstruct_as_class.component_part
    ! splicer end class.Cstruct_as_class.component_part
contains
    procedure :: get_x1 => cstruct_as_class_get_x1
    procedure :: set_x1 => cstruct_as_class_set_x1
    procedure :: get_y1 => cstruct_as_class_get_y1
    procedure :: set_y1 => cstruct_as_class_set_y1
    procedure :: sum => cstruct_as_class_sum
    ! splicer begin class.Cstruct_as_class.type_bound_procedure_part
    ! splicer end class.Cstruct_as_class.type_bound_procedure_part
end type cstruct_as_class

Now the struct is treated as a class in the Fortran wrapper.

use struct_mod
type(cstruct_as_class) var     ! Create Fortran variable.
integer(C_INT) i
var = cstruct_as_class()       ! Create struct in C++.
i = var%sum()

A full example is at Struct as a Class.