Clixir
Disclaimer: for now, this is design documentation, actual implementation may have differences. See the code, the test, and the demo app for the truth
We write the C code in Elixir and use macros to generate both the Elixir bindings and the C code. This idea was blatantly stolen from Squeak Smalltalk. As we aim for a very low level interface, all the functions will have the same structure in C: unmarshall arguments, make call, marshall return values; so "Clixir" can start out simple.
defgfx glfw_get_cursor_pos(window, pid) do
cdecl "GLFWwindow *": window
cdecl erlang_pid: pid
cdecl double: [mx, my]
glfwGetCursorPos(window, &mx, &my)
{pid, {mx, my}}
end
will result in this Elixir code (both a regular and a blocking synchronous version are generated, although I think the sync version shouldn't be used ;-)):
@spec glfw_get_cursor_pos(integer, pid) :: none
def glfw_get_cursor_pos(window, pid) do
GraphicsServer.send_command(GraphicsServer, {:glfw_get_cursor_pos, window, pid})
end
@spec glfw_get_cursor_pos_s(integer) :: {float, float}
def glfw_get_cursor_pos(window) do
glfw_get_cursor_pos(window, self)
receive do
{mx, my} when is_float(mx) and is_float(my) -> {mx, my}
end
end
and the following C code:
static void _dispatch_glfw_get_cursor_pos(const char *buf, unsigned short len, int *index) {
GLFWwindow *window;
erlang_pid pid;
double mx, my;
assert(ei_decode_longlong(buf, index, (long long *) &window) == 0);
assert(ei_decode_pid(buf, index, &pid) == 0);
glfwGetCursorPos(window, &mx, My);
ei_encode_version(response, &response_index);
ei_encode_tuple_header(response, &response_index, 2);
ei_encode_pid(response, &response_index, &pid);
ei_encode_tuple_header(response, &response_index, 2);
ei_encode_double(response, &response_index, mx);
ei_encode_double(response, &response_index, my);
write_response_bytes(response, response_index);
}
Usage
Although Clixir was specifically created for Uderzo, it is very easy to use stand-alone. Uderzo's repository has a Clixir example application that shows a minimal "hello, world". The following files are relevant:
c_src/example.hx is the required Clixir header file. This file should pull in all the includes, define macros, etcetera - stuff that cannot be (yet) done in Clixir. It is slapped on top of the generated code;
lib/example.ex is a minimal Elixir module that uses the Clixir
def_c
macro. Please take the warning about not sending anything to stdout to heart, because it's likely to really mess up things :-).Finally, lib/example_application.ex is the "application". It invokes the Clixir-generated method. Note how it looks like any other Elixir invocation - the whole idea is to make invoking C code as transparent as possible.
To get this all to build, some things need to be setup correctly:
mix.exs needs to incorporate the Clixir compiler and setup the build environment for the
elixir_make
plugin so that we can find the correct location for the Erlang libraries we use for deserializing Erlang terms.config/config.exs must have a
:clixir
configuration that points to the application that provides theclixir
executable. You might have multiple dependencies that use Clixir and they each build their own version of this executable, but only one can be started at run-time. Usually, your top-level application integrates everything and that application's executable will have aclixir
executable that incorporates all Clixir code from your application and your dependencies.Finally, Makefile is basically boilerplate code you can copy-paste; it takes the generated C file in
c_src
(containing all Clixir functions in all applications and agperf
jump table) and compiles it together with the Clixir run-time library into the executable that will be started.
Performance
Performance of the protocol should be very good, for the following reasons:
- the
ei_decode_..
family seems to do very little copying and is efficient by keeping a pointer in a buffer that moves up; ideally, this means that a message is scanned in a single loop the length of the message; - dispatching is done using a
gperf
generated hashtable. These perfect hashtables are very fast, usually requiring just two memory lookups to find the function pointer to dispatch to. - the default messaging flow is fully asynchronous so that waiting for I/O should never be a blocker.
One of the Uderzo examples I coded runs 100 "boids" at 100fps, meaning that 10,000 draw commands are sent every second; the Clixir executable takes negligible CPU.