Lever's libuv integration
Although I integrated libuv into Lever weeks ago, I figured out some improvements only about a week ago. I was motivated to do a walkthrough from the implementation details. This may help you if you are studying Lever's internals in general because we go through lot of details.
Lets study a bit what happens when we issue a read call.
Currently our libuv read function is accessible through
fs.raw_read as a temporary measure.
fs.raw_read(fd, [buffer], 0)
When the evaluator runs the 'call' opcode, it does an
.call(argv). The code presented below is RPython
code. It is 'source code' of the Lever runtime.
Here's the implementation of the
raw_read, located in
@signature(Integer, List, Integer)
def raw_read(fileno, arrays, offset):
L = len(arrays.contents)
bufs = lltype.malloc(rffi.CArray(uv.buf_t), L, flavor='raw', zero=True)
i = 0
for obj in arrays.contents:
obj = cast(obj, Uint8Array, u"raw_read expects uint8arrays")
bufs[i].c_base = rffi.cast(rffi.CCHARP, obj.uint8data)
bufs[i].c_len = rffi.r_size_t(obj.length)
i += 1
fileno.value, bufs, L, offset.value)))
@signature are python decorators that
have ran before the rpython translates this program. Roughly
they wrap the raw_read with the signature and builtin
object, then place it inside the fs -module.
Objects that can be called in the runtime have a
.call(argv) -method. The argv is a list of objects passed
as arguments. The signature points out they have to be
converted to be (Integer, List, Integer) before they are
passed to the implementation.
Most of the code in
raw_read is just doing conversions to
place values into their places. The
fs_read is doing the
actual work. Here is the implementation of the
def fs_read(fileno, bufs, nbufs, offset):
req = lltype.malloc(uv.fs_ptr.TO, flavor='raw', zero=True)
response = uv_callback.fs(req)
fileno, bufs, nbufs, offset,
if req.c_result < 0:
Here it actually calls
uv_fs_read. The C type signature of
that function is:
int uv_fs_read(uv_loop_t* loop,
const uv_buf_t bufs,
unsigned int nbufs,
Libuv requires that it gets a callback, that is called when
the function completes. You should note we pass
response.ec.uv_loop as a loop, and
ec stands for the execution context. It contains the
context for the interpreter. The contents are described in
runtime/core.py. Lever starts up directly into the
libuv event loop. In identical manner to node.js. The call
uv_run can be found from the
uv_callback.fs.cb is created with a decorator, and the
implementation of the callback inside the decorator looks
def _callback_(handle, *data):
ec = core.get_ec()
resp = pop_handle(getattr(ec, uv_name), handle)
if len(data) > 0:
resp.data = data
resp.pending = False
if resp.greenlet is not None:
core.get_ec()call returns the current execution context. We expect to return in the same thread as where we started a request.
pop_handleretrieves a response object for the handle from the execution context. The
getattr(ec, uv_name)is equivalent to
ec.uv__fs. This is roughly equivalent to
ec.uv__fs.pop(rffi.cast_ptr_to_adr(handle))but the cast operation is not understood by the JIT, therefore it is cloaked from the JIT generator into a small wrapper function.
- If there's arguments to the callback, they are passed
.data-field. The resp.wait() returns these fields as a tuple.
resp.pending = Falsemeans that response was processed. It is possible for libuv callbacks to invoke as soon as the request was made. In such cases we can avoid switching into the eventloop.
- If the response didn't return immediately, the
resp.waithas set a greenlet where to resume after the request has completed. In that case the call is coming from the eventloop so it needs some handling for exceptions. They are provided by the
I found it necessary that the callback handler is not doing other extra work in the callback handler than forwarding the information.
There's yet one detail unmentioned here that is important for the libuv integration. This:
response = uv_callback.fs(req)
uv_callback.fs is a class. It is derived from a
decorator just like the fs.cb was. Here's the
_immutable_fields_ = ["ec", "handle"]
cb = _callback_
def __init__(self, handle):
self.ec = core.get_ec()
self.handle = handle
self.data = None
self.greenlet = None
self.pending = True
push_handle(getattr(self.ec, uv_name), handle, self)
def wait(self, status=0):
if status < 0:
pop_handle(getattr(self.ec, uv_name), self.handle)
self.greenlet = self.ec.current
# TODO: prepare some mechanic, so that
# we can be interrupted.
.wait() jumps into the eventloop if the request has
not completed when it runs.
There are potentially complex interactions between the requests and the greenlets. For example, greenlets may require locks that prevent them from being switched into when they're waiting for content from an eventloop.
Another question is whether synchronously running requests should be allowed. I will wait for the implementations of such complex concepts until I find out more in practice.
In short, when Lever calls an async libuv function. It stores the current greenlet into a request and switches into the eventloop. Eventually the eventloop calls back as response and switches back into the greenlet that made the request.