Erlang port driver pitfalls

February 10, 2014 by Quinlan Pfiffer

Starting back in january I’ve been working on a homespun database called OlegDB. It’s written primarily in C with an Erlang front-end.

The reason for this was primarily that C is fast but not good at concurrency, while Erlang is not-so-fast yet amazing at concurrency. Erlang is also a pretty common choice for this kind of thing, see ActorDB, Riak or CouchDB. I imagine we’re not really unique with our C backend either, Jon at BugSense hinted at a very interesting side project called “Lethe”. We’re hardly out in the sticks here.

Anyway, onto some code. Erlang has a lot of different ways to interface with C libraries, NIFs, C Nodes and Port Drivers being the primary examples. I chose to go with writing my own port driver because I needed some implicit state (the database object) and I didn’t want to hardcode a connection uri.

Port drivers are neat in that they allow you to load up a C library, provided it has some interface code (the port driver). If you just want to jump straight to the OlegDB code, go read it here.

The way Erlang knows what it’s doing is primary the ErlDrvEntry struct and the DRIVER_INIT function, as seen here:

ErlDrvEntry ol_driver_entry = {
    NULL,
    oleg_start,
    oleg_stop,
    oleg_output,
    NULL,
    NULL,
    "libolegserver",
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    ERL_DRV_EXTENDED_MARKER,
    ERL_DRV_EXTENDED_MAJOR_VERSION,
    ERL_DRV_EXTENDED_MINOR_VERSION,
    0,
    NULL,
    NULL,
    NULL
};

DRIVER_INIT(libolegserver) {
    return &ol_driver_entry;
}

I’m not even using most of the functionality, the meat of the interface happens in the three functions defined at the top, oleg_start, oleg_stop and oleg_output. The rest can just be nulled out, for the most part.

One pretty important caveat is the “libolegserver” string, this must be the same as what you pass to erl_ddll:load_driver! If they aren’t, obviously Erlang won’t be able to load your library.

Every function you defined (start, stop, output) all take an ErlDrvData argument, which allows you to pass around some state in between calls. For me, this happens to be the database object I want to use. The start and stop calls are responsible for allocating and freeing any memory you might use.

Your output function is where all the magic happens, and it takes a couple of interesting arguments. You’ve got your ErlDrvData, a random string (cmd, in my case) and an ErlDrvSizeT which shows you how long your cmd is. Before we can get to that, though, we need to know what kind of data Erlang is giving us.

Enter the message passing side of things. When you run erl_ddll:load_driver, I’m pretty sure whats happening is that you’re linking the Erlang vm up with your .so file, which allows you to later call open_port.

open_port is a function that takes a tuple and some arguments, in my case I use the following init() function:

init() ->
    register(complex, self()),
    Port = open_port({spawn, ?SHAREDLIB}, [binary]),
    loop(Port).

This, in sequence, register the atom complex as the init() function, opens a port, and then goes into the main loop. An important thing to note here is the [binary] argument to open_port! Without it you won’t get binary data back from your port driver. In Erlang, binary = speed.

The rest of ol_database.erl is pretty simple. We have some exposed functions (ol_jar, ol_unjar) that look nice and work pretty easily, and some encoding functions. The encoding functions are apparently the standard way of communicating to the port_driver. I map 1 to ol_jar, and 2 to ol_unjar.

Then, later on after I’ve done some message passing to the open_port (love dat message passing) we are finally ready to start digging into oleg_output.

if (fn == 1) {
    /* ol_jar */
    ...
} else if (fn == 2) {
    /* ol_unjar */
    ...
} else {
    /* Don't know what to do. */
    ...
}

This is where we switch on that first byte. Here you’ll map to whatever C functions you need before finally assembling some data to send back. This is the last thing that tripped me up. Handily, “ei.h” exports some awesome helper functions that make encoding data trivial.

ei_x_buff to_send;
ei_x_new_with_version(&to_send);
ei_x_encode_tuple_header(&to_send, 2);
ei_x_encode_atom(&to_send, "ok");
ei_x_encode_binary(&to_send, data, strlen((char*)data));
driver_output(d->port, to_send.buff, to_send.index);
ei_x_free(&to_send);

ei_x_buff objects are magic, dynamic chunks of binary that Erlang understands pretty well. I believe this mostly speaks for itself, I’m sending back an object of {ok, <<"random binary data here">>} that I can pattern match on, deconstruct ot whatever else I feel like. Then I send it back to Erlang. Easy!

The last thing I feel I should mention to any prospective googlers is that no, driver_output, driver_alloc, driver_free are not defined anywhere. You cannot link to them via libei or whatever. I believe that when erlang does a ‘load_driver’, those functions are defined inside of the Erlang VM itself, which is frankly pretty awesome. This means that whenever you allocate memory or free memory using those functions, you’re actually getting memory on the Erlang VM’s heap. Crazy, yeah?

Anyway, that pretty much covers it. OlegDB is available here, and all code is available in full here. Critiques welcome, email me at qpfiffer@gmail.com or yell at me over Twitter.