Getting Started
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 multithreadedworker
API
snapshot_builder
Enables thesnapshot_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 usebig_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.
- Any type implementing
Serde::Deserialize
can be used.
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
andimmediate
.
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.
ImportProvider Trait
The ImportProvider
trait can be implemented to provide custom module loading behavior
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 filesystemurl_import
will allow loading modules from network location
- A couple of crate features can change this:
- 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
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
- base_url: Base URL for some
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:
- Managing asynchronous JS, including promises, and background tasks
- Call sync and async rust from inside the JS runtime, as well as transfering data between the two
- Extending the JS runtime with more advanced custom functionality (extensions and op2)
- Creating a static runtime instance
- Running multiple runtimes in parallel, and sharing runtime data between threads
- NodeJS compatibility, including using the NodeJS standard library
- Restricting access from inside a non-sandboxed JS runtime
- Using snapshots to increase startup time
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 supported types for arguments can be found listed in https://github.com/denoland/deno_core/blob/main/ops/op2/valid_args.md
- And return types, here: https://github.com/denoland/deno_core/blob/main/ops/op2/valid_retvals.md
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 aFuture
of some kind- Potential pitfall: the returned future cannot have a lifetime
#[serde]
means that the return value will be a type decoded withserde::Deserialize
.- In this fase, there return value is
Future<Output = Result<serde_json::Value, Error>>
- A future that resolves to aserde_json::Value
, or an error.
- In this fase, there return value is
- 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.
- The
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 isWorkerPool
, sen below, which will callinit_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
andcall_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.
- See this example
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:
safe_extensions
- On by defaultio_extensions
- For file system accessnetwork_extensions
- For network accessnode_experimental
- For compatibility with Deno's NodeJS standard library polyfills
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 loggingcrypto
- For cryptographic functionsurl
- For URL parsingweb_stub
- A stub for theweb
extension, providing timers, and base64 encoding/decoding
The remaining extensions can be broadly categorized as either io
or network
.
Important Note
With the exception ofcron
,webstorage
, andffi
All the remaining extensions depend on theweb
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 theweb
extension.
This extensions is sandbox safe. It is enabled by default.
Enables the following from javascript:
DOMException
setImmediate
setInterval
, andclearInterval
setTimeout
, andclearTimeout
atob
andbtoa
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 accessio
- input/output primitives (stdio streams, etc)cache
- Cache support, API reference hereffi
- Deno FFI supportwebgpu
- WebGPU support, API reference herekv
- Key-value store, API reference herecron
- 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 allowedcheck_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 operationscheck_read
- Check if a given path is allowed to be readcheck_read_blind
-check_read
, but is expected to use thedisplay
argument to anonymize the path
check_write_all
- Can be used to disable all write operationscheck_write
- Check if a given path is allowed to be written tocheck_write_blind
-check_write
, but is expected to use thedisplay
argument to anonymize the pathcheck_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
web
- Timers, events, text encoder/decoder, See here and herewebstorage
- Web storage API, API reference herewebsocket
- Websocket API, API reference herehttp
- Fetch primitives, API reference herebroadcast_channel
- Web messaging, API reference here
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 theweb_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
andDeno.createHttpClient
Deno.connect
,Deno.listen
,Deno.resolveDns
,Deno.listenDatagram
Deno.connectTls
,Deno.listenTls
, andDeno.startTls
Deno.refTimer
, andDeno.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 fetchedcheck_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 tocheck_read
- Check if a given path is allowed to be readcheck_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