Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1707491
  • 博文数量: 607
  • 博客积分: 10031
  • 博客等级: 上将
  • 技术积分: 6633
  • 用 户 组: 普通用户
  • 注册时间: 2006-03-30 17:41
文章分类

全部博文(607)

文章存档

2011年(2)

2010年(15)

2009年(58)

2008年(172)

2007年(211)

2006年(149)

我的朋友

分类:

2009-01-14 11:21:32

Erlang Foreign Function Interface (FFI) - Introduction

16 Feb 2008 - Tenerife

The foreign function interface (FFI) of a language is a mechanism for calling libraries in functions written, usually, in the C language. Erlang, by design, has one of the most awkward approaches to interfacing with C. This mechanism has been in place for over 10 years now and the Erlang team is in no rush to make significant changes.

According to Kenneth Lundin,

Actually one of the advantages with Erlang is that you can make robust systems. One of the major reasons for the robustness is that there are no or very few linked in drivers written by the application developers.

Kenneth adds that

There are several products which don't have any linkedin drivers at all except those included in the standard distribution This makes it very easy for us as maintainers since when we get a core dump we know that the fault is in our code."

And closes with

So the 'awkward' interface for linked-in drivers can actually be seen as a stabilizing factor since only those that really know what they are doing will manage to implement a working driver.

Linked-in drivers, also called port drivers, are so named because they are dynamically loaded into a running Erlang program. But do you need port drivers at all? Unfortunately, port drivers are the only way to load a library into Erlang and access its functions. They are used by the SSL implementation, crypto and various other Erlang applications. Erlang is slow by nature and port drivers are the most efficient way to interface with the outside world and access foreign functions.

You and I are going to dig deep into the murky depths of Erlang port drivers and learn everything there is about them! Lets start with a simple application from the Erlang manual to illustrate basic port driver concepts.

A simple example

Suppose that we have a shared library with the following C functions.

/* complex.c */

int foo(int x) {
return x+1;
}

int bar(int y) {
return y*2;
}

We would like to call these C functions as regular Erlang functions, like this Res = complex:foo(X),.

The Erlang side

Given that a process is one of most basic units of functionality in Erlang, it's not surprising that a port driver is represented by a process. All communication goes through the Erlang process that is the connected process of the port driver. Terminating this process closes the port driver.

We start by creating the module that will house our Erlang code and by writing the code to load our shared library and start the port process.

-module(complex5).
-export([start/1, stop/0, init/1]).
-export([foo/1, bar/1]).

start(SharedLib) ->
case erl_ddll:load_driver(".", SharedLib) of
ok -> ok;
{error, already_loaded} -> ok;
_ -> exit({error, could_not_load_driver})
end,
spawn(?MODULE, init, [SharedLib]).

erl_ddll is the Erlang Dynamic Driver Loader and Linker. The erl_ddll module provides an interface for loading and unloading Erlang linked in drivers in runtime. The module is extensively documented and I suggest that you briefly review the module documentation before proceeding.

We use erl_ddll:load_driver/2 to load the port driver from a shared library located in the directory given by its first argument, the current directory (".") in our case. Note that only the name of the shared library needs to be supplied. The extension will be added by Erlang depending on the platform, e.g. ".so" on Unix or ".dll" on Windows. The name of the shared library will be the name of the port driver.

The last thing we do is create a new process that will be connected to the port that we are setting up. spawn/3 takes module name, function name and arguments and will call the function in a new process after starting it. We use spawn/3 to run init/1 which should be exported from our module for this to work.

init(SharedLib) ->
register(complex, self()), % [1]
Port = open_port({spawn, SharedLib}, []), % [2]
loop(Port). % [3]

[1] register/2_associates the atom complex with the our process id returned by self/0_. From that point on we can send messages to ourselves like this complex ! 'foo'.

[2] open_port/2 returns a port identifier as the result of opening a new Erlang port. Passing {spawn, SharedLib} will start the driver named SharedLib.

[3] loop(Port) is where we enter the main event loop. We supply the port identifier to be able to refer to it later on.

Essentially, our initialization function registers the freshly started process under the name complex, starts the port driver and enters the main event loop.

stop() ->
complex ! stop.

Only one process controls the port and talks to the port driver and giving it a unique name is a matter of convenience. It helps us implement stop/0, for example.

Our main event loop is idiomatic Erlang.

loop(Port) ->
receive
{call, Caller, Msg} -> % [1]
Port ! {self(), {command, encode(Msg)}},
receive % [2]
{Port, {data, Data}} ->
Caller ! {complex, decode(Data)}
end,
loop(Port); % [3]
stop -> % [4]
Port ! {self(), close},
receive
{Port, closed} ->
exit(normal)
end;
{'EXIT', Port, Reason} -> % [5]
io:format("~p ~n", [Reason]),
exit(port_terminated)
end.

[1] Receive a message from other Erlang processes, encode it and forward to the port driver. The received tuple has the Caller process id and we likewise supply our process id to the port when forwarding the message.

[2] Wait for a message from the port and forward decoded data back to the process that called us in the first place.

[3] Start another processing iteration by calling ourselves recursively.

[4] Tell the port to close once a stop message is received. Wait for a confirmation from the port before exiting normally.

[5] Any errors resulting from sending messages to the port will be sent to use using {'EXIT', Port, Reason} messages. We print the error reason and exit.

Erlang processes communicate by sending and receiving messages and the customary approach is for the function invoked by spawn to enter an event loop, a function that never exits unless told to do so.

Our event loop receives calculation requests from other Erlang processes, forwards them to the port driver, waits for a reply, forwards the reply back and repeats the process. The event loop terminates when a stop or error message is recevied. The event loop also takes care of encoding and decoding data sent to the port driver.

encode({foo, X}) -> [1, X];
encode({bar, Y}) -> [2, Y].

decode([Int]) -> Int.

We use a simple encoding scheme where 1 stands for a call to function foo and 2 stands for bar.

foo(X) ->
call_port({foo, X}).
bar(Y) ->
call_port({bar, Y}).

Our interface to the outside world is very straightfoward. We forward call to either foo or bar as tuples to the port.

call_port(Msg) ->
complex ! {call, self(), Msg},
receive
{complex, Result} ->
Result
end.

Finally, we send the message to the port and wait for a reply back. We use the atom call to make this message stand out from any other messages that may be sent to the port. Similarly, all the results sent back to us are tagged with the atom complex.

It's now time to cross over to the dark side and implement the port driver in C!

The C side

The C portion of a port driver is a module that is compiled and linked into a shared library so there's no main function. Lets review the code step by step.

#include 
#include "erl_driver.h"

typedef struct {
ErlDrvPort port;
} example_data;

example_data is a handle to our driver-specific data. You will see it being passed around all the time. ErlDrvPort is the type of the handle used to represent the port that's connected to our driver. We must save this handle when the driver is first started since we need it to send data back to Erlang.

static ErlDrvData example_drv_start(ErlDrvPort port, char *buff)
{
example_data* d = (example_data*)driver_alloc(sizeof(example_data));
d->port = port;
return (ErlDrvData)d;
}

example_drv_start is the driver's entry point the only function that is called with a handle to the port instance. We must not use a global variable to save the port handle since multiple instances of our driver can be spawned by multiple Erlang processes. We allocate an instance of our driver-specific structure, save the port handle and return the pointer.

static void example_drv_stop(ErlDrvData handle)
{
driver_free((char*)handle);
}

When our driver is stopped we must deallocate the handle to our internal data.

static void example_drv_output(ErlDrvData handle, char *buff, int bufflen)
{
example_data* d = (example_data*)handle;
char fn = buff[0], arg = buff[1], res;
if (fn == 1) {
res = foo(arg);
} else if (fn == 2) {
res = bar(arg);
}
driver_output(d->port, &res, 1);
}

example_drv_output is our workhorse and the means of interaction between the driver and the Erlang port process. This driver callback receives the handle to our internal data structures, the one we set up in example_drv_start, as well as a buffer and its length. It's up to us to extract and decode data from the buffer.

Finally, we use driver_output to send the result back to the Erlang port. Notice that it takes a buffer and a length as well.

ErlDrvEntry example_driver_entry = {
NULL, /* F_PTR init, N/A */
example_drv_start, /* L_PTR start, called when port is opened */
example_drv_stop, /* F_PTR stop, called when port is closed */
example_drv_output, /* F_PTR output, called when erlang has sent */
NULL, /* F_PTR ready_input, called when input descriptor ready */
NULL, /* F_PTR ready_output, called when output descriptor ready */
"example_drv", /* char *driver_name, the argument to open_port */
NULL, /* F_PTR finish, called when unloaded */
NULL, /* F_PTR control, port_command callback */
NULL, /* F_PTR timeout, reserved */
NULL /* F_PTR outputv, reserved */
};

DRIVER_INIT(example_drv) /* must match name in driver_entry */
{
return &example_driver_entry;
}

The driver structure is populated with the driver name and callback function pointers. It is returned from the driver entry point, declared with the macro DRIVER_INIT(example_drv). We will go over the unpopulated fields of this structure later in this chapter.

Building and running our example code

Compile the C code.

gcc -o exampledrv -fpic -shared complex.c port_driver.c

Start Erlang and compile the Erlang code.

erl
Erlang (BEAM) emulator version 5.1

Eshell V5.1 (abort with ^G)
c(complex5).
{ok,complex5}

Run the example.

complex5:start("example_drv").
<0.34.0>
complex5:foo(3).
4
complex5:bar(5).
10
complex5:stop().
stop

There's more to come!

阅读(1236) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~