Roll you own JavaScript runtime
From Author:
In this talk, we’ll create a small JavaScript runtime from scratch in Rust. We’ll leverage the same ecosystem of components that Deno uses to show that rolling a bespoke runtime fitting your needs is simple and fun in 2023
This talk has been presented at Node Congress 2023, check out the latest edition of this JavaScript Conference.
FAQ
Creating a custom JavaScript runtime can be beneficial for running untrusted code in a more controlled environment, reducing resource usage by using a subset of APIs, and shipping a self-contained single binary tailored to specific needs.
Crates in Deno are equivalent to npm packages and are self-contained, reusable libraries. Deno CLI is a collection of these crates, which allows for modular and efficient code management. This architecture also enables creating multiple runtimes like Deno CLI and Deno Deploy.
Deno provides a formatter, test runner, linter, and the ability to create self-contained binaries. It supports various web APIs like Fetch, TextEncoder, TextDecoder, Blob, WebStreams API, and WebWorker API.
Deno ships two different runtimes: Deno CLI, which is geared towards local development and deploying code on personal servers or Lambdas, and Deno Deploy, which is geared towards cloud-native containers.
To implement a custom setTimeout function in a JavaScript runtime, you need to define the function in JavaScript, call an asynchronous Rust function to handle the delay, and register this function in the runtime's extension. This involves minimal code in both JavaScript and Rust.
Deno and Node.js both use the same JavaScript engine, but Deno is built using Rust for its native code, while Node.js uses C++. Deno also comes with built-in tools and supports modern web APIs extensively.
Deno was initially written in Go, but it was rewritten in Rust because Go has garbage collection similar to JavaScript, which could cause conflicts. Rust, being a system-level language without a garbage collector, allows for more efficient resource management.
Run.js is a small custom JavaScript runtime that supports ES modules, transpiles JavaScript to TypeScript on the fly, and includes basic APIs for file operations, console logging, and a rudimentary fetch API. It demonstrates how to build a custom runtime with minimal code.
Bartek is a member of the Deno team. He started contributing to Deno in 2018, became an employee in late 2019, and currently leads the Deno CLI team.
Deno is a JavaScript and TypeScript runtime that aims to be fun and productive to use. It supports importing source codes from local file systems, remote servers using HTTP/HTTPS, and NPM imports. Deno can transpile and type-check TypeScript sources, and it includes tools like a formatter, test runner, linter, and the capability to create self-contained binaries.
Video Transcription
1. Introduction to Deno and Custom JavaScript Runtime
Hi, folks. I'm Bartek from the Deno team. We'll talk about creating a custom JavaScript runtime, based on our experience with Run.js. Deano is a JavaScript and TypeScript runtime, similar to Node.js. It supports importing source codes, transpiling TypeScript, and has a lot of tooling. Deano also provides support for many web APIs.
Hi, folks. My name is Bartek, and I'm from the Deno team, and welcome to your own JavaScript runtime. Shortly about me. I started contributing to Deno in 2018, and then I've become the employee of the company in late 2019, and I've been with the company ever since. I'm currently leading the Deno CLI team, and should you have any questions or follow-ups, feel free to reach out with one of the links provided here.
So, in this talk, we're going to talk a bit how to create or roll your own custom JavaScript runtime. We're not going to be doing this from scratch. This talk is based on a couple of blog posts we did in the past. We created a small JavaScript runtime called Run.js, and you can follow the links and the recording here if you want to learn a bit more about the history of the project. Well, in our opinion, it's quite easy to get started if you wanted to create your own JavaScript runtime. Thanks to the technologies we can now use, which are the engine for executing JavaScript, but also some of the infrastructure the Deano team provides. So, without a further ado, let's jump right into it.
But before we do that, we need to learn a bit more about Deano itself. So, Deano is a JavaScript and TypeScript runtime. It aims to be fun and productive to use. Deano supports importing source codes from a file system, from remote servers, using HTTP or HTTPS protocol, and we also support NPM imports using these NPM column specifiers. Deano is a TypeScript runtime in a sense that it's not needed to explicitly build your TypeScript sources to JavaScript before executing them. Deano can take care of that for you. It can transpile the sources, but it also can type check them. And Deano is built using Rust and using the JavaScript engine, so in one sense it's very similar to Node.js because we both use the same JavaScript engine. However, Deano uses Rust for its native code while Node.js uses C++.
Deano comes with batteries included. We got a lot of tooling that allows you to get up and running in seconds. You got Formatter, TestRunner, Linter. You can even create a self-contained binary, that is you'll package all of the source code you provide and then give you a single executable file. It's kind of similar to the PKG MPM package you can use. Deano is also very heavily invested in web APIs. We've got a lot of them, most notably Fetch, TextEncoder, TextDecoder, Blob. We got support for WebStreams API as well as WebWorker API. There's many, many more of these APIs and I strongly suggest you visit the MDM page for the API you're interested in and see if Deano supports the API.
2. Deano in Rust: Building Custom Runtimes
Deano in Rust was a significant decision for the project, as it allowed for quick development and leveraged a vast ecosystem of high-quality crates. Building Deano is easy with Cargo integration, and pre-built binary archives save time. Rust's crate system enables modular code organization, and Deano offers two runtimes: CLI for local development and Deano deploy for cloud deployment. Users can mix and match crates and APIs to create custom runtimes. The run.js example runtime already supports ES modules, transpilation, file I/O, console APIs, and a basic fetch API.
Deano in Rust is, well, we need to understand a bit of history here in that the first iteration of Deano, the first prototype was not really written in Rust. Instead, it was written in Go. However, due to the nature of Go that is garbage collected just like JavaScript, it was decided that we should go away from Go so we don't run into a situation where two garbage collectors are contesting against each other.
So Deano was written in Rust, which is a low level language, or system's language that doesn't have a garbage collector. And I can say that this is probably the most influential decision ever made for the Deano project. Rust allows us to move very quickly and it allows us to leverage a huge ecosystem of the crates available. Most of the crates are very high quality, for example, an http crate that implements the http protocol is very well tested and it has a great coverage, so we're very confident using it that we will be adhering to the defined protocol.
And last but not least, building Deano is really a breeze. Since Deano is fully integrated with Cargo, which is Rust's build tool or build system, it's just one command to run that will build your whole Deano binary, including V8, but not really. We don't force you to build V8 yourself, instead we do that in our CI pipeline so that you can use already pre-built binary archives, which saves you a lot of hassle, since V8 is a huge project that takes quite a long time to compile. Another thing is that Deano is not a monolith. Rust provides you with an easy way to factor out pieces of your code to so-called crates. Crates are the equivalent of npm packages so these are self-contained and reusable libraries that you can download and use whenever you want and Deano, the CLI that you can download, is actually a collection of these crates that is intertwined together and they are made to work together very well. So one of the consequences is that we actually, at Deano here at Deano company, we don't actually ship just one runtime. We actually ship two different runtimes. We have the Deano CLI but we also have Deano deploy which is our cloud offering. These runtimes are very similar however they are not identical. Deano CLI is geared towards local development and deploying your code on your own server or Lambdas while Deano deploy is geared towards being deployed to cloud native containers and such. So as a consequence you as a user can choose which crates and APIs you need and then you can mix and match them to roll your own runtime that fits your needs. So back to our runjs example runtime. What can it do right now? Well, it's just 140 lines of Rust and 37 lines of JavaScript so that's not very, very much code but it already has support for ES modules. It can transpile JavaScript to typescript on the fly just before executions similar to how Deno can do it. We got an API to read a file, an API to write a file, and delete a file. We got some rudimentary console APIs so we can do console.log and console.error and we have a very basic fetch API, so it looks like fetch but it can only really do get requests. However, with only 180 lines of both Rust and JavaScript, I think this is pretty phenomenal. So let me give you a super quick demo of the run.js runtime. So I am in the repository that was already linked to you and we can do cargo.run dash dash example.ts. So we'll be executing a typescript file right here and we got hello run.js as well content from fetch. So if we decide to inspect our example.ts file, we can see that it uses console.log, then we got an interface. So that's definitely typescript.
Comments