Creating Your Own Js Runtime
Creating Your Own Js Runtime
448-93 - HP152416819129750
ABOUT THE AUTHOR
Erick Wendel is an active Node.js core team developer,
Keynote Speaker, and professional educator. He has given over 100
tech talks in more than 10 different countries worldwide. He was
awarded as a Node.js Specialist with the Google Developer Expert,
Microsoft MVP, and GitHub Stars awards. Erick Wendel has
trained more than 100K people around the world in his own
This is the first and only content on the Internet (as of the
time of this writing) that shows how to create a JavaScript Runtime
from scratch, using V8, libuv, and more.
6 Conclusion 52
Version: 27.9.18
CHAPTER 1
INTRODUCTION
1 INTRODUCTION 1
1.1 THE SEA OF RUNTIMES
You have probably noticed that there are a lot of JavaScript
runtimes being created and launched to the world nowadays. Tools
such as Deno, Bun, and Cloudflare Workers all came up in a short
period of time, this wasn't like this in the past.
This code snippet that I got from their public repo on GitHub
deno_core::v8_set_flags(env::args().collect());
But in the end, can you guess what it does? Sends JS code to a
JS runtime, this time it's the JavaScriptCore engine, also known as
SquirrelFish. This is pretty evident on the runtime calls to import
JSCore modules:
#include "JavaScriptCore/JavaScript.h"
#include "wtf/FileSystem.h"
#include "wtf/MemoryFootprint.h"
Imagine that there are two different cars with the same engine.
The way they're designed to resist the air, the weight, and the shape
Bun uses two key components that make them claim they're
faster than any other runtime currently in the market.
UNDERSTANDING THE
MAIN COMPONENTS
USED IN NODE.JS
printf("%s", *str);
}
Don't believe in me? look at this code from Node v0.12 which
implements the fs module:
NODE_SET_METHOD(target, "access", Access);
NODE_SET_METHOD(target, "close", Close);
NODE_SET_METHOD(target, "open", Open);
NODE_SET_METHOD(target, "read", Read);
NODE_SET_METHOD(target, "fdatasync", Fdatasync);
This is how other modules like crypto, HTTP, net, and child
process are also built. They're just C++ functions that extend V8's
default behavior. And this is also how Bun and Deno can
implement those functions differently and claim to be faster than
Node.js.
Async Functions
Timers are async functions that run in the background and call
back the main context when they're done. Every time we run
setTimeout in JavaScript, it's Libuv executing that code in the
background and calling the provided callback. This is similar to
how a game loop functions in games, an endless while loop that
keeps asking if there are new events in a game and calls the
provided functions back when finishing executing.
Every time a task has finished it'll send a message back to the
event loop and the event loop will call its callback and remove the
The last part of the Node.js system is what I call the C++ layer.
The C++ Layer is the mediator between the JavaScript code you
Click the link, log in to your account, and you are ready to go!
[nodemon] 2.0.21
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: cc,h,js,cpp,hpp
[nodemon] starting `make && ./bin/capivara index.js`
mkdir -p bin
Hello World
[nodemon] clean exit - waiting for changes before restart
#include <uv.h>
uv_loop_t *loop;
uv_timer_t gc_req;
uv_timer_t fake_job_req;
void callbackFn() {
printf("callback executed!\n");
}
struct timer {
uv_timer_t req;
std::string text;
functionTemplate *callback;
};
printf("%s", timerWrap->text.c_str());
}
int main() {
loop = uv_default_loop();
for (size_t i = 0; i < 10; i++) {
timer *timerWrap = new timer();
timerWrap->callback = (functionTemplate *)callbackFn;
timerWrap->text = "hello\n";
The main function will set up the libuv event loop and
schedule 10 functions that will be executed in the future.
Run make uv-timers you'll see the code with the output.
callback executed!
hello
callback executed!
hello
callback executed!
You can also see where headers are being imported on the
makefile file:
define INCLUDE
v8/include/
endef
define INCLUDEUV
libuv/include/
endef
Notice that I'm not a C++ developer. So you may find C++
bad practices in the following sections.
I put those functions there just to make our life easier and
avoid replicating code everywhere.
#include "v8.h"
#include "./src/capivara.hpp"
int main(int argc, char *argv[]) {
char *filename = argv[1];
auto *capivara = new Capivara();
std::unique_ptr<v8::Platform> platform =
capivara->initializeV8(argc, argv);
capivara->initializeVM();
capivara->InitializeProgram(filename);
capivara->Shutdown();
return 0;
This code will grab the filename passed by the CLI (eg.
./bin/capivara index.js ) and initialize the project.
WaitForEvents();
}
}
class Capivara {
private:
v8::Isolate *isolate;
v8::Local<v8::Context> context;
std::unique_ptr<v8::Platform> *platform;
v8::Isolate::CreateParams create_params;
void WaitForEvents() {
uv_run(DEFAULT_LOOP, UV_RUN_DEFAULT);
}
return 0;
}
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();
this->platform = &platform;
return platform;
}
ExecuteScriptAndWaitForEvents(filename);
}
If you check your console after the execution, you'll see that
setTimeout is not a defined function, which means it's not a part
of JavaScript and we should implement it.
index.js:0: Uncaught ReferenceError: setTimeout is not defined
#
# Fatal error in v8::ToLocalChecked
# Empty MaybeLocal
#
Trace/breakpoint trap
[nodemon] app crashed - waiting for file changes before starting.
..
// timer.hpp
#include <v8.h>
#include <uv.h>
uv_loop_t *loop;
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
}
// capivara.hpp
#include "./fs.hpp"
#include "./util.hpp"
// ...omitted
// 2 - Initialize timer
Timer timer;
timer.Initialize(DEFAULT_LOOP);
this->context = v8::Context::New(
this->isolate,
NULL,
global
);
ExecuteScriptAndWaitForEvents(filename);
}
Now, if you execute our file again, you won't get any other
errors, as we already have defined the timeout function. What's
uv_loop_t *loop;
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
}
Then you need to get the parameters. The first two parameters
will be integers, so let's print them:
// timer.hpp
#include <v8.h>
#include <uv.h>
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
}
// 2 - interval param
int64_t interval = args[1]->IntegerValue(context)
.ToChecked();
uv_loop_t *loop;
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
}
If you run that, you'll see that the code is printing the string of
the function, which means this is working.
sleep 200, interval 0
() => {
print(`1 ${new Date().toISOString()}`)
}
uv_loop_t *loop;
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
// 1 - here
static void onTimerCallback(uv_timer_t *handle) {
printf("Hey I was called!");
}
};
uv_loop_t *loop;
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
}
// 1 - added here
uv_timer_init(loop);
}
uv_loop_t *loop;
class Timer {
public:
uv_loop_t *loop;
struct timer {
uv_timer_t uvTimer;
class Timer {
// omitted
};
You'll now
uv_loop_t *loop;
struct timer {
uv_timer_t uvTimer;
v8::Isolate *isolate;
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
}
if(!callback->IsFunction()) {
printf("Callback is not a function");
return;
}
uv_timer_init(loop, &timerWrap->uvTimer);
uv_timer_start(
&timerWrap->uvTimer,
onTimerCallback,
sleep,
interval
);
When the timeout finishes you'll need to access data from that
specific request that was stored on the struct within the
onTimerCallback function.
if (isolate->IsDead()) {
printf("isolate is dead");
return;
v8::Local<v8::Function> callback =
v8::Local<v8::Function>::New(
isolate,
timerWrap->callback
);
}
if (isolate->IsDead()) {
printf("isolate is dead");
return;
}
v8::Local<v8::Function> callback =
v8::Local<v8::Function>::New(
isolate,
timerWrap->callback
);
if (callback->Call(
context,
v8::Undefined(isolate),
2,
resultr
).ToLocal(&result)) {
In case you missed something, below you'll find the full code
for the timer.hpp class.
// timer.hpp
#include <v8.h>
#include <uv.h>
uv_loop_t *loop;
struct timer {
uv_timer_t uvTimer;
v8::Isolate *isolate;
v8::Global<v8::Function> callback;
};
class Timer {
public:
static void Initialize(uv_loop_t *evloop) {
loop = evloop;
}
if(!callback->IsFunction()) {
printf("Callback is not a function");
return;
}
uv_timer_init(loop, &timerWrap->uvTimer);
uv_timer_start(
&timerWrap->uvTimer,
onTimerCallback,
sleep,
interval
);
v8::Local<v8::Function> callback =
v8::Local<v8::Function>::New(
isolate,
timerWrap->callback
);
v8::Local<v8::Value> result;
v8::Handle<v8::Value> resultr [] = {
v8::Undefined(isolate),
if (callback->Call(
context,
v8::Undefined(isolate),
2,
resultr).ToLocal(&result)) {
// callback is a success
} else {
// failed
}
}
};
That's breathtaking, isn't it? I've spent weeks just to make the
onTimerCallback get the callback instance and properly call it.
// index.js
let interval = 0
let sleep = 200
;(async function () {
print(new Date().toISOString(), 'waiting a bit')
await setTimeoutAsync(1000)
print(new Date().toISOString(), 'waiting a bit')
await setTimeoutAsync(1000)
print(new Date().toISOString(), 'finished')
})()
GOING FURTHER -
READING FILES USING
LIBUV (CHALLENGE)
Let's say that you paste the content into the uv-threads.cpp .
Then run make uv-threads and you'll see an error of invalid
argument but don't worry.
Are you ready? After you finish this challenge, make a post on
your social media mentioning that you were able to do it and
mention my profile there (I'm @ErickWendel or @ErickWendel_)
in the networks so I can get to know that you're done.
CONCLUSION
Check out the full video that was used as a reference to build
this content. There I show even more concepts and examples for
you to go deeper into the JavaScript runtimes world.
Thank you for your time and for learning something new with
me. I'm Erick Wendel and until next time!
52 6 CONCLUSION