Recreate Deno from Scratch (#re-deno) #2: Basic concept of V8 JavaScript Engine

Recreate Deno from Scratch (#re-deno) #2: Basic concept of V8 JavaScript Engine

Note: this article compiled and tested on CentOS 7, based on commit 246 on the master branch.

In jsconf2018, Ryan Dahl gave a talk, Design Mistakes in Node, and brought his next generation server-side TypeScript runtime, deno, to the public. At this moment, deno is still a very early stage project. It has no stable API and actually, even a very basic runnable binary is not provided. Re-deno is a project which aims to recreate deno from scratch, and to understand the underlying technology of deno's implementation.

Since deno was created, it has already been refactored and recreated few times. The first version of deno, Dahl used a golang V8 binding to handle the communication between TypeScript and the native API. In the recent commits, Dahl dropped golang to avoid double GC problems and started using a C++ and Rust backend. So, currently the communication between TypeScript and V8 is handled by a C++ and C library, libdeno and the language logic, compiler driver, deno's native APIs are implemented in Rust.

What is V8?

V8 is Google’s open source high-performance JavaScript engine, written in C++. It is used in Google Chrome, the open source browser from Google, and in Node.js, among others. It implements ECMAScript as specified in ECMA-262, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use IA-32, ARM, or MIPS processors. V8 can run standalone, or can be embedded into any C++ application.

NodeJS, Deno and V8

NodeJS is build on top of V8 engine and so as deno. To be more specific, nodejs and deno are the runtime environment, or the host of JavaScript, but the JavaScript code itself will be executed in V8. NodeJS provides lots of native API by binding the JavaScript code to native code. Moreover, browser is also a, and the most famous, JavaScript runtime. Since nodejs and browser provide different sets of native API, any JavaScript code using them will not compatible with each other. In another word, pure JavaScript code will be runnable on any runtimes.

Deno is TypeScript runtime (currently it internally converts to JavaScript excuted by V8). Deno's intention is to expose functionality as simply as possible. There should be little or no "ergonomics" APIs. (For example, deno.readFileSync only deals with ArrayBuffers and does not have an encoding parameter to return strings.) The intention is to make very easy to extend and link in external modules which can then add this functionality.

Deno does not aim to be API compatible with Node in any respect. Deno will export a single flat namespace "deno" under which all core functions are defined. We leave it up to users to wrap Deno's namespace to provide some compatibility with Node.

Basic concepts in V8

Let's start with V8's hello world example, which takes a JavaScript statement as a string argument, executes it as JavaScript code, and prints the result to standard out.

Before we start, let's review some concepts in V8 engine.

  • An isolate is a VM instance with its own heap.
  • In V8, a context is an execution environment that allows separate, unrelated, JavaScript applications to run in a single isolate of V8. You must explicitly specify the context in which you want any JavaScript code to be run.
  • A local handle is a pointer to an object. All V8 objects are accessed using handles. They are necessary because of the way the V8 garbage collector works. A handle provides a reference to a JavaScript object's location in the heap.
  • A handle scope can be thought of as a container for any number of handles. When you've finished with your handles, instead of deleting each one individually you can simply delete their scope.

To be more clear to beginers, an isolate is a process and a context is a thread in opreating system. Isolate provides all required facilities to run a JavaScript code. Two different isolates are totally not related to each other. But creating a new isolate is expensive and some resources are able to share between separate, unrelated, JavaScript applications. Thus, concept is used to differtiate each other. In this case, before executing any JavaScript code, you must explicitly specify the context in which you want any JavaScript code to be run.

handler scope and handler can be considered as a function and a variable. When the scope is released, all handlers inside the scope will be released as well.

So, to achive our goal, we need to

  1. create an isolate
  2. create a context
  3. create a handler scope
  4. create handler inside scope to represent the values
  5. execute the code inside context
  6. get result
  7. clean up

Now let's take a look at the main function in hello world example.

// Create a new Isolate and make it the current one.
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
v8::Isolate* isolate = v8::Isolate::New(create_params);
v8::Isolate::Scope isolate_scope(isolate);
// Create a stack-allocated handle scope.
v8::HandleScope handle_scope(isolate);
// Create a new context.
v8::Local<v8::Context> context = v8::Context::New(isolate);
// Enter the context for compiling and running the hello world script.
v8::Context::Scope context_scope(context);
// Create a string containing the JavaScript source code.
v8::Local<v8::String> source =
    v8::String::NewFromUtf8(isolate, "'Hello' + ', World!'",
                            v8::NewStringType::kNormal)
        .ToLocalChecked();
// Compile the source code.
v8::Local<v8::Script> script =
    v8::Script::Compile(context, source).ToLocalChecked();
// Run the script to get the result.
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
// Convert the result to an UTF8 string and print it.
v8::String::Utf8Value utf8(isolate, result);
printf("%s\n", *utf8);

isolate->Dispose();