Unfortunately, today I had very little time to dive into interesting topics. I spent some time on a side project (which I’ll probably write about in the coming days), worked on my day job, and did some sports. Still, my focus remains on Deno, Rust, and the V8 engine, just like in my previous article.
I managed to review the fantastic article by Matouš Dzivjak, where he explains how to build a runtime using Rust V8.
The code from the article is a bit outdated and doesn’t run with the current version of Rust. I managed to get it working somehow, even though I don’t actually know Rust yet (so it’s either an easy task or I just got really lucky).
Below is the updated code with minor tweaks and some comments to help with understanding. All credits, of course, go to Matouš.
Quick Breakdown
This code demonstrates how to build a minimal JavaScript runtime using Rust and the V8 engine:
Platform and V8 Initialization: The main.rs file initializes the V8 engine and platform, preparing it to execute JavaScript code.
Runtime Setup: A global object (globalThis.workerHandler) is exposed in the runtime using JavaScript, enabling a Rust function to be callable from JavaScript.
Script Compilation: The build_worker function compiles and evaluates the JavaScript code, combining the runtime script and a worker_script that defines a handler function.
Global Context Binding: Rust functions, such as sayHello, are exposed to the V8 runtime by binding them to global object properties.
Function Execution: The run_worker function calls the JavaScript handler function from Rust, passing parameters and printing the result (Hello World) to the console.
// runtime.js
globalThis.workerHandler = (x) => {
return handler(x);
}
// main.rs
use v8;
fn main() {
// Platform and V8 initialization
let platform = v8::Platform::new(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
// `include_str!` is a Rust macro that loads a file and converts it into a Rust string
let runtime = include_str!("runtime.js");
let worker_script = r#"
export function handler(y) {
return sayHello(y);
};
"#;
// The runtime.js file exposes the `handler` function as a global object
let script = format!(
r#"
{runtime}
{worker_script}
"#
);
{
// Create a V8 isolate with default parameters
let mut isolate = v8::Isolate::new(v8::CreateParams::default());
let global = setup_runtime(&mut isolate);
let worker_scope = &mut v8::HandleScope::with_context(isolate.as_mut(), global.clone());
let handler = build_worker(script.as_str(), worker_scope, &global);
run_worker(handler, worker_scope, &global);
}
unsafe {
v8::V8::dispose();
}
v8::V8::dispose_platform();
}
// Set up the global runtime context
fn setup_runtime(isolate: &mut v8::OwnedIsolate) -> v8::Global<v8::Context> {
// Create a handle scope for all isolate handles
let isolate_scope = &mut v8::HandleScope::new(isolate);
// ObjectTemplate is used to create objects inside the isolate
let globals = v8::ObjectTemplate::new(isolate_scope);
// The function name to bind to the Rust implementation
let resource_name = v8::String::new(isolate_scope, "sayHello").unwrap().into();
// Expose the function to the global object
globals.set(
resource_name,
v8::FunctionTemplate::new(isolate_scope, say_hello_binding).into()
);
// Create a context for isolate execution
let context_options = v8::ContextOptions {
global_template: Some(globals),
..Default::default()
};
let global_context = v8::Context::new(isolate_scope, context_options);
// Create and return the global context
v8::Global::new(isolate_scope, global_context)
}
// Define the Rust binding for the sayHello function
pub fn say_hello_binding(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue,
) {
let to = args.get(0).to_rust_string_lossy(scope);
let hello = v8::String::new(scope, format!("Hello {}", to).as_str())
.unwrap().into();
retval.set(hello);
}
// Build the worker by compiling and instantiating the script
fn build_worker(
script: &str,
worker_scope: &mut v8::HandleScope,
global: &v8::Global<v8::Context>,
) -> v8::Global<v8::Function> {
let code = v8::String::new(worker_scope, script).unwrap();
let resource_name = v8::String::new(worker_scope, "script.js").unwrap().into();
// The source map is optional and used for debugging purposes
let source_map_url: Option<v8::Local<'_, v8::Value>> = Some(v8::String::new(worker_scope, "placeholder").unwrap().into());
let mut source = v8::script_compiler::Source::new(
code,
Some(&v8::ScriptOrigin::new(
worker_scope,
resource_name,
0,
0,
false,
i32::from(0),
source_map_url,
false,
false,
true,
None
)),
);
// Compile and evaluate the module
let module = v8::script_compiler::compile_module(worker_scope, &mut source).unwrap();
let _ = module.instantiate_module(worker_scope, |_, _, _, _| None);
let _ = module.evaluate(worker_scope);
// open a global scope associated to the worker_scope
let global = global.open(worker_scope);
// create and assign the handler to the global context
let global = global.global(worker_scope);
let handler_key = v8::String::new(worker_scope, "workerHandler").unwrap();
let js_handler = global.get(worker_scope, handler_key.into()).unwrap();
let local_handler = v8::Local::<v8::Function>::try_from(js_handler).unwrap();
v8::Global::new(worker_scope, local_handler)
}
// Run the worker and execute the `handler` function
pub fn run_worker(
worker: v8::Global<v8::Function>,
scope: &mut v8::HandleScope,
global: &v8::Global<v8::Context>,
) {
let handler = worker.open(scope);
let global = global.open(scope);
let global = global.global(scope);
let param = v8::String::new(scope, "World").unwrap().into();
// call the handler and get the result
match handler.call(scope, global.into(), &[param]) {
Some(response) => {
let result = v8::Local::<v8::String>::try_from(response)
.expect("Handler did not return a string");
let result = result.to_string(scope).unwrap();
println!("{}", result.to_rust_string_lossy(scope));
}
None => todo!(),
};
}
I strongly encourage you to read Matouš Dzivjak’s excellent article for a deeper dive into the code and its concepts. This was just a summary—his detailed explanations are essential for understanding the technical nuances!
JavaScript Petition!
By the way, I had completely forgotten that Oracle owns the JavaScript trademark—something I already knew but slipped my mind. This week, Deno’s petition to cancel the trademark served as a reminder of this curious fact.
If you're interested in learning more, check out:
Top comments (0)