StingerWrapper.jl - Part 1
14 Sep 2016This post will be the first among a series of blog posts describing my experiences while developing StingerWrapper.jl, a Julia interface to the STINGER C package for dynamic graph analysis. I have been working on StingerWrapper along with Dr. James Fairbanks.
In this blog post, I will describe our experiments with mapping C structures to Julia types and the design decisions we made to provide a Julia API to the STINGER C library.
Julia and C
Julia provides a C foreign function interface (FFI) to call C functions exposed
through a shared library. Using ccall
s as documented here, let’s us call C
functions while passing in the required parameters. Julia has a mapping of its
internal types to C which can be seen here and automatically casts the Julia
types to the C types. The ccall
syntax is pretty long and easy to get
wrong for untrained users. Writing a Julia function to abstract away this ccall
from the users would be the first step to providing a Julia API to the C library.
For example, the add
function is much easier for an end user to think about.
import Libdl: dlopen, dlsym
libtest = dlopen("shared_library_name") #Open a shared library in Julia
function add(a::Int32, b::Float32)
ccall(dlsym(libtest, :add_nums), Float32, (Int32, Float32), (1, 2))
end
Julia also supports direct C structs mappings. Just defining a immutable composite Julia type with the attributes and types of the C struct, let’s Julia decode these structures. This means you can create objects in Julia, pass them to C, let C do its thing and get it back to Julia.
struct foo {
int a;
float b;
}
float sum(struct foo bar){
return bar.a + bar.b;
}
immutable Foo
a::Int32
b::Float32
end
bar = Foo(1, 2.0)
ccall(dlsym(libtest, :sum), Float32, (Foo,), bar) #Returns 3.0!
However, there are a few restrictions. For example, Julia does not support arrays of unknown sizes. And the Stinger C struct has a zero size array allocated to it. This stops us from using this clean method to work with the Stinger C structure. :sad:
As we can’t make a Julia type with all the mappings to the Stinger C structure,
the approach we ended up using was to hand over the memory allocation parts to C
and keep a reference to that memory using a pointer in Julia as a handle to the
STINGER C object. To facilitate this, we created a wrapper type to just store the
handle to the pointer returned from C. We store this as a Ptr{Void}
, because well,
“when in doubt, use void pointers!” - James.
type Stinger
handle::Ptr{Void}
end
We defined the constructor of Stinger
to call the C memory allocator for a STINGER C
structure on initialization of a Stinger
type in Julia. This let’s users just
say Stinger()
and the C memory is allocated and the pointer is stored as the handle
attribute.
Now that we’ve allocated memory, we need to free it too! A Julia finalizer
is registered that takes care of this. It ensures that the calls the corresponding
free function in C when the Julia garbage collector gets around to it. All of this
occurs transparently to the user, who does not need to worry about memory
management. Our wrapper type now looks like this.
type Stinger
handle::Ptr{Void}
#Default constructor to create a Stinger data structure
function Stinger()
s = new(ccall(dlsym(stinger_core_lib, "stinger_new"), Ptr{Void}, ()))
finalizer(s, stinger_free)
s
end
end
function stinger_free(x::Stinger)
# To prevent segfaults
if x.handle != C_NULL
x.handle = ccall(dlsym(stinger_core_lib, "stinger_free"), Ptr{Void}, (Ptr{Void},), x)
end
end
This basic definition will let us write convenience wrapper functions
that make ccall
s to the STINGER C library. However, it is limited in terms of
being able to access or modify data directly from Julia and basically forces
us to call C to do anything useful. We will look at ways in which we can expose
the data to users in better way in part 2.