This document was written a long time ago. A lot of it is still
interesting since it explains important concepts, but it was
written for an older driver interface so the examples does not
work anymore. The reader is encouraged to read
This chapter tells you how to build your own driver for erlang.
A driver in Erlang is a library written in C, that is linked to the Erlang emulator and called from erlang. Drivers can be used when C is more suitable than Erlang, to speed things up, or to provide access to OS resources not directly accessible from Erlang.
A driver can be dynamically loaded, as a shared library (known as a DLL on windows), or statically loaded, linked with the emulator when it is compiled and linked. Only dynamically loaded drivers are described here, statically linked drivers are beyond the scope of this chapter.
When a driver is loaded it is executed in the context of the emulator, shares the same memory and the same thread. This means that all operations in the driver must be non-blocking, and that any crash in the driver will bring the whole emulator down. In short: you have to be extremely careful!
This is a simple driver for accessing a postgres
database using the libpq C client library. Postgres
is used because it's free and open source. For information
on postgres, refer to the website
The driver is synchronous, it uses the synchronous calls of the client library. This is only for simplicity, and is generally not good, since it will halt the emulator while waiting for the database. This will be improved on below with an asynchronous sample driver.
The code is quite straight-forward: all
communication between Erlang and the driver
is done with
An Erlang driver only exports one function: the driver
entry function. This is defined with a macro,
The
There is an entry
The
The
The code is available in
The driver entry contains the functions that
will be called by the emulator. In our simple
example, we only provide
We have a structure to store state needed by the driver, in this case we only need to keep the database connection.
These are control codes we have defined.
This just returns the driver structure. The macro
Here we do some initialization,
conn = NULL;
set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY);
return (ErlDrvData)data;
}
]]>
We call disconnect to log out from the database. (This should have been done from Erlang, but just in case.)
We use the binary format only to return data to the emulator;
input data is a string paramater for
The functions
conn = conn;
return 0;
}
]]>
If we are connected (if the connection handle is not
conn == NULL)
return 0;
PQfinish(data->conn);
data->conn = NULL;
if (x != NULL)
encode_ok(x);
return 0;
}
]]>
We execute a query and encode the result. Encoding is done
in another C module,
conn, s);
encode_result(x, res, data->conn);
PQclear(res);
return 0;
}
]]>
Here we simply check the result from postgres, and
if it's data we encode it as lists of lists with
column data. Everything from postgres is C strings,
so we just use
The driver should be compiled and linked to a shared
library (DLL on windows). With gcc this is done
with the link flags
Before a driver can be called from Erlang, it must be
loaded and opened. Loading is done using the
When the port has been opened, the driver can be called. In
the
The following code is the Erlang part of the synchronous
postgres driver,
case erl_ddll:load_driver(".", "pg_sync") of
ok -> ok;
{error, already_loaded} -> ok;
E -> exit({error, E})
end,
Port = open_port({spawn, ?MODULE}, []),
case binary_to_term(port_control(Port, ?DRV_CONNECT, ConnectStr)) of
ok -> {ok, Port};
Error -> Error
end.
disconnect(Port) ->
R = binary_to_term(port_control(Port, ?DRV_DISCONNECT, "")),
port_close(Port),
R.
select(Port, Query) ->
binary_to_term(port_control(Port, ?DRV_SELECT, Query)).
]]>
The API is simple:
The driver is loaded with
We use the
Sometimes database queries can take long time to
complete, in our
The asynchronous version of the driver is in the
sample files
Here some things have changed from
Our driver data is also extended, we keep track of the
socket used for communication with postgres, and also
the port, which is needed when we send data to the port with
port, x.buff, x.index);
ei_x_free(&x);
}
PQconnectPoll(conn);
int socket = PQsocket(conn);
data->socket = socket;
driver_select(data->port, (ErlDrvEvent)socket, DO_READ, 1);
driver_select(data->port, (ErlDrvEvent)socket, DO_WRITE, 1);
data->conn = conn;
data->connecting = 1;
return 0;
}
]]>
The
Note that we only return data (with
connecting = 0;
PGconn* conn = data->conn;
/* if there's an error return it now */
if (PQsendQuery(conn, s) == 0) {
ei_x_buff x;
ei_x_new_with_version(&x);
encode_error(&x, conn);
driver_output(data->port, x.buff, x.index);
ei_x_free(&x);
}
/* else wait for ready_output to get results */
return 0;
}
]]>
The
conn;
ei_x_buff x;
ei_x_new_with_version(&x);
if (data->connecting) {
ConnStatusType status;
PQconnectPoll(conn);
status = PQstatus(conn);
if (status == CONNECTION_OK)
encode_ok(&x);
else if (status == CONNECTION_BAD)
encode_error(&x, conn);
} else {
PQconsumeInput(conn);
if (PQisBusy(conn))
return;
res = PQgetResult(conn);
encode_result(&x, res, conn);
PQclear(res);
for (;;) {
res = PQgetResult(conn);
if (res == NULL)
break;
PQclear(res);
}
}
if (x.index > 1) {
driver_output(data->port, x.buff, x.index);
if (data->connecting)
driver_select(data->port, (ErlDrvEvent)data->socket, DO_WRITE, 0);
}
ei_x_free(&x);
}
]]>
The
If we have a result from a connect, indicated by having data in
the
If we're not connecting, we're waiting for results from a
We should add error handling here, for instance checking that the socket is still open, but this is just a simple example.
The Erlang part of the asynchronous driver consists of the
sample file
case erl_ddll:load_driver(".", "pg_async") of
ok -> ok;
{error, already_loaded} -> ok;
_ -> exit({error, could_not_load_driver})
end,
Port = open_port({spawn, ?MODULE}, [binary]),
port_control(Port, ?DRV_CONNECT, ConnectStr),
case return_port_data(Port) of
ok ->
{ok, Port};
Error ->
Error
end.
disconnect(Port) ->
port_control(Port, ?DRV_DISCONNECT, ""),
R = return_port_data(Port),
port_close(Port),
R.
select(Port, Query) ->
port_control(Port, ?DRV_SELECT, Query),
return_port_data(Port).
return_port_data(Port) ->
receive
{Port, {data, Data}} ->
binary_to_term(Data)
end.
]]>
The Erlang code is slightly different, this is because we
don't return the result synchronously from
As a final example we demonstrate the use of
The asynchronous API for drivers is quite complicated. First
of all, the work must be prepared. In our example we do this
in
The code below is from the sample file
The driver entry looks like before, but also contains the
call-back
The
The
data;
our_async_data(ErlDrvPort p, int command, const char* buf, int len);
};
our_async_data::our_async_data(ErlDrvPort p, int command,
const char* buf, int len)
: prev(command == 2),
data((int*)buf, (int*)buf + len / sizeof(int))
{
}
static void do_perm(void* async_data);
static void output(ErlDrvData drv_data, char *buf, int len)
{
if (*buf < 1 || *buf > 2) return;
ErlDrvPort port = reinterpret_cast(drv_data);
void* async_data = new our_async_data(port, *buf, buf+1, len);
driver_async(port, NULL, do_perm, async_data, do_free);
}
]]>
In the
(async_data);
if (d->prev)
prev_permutation(d->data.begin(), d->data.end());
else
next_permutation(d->data.begin(), d->data.end());
}
]]>
In the
When the data is returned we deallocate our data.
(drv_data);
our_async_data* d = reinterpret_cast(async_data);
int n = d->data.size(), result_n = n*2 + 3;
ErlDrvTermData *result = new ErlDrvTermData[result_n], *rp = result;
for (vector::iterator i = d->data.begin();
i != d->data.end(); ++i) {
*rp++ = ERL_DRV_INT;
*rp++ = *i;
}
*rp++ = ERL_DRV_NIL;
*rp++ = ERL_DRV_LIST;
*rp++ = n+1;
driver_output_term(port, result, result_n);
delete[] result;
delete d;
}
]]>
This driver is called like the others from Erlang, however, since
we use
The input is changed into a list of integers and sent to the driver.
case whereis(next_perm) of
undefined ->
case erl_ddll:load_driver(".", "next_perm") of
ok -> ok;
{error, already_loaded} -> ok;
E -> exit(E)
end,
Port = open_port({spawn, "next_perm"}, []),
register(next_perm, Port);
_ ->
ok
end.
list_to_integer_binaries(L) ->
[<> || I <- L].
next_perm(L) ->
next_perm(L, 1).
prev_perm(L) ->
next_perm(L, 2).
next_perm(L, Nxt) ->
load(),
B = list_to_integer_binaries(L),
port_control(next_perm, Nxt, B),
receive
Result ->
Result
end.
all_perm(L) ->
New = prev_perm(L),
all_perm(New, L, [New]).
all_perm(L, L, Acc) ->
Acc;
all_perm(L, Orig, Acc) ->
New = prev_perm(L),
all_perm(New, Orig, [New | Acc]).
]]>