In the last article, we extended the simplest Event Loop for the JS engine. But implementing runtimes like this, directly based on the different apis of each operating system, is definitely a chore. Is there a better way to play it? It’s time for Libuv to make its debut.

As we know, Libuv is an asynchronous IO library derived from node.js development process, which enables Event Loop to run on different platforms with high performance. Today, Node.js is the equivalent of V8 and Libuv spliced together as a runtime. But Libuv is also highly versatile and has been used to implement asynchronous non-blocking runtimes in Lua, Julia, and other languages. Next, we’ll show you how to do both with the same simple code:

  • Switch the Event Loop to a Libuv-based implementation
  • Supports macro and micro tasks

By the end of this article, we’ll be able to combine the QuickJS engine with Libuv to achieve a (toy-level) JS runtime with simpler code.

Support libuv Event Loop

Before attempting to combine a JS engine with Libuv, we need to be at least familiar with the basics of libuv. As such, it is a third-party library that follows the same usage described in the previous article:

  1. Compile libuv source code into library files.
  2. Include the corresponding header file in the project, using libuv.
  3. Compile the project, link the libuv library file, and generate the executable file.

How to compile Libuv is unnecessary here, but what does the code that actually uses it look like? The following is a simple example: libuv implements a setInterval in a few lines:

#include <stdio.h>
#include 
       
         // This assumes that libuv is installed globally
       

static void onTimerTick(uv_timer_t *handle) {
  printf("timer tick\n");
}

int main(int argc, char **argv) {
    uv_loop_t *loop = uv_default_loop();
    uv_timer_t timerHandle;
    uv_timer_init(loop, &timerHandle);
    uv_timer_start(&timerHandle, onTimerTick, 0.1000);
    uv_run(loop, UV_RUN_DEFAULT);
    return 0;
}
Copy the code

In order for this code to compile correctly, we need to modify the CMake configuration to include libuv dependencies. The complete cmakelists.txt build configuration is shown below, which is essentially a copycat:

Cmake_minimum_required (VERSION 3.10) Project (Runtime) add_executable(Runtime SRC /main.c)# quickjs
include_directories(/usr/local/include)
add_library(quickjs STATIC IMPORTED)
set_target_properties(quickjs
        PROPERTIES IMPORTED_LOCATION
        "/usr/local/lib/quickjs/libquickjs.a")

# libuv
add_library(libuv STATIC IMPORTED)
set_target_properties(libuv
        PROPERTIES IMPORTED_LOCATION
        "/usr/local/lib/libuv.a")

target_link_libraries(runtime
        libuv
        quickjs)
Copy the code

In this way, both Quickjs. h and uv.h can be included. So, how to further encapsulate the above libuv timer for use by the JS engine? We need to familiarize ourselves with the basic concepts of libuv in the previous code:

  • Callback – The Callback that is triggered when an event occurs, such as the onTimerTick function here. Don’t forget C also supports passing functions as arguments.
  • Handle– Long-standing objects for which callbacks can be registered, such as hereuv_timer_tType timer.
  • Loop– Encapsulates the underlying asynchronous I/O differences. You can add a Handle Event Loop to it, such as hereuv_loop_tLoop variable of type.

So, in a nutshell, the basic usage of libuv is as follows: bind Callback to Handle, bind Handle to Loop, and start Loop. Of course, there are important concepts like Request in Libuv, but I’m not going to digresse here.

With this background, the above sample code is clear:

// ...
int main(int argc, char **argv) {
    // Create a loop object
    uv_loop_t *loop = uv_default_loop();

    // Bind handle to loop
    uv_timer_t timerHandle;
    uv_timer_init(loop, &timerHandle);

    // Bind callback to handle and start timer
    uv_timer_start(&timerHandle, onTimerTick, 0.1000);

    // Start event loop
    uv_run(loop, UV_RUN_DEFAULT);
    return 0;
}
Copy the code

Here the final uv_run is just like the js_std_loop in the previous article, which is an infinite loop that can “hang itself for a long time” inside. Before entering this function, other calls to the Libuv API are very lightweight and return synchronously. So we can assume that if we can call libuv in the same order as in the previous code, and finally start libuv’s Event Loop, then libuv can take over the lower implementation of the runtime.

More specifically, the actual implementation looks like this:

  • Before mounting the native module, initialize libuv’s Loop object.
  • During the initial JS engine eval, every time setTimeout is called, the Handle of a timer is initialized and started.
  • After the initial eval, the Event Loop of Libuv is started. Libuv will trigger the C callback at the appropriate time, and then execute the callback in JS.

The extra thing you need to provide here is the timer’s C callback, which is responsible for executing the due callback in the JS engine context at the appropriate time. In the previous implementation, this is hard-coded logic in js_STd_loop and is not easily extensible. The new function we implement for this purpose is shown below, with the core being a JS_Call line that calls the function object. But beyond that, we need to work with JS_FreeValue to manage object reference counts, otherwise we will have memory leaks:

static void timerCallback(uv_timer_t *handle) {
    // Libuv supports mounting arbitrary data on handle
    MyTimerHandle *th = handle->data;
    // Get the engine context from the handle
    JSContext *ctx = th->ctx;
    JSValue ret;

    // Call the callback, where th->func is ready at setTimeout
    ret = JS_Call(ctx, th->func, JS_UNDEFINED, th->argc, (JSValueConst *) th->argv);

    // Destroy the callback function and its return value
    JS_FreeValue(ctx, ret);
    JS_FreeValue(ctx, th->func);
    th->func = JS_UNDEFINED;

    // Destroy function arguments
    for (int i = 0; i < th->argc; i++) {
        JS_FreeValue(ctx, th->argv[i]);
        th->argv[i] = JS_UNDEFINED;
    }
    th->argc = 0;

    // Destroy the timer returned by setTimeout
    JSValue obj = th->obj;
    th->obj = JS_UNDEFINED;
    JS_FreeValue(ctx, obj);
}
Copy the code

That’s it! This is what the JAVASCRIPT engine should do in the libuv callback when setTimeout is triggered in the Event Loop.

Accordingly, in js_uv_setTimeout, you need to call uv_timer_init and uv_timer_start in sequence, so that the entire process can be strung together as soon as the Event Loop is started at uv_run after eval. This part of the code only needs to make a few minor changes on the previous basis, so I won’t repeat it.

A nice tip is to add polyfill to JS to make sure setTimeout is globally mounted like it is in browsers and Node.js:

import * as uv from "uv"; // All based on libuv, change the name

globalThis.setTimeout = uv.setTimeout;
Copy the code

At this point, setTimeout can run based on libuv’s Event Loop.

Supports macro and micro tasks

As experienced front-end students know, setTimeout is not the only asynchronous source. The famous Promise, for example, can do something similar:

// The log sequence is A, B
Promise.resolve().then((a)= > {
  console.log('B')})console.log('A')
Copy the code

However, if you execute this code based on the runtime we implemented in the previous step, you’ll see that only A is printed and the Promise callback disappears. What’s going on here?

According to the WHATWG specification, each Tick in the standard Event Loop will only execute a Task such as setTimeout. However, during the execution of a Task, it is also possible to encounter multiple tasks that “need to be asynchronous but do not need to be moved to the next Tick to execute”. The typical example is Promise. These tasks are called microtasks and should be performed in this Tick. Accordingly, the unique Task corresponding to each Tick is also called Macrotask, which is the origin of the concept of Macrotask and micro Task.

A Microtask is not a Task but a Framebuffer.

Therefore, the asynchronous execution of a Promise is a microtask that needs to be executed immediately after eval a JS in a Tick. However, in the current implementation, we do not call the JS engine to perform these micro-tasks within a single Tick of Libuv, which is why the Promise callback is missing.

Knowing why, it’s not hard to find a solution to the problem: As long as we can execute a fixed callback at the end of each Tick, we can clear the microtask queue here. In Libuv, it is also possible to register a different Handle at different stages of each Tick to trigger a callback, as follows:

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

The poll phase in the figure above is where the JS engine eval is actually called to perform various JS callbacks. The check phase, which follows this phase, can be used to execute all the microtasks left behind by eval. How to perform a fixed callback in each Tick check phase? Add a Handle of type uv_check_t to the Loop:

// ...
int main(int argc, char **argv) {
    // Create a loop object
    uv_loop_t *loop = uv_default_loop();

    // Bind handle to loop
    uv_check_t *check = calloc(1.sizeof(*check));
    uv_check_init(loop, check);

    // Bind callback to Handle and enable it
    uv_check_start(check, checkCallback);

    // Start event loop
    uv_run(loop, UV_RUN_DEFAULT);
    return 0;
}
Copy the code

This allows the checkCallback to be executed after each poll. The C callback is responsible for emptying the JS engine of microtasks like this:

void checkCallback(uv_check_t *handle) {
    JSContext *ctx = handle->data;
    JSContext *ctx1;
    int err;

    // Execute the microtask until the microtask queue is empty
    for (;;) {
        err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);
        if (err <= 0) {
            if (err < 0)
                js_std_dump_error(ctx1);
            break; }}}Copy the code

That way, the Promise callback will execute smoothly! Doesn’t it look like we have an Event Loop that supports both macro and micro tasks? For the final step, consider the following JS code:

setTimeout((a)= > console.log('B'), 0)

Promise.resolve().then((a)= > console.log('A'))
Copy the code

The macro task of setTimeout should be executed at the end of the next Tick, and the Promise microtask should be executed at the end of the Tick. However, based on the current implementation of the check callback, you will find that the log order is reversed, which is clearly not in compliance with the specification. Why is that?

I’m not the only one making this stupid mistake; Saghul, libuv’s core developer, has also encountered this problem with the Txiki runtime built for QuickJS. However, this Issue of Txiki is both discovered by me and fixed by me. Here is a brief talk about the problem.

Indeed, the microtask queue should be emptied during the Check phase. This is the norm for common cases such as file IO and is implemented in the Node.js source code, but there are exceptions for timer. Let’s revisit the Tick phases in Libuv:

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

Notice that? The timer callback is always executed first, before the check callback. This means that every Tick after eval is finished, the timer callback for setTimeout is performed first, followed by the Promise callback. This leads to execution order problems.

To solve the timer problem, we can do something special: empty the microtask queue in the timer callback. This is equivalent to running JS_ExecutePendingJob’s for loop again in the TIMER’s C callback. For the code implementation, refer to the PR I proposed for Txiki, which also includes test cases for such asynchronous scenarios.

At this point, we have implemented a standard JS runtime Event Loop based on Libuv — although it only supports timers, it is not difficult to extend other capabilities based on Libuv. Txiki is also a good place to start if you’re interested in how to plug more libuv power into a JS engine.

Question to consider: Can the microtask queue support adjusting the limit of the number of tasks executed in a single session? Can it be adjusted dynamically at run time? If so, how to construct the corresponding JS test case?

The resources

Finally, here are some of the main references for learning about Libuv and Event Loop:

  • Libuv Design Overview
  • The Task Queue specification
  • Microtask/Macrotask difference

This is my Minimal JS Runtime project. It compiles without modifying QuickJS and Libuv upstream code. Welcome to try it. See README for the QuickJS native Event Loop integration example from the previous article.

Afterword.

Perhaps the special Spring Festival of 2020 is the only one where people can seriously study technology and serialize columns at home. I thought the most difficult part in the whole article was finished in a small village in Putian on the evening of New Year’s Eve, which was a special experience.

For several years after graduation, my job has been to write JS. This time from JS to write a point C, in fact, there is nothing particularly difficult, but some inconvenient, probably equivalent to the smart phone into Nokia bar… After all, they are tools designed for people in different times, so don’t worry too much about them. After all, real bulls can write the C to perfection, and for me, there’s still a long way to go.

This article is obviously far from in-depth due to its limitations (how to integrate the debugger, how to support workers, how to interact with native render threads…). . But if you are interested in implementing the JS runtime, this article should be a good enough guide to get started. And I believe that this route will open up a new possibility for the front end students: with a small amount of C/C++ and modern JavaScript, you can take the traditional Web stack out of the browser and embed JavaScript like Lua. What other interesting things can be done along this route? Stay tuned