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.
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. - most of the tight rendering code is (and should be kept) async - you’re just sending messages over a local file descriptor pipe and keeping the pipe filled when drawing a frame should be very easy.