rustyscript

repository·master·Indexed 18 days ago

https://github.com/rscarson/rustyscript

A Rust library for effortless JavaScript and TypeScript integration using the V8 engine via deno_core. It provides a simplified API to operate on Rust types while abstracting V8 complexities. Features include a sandboxed-by-default runtime, support for asynchronous JS, multi-threading via workers, and the ability to register Rust functions for use within JavaScript. Version 0.12.3.

Tokens
18.7K
Snippets
44
Records
61
Agent score
63%

What's inside rustyscript

  1. Overview of Rustyscript

    master

    Rustyscript is a library for integrating JavaScript or TypeScript runtimes into Rust applications. It uses the V8 engine via deno_core and provides a simplified API that allows developers to operate directly on Rust types while abstracting away V8 complexities.

    Key Features

    • Sandboxed by default: Code runs without filesystem or network access unless explicitly granted via extensions.
    • Flexible: Supports asynchronous JS, TypeScript (transpiled to JS), and experimental Node.js compatibility.
    • Unopinionated: Acts as a thin, high-performance wrapper over the Deno runtime.
  2. How to handle Asynchronous JavaScript

    master

    Rustyscript provides two primary ways to handle asynchronous JavaScript (functions marked async or returning a Promise).

    1. Using call_function_async and Tokio

    Use this approach when you want to await the result using a future. The runtime includes its own Tokio runtime, which you can access via runtime.tokio_runtime().

    2. Using call_function_immediate and js_value::Promise

    Use this approach to call a function without waiting for the event loop or resolving the promise immediately. This returns a Promise<T> handle. You can later resolve this promise into a value using into_value or convert it into a Rust future using into_future.

    // Method 1: Async/Await with Tokio
    let tokio_runtime = runtime.tokio_runtime();
    let result: i32 = tokio_runtime.block_on(async {
        let handle = runtime.load_module_async(&module).await?;
        runtime.call_function_async(Some(&handle), "foo", json_args!()).await
    })?;
    
    // Method 2: Immediate call with Promise handle
    let result: Promise<i32> = runtime.call_function_immediate(Some(&handle), "foo", json_args!())?;
    let result = result.into_value(&mut runtime)?;
  3. Manual Runtime management and function calling

    master

    For more control, you can manually manage the Runtime lifecycle. This allows you to set timeouts, define default entrypoints, and call specific functions from a loaded module handle.

    Steps:

    1. Create a Module.
    2. Initialize a Runtime with RuntimeOptions (e.g., setting a timeout or default_entrypoint).
    3. Load the module into the runtime using runtime.load_module(&module).
    4. Call the entrypoint using runtime.call_entrypoint or call specific functions using runtime.call_function.
    use rustyscript::{json_args, Runtime, RuntimeOptions, Module, Error, Undefined};
    use std::time::Duration;
    
    let module = Module::new(
        "test.js",
        "
        let internalValue = 0;
        export const load = (value) => internalValue = value;
        export const getValue = () => internalValue;
        "
    );
    
    let mut runtime = Runtime::new(RuntimeOptions {
        timeout: Duration::from_millis(50),
        default_entrypoint: Some("load".to_string()),
        ..Default::default()
    })?;
    
    let module_handle = runtime.load_module(&module)?;
    runtime.call_entrypoint::<Undefined>(&module_handle, json_args!(2))?;
    
    let internal_value: i64 = runtime.call_function(Some(&module_handle), "getValue", json_args!())?;
  4. Implement the ImportProvider trait for custom module loading

    master

    The ImportProvider trait allows you to modify how Rustyscript handles JavaScript/TypeScript module imports. By implementing this trait, you can introduce custom URL schemes, implement custom caching strategies, or enforce granular permissions on which modules are allowed to be imported.

    To use the default resolution and import behavior, return None from the resolve and import methods. If you return Some(Ok(...)), you override the default behavior with your own logic. If you return Some(Err(...)), you can deny an import entirely.

    // Example of implementing the trait
    impl ImportProvider for MyCustomLoader {
        fn resolve(
            &mut self, 
            specifier: &ModuleSpecifier, 
            referrer: &str, 
            kind: deno_core::ResolutionKind
        ) -> Option<Result<ModuleSpecifier, ModuleLoaderError>> {
            // Custom logic to modify or deny imports
            None
        }
    
        fn import(
            &mut self, 
            specifier: &ModuleSpecifier, 
            referrer: Option<&ModuleSpecifier>, 
            is_dyn_import: bool
        ) -> Option<Result<String, ModuleLoaderError>> {
            // Custom logic to fetch module source code
            None
        }
    
        fn post_process(
            &mut self, 
            specifier: &ModuleSpecifier, 
            source: ModuleSource
        ) -> Result<ModuleSource, ModuleLoaderError> {
            // Custom logic to transform source code
            Ok(source)
        }
    }
  5. Configure Rustyscript crate features

    master

    Rustyscript uses Cargo features to enable or disable specific JavaScript APIs and capabilities within the runtime.

    Sandboxing Warning

    Features marked as Preserves Sandbox: NO break the isolation between loaded JS modules and the host system. Use these with caution as they grant the JS runtime access to the filesystem, network, or other host resources.

    Key Feature Groups

    • default: Enables only the extensions that preserve sandboxing (deno_console, deno_crypto, deno_webidl, deno_url).
    • all: Enables all available functionality, including those that break sandboxing.
    • no_extensions: Disables all extensions, providing a bare JS runtime. You can still manually add your own extensions in this mode.
    • web: Provides standard Web APIs (Event, TextEncoder, TextDecoder, File, Web Cryptography, and fetch). Note: Enabling web also automatically enables fs_import and url_import, allowing arbitrary filesystem and network access via import statements.
    • web_stub: Provides a subset of web features that do not break sandboxing.

    Sandbox-Breaking Features (Preserves Sandbox: NO)

    Use these when your JS code requires direct access to host resources:

    • broadcast_channel: Web-messaging API.
    • cache: Cache API.
    • cron: Scheduled tasks API.
    • ffi: Dynamic library FFI.
    • fs: Filesystem operations.
    • http: fetch standard implementation.
    • kv: Deno KV Connect protocol.
    • io: IO primitives (stdio streams, File System abstractions).
    • webgpu: WebGPU API.
    • webstorage: WebStorage API.
    • websocket: WebSocket API.
    • fs_import: Import code from the local filesystem.
    • url_import: Import code from network locations.
    • node_experimental: Highly experimental Node.js support (enables all Deno extensions).
    # Example Cargo.toml configuration
    [dependencies]
    rustyscript = {
        version = "0.12.3",
        features = ["web", "fs", "http"]
    }
  6. How Runtime execution variants work

    master

    Most Runtime functions (like calling functions or evaluating code) provide three distinct execution patterns depending on how you want to handle the JavaScript event loop and Promises:

    1. Blocking (_): Blocks the current thread until the function is resolved and the event loop is empty. Use this for simple synchronous-style flows in a synchronous Rust context.
    2. Async (_async): Returns a Future that resolves when the function is resolved and the event loop is empty. Use this in async Rust applications.
    3. Immediate (_immediate): Returns the result immediately without resolving Promises or running the event loop. If the JS code returns a Promise, you must capture it as a crate::js_value::Promise and manually run the event loop using Runtime::await_event_loop to resolve it.
  7. How the `static_runtime!` macro works

    master

    The static_runtime! macro generates a module named after the identifier you provide. This module encapsulates a thread_local! static instance of StaticRuntime.

    Because it uses thread_local!, the runtime is unique to each thread. This ensures thread safety by preventing concurrent access to the same runtime instance from different threads. The macro handles the initialization logic via an internal init_options function and provides a high-level .with() method to execute closures against the underlying Runtime.

  8. Handle Asynchronous JavaScript with `call_function_async` and `Promise`

    master

    Rustyscript supports asynchronous JavaScript in two ways:

    1. Using call_function_async: This returns a future that resolves when the JS promise resolves. This is best used within a Tokio runtime.
    2. Using js_value::Promise: This returns a Promise handle immediately without waiting for the event loop. You can later resolve it using .into_value(&mut runtime) or convert it into a future using .into_future().
    // Method 1: Using call_function_async with Tokio
    let result: i32 = tokio_runtime.block_on(async {
        let handle = runtime.load_module_async(&module).await?;
        runtime.call_function_async(Some(&handle), "foo", json_args!()).await
    })?;
    
    // Method 2: Using js_value::Promise
    let result: Promise<i32> = runtime.call_function_immediate(Some(&handle), "foo", json_args!())?;
    let result = result.into_value(&mut runtime)?;
  9. Create V8 snapshots with SnapshotBuilder

    master

    Snapshots can be used to massively decrease the startup time of a Runtime instance (e.g., from 15ms to 3ms) by pre-loading extensions and modules into the runtime state.

    Requirements:

    • The snapshot_builder feature must be enabled in your Cargo.toml.
    • The runtime using the snapshot must have the exact same set of extensions and options as the runtime that created it.

    Workflow:

    1. Initialize a SnapshotBuilder with RuntimeOptions.
    2. Load modules using .with_module(&module) or evaluate setup code using .with_expression(expr).
    3. Call .finish() to consume the builder and receive a Box<[u8]> representing the snapshot.
    4. Save the resulting bytes to a file.
    5. Use include_bytes! to load the bytes at compile time and pass them to the startup_snapshot field in RuntimeOptions for future Runtime instances.
    use rustyscript::{SnapshotBuilder, Module, Error};
    use std::fs;
    
    fn main() -> Result<(), Error> {
        let module = Module::new("example.js", "export function example() { return 42; }");
        let snapshot = SnapshotBuilder::new(Default::default())?
           .with_module(&module)?
           .finish();
    
        // Save the snapshot to a file
        fs::write("snapshot.bin", snapshot)?;
    
        // To use the snapshot, load it with `include_bytes!` into the `RuntimeOptions` struct:
        // const STARTUP_SNAPSHOT: &[u8] = include_bytes!("snapshot.bin");
        // RuntimeOptions {
        //     startup_snapshot: Some(STARTUP_SNAPSHOT),
        //     ..Default::default()
        // };
    
        Ok(())
    }
  10. Initialize a new runtime with RuntimeBuilder

    master

    Use RuntimeBuilder to configure and instantiate a new Runtime. The builder follows a fluent interface pattern, allowing you to set various options such as timeouts, entrypoints, extensions, and feature-specific configurations (like web or crypto) before calling .build().

    Note that some methods are gated behind specific crate features (e.g., crypto, io, web, kv).

    use rustyscript::RuntimeBuilder;
    
    let runtime = RuntimeBuilder::new()
        .with_timeout(std::time::Duration::from_secs(5))
        .with_default_entrypoint("main".to_string())
        .with_cryto_seed(42)
        .build()
        .expect("Failed to create runtime");
  11. Migrate from import_with_type() to import()

    master

    The import_with_type method is deprecated as of version 0.8.0. The requested_module_type parameter is no longer used by the runtime. All implementations should migrate to the new import() method.

    Migration Example:

    // Old implementation:
    fn import_with_type(
        &mut self,
        specifier: &ModuleSpecifier,
        referrer: Option<&ModuleSpecifier>,
        is_dyn_import: bool,
        requested_module_type: RequestedModuleType,
    ) -> Option<Result<String, ModuleLoaderError>> {
        // your implementation
    }
    
    // New implementation:
    fn import(
        &mut self,
        specifier: &ModuleSpecifier,
        referrer: Option<&ModuleSpecifier>,
        is_dyn_import: bool,
    ) -> Option<Result<String, ModuleLoaderError>> {
        // same implementation, but do not use requested_module_type
    }
  12. Create a thread-local static runtime with `static_runtime!`

    master

    The static_runtime! macro allows you to create a safe, thread-local static instance of a Runtime. This is useful for accessing a single JavaScript runtime across different parts of your application without manually managing its lifecycle or passing it through every function.

    There are two ways to use the macro:

    1. Default Options: Use only the name to initialize the runtime with RuntimeOptions::default().
    2. Custom Options: Provide a block that returns a configured RuntimeOptions instance.

    Once created, you access the runtime using the .with() method provided by the generated module, passing a closure that performs operations on a mutable reference to the Runtime.

    use rustyscript::{RuntimeOptions, Error, static_runtime};
    use std::time::Duration;
    
    // 1. Create with default options
    static_runtime!(MY_DEFAULT_RUNTIME);
    
    // 2. Create with custom options
    static_runtime!(MY_CUSTOM_RUNTIME, {
       RuntimeOptions {
           timeout: Duration::from_secs(5),
           ..Default::default()
       }
    });
    
    fn main() -> Result<(), Error> {
        // Use the default runtime
        MY_DEFAULT_RUNTIME::with(|runtime| {
            runtime.eval::<()>("console.log('Hello, world!')")
        })?;
    
        // Use the custom runtime
        MY_CUSTOM_RUNTIME::with(|runtime| {
            runtime.eval::<()>("console.log('Hello, world!')")
        })?;
    
        Ok(())
    }