How to make Julia call C/C++ coded function

To call C you don’t need anything, as it is native in Julia core. To call C++ you need instead a package.
Here an excerpt from the relevant chapter of my “Julia Quick Syntax Reference: A Pocket Guide for Data Science Programming” book where CxxWrap.jl is used.

Julia ⇄ C

As said, calling C code is native to the language and doesn’t require any third-party code.

Let’s fist build a C library. We show this in Linux using the GCC compiler. The procedure in other environments would be similar but not necessarily identical:

myclib.h:

extern int get2 ();
extern double sumMyArgs (float i, float j);

myclib.c:

int get2 (){
 return 2;
}
double sumMyArgs (float i, float j){
 return i+j;
}

Note that we need to define the function we want to use in Julia as extern in the C header file.

We can then compile the shared library with gcc, a C compiler:

  • gcc -o myclib.o -c myclib.c
  • gcc -shared -o libmyclib.so myclib.o -lm -fPIC

We are now ready to use the C library in Julia:

const myclib = joinpath(@__DIR__, "libmyclib.so")
a = ccall((:get2,myclib), Int32, ())
b = ccall((:sumMyArgs,myclib), Float64, (Float32,Float32), 2.5, 1.5)

The ccall function takes as the first argument a tuple (function name, library path) where the library path must be expressed in term of full path, unless the library is in the search path of the OS.
If it isn’t, and we still want to express it in terms relative to the file we are using, we can use the macro @__DIR__ that expands to the absolute path of the directory of the file itself.
The variable hosting the full path of the library must be set constant, as the tuple acting as first argument of ccall must be literal.

The second argument of ccall is the return type of the function. Note that while C int maps to either Julia Int32 or Int64, C float maps to Julia Float32 and C double maps to Julia Float64. In this example we could have used instead the corresponding Julia type aliases Cint,Cfloat and Cdouble (within others) in order to avoid memorising the mapping.

The third argument is a tuple of the types of the arguments expected by the C function. If there is only one argument, it must still be expressed as a tuple, e.g. (Float64,).

Finally the remaining arguments are the arguments to pass to the C function.

We have just “touched the surface” here. Linking C (or Fortran) Code can become pretty complex in real-case situations, and consulting the Calling C and Fortran Code · The Julia Language[official documentation] indispensable.

Julia ⇄ C++

As for C, C++ workflow is partially environment-dependent. We use in this section Cxx.jl under Linux, although Cxx.jl has also an experimental support for Windows footnote:cxxRequirement[Cxx requires a version of Julia ≥ 1.1]…

It’s main advantage over other C++ wrap modules (notably CxxWrap.jl)
is that it allows working on {C++ code in multiple ways depending on the workflow that is required.

Interactive C++ prompt

[…]

Embed C++ code in a Julia program

Aside the REPL prompt, we can use C++ code in our Julia program without ever leaving the main Julia environment. Let’s start with a simple example:

using Cxx

# Define the C++ function and compile it
cxx"""
#include<iostream>
void myCppFunction() {
   int a = 10;
   std::cout << "Printing " << a << std::endl;
}
"""
# Execute it
icxx"myCppFunction();" # Return "Printing 10"
# OR
# Convert the C++ to Julia function
myJuliaFunction() = @cxx myCppFunction()
# Run the function
myJuliaFunction() # Return "Printing 10"

The workflow is straight-forward: we first embed the {C++ code with the cxx"..." string macro.

We are then ready to “use” the functions defined in cxx"..." either calling them directly with C++ code embedded in icxx"..." or converting them in Julia function with the @cxx macro and then using Julia code to call the (Julia) function.

The above example however doesn’t imply any transfer of data between Julia and C++, while if we want to embed C++, most likely it is in order to pass some data to C++ and retrieve the output to use in our Julia program.

The following example shows how data transfer between Julia and C++ (both ways) is handled automatically for elementary types:

----
using Cxx
cxx"""
#include<iostream>
int myCppFunction2(int arg1) {
   return arg1+1;
}
"""
juliaValue = icxx"myCppFunction2(9);" # 10
# OR
myJuliaFunction2(arg1) = @cxx myCppFunction2(arg1)
juliaValue = myJuliaFunction2(99)     # 100

However when the transfer involves complex data structures (like arrays or, as in the following example, arrays of arrays) things become more complex, as the Julia types have to be converted in the C++ types, and vice versa:

using Cxx
cxx"""
#include <vector>
using namespace std;
vector<double> rowAverages (vector< vector<double> > rows) {
  // Compute average of each row..
  vector <double> averages;
  for (int r = 0; r < rows.size(); r++){
    double rsum = 0.0;
    double ncols= rows[r].size();
    for (int c = 0; c< rows[r].size(); c++){
      rsum += rows[r][c];
    }
    averages.push_back(rsum/ncols);
  }
  return averages;
}
"""
rows_julia = [[1.5,2.0,2.5],[4,5.5,6,6.5,8]]
rows_cpp   = convert(cxxt"vector< vector< double > > ", rows_julia)
rows_avgs  = collect(icxx"rowAverages($rows_cpp);")
# OR
rowAverages(rows) = @cxx rowAverages(rows)
rows_avgs  = collect(rowAverages(rows_cpp))

The conversion from Julia data to C++ data (to be used as argument(s) of the C++ function) is done using the familiar convert(T,source) function, but where T is here given by the expression returned by cxxt"[Cpp type]". +
The converted data can then be used in the C++ function call, with the note that if the direct call form of icxx" is used, this has to be interpolated using the dollar $ operator.

Finally, collect is used to copy data from the C++ structures returned by the C++ function to a Julia structure (avoiding copying is also possible using pointers).

Load a C++ library

The third way to use C++ code is to load a C++ library and call directly the functions defined in that library. The advantage is that the library doesn’t need to be aware that will be used in Julia (i.e. no special Julia headers or libraries are required at compile time of the C++ library) as the wrap is done entirely in Julia. This has the advantage that pre-existing libraries can be reused.

Let’s reuse our last example, but this time we compile it in a shared library.

mycpplib.cpp:

#include <vector>
#include "mycpplib.h"
using namespace std;
vector<double> rowAverages (vector< vector<double> > rows) {
  // Compute average of each row..
  vector <double> averages;
  for (int r = 0; r < rows.size(); r++){
    double rsum = 0.0;
    double ncols= rows[r].size();
    for (int c = 0; c< rows[r].size(); c++){
      rsum += rows[r][c];
    }
    averages.push_back(rsum/ncols);
  }
  return averages;
}

mycpplib.h:

#include <vector>
std::vector<double> rowAverages (std::vector< std::vector<double> > rows);

We can then compile the shared library with g++, a C++ compiler:

  • g++ -shared -fPIC -o libmycpplib.so mycpplib.cpp

We are now ready to use it in Julia:

using Cxx
using Libdl # <1>

const path_to_lib = pwd()
addHeaderDir(path_to_lib, kind=C_System) # <2>
cxxinclude("mycpplib.h") # <3>
Libdl.dlopen(joinpath(path_to_lib, "libmycpplib.so"), Libdl.RTLD_GLOBAL) # <4>

rows_julia = [[1.5,2.0,2.5],[4,5.5,6,6.5,8]]
rows_cpp   = convert(cxxt"std::vector< std::vector< double > >", rows_julia)
rows_avgs  = collect(icxx"rowAverages($rows_cpp);")

The only new things here is that we need to explicitly load the standard lib Libdl (1 ), add the C++ header(s) (2 and 3) and finally open the shared library (4).

We are now able to use the functions defined in the library as if we embedded the C++ code in the Julia script.

7 Likes