rscarson / rustyscript

Effortless JS integration for rust
MIT License
98 stars 10 forks source link

Effortless JS Integration for Rust

Crates.io Build Status License

This crate is meant to provide a quick and simple way to integrate a runtime javacript or typescript component from within rust.


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

let module = Module::new( "test.js", " rustyscript.register_entrypoint( (string, integer) => { console.log(Hello world: string=${string}, integer=${integer}); return 2; } ) " );

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

assert_eq!(value, 2);


Modules can also be loaded from the filesystem with `Module::load` or `Module::load_dir` if you want to collect all modules in a given directory.

----

If all you need is the result of a single javascript expression, you can use:
```rust
let result: i64 = rustyscript::evaluate("5 + 5").expect("The expression was invalid!");

Or to just import a single module for use:

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

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


A more detailed version of the crate's usage can be seen below, which breaks down the steps instead of using the one-liner Runtime::execute_module:

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;
    "
);

// 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, json_args!(2))?;

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

There are also _async and immediate versions of most runtime functions; _async functions return a future that resolves to the result of the operation, while immediate functions will make no attempt to wait for the event loop, making them suitable for using [js_value::Promise]

Rust functions can also be registered to be called from javascript:

use rustyscript::{ Runtime, Module, serde_json::Value };

let module = Module::new("test.js", " rustyscript.functions.foo(); ");
let mut runtime = Runtime::new(Default::default())?;
runtime.register_function("foo", |args| {
    if let Some(value) = args.get(0) {
        println!("called with: {}", value);
    }
    Ok(Value::Null)
})?;
runtime.load_module(&module)?;

Asynchronous JS can be called in 2 ways;

The first is to use the async keyword in JS, and then call the function using [Runtime::call_function_async]

use rustyscript::{ Runtime, Module };

let module = Module::new("test.js", "export async function foo() { return 5; }");
let mut runtime = Runtime::new(Default::default())?;

// The runtime has its own tokio runtime; you can get a handle to it with `tokio_runtime`
// You can also build the runtime with your own tokio runtime, see [Runtime::with_tokio_runtime]
let tokio_runtime = runtime.tokio_runtime();

let result: i32 = tokio_runtime.block_on(async {
    // Top-level await is supported - we can load modules asynchronously
    runtime.load_module_async(&module)?;

    // Call the function asynchronously
    runtime.call_function_async(None, "foo", vec![]).await
})?;

assert_eq!(result, 5);

The second is to use [js_value::Promise]

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

let module = Module::new("test.js", "export async function foo() { return 5; }");

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

// We call the function without waiting for the event loop to run, or for the promise to resolve
// This way we can store it and wait for it later, without blocking the event loop or borrowing the runtime
let result: Promise<i32> = runtime.call_function_immediate(None, "foo", vec![])?;

// We can then wait for the promise to resolve
// We can do so asynchronously, using [js_value::Promise::into_future]
// But we can also block the current thread:
let result = result.into_value(&mut runtime)?;
assert_eq!(result, 5);

- See [Runtime::register_async_function] for registering and calling async rust from JS
- See `examples/async_javascript.rs` for a more detailed example of using async JS

----

For better performance calling rust code, consider using an extension instead of a module - see the `runtime_extensions` example for details

----

A threaded worker can be used to run code in a separate thread, or to allow multiple concurrent runtimes.

the `worker` module provides a simple interface to create and interact with workers.
The `InnerWorker` trait can be implemented to provide custom worker behavior.

It also provides a default worker implementation that can be used without any additional setup:
```rust
use rustyscript::{Error, worker::{Worker, DefaultWorker, DefaultWorkerOptions}};
use std::time::Duration;

fn main() -> Result<(), Error> {
    let worker = DefaultWorker::new(DefaultWorkerOptions {
        default_entrypoint: None,
        timeout: Duration::from_secs(5),
    })?;

    let result: i32 = worker.eval("5 + 5".to_string())?;
    assert_eq!(result, 10);
    Ok(())
}

Utility Functions

These functions provide simple one-liner access to common features of this crate:

Crate features

The table below lists the available features for this crate. Features marked at Preserves Sandbox: NO break isolation between loaded JS modules and the host system. Use with caution.

Please note that the web feature will also enable fs_import and url_import, allowing arbitrary filesystem and network access for import statements

Feature Description Preserves Sandbox Dependencies
console Provides console.* functionality from JS yes deno_console
crypto Provides crypto.* functionality from JS yes deno_crypto, deno_webidl
url Provides the URL, and URLPattern APIs from within JS yes deno_webidl, deno_url
io Provides IO primitives such as stdio streams and abstraction over File System files. NO deno_io, rustyline, winapi, nix, libc, once_cell
web Provides the Event, TextEncoder, TextDecoder, File, Web Cryptography, and fetch APIs from within JS NO deno_webidl, deno_web, deno_crypto, deno_fetch, deno_url, deno_net
webstorage Provides the WebStorage API NO deno_webidl, deno_webstorage
default Provides only those extensions that preserve sandboxing yes deno_console, deno_crypto, deno_webidl, deno_url
no_extensions Disables all extensions to the JS runtime - you can still add your own extensions in this mode yes None
all Provides all available functionality NO deno_console, deno_webidl, deno_web, deno_net, deno_crypto, deno_fetch, deno_url
fs_import Enables importing arbitrary code from the filesystem through JS NO None
url_import Enables importing arbitrary code from network locations through JS NO reqwest
worker Enables access to the threaded worker API [rustyscript::worker] yes None
snapshot_builder Enables access to [rustyscript::SnapshotBuilder] yes None

There is also a snapshot_builder feature enables access to an alternative runtime used to create snapshots of the runtime for faster startup times. See [SnapshotBuilder] for more information


Please also check out @Bromeon/js_sandbox, another great crate in this niche