Simplest Linked-In Driver EVAH

Posted: March 24th, 2009 | Author: kevin | Filed under: Erlang | View Comments

I’ve been tinkering around with linked-in drivers lately. There are several interesting projects using linked-in drivers so I thought I should learn how to write one.

I quickly discovered a dearth of simple examples on the web. All of the examples I found were either poorly documented or fairly complicated. I really wanted to see a basic “hello, world” style driver so I could see the “flow” of a driver. The closest I came to a minimal example was Cliff Moon’s cherly project.

So, in the finest open source tradition, I’ve taken Cliff’s project and cut it down to its absolute essentials. What’s left is a very basic driver which receives data via port_command/2 and echoes the received data back via driver_output_term().

The C side of the driver is contained in a single source file, basic_driver.c. The structure of the driver is straightforward. First, the driver includes the required header files, declares a struct to contain the driver’s state and exports functions required by erl_driver

#include <erl_driver.h>
#include <ei.h>

#include "config.h"

typedef struct _basic_drv_t {
  ErlDrvPort port;
} basic_drv_t;

static ErlDrvData start(ErlDrvPort port, char* cmd);
static void stop(ErlDrvData handle);
static void process(ErlDrvData handle, ErlIOVec *ev);

Next, it defines a static instance of the ErlDrvEntry struct which is used by Erlang to interface with the driver. The driver initialization function is defined using the DRIVER_INIT macro.

static ErlDrvEntry basic_driver_entry = {
    NULL,                             /* init */
    start,                            /* startup */
    stop,                             /* shutdown */
    NULL,                             /* output */
    NULL,                             /* ready_input */
    NULL,                             /* ready_output */
    "basic_drv",                      /* the name of the driver */
    NULL,                             /* finish */
    NULL,                             /* handle */
    NULL,                             /* control */
    NULL,                             /* timeout */
    process,                          /* process */
    NULL,                             /* ready_async */
    NULL,                             /* flush */
    NULL,                             /* call */
    NULL,                             /* event */
    ERL_DRV_EXTENDED_MARKER,          /* ERL_DRV_EXTENDED_MARKER */
    ERL_DRV_EXTENDED_MAJOR_VERSION,   /* ERL_DRV_EXTENDED_MAJOR_VERSION */
    ERL_DRV_EXTENDED_MAJOR_VERSION,   /* ERL_DRV_EXTENDED_MINOR_VERSION */
    ERL_DRV_FLAG_USE_PORT_LOCKING     /* ERL_DRV_FLAGs */
};

DRIVER_INIT(basic_drv) {
  return &basic_driver_entry;
}

Driver startup and teardown is handled by the start() and stop() functions. Since the driver doesn’t do very much, these functions merely allocate and free memory associated with the driver’s state.

static ErlDrvData start(ErlDrvPort port, char* cmd) {
  basic_drv_t* retval = (basic_drv_t*) driver_alloc(sizeof(basic_drv_t));
  retval->port = port;
  return (ErlDrvData) retval;
}

static void stop(ErlDrvData handle) {
  basic_drv_t* driver_data = (basic_drv_t*) handle;
  driver_free(driver_data);
}

The real work is performed in process(). The function is called with the driver’s current state and a vector, or list, of binaries sent by the caller. The driver unbundles the user data from the vector, wraps it up in a tuple and send it back to the caller.

static void process(ErlDrvData handle, ErlIOVec *ev) {
  basic_drv_t* driver_data = (basic_drv_t*) handle;
  ErlDrvBinary* data = ev->binv[1];
  ErlDrvTermData spec[] = {ERL_DRV_ATOM, driver_mk_atom("ok"),
			   ERL_DRV_BINARY, (ErlDrvTermData) data, data->orig_size, 0,
			   ERL_DRV_TUPLE, 2};
  driver_output_term(driver_data->port, spec, sizeof(spec) / sizeof(spec[0]));
}

Finally, the driver is loaded and started from Erlang code in the Erlang module basic_driver.

-module(basic_driver).

-export([test/0, test/1]).

test(Message) when is_list(Message) ->
  {ok, P} = load_driver(),
  port_command(P, list_to_binary(Message)),
  receive
    Data ->
      io:format("Data: ~p~n", [Data])
  after 100 ->
      io:format("Received nothing!~n")
  end,
  port_close(P).

test() ->
  {ok, P} = load_driver(),
  port_command(P, [<<"a">>, <<"b">>, <<"c">>]),
  receive
    Data ->
      io:format("Data: ~p~n", [Data])
  after 100 ->
      io:format("Received nothing!~n")
  end,
  port_close(P).

%% Private functions
load_driver() ->
  SearchDir = filename:join([filename:dirname(code:which(basic_driver)), "..", "priv"]),
  case erl_ddll:load(SearchDir, "basic_drv") of
    ok ->
      {ok, open_port({spawn, 'basic_drv'}, [binary])};
    Error ->
      Error
  end.

Source code for the basic_driver project is available here.


  • Thanks, Kevin! I've been meaning to try out linked in drivers for a while now.
  • What @evgen said :)
  • evgen
    There are two ways to access external code in Erlang, via a linked-in driver or a port. The difference between the two is that a linked-in driver runs in the Erlang VM process space and has much faster communication with the library (with the dangerous downside that a bug or crash in the driver will take down the VM) while a standard port runs in its own process space (safer) but uses a slower communication mechanism.

    You can open multiple ports to the same linked in driver and get multiple instances of the driver which can sometimes increase throughput (e.g. the crypto module does this to create multiple instances of crypto routines that each maintain their own internal state for a particular crypto operation.) In general the benefits of this are limited depending on the specifics of the library; it seems to be most frequently used with standard ports when you want several "workers" that have a bit of launch overhead (e.g. starting up multiple python interpreters if you are on a multi-core box.)

    You can do anything you want inside the driver. Depending on whether or not the driver is linked-in or not you might even be able to have the driver make calls directly into the Erlang VM. A linked-in driver can do anything and get the data quickly to/from the other bits of the Erlang VM, but it can also crash the whole VM. If you are using pthreads inside a linked-in driver you are probably doing something wrong :)
  • I've a few questions about linked-in drivers:

    * When you create a linked-in driver like above, what's going on underneath? The driver is loaded within that Erlang VM process space? Or does it spawn a child process?

    * Can you open multiple ports (via open_port/2) to the same linked-in driver? If you do, what'll be the behavior? Is there a use-case for this?

    * Can you use regular pthreads inside the driver? And can you spawn child procs? Basically can the linked-in driver do anything that an external C program can do, or are there any restrictions?
blog comments powered by Disqus