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

Hi all,

I am a complete beginner in Julia.
My question is: how to make a julia script call C or C++ coded function. If it’s possible, can this enhance the performance in term of speed of execution (if we compare it with a fully julia coded script)?

I am using Debian GNU/Linux.

Best !

2 Likes

Hello and welcome to the community!
You find the answer to your first question in the manual
https://docs.julialang.org/en/v1/manual/calling-c-and-fortran-code/
Well written julia code can often be just as fast as well written C/C++ code, so you shouldn’t need to call out to C for performance reasons alone.

8 Likes

Okey thank you! I won’t use it then if there is no improvement in performance :slight_smile:
Thank you for the manual link!

1 Like

There are a few cases, where this works. BLAS is the prime example of a library that often outperforms pure julia. On the other hand BLAS performs so well, since critical parts are often written in assembler.

So if you have a highly performant library at hand, try calling it. If you try to call your own C/C++ code, you will find that a good julia implementation is often just as good.

1 Like

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

Isn’t Cxx.jl broken for Julia 1.4 and above?

1 Like

You are right. I just tested the above example in Julia 1.3, while in Julia >= 1.4 Cxx doesn’t compile. It’s a pity… it was my experience that they were pretty fast in updating to a newer Julia release, so I didn’t expect for them to remain stuck on one particular release…