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 internal .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 runtime/stdlib/fs.py:

@builtin
@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)
    try:
        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
        return Integer(rffi.r_long(fs_read(
            fileno.value, bufs, L, offset.value)))
    finally:
        lltype.free(bufs, flavor='raw')

The @builtin and @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 fs_read:

def fs_read(fileno, bufs, nbufs, offset):
    req = lltype.malloc(uv.fs_ptr.TO, flavor='raw', zero=True)
    try:
        response = uv_callback.fs(req)
        response.wait(uv.fs_read(response.ec.uv_loop, req,
            fileno, bufs, nbufs, offset,
            uv_callback.fs.cb))
        if req.c_result < 0:
            raise uv_callback.to_error(req.c_result)
        return req.c_result
    finally:
        uv.fs_req_cleanup(req)
        lltype.free(req, flavor='raw')

Here it actually calls uv_fs_read. The C type signature of that function is:

int uv_fs_read(uv_loop_t* loop,
               uv_fs_t* req,
               uv_file file,
               const uv_buf_t bufs[],
               unsigned int nbufs,
               int64_t offset,
               uv_fs_cb cb)

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 uv_callback.fs.cb as a callback.

ec stands for the execution context. It contains the context for the interpreter. The contents are described in the runtime/core.py. Lever starts up directly into the libuv event loop. In identical manner to node.js. The call to uv_run can be found from the runtime/main.py.

The uv_callback.fs.cb is created with a decorator, and the implementation of the callback inside the decorator looks like this:

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.root_switch(ec, [resp.greenlet])

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)
response.wait(...)

The uv_callback.fs is a class. It is derived from a decorator just like the fs.cb was. Here's the implementation:

class response:
    _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)
            raise to_error(status)
        elif self.pending:
            self.greenlet = self.ec.current
            core.switch([self.ec.eventloop])
            # TODO: prepare some mechanic, so that
            #       we can be interrupted.
        return self.data

The .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.

Summary

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.