Getting Started

Crates.io Build Status docs.rs Static Badge License

Rustyscript - Effortless JS Integration for Rust

rustyscript provides a quick and simple way to integrate a runtime javascript or typescript component from within Rust.

It uses the v8 engine through the deno_core crate, and aims to be as simple as possible to use without sacrificing flexibility or performance.
I also have attempted to abstract away the v8 engine details so you can for the most part operate directly on rust types.

Core Principles

Sandboxed

By default, the code being run is entirely sandboxed from the host, having no filesystem or network access. extensions can be added to grant additional capabilities that may violate sandboxing

Flexible

The runtime is designed to be as flexible as possible, allowing you to modify capabilities, the module loader, and more.

  • Asynchronous JS is fully supported, and the runtime can be configured to run in a multithreaded environment.
  • Typescript is supported, and will be transpired into JS for execution.
  • Node JS is supported experimentally, but is not yet fully compatible See the NodeJS Compatibility section

Extension Features

The crate includes the following features that can be turned on or off as needed:
For a full list of available extensions, see the extensions section.

safe_extensions ON BY DEFAULT
console crypto url web_stub
Deno extensions that maintain a secure, sandboxed runtime environment

io_extensions
web cache cron ffi fs io kv webgpu
Deno extensions that grant runtimes access to the file system (but may also grant some level of network access - use caution)

network_extensions
broadcast_channel http web websocket webstorage
Deno extensions that grant runtimes access to system network resources

node_experimental
safe_extensions io_extensions network_extensions
Experimental NodeJS compatibility

Javascript Isolation Features

url_import
Enables executed Javascript to include modules from arbitrary URLs

fs_import
Enables executed Javascript to include modules from the file system, without needing to load them from rust first

Additional Features

worker ON BY DEFAULT
Enables the multithreaded worker API

snapshot_builder
Enables the snapshot_builder API

Getting Started

Hello World

Here is a very basic use of this crate to execute a JS module. It will:

  • Create a basic runtime
  • Load a javascript module,
  • Call a function and the resulting value
use rustyscript::{Runtime, Module};

fn main() -> Result<(), rustyscript::Error> {
    let module = Module::new(
        "test.js",
        "
        export default (string) => {
            console.log(`Hello world: string=${string}`);
            return 2;
        }
        "
    );

    let value: usize = Runtime::execute_module(
        &module, vec![],
        Default::default(),
        &("test"),
    )?;

    assert_eq!(value, 2);
    Ok(())
}

Modules can also be loaded from the filesystem with Module::load or Module::load_dir
Or included statically with the module! and include_module! macros.


Here is a more detailed version example above, which breaks down the steps instead of using the one-liner Runtime::execute_module:

use rustyscript::{Runtime, RuntimeOptions, Module, Undefined};
use std::time::Duration;

fn main() -> Result<(), rustyscript::Error> {
    let module = Module::new(
        "test.js",
        "
        let internalValue = 0;
        export const load = (value) => internalValue = value;
        export const getValue = () => internalValue;
        "
    );

    // Create a new runtime
    let mut runtime = Runtime::new(RuntimeOptions {
        timeout: Duration::from_millis(50), // Stop execution by force after 50ms
        default_entrypoint: Some("load".to_string()), // Run this as the entrypoint function if none is registered
        ..Default::default()
    })?;

    // The handle returned is used to get exported functions and values from that module.
    // We then call the entrypoint function, but do not need a return value.
    // Load can be called multiple times, and modules can import other loaded modules
    // Using `import './filename.js'`
    let module_handle = runtime.load_module(&module)?;
    runtime.call_entrypoint::<Undefined>(&module_handle, &(2))?;

    // Functions don't need to be the entrypoint to be callable!
    let _internal_value: i64 = runtime.call_function(Some(&module_handle), "getValue", &())?;
    Ok(())
}

Single Expression Evaluation

If all you need is the result of a single javascript expression, you can use:

fn main() {
    let result: i64 = rustyscript::evaluate("5 + 5").expect("The expression was invalid!");
    assert_eq!(result, 10);
}

Or, if you just need to import one Javascript module for use in rust:

use rustyscript::{import};

fn main() {
    let mut module = import("js/my_module.js").expect("Something went wrong!");
    let value: String = module.call("exported_function_name", &()).expect("Could not get a value!");
    println!("{value}");
}

There are a few other utilities included, such as validate and resolve_path


Getting Started

Calling Functions

You may have noticed the &() in the previous chapter's example - this is because the function getValue does not take any arguments.

Arguments are normally passed as references to tuples, for example:
&("test", 1), &("test"), or &()

They can be of any combination of types that implement serde::Serialize

Arguments can also be references to sized values, such as:
&"test".to_string() or &1

Important Note: Up to 16 arguments can be passed in this way
If you need more than 16 arguments, you can use big_json_args!

Please note: This macro is significantly slower — benchmark tests show it can be nearly 1,000 times slower than using a smaller argument set. Use sparingly to avoid performance bottlenecks.

Getting Started

Errors

The crate includes an error type that can catch any errors that can occur during runtime execution.

A few important variants include:

Error::JsError
This is the most common error you will see, and is thrown by the JS runtime. It contains the error message and the stack trace.

Error::JsonDecode
You will see this error if you try to decode a return value to an incompatible type.

Error::Timeout
This error is thrown when runtime execution takes longer than the runtime's configured timeout.

Error::ValueNotCallable
This error is thrown when you try to call a value that is not a function.

Error::ValueNotFound
This error is thrown when you try to access a value that does not exist.


You can get JS-side errors as a code-highlighted string by calling error.as_highlighted()

You will get a string of the form:

| let x = 1 + 2  
|       ^  
= Unexpected token '='  

You can customize this output to include the filename, line or column numbers.

Using JavaScript Types in Rust

Many functions on a Runtime are generic over a type parameter T, used to specify the expected return type from the executed javascript code.

Here is a simple example of calling the same function with different return types:

use rustyscript::{Runtime, Error};

fn main() -> Result<(), Error> {
    let mut runtime = Runtime::new(Default::default())?;

    let number: i32 = runtime.eval("1 + 1")?;
    let string: String = runtime.eval("1 + 1")?;
    let float: f64 = runtime.eval("1 + 1")?;

    println!("Number: {}", number);
    println!("String: {}", string);
    println!("Float: {}", float);

    Ok(())
}

If you don't care about the type of value being returned you can simply use ():

For example, to call a function with side-effects, where no value is returned, you would do:
runtime.eval::<()>("console.log('Hello, World!')");

Alternatively, if you want the value but do not care about the type, try js_value::Value

Special Types

The js_value module defines a number of special types that can be used, which map more-or-less directly to JavaScript types.

It is important to note that all of these cannot outlive their runtime's, or be used on other runtimes

js_value::Function

A Deserializable javascript function, that can be stored and used later.

Can be called with Function::call. async and immediate variants exist

See Async JavaScript for details on async and immediate.

js_value::Promise<T>

A stored javascript promise, that can be resolved or rejected later.

You can turn Promise<T> into Future<Output = T> by calling Promise::into_future This allows you to export multiple concurrent promises without borrowing the runtime mutably

You can also use Promise::into_value to block until the promise is resolved, and get the value.

js_value::Map

Read-only instance of a javascript Object. Allows conversion into a HashMap (Skips any non-utf8 keys).

This type can be faster for large objects than directly deserializing into a rust type.

js_value::String

A Javascript UTF-16 string, used to preserve data which can be lost converting to a Rust String.

js_value::Value

A generic type able to represent any JavaScript value. This mimics the behavior of the any type in TypeScript.

The primary use-case is to defer the normal type-decoding if the type is not known right away. This is done with Value::try_into, which must be called with the same runtime as the value was created with.

It can also be used to directly get the underlying deno value, using Value::into_v8.

Getting Started

On Modules and Including them

By default, any javascript code can only import modules that have already been loaded with Runtime::load_module. This is a security feature to prevent arbitrary code from being loaded from the filesystem or network.

However, this can be changed with the fs_import and url_import features. These features allow the runtime to load modules from the filesystem or network respectively.

URL Schemes

Custom URL Schemes can be added to a runtime with the schema_whlist field in the RuntimeOptions struct.

Entrypoints

When a module is loaded, the runtime will look for a function with the name provided in the default_entrypoint field of the RuntimeOptions struct, or which is exported as default in the module.

Additionally, you can call rustyscript.register_entrypoint from JS to register a function as an entrypoint at runtime

Runtime::call_entrypoint will call the entrypoint function with the provided arguments.

Example 1
Example 2

ImportProvider Trait

The ImportProvider trait can be implemented to provide custom module loading behavior

  • It can be used to implement a cache: Example
  • Or to provide custom import logic: Example

Getting Started

The Sandbox

One of the guiding principles of rustyscript is to provide a safe sandboxed environment for running JavaScript code by default.

It should not be possible to access the host system's resources such as the file system, network, or environment variables through JS in the default runtime configuration

Only the safe_extensions, worker, and snapshot_builder features can be enabled without breaking the sandbox.

With the default configuration and crate features, sandboxing is enforced by the following mechanisms:

  • Op safety - All the ops provided by default have been vetted and whitelisted to ensure they are safe
  • Import isolation - The module loader will by default only allow modules that have been loaded with Runtime::load_module
    • A couple of crate features can change this:
      • fs_import will allow loading modules from the filesystem
      • url_import will allow loading modules from network location
  • Extension limiting - Only a subset of extensions are enabled by default, using a safe stub of the deno_web API
    • See the extensions section for more information on the available optional extensions

Note: Extension is a Deno term referring to a subset of the JS standard API. rustyscript provides these as crate features that can be enabled or disabled at compile time.

Getting Started

Runtime Options

To create a runtime, you will need to provide a RuntimeOptions struct. This struct contains all the configuration options for the runtime, such as the extensions to load, the entrypoint to use, and the maximum heap size.

It implements the Default trait, so you can create a default configuration by calling RuntimeOptions::default().

RuntimeOptions has the following fields:

extensions

A set of deno_core extensions to add to the runtime
See Custom Extensions

extension_options

Additional options for the built-in extensions
See Extension Options

default_entrypoint

Function name to use as entrypoint if the module does not explicitely provide one
An entrypoint is a function to be pre-registered for calling from Rust on module load See On Modules and import

timeout

Amount of time to run async tasks before failing.
Only affects blocking functions

max_heap_size

Maximum heap size for the runtime. The runtime will fail gracefully if this limit is reached

import_provider

Optional import provider for the module loader
Acts as cache, and handler for custom schema prefixes and data sources
See On Modules and import

startup_snapshot

Optional snapshot to load into the runtime
This will reduce load times, but come with some limitations
See Snapshots

isolate_params

Optional configuration parameters for building the underlying v8 isolate
This can be used to alter the behavior of the runtime
See v8::CreateParams for more information

shared_array_buffer_store

Optional shared array buffer store to use for the runtime
Allows data-sharing between runtimes across threads

schema_whlist

A whitelist of custom schema prefixes that are allowed to be loaded from javascript
By default only http/https (url_import crate feature), and file (fs_import crate feature) are allowed
See On Modules and import

Getting Started

Back to Runtime Options

Extension Options

The built-in deno extensions have additional options that can be configured when creating a runtime. These options are provided in theExtensionOptions struct, which is a field of the RuntimeOptions struct.

The fields it contains depend on the features enabled in the rustyscript crate. Here is a list of the fields and the features they depend on:

kv_store kv

Defines the key-value store to use for the deno_kv extension

broadcast_channel broadcast_channel

Defines the in-memory broadcast channel to use for the deno_broadcast_channel extension

filesystem fs

Defines the filesystem implementation to use for the deno_fs extension

  • Default is deno_fs::RealFs

cache cache

Defines the cache configuration to use for the deno_cache extension

  • Uses a non-persistent in-memory cache by default

webstorage_origin_storage_dir webstorage

Defines the directory where the webstorage extension will store its data

io_pipes io

Defines the stdin/out/err pipes for the deno_io extension

crypto_seed crypto

Defines the seed for the deno_crypto extension

  • Default is None

node_resolver node_experimental

Defines the package resolver to use for the deno_node extension

  • Default is RustyResolver::new()
  • The RustyResolver type allows you to select the base dir for modules and the filesystem implementation to use

web web

Defines the options for the deno_web, deno_fetch, and deno_net extensions

  • Also defines permissions for related APIs
  • fields:
    • base_url: Base URL for some deno_web OPs
    • user_agent: User agent to use for fetch
    • root_cert_store_provider: Root certificate store for TLS connections for fetches and network OPs
    • proxy: Proxy for fetch
    • request_builder_hook: Request builder hook for fetch
    • unsafely_ignore_certificate_errors: List of domain names or IP addresses to ignore SSL errors for
    • client_cert_chain_and_key: Client certificate and key for fetch
    • file_fetch_handler: File fetch handler for fetch
    • permissions: Permissions manager for sandbox-breaking extensions
    • blob_store: Blob store for the web related extensions

Advanced Topics

This chapter will cover more advanced topics that may not be necessary for most users, but can be useful for those who need them.

It will cover:

Asynchronous JavaScript

By default, rustyscript can resolve most asynchronous javascript, including promises, by blocking the current thread.
In many cases, however, this will not be sufficient.
For example, handling ongoing background tasks, dealing with promises, or running async functions in parallel.

To this end, many functions will have _async and _immediate variants.

_async

_async functions will return a Future that that resolves when:

  • The event loop is resolved, and
  • If the value is a promise, the promise is resolved

For example call_function_async will return a Future that resolves when the function call completes,
And, if the function returns a promise, when the promise is resolved.

_immediate

_immediate functions will return a value immediately, but will not resolve promises or advance the event loop.

  • Promises can be returned by specifying the return type as js_value::Promise
  • The event loop should be run using [Runtime::await_event_loop]

For example, call_function_immediate on a function returning a promise, will return a js_value::Promise<T> object, which can be resolved later.

You can turn Promise<T> into Future<Output = T> by calling Promise::into_future This allows you to export multiple concurrent promises without borrowing the runtime mutably

You can also use Promise::into_value to block until the promise is resolved, and get the value.


A long-form example can be found here

Futures, with async functions

In the example below, we define a simple async function in javascript, and call it from rust.
The runtime's own tokio runtime is used to resolve the future.

The event loop, and the implicit promise resolution, is handled by the runtime, transparently.
This is the simplest way to use async functions.

use rustyscript::{Error, Module, Runtime};

fn main() -> Result<(), Error> {
    let module = Module::new(
        "test.js",
        "
        export const my_func = async () => 42;
    ",
    );

    // Create a new runtime
    // We don't need to create a tokio runtime, as the runtime will create one for us
    let mut runtime = Runtime::new(Default::default())?;
    let handle = runtime.load_module(&module)?;
    let tokio_runtime = runtime.tokio_runtime();

    // Call the function, and await the result
    let value: u32 = tokio_runtime.block_on(async {
        runtime
            .call_function_async(Some(&handle), "my_func", &())
            .await
    })?;

    assert_eq!(value, 32);
    Ok(())
}

Another way is with js_value::Promise

Normally, calling a function would resolve the promise. So we need to use call_function_immediate instead.

use rustyscript::{js_value::Promise, Error, Module, Runtime};

fn main() -> Result<(), Error> {
    let module = Module::new(
        "test.js",
        "
        export const my_func = async () => 42;
    ",
    );

    // Create a new runtime
    let mut runtime = Runtime::new(Default::default())?;
    let handle = runtime.load_module(&module)?;

    // Call the function, and get the promise
    let promise: Promise<u32> = runtime.call_function_immediate(Some(&handle), "my_func", &())?;

    // Resolve the promise
    // You could instead call `into_future` here, and await it, for a non-blocking version
    let value = promise.into_value(&mut runtime)?;
    assert_eq!(value, 42);

    Ok(())
}

Background Tasks

Sometimes, a module may begin background tasks, which would tie up the event loop for a long time, causing the async and blocking functions to hang.

In this case, you can use one of the following methods:

Combined with the immediate variants of most functions, this will allow you full control over execution of the event loop.

See this example for a demonstration.

Threading

The last way to handle async functions is to use a separate thread.

See Multi-Threading for more information.

Calling Rust from JavaScript

rustyscript supports registering rust functions to be callable from JavaScript.

A more advanced and performant way to call rust from JS is through custom extensions.

Blocking Functions

use rustyscript::{sync_callback, Error, Runtime};

fn main() -> Result<(), Error> {
    // Let's get a new runtime first
    let mut runtime = Runtime::new(Default::default())?;

    // We can use the helper macro to create a callback
    // It will take care of deserializing arguments and serializing the result
    runtime.register_function(
        "add",
        sync_callback!(|a: i64, b: i64| {
            a.checked_add(b)
                .ok_or(Error::Runtime("Overflow".to_string()))
        }),
    )?;

    // The registered functions can now be called from JavaScript
    runtime.eval::<()>("rustyscript.functions.add(1, 2)")?;

    Ok(())
}

Another option is to use a normal function, which can also be move if we want to capture some state:
You will need to handle serialization and deserialization yourself.

use rustyscript::{serde_json, Error, Runtime};

fn main() -> Result<(), Error> {
    // Let's get a new runtime first
    let mut runtime = Runtime::new(Default::default())?;

    // We can use a normal function, if we wish
    // It can also be `move` if we want to capture some state
    runtime.register_function("echo", |args| {
        // Decode the input
        let input = args
            .first()
            .ok_or(Error::Runtime("No input".to_string()))
            .map(|v| serde_json::from_value::<String>(v.clone()))??;

        // Encode the output
        let output = format!("Hello, {input}!");
        Ok::<_, Error>(serde_json::Value::String(output))
    })?;

    // The registered functions can now be called from JavaScript
    runtime.eval::<()>("rustyscript.functions.echo('test')")?;

    Ok(())
}

Async Functions

Async functions can be defined as well:

use rustyscript::{async_callback, Error, Runtime};

fn main() -> Result<(), Error> {
    let mut runtime = Runtime::new(Default::default())?;

    // There is also an async version
    runtime.register_async_function(
        "asyncEcho",
        async_callback!(|input: String| {
            async move { Ok::<_, Error>(format!("Hello, {input}!")) }
        }),
    )?;

    // The registered functions can now be called from JavaScript
    runtime.eval::<()>("rustyscript.async_functions.asyncEcho('test')")?;

    Ok(())
}

Custom Extensions

Important note: that the examples in this chapter require your crate to supply the same version of deno_core as rustyscript uses - this version can be checked here

The most performant way to extend rustyscript is to use the extension feature of deno_core.
The following example demonstrates how to create a simple extension that adds two numbers together.

use rustyscript::{Error, Runtime, RuntimeOptions};
use deno_core::{extension, op2};

#[op2(fast)]
#[bigint]
fn op_add_example(#[bigint] a: i64, #[bigint] b: i64) -> i64 {
    a + b
}

extension!(
    example_extension,                                  // The name of the extension
    ops = [op_add_example],                             // The ops to include in the extension
    esm_entry_point = "simple_extension.js",            // The entry point for the extension
    esm = [ dir "js_examples", "simple_extension.js" ], // The javascript files to include
);

fn main() -> Result<(), Error> {
    // If you were loading from a snapshot, you would use init_ops instead of init_ops_and_esm
    // let my_extension = example_extension::init_ops();
    let my_extension = example_extension::init_ops_and_esm();

    let mut runtime = Runtime::new(RuntimeOptions {
        extensions: vec![my_extension],
        ..Default::default()
    })?;

    let result: i64 = runtime.eval("my_add(5, 5)")?;
    assert_eq!(10, result);

    Ok(())
}

And the corresponding javascript file:

export const add = (a, b) => Deno.core.ops.op_add_example(a, b);
globalThis.my_add = add;

Important note: All javascript files included in an extension MUST be included somewhere. I recommend using the file specified in esm_entry_point to include all other files.

Op2

Op2 is a deno-provided macro that allows you to define an extension function.

The fast attribute used when possible to denote that the types involved can be converted fast. Don't stress too much on when to use this, as the compiler will tell you if you need it, or do not.

Ops defined in an extension can then be called from javascript using Deno.core.ops.op_name(args...).


Async functions can be defined as well.

Let us break down a more complex example taken from rustyscript's own internals:


#![allow(unused)]
fn main() {
#[op2(async)]
#[serde]
fn call_registered_function_async(
    #[string] name: String,
    #[serde] args: Vec<serde_json::Value>,
    state: &mut OpState,
) -> impl std::future::Future<Output = Result<serde_json::Value, Error>> {
    if state.has::<AsyncFnCache>() {
        let table = state.borrow_mut::<AsyncFnCache>();
        if let Some(callback) = table.get(&name) {
            return callback(args);
        }
    }

    Box::pin(std::future::ready(Err(Error::ValueNotCallable(name))))
}
}
  • #[op2(async)] is used to denote that this is an async function - it must return a Future of some kind
    • Potential pitfall: the returned future cannot have a lifetime
  • #[serde] means that the return value will be a type decoded with serde::Deserialize.
    • In this fase, there return value is Future<Output = Result<serde_json::Value, Error>> - A future that resolves to a serde_json::Value, or an error.
  • The arguments are annotated with #[string] and #[serde] to denote that they are a string, and a deserializable type, respectively.
    • The state argument is a special case that can be used for persistent storage inside of ops. In this case, it is used to store a cache.

And finally, the returned future is run through Box::pin before being returned.


You can also customize the names of your extension modules
If you instead define the extension as follows:


#![allow(unused)]
fn main() {
extension!(
    example_extension,
    ops = [op_add_example],
    esm_entry_point = "example:calculator",
    esm = [ dir "examples/example_extension", "example:calculator" = "example_extension.js" ],
);
}

Then provide a schema whitelist in your RuntimeOptions:


#![allow(unused)]
fn main() {
    let mut schema_whlist = HashSet::new();
    schema_whlist.insert("example:".to_string());
    options.schema_whlist = schema_whlist;
}

You would be able to import { add } from "example:calculator"; from inside of your js modules.

Static Runtimes

Since the runtime must be mutable, and cannot be safely sent between threads, it can be tricky to use it in a static context.

To this end, rustyscript includes the static_runtime module:

use rustyscript::{static_runtime, RuntimeOptions, Error};

// Can have the default options
static_runtime!(MY_DEFAULT_RUNTIME);

// Or you can provide your own
static_runtime!(MY_CUSTOM_RUNTIME, {
    let mut options = RuntimeOptions::default();
    options.timeout = std::time::Duration::from_secs(5);
    options
});

fn main() -> Result<(), Error> {
    MY_DEFAULT_RUNTIME::with(|runtime| {
        runtime.eval::<()>("console.log('Hello, World!')")
    })
}

Under the hood, a static runtime is effectively:


#![allow(unused)]
fn main() {
thread_local! {
    static MY_RUNTIME: OnceCell<RefCell<Result<Runtime, Error>>>;
}
}

Which provides thread safety, static initialization, interior mutability, and initializer error handling.

Note: While it is possible to initialize a StaticRuntime object directly, it is not recommended, as it bypasses the thread_local safety layer.

MultiThreading

rustyscript is not thread-safe, due to limitations of the underlying engine, deno_core

The worker feature (enabled by default) gets around this by defining worker threads, which can be used in parallel.
Workers use queries sent over channels to communicate with the main thread.

Important Note: You must call init_platform If you are using runtimes on multiple threads. The only exception is WorkerPool, sen below, which will call init_platform for you.

DefaultWorker is a very simple built-in worker implementation:

use deno_core::serde_json;
use rustyscript::{
    worker::{DefaultWorker, DefaultWorkerQuery, DefaultWorkerResponse},
    Error, Module,
};

fn main() -> Result<(), Error> {
    let worker = DefaultWorker::new(Default::default())?;

    // Instruct the worker to load a module
    // We can do with provided helper functions
    let module = Module::new("test.js", "export function add(a, b) { return a + b; }");
    let query = DefaultWorkerQuery::LoadModule(module);
    let response = worker.as_worker().send_and_await(query)?;
    let module_id = if let DefaultWorkerResponse::ModuleId(id) = response {
        id
    } else {
        let e = Error::Runtime(format!("Unexpected response: {:?}", response));
        return Err(e);
    };

    // Now we can call the function
    let query = DefaultWorkerQuery::CallFunction(
        Some(module_id),
        "add".to_string(),
        vec![1.into(), 2.into()],
    );
    let response = worker.as_worker().send_and_await(query)?;
    if let DefaultWorkerResponse::Value(v) = response {
        let value: i32 = serde_json::from_value(v)?;
        assert_eq!(value, 3);
    }

    Ok(())
}

Note that the worker has built-in helpers for many operations - this example could be far more succinct using the load_module and call_function helpers, which take care of the query/response boilerplate, as seen here.

However, for many applications the default worker will not be sufficient, in which case you can implement your own worker - See this example


The worker module also provides WorkerPool, which can be used to manage multiple workers.

NodeJS Compatibility

With the node_experimental crate-level feature, you can enable support for some NodeJS APIs. It will also enable all other extension features.

Please note that this API is highly experimental, and likely does not support all node modules. Kindly report any issues you encounter.

Usage

To enable the feature, add node_experimental to the features list in your Cargo.toml.

Node modules will be located in the node_modules directory using the package.json file in the current working directory.

You can import from the node standard library (Deno polyfills)

  • For example import os from "node:os"

Or from the node_modules directory

  • For example import chalk from "npm:chalk@5";

See this example for more information.

Permissions

Many of the extension features allow for custom permissions to be set.
This allows for a more fine-grained control over what can be accessed.

This can be done by specifying a value for the permissions: Arc<dyn WebPermissions>, field in RuntimeOptions::web.

The default value is DefaultWebPermissions, which simply allows everything.

AllowlistWebPermissionsSet is also built-in, and allows for specific permissions to be turned on or off:

use rustyscript::{AllowlistWebPermissions, Error, Runtime, RuntimeOptions};
use std::sync::Arc;

fn main() -> Result<(), Error> {
    let permissions = Arc::new(AllowlistWebPermissions::default());
    let mut options = RuntimeOptions::default();
    options.extension_options.web.permissions = permissions.clone();

    let mut runtime = Runtime::new(options)?;

    // Fetching any URL will fail
    let result = runtime.eval::<()>("fetch('https://example.com')");
    assert!(result.is_err());

    // But if we allow it:
    permissions.allow_url("https://example.com");
    let result = runtime.eval::<()>("fetch('https://example.com')");
    assert!(result.is_ok());

    Ok(())
}

You can also implement the WebPermissions trait yourself, and use that instead for even more precise control.

Snapshots

Deno's runtime supports the use of memory snapshots to skip the lengthy process of spinning up a new runtime and loading all the necessary code. This can give gains of ~10x in startup time, but comes with some caveats:

  • The snapshot must be generated on the same system it will be used on
  • The extensions in the snapshot builder runtime must be the same, and the same order as the runtime that will use the snapshot
  • The snapshot_builder feature must be enabled on rustyscript - This feature DOES preserve the integrity of the sandbox

Generating a snapshot

The snapshot must be provided to the runtime via the startup_snapshot field of RuntimeOptions, which as a type of &'static [u8].

Therefore the snapshot will typically be created in build.rs (short of using Box::leak, lazy_static, or similar)

The SnapshotBuilder struct is used to generate the snapshot, and can be used to pre-load modules into the snapshot. It has methods similar to the normal Runtime struct.

Below is a sample that generates a snapshot with a pre-loaded module:

You could also do this in build.rs and write to concat!(env!("OUT_DIR"), "/snapshot.bin")

use rustyscript::{Error, Module, SnapshotBuilder};
use std::fs;

static SNAPSHOT_PATH: &str = "examples/snapshot.bin";

fn main() -> Result<(), Error> {
    // A module we want pre-loaded into the snapshot
    let module = Module::new(
        "my_module.js",
        "globalThis.importantFunction = function() {
            return 42;
        }",
    );

    let snapshot = SnapshotBuilder::new(Default::default())?
        .with_module(&module)?
        .finish();

    fs::write(SNAPSHOT_PATH, snapshot)?;
    Ok(())
}

Then, in order to use the snapshot:

use rustyscript::{Runtime, RuntimeOptions};

static SNAPSHOT: &[u8] = include_bytes!("example_snapshot.bin");

fn main() -> Result<(), rustyscript::Error> {
    let mut runtime = Runtime::new(RuntimeOptions {
        startup_snapshot: Some(SNAPSHOT),
        ..Default::default()
    })?;
    let important_value: u32 = runtime.eval("importantFunction()")?;
    assert_eq!(important_value, 42);
    Ok(())
    }

The startup time of the runtime will fall from ~50ms, to less than 1 with the snapshot.


You could also generate the snapshot at runtime, in a lazy_static block, or similar:

use rustyscript::{Error, Module, Runtime, RuntimeOptions, SnapshotBuilder};
use std::sync::OnceLock;

static SNAPSHOT: OnceLock<Box<[u8]>> = OnceLock::new();
fn get_snapshot() -> &'static [u8] {
    SNAPSHOT.get_or_init(|| {
        let module = Module::new(
            "my_module.js",
            "globalThis.importantFunction = function() {
            return 42;
        }",
        );

        SnapshotBuilder::new(Default::default())
            .unwrap()
            .with_module(&module)
            .unwrap()
            .finish()
    })
}

fn main() -> Result<(), Error> {
    let mut runtime = Runtime::new(RuntimeOptions {
        startup_snapshot: Some(get_snapshot()),
        ..Default::default()
    })?;
    runtime.eval::<()>("1 + 1")?;
    Ok(())
}

Deno Extensions

The Deno JS runtime on which Rustyscript is based boasts very good compatibility with the complete JS standards.

This is done through a series of extensions, listed below, which each provide a part of the full API.
This gives us the ability to use the full JS standard library, or just the parts we need.

By default, Rustyscript includes only those extensions that maintain a secure, sandboxed runtime environment.

See the Safe Extensions section for more information.


Extensions can be activated using crate features, either individually or in groups:

Deno Extensions

Safe Extensions

All the extensions mentioned below can be activated using the safe_extensions crate feature.

By default, Rustyscript includes only those extensions that maintain a secure, sandboxed runtime environment.

Important Note
By default the Javascript code you run have no access to system resources such as the file system, network, or environment variables.

The safe extensions included by default are:

  • console - For logging
  • crypto - For cryptographic functions
  • url - For URL parsing
  • web_stub - A stub for the web extension, providing timers, and base64 encoding/decoding

The remaining extensions can be broadly categorized as either io or network.

Important Note
With the exception of cron, webstorage, and ffi
All the remaining extensions depend on the web extension.

Safe Extensions

Console

Crate features: [console, safe_extensions]
https://crates.io/crates/deno_console/
https://console.spec.whatwg.org/

Populates the global console object with methods for logging and debugging.
This extensions is sandbox safe. It is enabled by default.

Usage Example

console.log("Hello, world!");

Safe Extensions

Crypto

Crate features: [crypto, safe_extensions]
https://crates.io/crates/deno_crypto/
https://www.w3.org/TR/WebCryptoAPI/

Populates the global CryptoKey, Crypto, crypto, and SubtleCrypto objects This extensions is sandbox safe. It is enabled by default.

Options

RuntimeOptions::extension_options::crypto_seed

  • Optional seed the deno_crypto RNG
  • Default: None

If a seed is provided, then rand::rngs::StdRng will be used to generate random numbers.
Otherwise, rand::thread_rng will be used.

Usage Example

const key = await crypto.subtle.generateKey(
  {
    name: "AES-GCM",
    length: 256,
  },
  true,
  ["encrypt", "decrypt"],
);

Safe Extensions

Url

Crate features: [url, safe_extensions]
https://crates.io/crates/deno_url/
https://url.spec.whatwg.org/ https://wicg.github.io/urlpattern/

Populates the global URL, URLPattern, URLSearchParams objects
This extensions is sandbox safe. It is enabled by default.

Usage Example

const url = new URL("https://example.com");
const pattern = new URLPattern("https://example.com/*");
const params = new URLSearchParams("a=1&b=2");

Safe Extensions

Web Stub

Crate features: [web_stub, safe_extensions]
Mutually exclusive with the web extension.

This extensions is sandbox safe. It is enabled by default.

Enables the following from javascript:

  • DOMException
  • setImmediate
  • setInterval, and clearInterval
  • setTimeout, and clearTimeout
  • atob and btoa

Usage Example:

const base64 = btoa("Hello, world!");
setImmediate(() => {
  console.log(atob(base64));
});

Deno Extensions

IO Extensions

All the extensions mentioned below can be activated using the io_extensions crate feature.

These extensions grant runtimes access to the file system - but may also grant some level of network access - use caution.

  • fs - For file system access
  • io - input/output primitives (stdio streams, etc)
  • cache - Cache support, API reference here
  • ffi - Deno FFI support
  • webgpu - WebGPU support, API reference here
  • kv - Key-value store, API reference here
  • cron - Implements scheduled tasks (crons) API

IO Extensions

Cache

Crate features: [cache, io_extensions]
https://crates.io/crates/deno_cache
https://w3c.github.io/ServiceWorker/#cache-interface

Populates the global Cache, CacheStorage, and caches objects.
Not sandbox safe. Off by default

Note query options are not yet supported for the Cache.match method.

Options

RuntimeOptions::extension_options::cache

  • The optional persistent caching backend used by the extension.
  • Default: A non-persistent, in-memory cache

The cache option can also be set to None, which will effectively disable the cache functionality.

To configure the persistent cache, create an instance of the SQLite backend and pass it to the extension:

use rustyscript::{CacheBackend, ExtensionOptions, RuntimeOptions};

fn main() {
    // Will store the cache in a directory called "deno_cache"
    let cache = CacheBackend::new_sqlite("deno_cache").unwrap();
    let _options = RuntimeOptions {
        extension_options: ExtensionOptions {
            cache: Some(cache),
            ..Default::default()
        },
        ..Default::default()
    };
}

Usage Example

let cache = await caches.open('my_cache');

fetch('http://web.simmons.edu/').then((response) => {
    cache.put('http://web.simmons.edu/', response);
});

cache.match('http://web.simmons.edu/').then((response) => {
    console.log('Got response from cache!');
});

IO Extensions

Cron

Crate features: [cron, io_extensions]
https://crates.io/crates/deno_cron

Populates the global Deno.cron object.
Not sandbox safe. Off by default

Usage Example

let cronHandler = () => new Promise((resolve) => {
    console.log('Hello, world!');
    resolve();
});

Deno.cron('my-cron-job', '* * * * *', cronHandler)

IO Extensions

FFI

Crate features: [ffi, io_extensions].
https://crates.io/crates/deno_ffi

Populates the Deno.dlopen, Deno.UnsafeCallback, Deno.UnsafePointer, Deno.UnsafePointerView, Deno.UnsafeFnPointer globals.
Not sandbox safe. Off by default

Permissions

This extension is affected by the following methods in the permissions trait:

  • check_exec - Check if FFI execution is allowed
  • check_read - Of FFI exec is allowed, check if a specific file is allowed to be read

Usage Example

const buffer = new ArrayBuffer(1024);
const baseAddress = Deno.UnsafePointer.value(Deno.UnsafePointer.of(buffer));

const throwCb = () => throw new Error("Error");
const cb = new Deno.UnsafeCallback({
    parameters: [],
    result: "void",
}, throwCb);

const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, {
    parameters: [],
    result: "void",
});

assertThrows(() => fnPointer.call());
cb.close();

IO Extensions

FS

Crate features: [fs, io_extensions] https://crates.io/crates/deno_fs

Populates a large number of filesystem functions under Deno.*:
Not sandbox safe. Off by default

Full list of functions:
writeFileSync writeFile writeTextFileSync writeTextFile readTextFile readTextFileSync readFile readFileSync watchFs chmodSync chmod chown chownSync copyFileSync cwd makeTempDirSync makeTempDir makeTempFileSync makeTempFile mkdirSync mkdir chdir copyFile readDirSync readDir readLinkSync readLink realPathSync realPath removeSync remove renameSync rename statSync lstatSync stat lstat truncateSync truncate FsFile open openSync create createSync symlink symlinkSync link linkSync utime utimeSync umask

Permissions

This extension is affected by the following methods in the permissions trait:

  • check_open - Check if a given path is allowed to be opened

  • check_read_all - Can be used to disable all read operations
  • check_read - Check if a given path is allowed to be read
  • check_read_blind - check_read, but is expected to use the display argument to anonymize the path

  • check_write_all - Can be used to disable all write operations
  • check_write - Check if a given path is allowed to be written to
  • check_write_blind - check_write, but is expected to use the display argument to anonymize the path
  • check_write_partial - Used to check if a given path is allowed to be written to partially (non-recursive removes, use this)

Usage Example

const file = Deno.openSync("/dev/zero");
const buf = new Uint8Array(100);
file.readSync(buf);

const file = Deno.openSync("/dev/null", { write: true });
file.writeSync(buf);

IO Extensions

Io

Crate features: [io, io_extensions] https://crates.io/crates/deno_io

Provides low-level Io primitives Populates the global Deno.SeekMode, Deno.stdin, Deno.stdout, and Deno.stderr objects

Options

RuntimeOptions::extension_options::io_pipes

  • Optional IO pipes to use for stdin, stdout, and stderr
  • Default: Some(deno_io::Stdio::default())

Usage Example

const encoder = new TextEncoder();
const data = encoder.encode("Hello world");
const bytesWritten = await Deno.stdout.write(data); // 11

IO Extensions

KV

Crate features: [kv, io_extensions]
https://crates.io/crates/deno_kv
https://docs.deno.com/deploy/kv/manual
https://crates.io/crates/deno_kv#kv-connect
https://deno.com/deploy
https://github.com/denoland/denokv/blob/main/proto/kv-connect.md

Provides a key/value store Populates the global Deno.openKv, Deno.AtomicOperation, Deno.KvU64, and Deno.KvListIterator objects

Options

RuntimeOptions::extension_options::kv_store
A KvStore defining the store to use for KV operations

  • Default: KvStore::new_local(None, None, KvConfig::default) (An in-memory local store)

Usage Example

const db = await Deno.openKv();
await db.set(["foo"], "bar");

const result = await db.get(["foo"]);
result.key; // ["foo"]
result.value; // "bar"

IO Extensions

WebGPU

Crate features: [webgpu, io_extensions]
https://crates.io/crates/deno_webgpu
https://gpuweb.github.io/gpuweb/
https://github.com/gfx-rs/wgpu
https://gpuweb.github.io/gpuweb/
https://github.com/gpuweb/gpuweb/tree/main/design>
https://github.com/crowlKats/webgpu-examples

Deno Extensions

Network Extensions

All the extensions mentioned below can be activated using the network_extensions crate feature.

These extensions grant runtimes access to system network resources

Network Extensions

Broadcast Channel

Crate features: [broadcast_channel, network_extensions] https://crates.io/crates/deno_broadcast_channel/
https://html.spec.whatwg.org/multipage/web-messaging.html

Populates the global BroadcastChannel object.
Not sandbox safe. Off by default

Options

RuntimeOptions::extension_options::broadcast_channel

  • The channel object used by the extension.
  • Default: InMemoryBroadcastChannel::default()

The channel can be cloned, and shared between runtimes to communicate between them.

Usage Example

Below is an example of using the broadcast_channel extension to transfer data between Rust and JavaScript. The same technique can be used to communicate between seperate runtimes, by sharing the channel object.

use rustyscript::{
    BroadcastChannelWrapper, Error, Module, Runtime, RuntimeOptions,
};

fn main() -> Result<(), Error> {
    // Let's extract the channel from the options
    let options = RuntimeOptions::default();
    let channel = options.extension_options.broadcast_channel.clone();
    let channel = BroadcastChannelWrapper::new(&channel, "my_channel")?;

    // Set up our runtime
    let mut runtime = Runtime::new(options)?;
    let tokio_runtime = runtime.tokio_runtime();

    // Load our javascript
    let module = Module::new(
        "test.js",
        "
        const channel = new BroadcastChannel('my_channel');
        channel.onmessage = (event) => {
            console.log('Got message: ' + event.data);
            channel.close();
        };
    ",
    );
    tokio_runtime.block_on(runtime.load_module_async(&module))?;

    // Use a built-in helper function to serialize the data for transmission
    channel.send_sync(&mut runtime, "foo")?;

    // And run the event loop to completion
    runtime.block_on_event_loop(Default::default(), None)?;
    Ok(())
}

Network Extensions

HTTP

Crate features: [http, network_extensions]
https://crates.io/crates/deno_http
https://fetch.spec.whatwg.org/

Implements server-side HTTP.
Populates the Deno.serve, Deno.serveHttp, and Deno.upgradeWebSocket functions.
Not sandbox safe. Off by default

Usage Example

Deno.serve((_req: Request) => {
  return new Response("Hello, world!");
});

Web

Crate features: [web, network_extensions, io_extensions]
Mutually exclusive with the web_stub extension.
https://crates.io/crates/deno_web
https://crates.io/crates/deno_fetch
https://crates.io/crates/deno_tls
https://crates.io/crates/deno_net
https://w3c.github.io/FileAPI
https://fetch.spec.whatwg.org/

Base Deno web API kit encompassing the deno_web, deno_tls, deno_fetch, and deno_net extensions.

This extension is required by all non-safe extensions.

The web_stub extension is a minimal subset of this extension, used to instantiate the safe extensions.

Also populates the following:

  • Deno.HttpClient and Deno.createHttpClient
  • Deno.connect, Deno.listen, Deno.resolveDns, Deno.listenDatagram
  • Deno.connectTls, Deno.listenTls, and Deno.startTls
  • Deno.refTimer, and Deno.unrefTimer

Options

RuntimeOptions::extension_options::web
The WebOptions struct contains the following fields:

base_url
The base URL to use for relative URL resolution

  • Default: None

user_agent
The user agent string to use for network requests

  • Default: Empty String

root_cert_store_provider
Root certificate store for TLS connections for fetches and network OPs

  • Default: None

proxy
Proxy to provide to fetch operations

  • Default: None

request_builder_hook
Request builder hook for fetch

  • Default: None

unsafely_ignore_certificate_errors
List of domain names or IP addresses for which fetches and network OPs will ignore SSL errors

  • Default: Empty Vec

client_cert_chain_and_key
Client certificate and key for fetch

  • Default: deno_tls::TlsKeys::Null

file_fetch_handler
File fetch handler for fetch

  • Default: deno_fetch::DefaultFileFetchHandler

permissions
The permissions manager to use for this extension, and several others.
See Permissions for more information.

  • Default: DefaultWebPermissions (Allows all operations)

blob_store The blob store to use for fetch

  • Default: deno_web::BlobStore

Permissions

Fetch is affected by the following methods in the permissions trait:

  • check_url - Check if a given URL is allowed to be fetched
  • check_read - Check if a given path is allowed to be read

Net is affected by the following methods in the permissions trait:

  • check_host - Check if a given host is allowed to be connected to
  • check_read - Check if a given path is allowed to be read
  • check_write - Check if a given path is allowed to be written to

Web is affected by the following methods in the permissions trait:

  • allow_hrtime - Allow high-resolution time measurements in timers

Usage Example

fetch("https://example.com/data.json")
  .then((response) => response.json())
  .then((data) => console.log(data));

Network Extensions

WebSocket

Crate features: [websocket, network_extensions]
https://crates.io/crates/deno_websocket
https://html.spec.whatwg.org/multipage/web-sockets.html

Populates the global WebSocket and WebSocketStream objects

Options

Uses the user_agent, root_cert_store_provider, and unsafely_ignore_certificate_errors fields of RuntimeOptions::extension_options::web

Permissions

This extension is affected by the check_url function in the permissions trait, which checks if a given URL is allowed to be accessed

Usage Example

const ws = new WebSocket("ws://localhost:8080");

ws.onopen = () => {
  console.log("Connected");
  ws.send("Hello, world!");
  ws.close();
};

IO Extensions

WebStorage

Crate features: [webstorage, io_extensions] https://crates.io/crates/deno_webstorage https://html.spec.whatwg.org/multipage/webstorage.html

Populates the global Storage, localStorage, and sessionStorage objects

Options

RuntimeOptions::extension_options::webstorage_origin_storage_dir

  • Optional directory for storage
  • Default: None

If a directory is provided, then the storage will be persisted to that directory.
Otherwise, most webstorage operations will be unavailable.

Usage Example

localStorage.setItem("key", "value");
const value = localStorage.getItem("key");
console.log(value);

Deno Extensions

NodeJS Extensions

Crate features: [node_experimental]
https://crates.io/crates/deno_node
https://crates.io/crates/deno_resolver
https://crates.io/crates/node_resolver
https://crates.io/crates/deno_npm
https://crates.io/crates/deno_semver
https://crates.io/crates/deno_napi
https://crates.io/crates/deno_runtime

Provides BYONM (bring-your-own-npm-module) support for Deno.
See NodeJS Compatibility for more information.

Includes a very large set of Deno APIs, most of which are needed to run Deno's NodeJS standard library polyfills.

Note: The list of APIs below is not exhaustive and does not include the NodeJS standard library polyfills themselves.

fs_events

Provides Deno.watchFs

os

Provides:
Deno.env, Deno.exit, Deno.execPath, Deno.loadavg, Deno.osRelease, Deno.osUptime, Deno.hostname, Deno.systemMemoryInfo, Deno.networkInterfaces, Deno.gid, Deno.uid

permissions

Provides:
Deno.permissions, Deno.Permissions, Deno.PermissionStatus

process

Provides:
Deno.Process, Deno.run, Deno.kill, Deno.Command, Deno.Process

signal

Provides:
Deno.addSignalListener, Deno.removeSignalListener

web_worker / worker_host

Provides worker support for the NodeJS API