CSlib documentation

Language requirements

This page describes language specific issues to be aware of when writing client or server apps in different languages.



Header files and modules to include

Include these lines in any app file that makes calls to the CSlib.

C++:

#include "cslib.h"              // found in src dir
using namespace CSLIB_NS; 

This is the CSlib class header file and its namespace If you prefer not to use the namespace, you can prepend the namespace to the call that instantiates the CSlib and the pointer it returns, e.g.

CSLIB_NS::CSlib *cs = new CSLIB_NS::CSlib(csflag,mode,str,&world); 

C:

#include "cslib_wrap.h"          // found in src dir 

This is the C-style wrapper on the CSlib class, found in src.

Fortran (2003 or later):

Include these lines in any program element (main program, subroutine, function, etc) which calls the CSlib:

USE iso_c_binding       ! standard Fortran module
USE cslib_wrap          ! found in src or test dir 

The iso_c_binding module defines the C-style data types and functions used in calls to CSlib. The cslib_wrap module defines how Fortran calls the C interface to the CSlib. The code for the interface is in src/cslib_wrap.f90 file (also test/cslib_wrap.f90). That file must be compiled with the app; you can simply copy it into the directory with other app source files.

Python:

import cslib as CS      # found in src dir
from mpi4py import MPI 

The first line references the file src/cslib.py which defines a Python CSlib class which wraps the C interface to the CSlib. The second line is only necessary if you are running a parallel Python application, so that you can pass an MPI communicator to the CSlib.

See more details in this section on how to insure these two lines will work in your Python.


Compatible data types

For the data types the CSlib supports, these are the equivalent native data types in different languages. If sending a message with this type of data, you need to be sure both the client and server app agree on how to declare the data.

Data type C/C++ Fortran Python/Numpy Python/ctypes
32-bit integer int integer(4) intc c_int
64-bit integer int64_t integer(8) int64 c_longlong
32-bit floating point float real(4) float32 c_float
64-bit floating point double real(8) float c_double
string char * character(c_char) N/A N/A

Note that a character string in Fortran can also be declared as "character(len=32)" for sending. But for receiving, declare it as "character(c_char), pointer" and use the c_f_pointer() function to convert the received C-style string into the correct Fortran length. This is discussed below.

Note that regular Python does not require declaration of data types. You can assign values like this:

and Python knows whether it is an integer (any precision), floating point, or string, and will send/receive it to/from the CSlib correctly. When using Numpy and ctypes within Python, it is different. When you allocate a vector or multidimentional array of numeric data types, you must use the type identifiers above to declare the data correctly. See the Python test codes in the test dir for examples.

Read more about Numpy data types here:

https://docs.scipy.org/doc/numpy-1.13.0/user/basics.types.html

Read more about ctypes data types here:

https://docs.python.org/2/library/ctypes.html


Strings in C/C++ versus Fortran

Character strings in C or C++ are NULL-terminated. This means the string "hello" requires 6 bytes of storage. Strings in Fortran are not NULL-terminated. So "hello" uses only 5 bytes of storage. Or it may be assigned like this:

character(len=32) :: string
string = "hello" 

in which case 27 space characters are added to the end, to fill a 32-character string.

Internally, the CSlib stores strings in the C/C++ format with a trailing NULL. This means a Fortran app that sends a string must pass it to the CSlib in that format, via the pack_string() method described in this section. For example,

character(len=32) :: str = "hello
call cslib_pack_string(cs,1,trim(str)//c_null_char) 

The trim() function trims trailing space (if any). A c_null_char is appended.

Likewise a Fortran app that receives a string will receive it in the C format, via the unpack_string() method described in this section. For examples,

character(c_char), pointer :: str(:) => null() ptr = cslib_unpack_string(cs,1) call c_f_pointer(ptr,str,[fieldlen(1)-1])

The c_f_pointer() function assigs the C pointer to the string to the Fortran variable str, and sets its lenght to one less than the received fieldlen(). This removes the trailing NULL.


Contiguous memory for 2d, 3d, etc arrays

As explained in this section and this section data in multidimensional arrays can be passed as an argument to pack() or pack_parallel() for sending (or received by unpack_parallel()), but only if the data is stored contiguously in memory.

For Fortran and Python (Numpy arrays or ctypes arrays) this is the case. However for C/C++ it may not be the case, depending on how the app creates a multidimensional array. The data in this kind of 2d Nx3 array:

int a10003; 

is stored contiguously. But its dimensions must be specified at compile time.

A "double **" pointer to an Nx3 2d array in C, is more flexible. It can be allocated at run time. But it may not store the 3N values in contiguous memory, depending on how it was allocated. It may be a pointer to N pointers, each of which points at 3 contiguous values. But the N 3-vectors are not contiguous with each other.

The example code in this section and this section assumes the allocation for a 2d array is done contiguously (and later freed) via methods like this. Note that in malloc2d() there are two mallocs, one for 3N doubles (the data), the other for N pointers. A reference like &a00 then points to the data as a 1d chunk and can be passed to pack() or unpack_parallel().

double **a = malloc2d(1000,3);
free2d(a); 
double **malloc2d(int n1, int n2)

  int nbytes = n1*n2 * sizeof(double);
  double *data = (double *) malloc(nbytes);
  nbytes = n1 * sizeof(double *);
  double **array = (double **) malloc(nbytes); 
  int n = 0;
  for (int i = 0; i < n1; i++) 
    arrayi = &datan;
    n += n2;
  
  return array;
 
void free2d(double **array)

  if (array == NULL) return;
  free(array0);
  free(array);
 

Similar mallocs/frees can be written for 3d, 4d, etc arrays.


Ordering of 2d, 3d, etc arrays in C/C++ versus Fortran

Multidimensional arrays in Fortran store their data contiguously in memory with the leftmost index varying fastest, and the rightmost index varying slowest. Thus for this array:

integer(4) :: a(2,2) 

the order of the elements in memory is a(1,1), a(2,1), a(1,2), a(2,2).

For C/C++ it is the oppposite (assuming the array data is stored in contiguous memory as described above, and not as pointers to pointers). The rightmost index varies fastest, the leftmost index varies slowest. So for this array:

int a22 

the order of the elements in memory is a00, a01, a10, a11.

Since the CSlib treats multidimensional arrays as simply a chunk of contiguous memory, if you wish to send an array from a C/C++ app to a Fortran app, or vice versa, you must declare them in the 2 languages consistently. For example, an array declared as Nx3 in C/C++ should be declared as 3xN in Fortran.


Splitting an MPI communicator in different languages

As explained in this section, if the CSlib is used with messaging mode = "mpi/one", then both the client and server app are running within the same MPI communicator which spans both the client and server processors, typically MPI_COMM_WORLD. That spanning communicator is passed as an argument when the CSlib is created (instantiated) by the app. A pworld argument is also passed which is the sub-communicator for the processors the client or server app is running on. To create the two sub-communicators, both apps must execute a MPI_Comm_split() before instantiating the CSlib. Note that the MPI_Comm_split() call is synchronous; all processors in both the client and server apps must call it at the same time. Here is example code for this operation in each language. The line with the call to CSlib or cslib is what instantiates the library with the two communicators as arguments.

C++:

int me,nprocs,csflag;
MPI_Comm world,partial,both; 
MPI_Init(&argc,&argv);
world = MPI_COMM_WORLD;
MPI_Comm_rank(world,&me);
csflag = 0;                    // 0 for client, 1 for server
MPI_Comm_split(world,csflag,me,&partial);
both = world;
world = partial;               // reset app's world,me,nprocs to sub-communicator
MPI_Comm_rank(world,&me);
MPI_Comm_size(world,&nprocs);
cs = new CSlib(csflag,mode,&both,&world); 
MPI_Comm_free(&world);         // free the sub-commuicator when app exits 

C:

identical to C++, except for this final line:
cslib_open(cslib,mode,&both,&world,cs); 

Fortran:

integer :: me,nprocs,csflag,partial,ierr
integer, target :: world,both 
call MPI_Init(ierr)
world = MPI_COMM_WORLD
call MPI_Comm_rank(world,me,ierr)
csflag = 0;                ! 0 for client, 1 for server
call MPI_Comm_split(world,csflag,me,partial,ierr)
both = world
world = partial            ! reset app's world,me,nprocs to sub-communicator
call MPI_Comm_rank(world,me,ierr)
call MPI_Comm_size(world,nprocs,ierr)
call cslib_open_fortran_mpi_one(0,trim(mode)//C_null_char, &
                                c_loc(both),c_loc(world),cs) 
call MPI_Comm_free(world,ierr)  ! free the sub-commuicator when app exits 

Python:

from mpi4py import MPI    # this performs MPI Init
world = MPI.COMM_WORLD
me = world.rank
csflag = 0                # 0 for client, 1 for server
partial = world.Split(csflag,me)
both = world
world = partial           # reset app's world,me,nprocs to sub-communicator
me = world.rank
nprocs = world.size
cs = CSlib(0,mode,both,world) 
world.Free()              # free the sub-commuicator when app exits