DEV Community

Cover image for #2 Daily Rabbit Holes: Diving Deeper into Rust, V8, and the JavaScript™️ Saga
pul
pul

Posted on • Originally published at pulbasso.dev

#2 Daily Rabbit Holes: Diving Deeper into Rust, V8, and the JavaScript™️ Saga

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);
}
Enter fullscreen mode Exit fullscreen mode
// 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!(),
    };
}
Enter fullscreen mode Exit fullscreen mode

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)