Table of Contents
A behaviour that ties together Erlang's gen_server
with Erlang's C
nodes.
C nodes are a way of implementing native binaries, written in C, that behave like Erlang nodes, capable of sending and receiving messages from Erlang processes. Amongst the ways of interfacing Erlang with C, C nodes are the most robust: since the nodes are independent OS processes, crashes do not affect the Erlang VM. Compare this to a NIF, for example, where a problem in the C code may cause the entire Erlang VM to crash. The price to pay is, of course, performance.
Making C nodes work with Erlang, however, is not a simple task. Your
executable is started by hand, and has to set up communications with an
existing Erlang process. Since the executable does not know where to connect
to in advance, the necessary parameters have to be given to it in the
command line when the C node is started.
Once all of that is done, the C node
enters a message loop, waiting for messages and replying to them. If the
computations it is performing take too long, the calling Erlang process may
infer that the C node is dead, and cut communications with it. This is
because Erlang will be sending TICK
messages to the C node to keep the
connection alive. The only way around this is to make the C node have
separate threads to deal with user-generated messages and system-generated
messages. The list does go on.
This module, and its accompanying library, libgen_c_server.a
, hide this
complexity and require the C node implementer to define only the equivalent
gen_server
callbacks in C, and a gen_c_server
callback module
in Erlang. Within the C callbacks, the developer uses
the ei
library to manipulate Erlang terms.
Inspired by Robby Raschke's work on interfacing with Lua.
- Write a C node
- Write an Erlang callback module with
gen_c_server
behaviour.- Call
c_init
frominit
, andc_terminate
fromterminate
. - Forward
handle_info
toc_handle_info
if desired.
- Call
We will start with the Erlang callback module description, since this part should be more familiar to Erlang users.
If you are not familiar with the
standard gen_server
behaviour, please take some time to study it before tackling this
one. The description that follows relies heavily on describing
gen_c_server
in terms of gen_server
.
We will describe how to write the C node
in another section. In this one, we'll describe
the behaviour functions and callbacks. In what follows, Mod
will
refer to the Erlang module implementing this behaviour. That module's
definition will contain something like this at the beginning:
-module(example_module).
-behaviour(gen_c_server).
The callback module should define a c_node
function, which returns a
string with the path to the C node executable.
c_node() ->
"/path/to/c/node/executable".
In gen_server
, the internals of the server implementation are
completely hidden. In gen_c_server
, however, there is one bit of
internal server state that gets passed around in some functions. The
internal server state is a tuple {State, Opaque}
, where State
is
the equivalent to gen_server
's state, and Opaque
is something
internal to the gen_c_server
. This tuple is passed in to every
callback that normally takes in a state argument. Those callbacks
should also return a {NewState, NewOpaque}
instead of the simple
state value.
The reason this has to be exposed is because, in callbacks such as
init/2
and terminate/2
, the callback has to ask the server to talk
to the C node, and the Opaque
structure gives gen_c_server
the
information needed to do so. This will become clearer when we document
the callbacks below, but now you know the reason why Opaque
will pop
up here and there.
In a gen_server
, the callback module needs to implement a init/1
callback, which is called by the various server start
functions. In
gen_c_server
, this function takes an extra argument, Opaque
,
representing the internal state of the server.
It is the job of this init/2
callback to initialize the C node, and
it can do that by a call to gen_c_server:c_init(Args, Opaque)
, where
Args
is an arbitrary value (usually the same as the first input
argument), and Opaque
is the second input value to init/2
. The
function will start the C node and call its gcs_init
function. Depending on what it returns, c_init
will return one of
{ok, {State, NewOpaque}}
{ok, {State, NewOpaque}, hibernate | Timeout}
{stop, Reason}
ignore
The meaning of those is similar to the usual gen_server
expected
returns from init/1
, except they need to include the Opaque
parameters in the state.
The init/2
callback should return one of those values too, so its
simplest implementation, if the Erlang portion does not care about
state or massaging the Args
parameters, is
init(Args, Opaque) -> gen_c_server:c_init(Args, Opaque).
Of course, the function is free to modify the input Args
, or the
State
returned by c_init
. What it should not do is mess
around with the Opaque
parameters.
Lastly, there is a c_init/3
function that takes as its third
argument the "spawn type" for the C node. The possible values are
spawn_executable
and spawn
. The first is the value c_init/2
uses. The second is useful when the C node executable is wrapped in a
shell script.
The logic behind the init/2
callback applies verbatim to the
terminate/2
callback. In gen_server
, the callback takes in the
Reason
and the State
, and does whatever it needs to do to clean
up. In gen_c_server
, there are two differences:
- The
terminate
state argument is instead{State, Opaque}
, for reasons described above. - The callback should call the
gen_c_server:c_terminate
function, which will take care of calling the C node'sgcs_terminate
function, and shutting the C node down.
The c_terminate
function accepts the same parameters as
terminate/2
, so the simplest callback implementation is
terminate(Reason, {State, Opaque} = ServerState) ->
gen_c_server:c_terminate(Reason, ServerState).
Synchronous calls to the Erlang callback module are done by calling
gen_c_server:call/2
. The Erlang callback handle_call/3
is called,
and again it takes in a {State, Opaque}
tuple instead of
gen_server
's single State
. When it returns new state, it must
return it in the form {NewState, Opaque}
.
The Opaque
value would only be useful if the handle_call/3
callback could call the C node somehow. Currently that is not
possible, but by having the parameter there we can add the
functionality without breaking backwards compatibility.
Synchronous calls to the C node are made via the
gen_c_server:c_call/2
function, which calls the gcs_handle_call
C
node callback. This callback does not receive the {State, Opaque}
tuple, since it doesn't need Opaque
for anything. Instead, it simply
receives the State
.
Asynchronous calls follow the same logic as synchronous ones, except
that call
is replaced by cast
: The gen_c_server:cast/2
function
calls the handle_cast
callback which gets {State, Opaque}
and
needs to reply with {NewState, Opaque}
. Asynchronous calls to the C
node are done with gen_c_server:c_cast/2
, and call the C node's
gcs_handle_call
callback with State
.
Lastly, out-of-band messages are always passed on to the Erlang
module's handle_info/2
callback, which, as usual, gets the
{State, Opaque}
tuple as the state argument. The callback can pass
the message on to the C node by calling gen_c_server:c_handle_info/2
with the same arguments as its input, which will result in a call to
the C node's gcs_handle_info
.
init/2
terminate/2
c_node/0
handle_call/3
handle_cast/2
handle_info/2
start/3
,start/4
,start_link/3
,start_link/4
: These are completely equivalent togen_server
's functions of the same signature.stop/1
: Terminates the server, including shutting down the C node.call/2
,call/3
cast/2
c_init/2
c_call/2
,c_call/3
c_terminate/2
c_cast/2
c_handle_info/2
A C node is written by implementing the necessary callbacks, and linking
the executable against libgen_c_server.a
, which defines main()
for you.
When implementing callbacks, the rule of thumb is this: for each callback
you would implement in an Erlang gen_server
callback module, implement a
gcs_
name function in C instead. This function takes in the same
arguments as the Erlang callback, plus a last ei_x_buff*
argument where the
function should build its reply (when a reply is needed). The other
arguments are const char*
pointing to ei
buffers containing the
arguments themselves. C node callbacks that receive a state receive
State
, not the {State, Opaque}
tuple the Erlang callbacks
receive. (The Opaque
argument is necessary in Erlang so the C node
can be called, so it is useless inside the C node itself.)
A full C node skeleton thus looks like this:
#include "gen_c_server.h"
void gcs_init(const char *args_buff, ei_x_buff *reply) {
/* IMPLEMENT ME */
}
void gcs_handle_call(const char *request_buff,
const char *from_buff,
const char *state_buff,
ei_x_buff *reply) {
/* IMPLEMENT ME */
}
void gcs_handle_cast(const char *request_buff,
const char *state_buff,
ei_x_buff *reply) {
/* IMPLEMENT ME */
}
void gcs_handle_info(const char *info_buff,
const char *state_buff,
ei_x_buff *reply) {
/* IMPLEMENT ME */
}
void gcs_terminate(const char *reason_buff, const char *state_buff) {
/* IMPLEMENT ME */
}
Let's see how this looks like in a C node that simply counts the number
of calls made to each function. (This C node is included in the source
distribution of gen_c_server
, as one of the tests.) In this C node,
We make use of an utility gcs_decode
function which is provided by the
gen_c_server
library, that allows us to decode ei
buffers in a
sscanf
-like manner. Consult the header file for more details.
#include "gen_c_server.h"
/*
* Utility function that encodes a state tuple based on the arguments.
* The state is a 3-tuple, { num_calls, num_casts, num_infos }
*/
static void encode_state(
ei_x_buff *reply,
long ncalls,
long ncasts,
long ninfos)
{
ei_x_encode_tuple_header(reply,3);
ei_x_encode_long(reply,ncalls);
ei_x_encode_long(reply,ncasts);
ei_x_encode_long(reply,ninfos);
}
/*
* Initialize the C node, replying with a zeroed-out state.
*/
void gcs_init(const char* args_buff, ei_x_buff *reply)
{
/* Reply: {ok,{0,0,0}} */
ei_x_encode_tuple_header(reply,2);
ei_x_encode_atom(reply,"ok");
encode_state(reply,0,0,0);
}
/*
* When called, increment the number of calls, and reply with the new state.
*/
void gcs_handle_call(
const char *request_buff,
const char *from_buff,
const char *state_buff,
ei_x_buff *reply)
{
long ncalls, ncasts, ninfos;
gcs_decode(state_buff,(int*)0,"{lll}",3,&ncalls,&ncasts,&ninfos);
/* Reply: {reply,Reply=NewState,NewState={ncalls+1,ncasts,ninfos}} */
ei_x_encode_tuple_header(reply,3);
ei_x_encode_atom(reply,"reply");
encode_state(reply,ncalls+1,ncasts,ninfos);
encode_state(reply,ncalls+1,ncasts,ninfos);
}
/*
* When casted, increment the number of casts, and reply with the new state.
*/
void gcs_handle_cast(
const char *request_buff,
const char *state_buff,
ei_x_buff *reply)
{
long ncalls, ncasts, ninfos;
gcs_decode(state_buff,(int*)0,"{lll}",3,&ncalls,&ncasts,&ninfos);
/* Reply: {noreply,NewState={ncalls,ncasts+1,ninfos}} */
ei_x_encode_tuple_header(reply,2);
ei_x_encode_atom(reply,"noreply");
encode_state(reply,ncalls,ncasts+1,ninfos);
}
/*
* When info-ed, increment the number of infos, and reply with the new state.
*/
void gcs_handle_info(
const char *info_buff,
const char *state_buff,
ei_x_buff *reply)
{
long ncalls, ncasts, ninfos;
gcs_decode(state_buff,(int*)0,"{lll}",3,&ncalls,&ncasts,&ninfos);
/* Reply: {noreply,NewState={ncalls,ncasts,ninfos+1}} */
ei_x_encode_tuple_header(reply,2);
ei_x_encode_atom(reply,"noreply");
encode_state(reply,ncalls,ncasts,ninfos+1);
}
/*
* We don't need to clean anything up when terminating.
*/
void gcs_terminate(const char *reason_buff, const char *state_buff) { }
Once you compile (with the appropriate flags so the compiler finds
gen_c_server.h
and ei.h
) and link (with the appropriate flags to the
linker links in libgen_c_server
, erl_interface
, and ei
libraries) this
node, you can define a simple Erlang callback module as follows:
-module(my_c_server).
-behaviour(gen_c_server).
-export([c_node/0, init/2, terminate/2, handle_info/2]).
c_node() -> "/path/to/my/c/node/executable".
init(Args, Opaque) -> gen_c_server:c_init(Args, Opaque).
terminate(Reason, ServerState) -> gen_c_server:c_terminate(Reason, ServerState).
handle_info(Info, ServerState) -> gen_c_server:c_handle_info(Info, ServerState).
With that, you can now:
{ok,Pid} = gen_c_server:start(my_c_server,[],[]),
{1,0,0} = gen_c_server:c_call(Pid,"Any message"),
ok = gen_c_server:c_cast(Pid,"Any old message"),
{2,1,0} = gen_c_server:c_call(Pid,"Any message"),
Pid ! "Any message, really",
{3,1,1} = gen_c_server:c_call(Pid,[]),
gen_c_server:stop(Pid).
Note that the Erlang shell has to have a registered name, which means
you have to start it with either the -name
or -sname
options passed in.
This is because the C node needs a registered Erlang node to connect to.
Currently, not all replies from the callback functions are accepted. In particular,
gcs_handle_call
can only reply{reply,Reply,NewState}
,{reply,Reply,NewState,hibernate}
or{stop,Reason,Reply,NewState}
.gcs_handle_cast
can only reply{noreply,NewState}
,{noreply,NewState,hibernate}
or{stop,Reason,NewState}
.gcs_handle_info
can only reply{noreply,NewState}
,{noreply,NewState,hibernate}
or{stop,Reason,NewState}
.
In a Unix system, typing ./gradlew install
should do it. Type
./gradlew tasks
to get a list of available build targets.
A makefile is provided with convenience targets used during
development. The ct
target, for example, sets up and tears down
epmd
automatically so that tests can be performed. make shell
will
bring you into an Erlang shell with the correct paths set for
interactively testing things.
Download the latest
pthreads-win32 pre-compiled
ZIP. Unzip it into a pthreads-win32
directory at the root of your
checkout of this project. This should allow you to build with the
Gradle script.