Calling C Code from Julia

If Julia was to be widely accepted in the scientific computing community, it was necessary to make it very simple and straight-forward to integrate C libraries and code into Julia applications.

In scientific computing it's very common for people to write as much code as possible in Python since it's a very easy to use language, offers high productivity, and is powerful enough for most things. Nonetheless, when it comes to many tasks, Python just isn't fast enough and so people would write some parts in C or C++, compile to a shared library, and call those functions wih Python.

Julia offers another path – just write it all in Julia. In a lot of scenarios, Julia is plenty fast, regularly matching C and C++ for speed. Still that doesn't account for all of the libraries which have already been written and still have value. In this post, I'll run through how to integrate a C library into Julia code.

First of all, we need a C library to work with so we'll implement Heron's Formula which is a simple method for finding the area of a triangle given the lengths of its sides.

\[ A = \sqrt{s(s-a)(s-b)(s-c)}\,,\]

where \(s = \frac{1}{2}(a + b + c)\).

We'll write two functions verify_triangle and heron. The first will verify that given \(a\), \(b\), and \(c\), we are able to form a triangle. The second, heron, will compute the area using Heron's formula. We'll name this file heron.c.

#include <math.h>

typedef int bool;

bool verify_triangle(double a, double b, double c) {
  int positive_lengths = (a > 0 && b > 0 && c > 0);
  int proper_construction = (a + b > c && a + c > b && b + c > a);
  return (positive_lengths && proper_construction);
}

double heron(double a, double b, double c) {
  double s = (a + b + c)/2.0;

  double area = sqrt(s * (s - a) * (s - b) * (s - c));
  return area;
}

To facilitate compilation, we'll also write a Makefile with filename Makefile.

CC=gcc
CFLAGS=-c -Wall -fPIC
SOURCES=heron.c
OBJECTS=$(SOURCES:.c=.o)

.c.o:
	$(CC) $(CFLAGS) $< -o $@

lib: $(OBJECTS)
	$(CC) -shared -fPIC -o libheron.so $(OBJECTS)

clean:
	rm *.o *.so

Now we simply compile our shared library by running make in the directory.

In our Julia code, we need to include the Libdl package and add the path of our shared library to the Libdl.DL_LOAD_PATH variable.

If the file, libheron.so is located at /full/path/to/heron/libheron.so, then we use the path /full/path/to/heron.

using Libdl

push!(Libdl.DL_LOAD_PATH, "/full/path/to/heron")

function heron(a::Float64, b::Float64, c::Float64)
    ccall((:heron, "libheron.so"), Float64, (Float64, Float64, Float64), a, b, c)
end

function verifytriangle(a::Float64, b::Float64, c::Float64)
    res = ccall((:verify_triangle, "libheron.so"), Int, (Float64, Float64, Float64), a, b, c)
    res == one(Int64) ? true : false
end

It's worth noting that we may assume that C code will always be faster and that we will usually gain something from integrating a C library. This isn't always necessarily the case though.

I've also re-written the heron function in pure Julia,

function jheron(a::Float64, b::Float64, c::Float64)
    s = (a + b + c)/2.0
    sqrt(s * (s - a) * (s - b) * (s - c))
end

When we benchmark it,

@btime heron(538., 5234., 5252.)
>  4.459 ns (0 allocations: 0 bytes)

@btime jheron(538., 5234., 5252.)
>  1.436 ns (0 allocations: 0 bytes)

We can see that the Julia version of the function requires approximately one-third of the time of the C function.