ferrosaur

crates.io documentation MIT/Apache-2.0 licensed

ferrosaur derives statically-typed Rust code — à la wasm-bindgen — for deno_core::JsRuntime to interface with your JavaScript code.

If you have:

// lib.js
export const slowFib = (n) =>
  n === 0 ? 0 : n === 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);

... and you write:

// lib.rs
use ferrosaur::js;

#[js(module("lib.js"))]
struct Math;

#[js(interface)]
impl Math {
    #[js(func)]
    fn slow_fib(&self, n: serde<usize>) -> serde<usize> {}
}

... then you get:

// let rt: &mut JsRuntime;
let lib = Math::main_module_init(rt).await?;
let fib = lib.slow_fib(42, rt)?;
assert_eq!(fib, 267914296);

tip

This is like the inverse of deno_core::op2:

  • #[op2] gives JavaScript programs easy access to your Rust implementation.
  • ferrosaur gives your Rust program easy access to JavaScript implementations.

License

This project is released under the Apache 2.0 License and the MIT License.

Examples

console.log

Introductory example using console.log — start from here!

calculator, fibonacci

Embedding ES modules for fun and portable programs.

ts, ts-blank-space

Workflows integrating NPM dependencies and a bundler.

screenshot of the ts-blank-space example

Output of the ts-blank-space example. Pretty-printing courtesy of bat.

Example: console.log

Source code for this example: examples/console

This example shows the gist of ferrosaur:

  • You can derive Rust types to represent JavaScript values.
  • You can derive Rust implementations to represent JavaScript interfaces.
  • You can compose these types and implementations to express JavaScript APIs of arbitrary shapes and complexities.

To run this example, run:

 cargo run --package example-console

Getting started

Everything starts with the js macro:

use ferrosaur::js;

Getting globalThis

Use #[js(global_this)] to derive a newtype struct that will hold a reference to globalThis:

#[js(global_this)]
struct Global;
// (this doesn't need to be named "Global")

Naming JavaScript values

Use #[js(value)] to derive a newtype struct that will hold an arbitrary JavaScript value:

/// the `Deno` namespace
#[js(value)]
struct Deno;

Declaring JavaScript APIs

Now that you have these "value types," use #[js(interface)] to describe them:

Properties

Use #[js(prop)] to derive a Rust function that will access a corresponding JavaScript property:

#[js(interface)]
impl Deno {
    #[js(prop)]
    fn pid(&self) -> serde<u32> {}
    // access the `Deno.pid` property
}
// if we were writing TypeScript, this would be:
interface Deno {
  readonly pid: number;
}

Data serialization

Thanks to serde_v8, Rust types that implement Serialize/DeserializeOwned can be passed to/from JavaScript. To indicate that a type T should be converted using serde_v8, write it as serde<T>, like the serde<u32> above.

Functions

Use #[js(func)] to derive a Rust function that will call a corresponding JavaScript function:

#[js(interface)]
impl Global {
    /// <https://docs.deno.com/api/web/~/btoa>
    #[js(func)]
    fn btoa(&self, to_encode: serde<&str>) -> serde<String> {}
}

Preserving object identities

But what if we want more than just the data? What if we would like to keep JavaScript objects and values around so that we can use them later? Here comes the fun part:

Thanks to the FromV8/ToV8 traits, any Rust type derived using this crate can also be passed from/to JavaScript (as can any type that implements those traits). This is the default conversion mechanism if you don't specify types as serde<T>.

Getting to console.log

Combining these attributes lets you statically declare JavaScript APIs of arbitrary shapes. For example, here's how you declare the existence of console.log:

#[js(interface)]
impl Global {
    // there's a `console` on `globalThis` ...
    #[js(prop)]
    fn console(&self) -> Console {}
}

#[js(value)]
struct Console;

#[js(interface)]
impl Console {
    // ... which has a `log` function
    #[js(func)]
    fn log(&self, message: serde<&str>) {}
    // note that we are only allowing a single `&str` message for now
}
// if we were writing TypeScript, this would be:
declare global {
  namespace globalThis {
    var console: Console;
  }
}
interface Console {
  log(message: string): void;
}

Running everything

Enough declaring! Let's finally run everything:

#[tokio::main]
async fn main() -> Result<()> {
    let rt: &mut JsRuntime = &mut deno()?;
    // all APIs derived using this crate require a &mut JsRuntime
    // here I'm using a preconfigured runtime, see examples/_runtime for more

    let global = Global::new(rt);
    let console = global.console(rt)?;
    let encoded = global.btoa(r#"{"alg":"HS256"}"#, rt)?;
    console.log(&encoded, rt)?;

    Ok(())
}

This will run the following equivalent JavaScript:

let console = globalThis.console;
let encoded = globalThis.btoa('{"alg":"HS256"}');
console.log(encoded);

Additional setup code for this example
use anyhow::Result;

use example_runtime::{deno, deno_core, JsRuntime};

tip

This page is generated from the example’s source code.

Example: calculator

Source code for this example: examples/calculator

This example showcases ES module loading and method chaining.

To run this example, run:

 cargo run --package example-calculator

Use #[js(module)] to embed an ECMAScript module into your Rust program:

use ferrosaur::js;

#[js(module("./main.js", fast))]
struct Main;

main.js exports a Calculator class:

#[js(interface)]
impl Main {
    #[js(new)]
    fn calculator(&self, value: serde<f64>) -> Calculator {}
    // export class Calculator ...
}
#[js(value)]
#[derive(Debug)]
struct Calculator;

#[js(interface)]
impl Calculator {
    #[js(func)]
    fn add(&self, value: serde<f64>) -> Self {}

    #[js(func)]
    fn sub(&self, value: serde<f64>) -> Self {}

    #[js(func)]
    fn mul(&self, value: serde<f64>) -> Self {}

    #[js(func)]
    fn div(&self, value: serde<f64>) -> Self {}

    #[js(func(Symbol(toPrimitive)))]
    fn print(&self) -> String {}

    #[js(prop)]
    fn value(&self) -> serde<f64> {}
}

Here's the main function:

#[tokio::main]
async fn main() -> Result<()> {
    let rt = &mut JsRuntime::new(RuntimeOptions::default());

    // Initialize the module:
    let main = Main::main_module_init(rt).await?;

    let calc = main
        .calculator(1.0, rt)?
        .add(2.0, rt)?
        .sub(3.0, rt)?
        .mul(4.0, rt)?
        .div(5.0, rt)?;
    // This is https://oeis.org/A261038

    println!("RPN: {}", calc.print(rt)?);
    // https://en.wikipedia.org/wiki/Reverse_Polish_notation

    assert_eq!(calc.value(rt)?, 0.0);

    Ok(())
}

Additional setup code for this example
use anyhow::Result;
use deno_core::{JsRuntime, RuntimeOptions};

tip

This page is generated from the example’s source code.

Example: Fibonacci

Source code for this example: examples/fibonacci

The example from Introduction, presented in full.

cargo run --package example-fibonacci

lib.js

/**
 * @param {number} n
 * @returns {number}
 */
export const slowFib = (n) =>
  n === 0 ? 0 : n === 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);

main.rs

use ferrosaur::js;

#[js(module("lib.js"))]
struct Math;

#[js(interface)]
impl Math {
    #[js(func)]
    fn slow_fib(&self, n: serde<usize>) -> serde<usize> {}
}

#[tokio::main]
async fn main() -> Result<()> {
    let rt = &mut JsRuntime::try_new(Default::default())?;

    let lib = Math::main_module_init(rt).await?;
    let fib = lib.slow_fib(42, rt)?;
    assert_eq!(fib, 267914296);

    Ok(())
}

use anyhow::Result;
use deno_core::JsRuntime;

Example: TypeScript

This example showcases a setup of multiple ES modules that can import from each other, as well as integration with JavaScript tooling during cargo build.

It embeds the typescript compiler in order to type check the example source code itself.

To run this example, run:

cargo run --package example-ts

important

This example requires the deno CLI to build.

screenshot of the example

Output of this example. Errors are expected since typescript as used in this example actually does not have access to these types.

tip

See also microsoft/typescript-go, which makes this whole thing kinda silly.

The setup

src/lib.rs

lib.rs does the following things:

  • Embed JS dependencies and export them as Rust structs.
  • Provide some reusable interface definitions and utility functions.

The ts-blank-space example reuses this module because they also require typescript.

Embedding typescript

use ferrosaur::js;

#[js(module("../dist/typescript.js", url("npm:typescript"), fast(unsafe_debug)))]
pub struct TypeScriptLib;

Embedding @typescript/vfs

#[js(module(
    "../dist/typescript-vfs.js",
    url("npm:@typescript/vfs"),
    fast(unsafe_debug)
))]
pub struct TypeScriptVfs;

This example additionally uses @typescript/vfs to setup the files necessary for typescript to type check. These files are also embedded into the program, albeit via a dedicated build step.

@typescript/vfs is the same system that enables the TypeScript playground to run in the browser.

Declaring interfaces for lib.ts

#[js(interface)]
pub trait Compiler {
    #[js(func)]
    fn create_program(&self, root: serde<HashMap<String, String>>) -> Program {}
}

#[js(value)]
pub struct Program;

#[js(interface)]
impl Program {
    #[js(func)]
    pub fn print_diagnostics(&self, colored: bool) -> serde<String> {}
}

#[js(interface)] is being used on a trait Compiler here. This turns Compiler into sort of a marker trait, and enables a form of duck typing.

It is essentially saying "any Rust type that implements Compiler will provide the create_program function." For example, the Example struct, which embeds lib.ts:

#[js(module("../dist/lib.js", fast))]
pub struct Example;

impl Compiler for Example {}

Of course, ferrosaur cannot actually verify such an implementation, so it is up to the programmer to guarantee that implementors of such traits actually provide the specified interfaces.

Helpers

inject_lib_dts

This function defines a few properties on globalThis to be used in the example.

Notably, it injects TYPESCRIPT_LIB. On the Rust side, this is the embedded declaration files. On the JavaScript side, this is used to create the virtual file system.

pub fn inject_lib_dts(rt: &mut JsRuntime) -> Result<()> {
    #[js(global_this)]
    struct Global;

    #[js(interface)]
    impl Global {
        #[js(set_index)]
        fn define_object(&self, name: serde<&str>, value: v8::Global<v8::Object>) {}

        #[js(set_index)]
        fn define_string(&self, name: serde<&str>, value: serde<&str>) {}
    }

    let global = Global::new(rt);

    let dts = {
        let scope = &mut rt.handle_scope();
        dts::dts(scope)?
    };

    global.define_object("TYPESCRIPT_LIB", dts, rt)?;
    global.define_string("CARGO_MANIFEST_DIR", env!("CARGO_MANIFEST_DIR"), rt)?;

    Ok(())
}
mod dts

lib.dts.rs is the generated file that embeds declaration files. See build.rs.

mod dts {
    include!(concat!(env!("OUT_DIR"), "/lib.dts.rs"));
}
Additional setup code
use std::collections::HashMap;

use anyhow::Result;

use example_runtime::deno_core::{self, JsRuntime};

src/lib.ts

lib.ts integrates typescript and implements functions used in this compiler. It is embedded by lib.rs.

File src/lib.ts
import ts from "npm:typescript";
import {
  createDefaultMapFromNodeModules,
  createSystem,
  createVirtualCompilerHost,
} from "npm:@typescript/vfs";

export function createProgram(root: Record<string, string>) {
  const options: ts.CompilerOptions = {
    strict: true,
    noEmit: true,
    lib: [ts.getDefaultLibFileName({ target: ts.ScriptTarget.ESNext })],
    target: ts.ScriptTarget.ESNext,
    module: ts.ModuleKind.ESNext,
    moduleResolution: ts.ModuleResolutionKind.Bundler,
    moduleDetection: ts.ModuleDetectionKind.Force,
  };

  let files: Map<string, string>;

  if (globalThis.TYPESCRIPT_LIB) {
    files = new Map();
    for (const [lib, dts] of Object.entries(globalThis.TYPESCRIPT_LIB)) {
      files.set(`/${lib}`, dts);
    }
  } else {
    files = createDefaultMapFromNodeModules(options, ts);
  }

  for (const [name, src] of Object.entries(root)) {
    files.set(name, src);
  }

  const { compilerHost } = createVirtualCompilerHost(createSystem(files), options, ts);

  const program = ts.createProgram(Object.keys(root), options, compilerHost);

  return {
    printDiagnostics: (colored = true) => {
      const diagnostics = [
        ...program.getGlobalDiagnostics(),
        ...program.getSyntacticDiagnostics(),
        ...program.getDeclarationDiagnostics(),
        ...program.getSemanticDiagnostics(),
      ];
      if (colored) {
        return ts.formatDiagnosticsWithColorAndContext(diagnostics, compilerHost);
      } else {
        return ts.formatDiagnostics(diagnostics, compilerHost);
      }
    },
  };
}

declare global {
  namespace globalThis {
    var TYPESCRIPT_LIB: Record<string, string> | undefined;
    var CARGO_MANIFEST_DIR: string | undefined;
  }
}

src/main.rs

#[tokio::main]
async fn main() -> Result<()> {
    let rt = &mut deno()?;

Initialize the modules:

    use example_ts::{inject_lib_dts, Example, TypeScriptLib, TypeScriptVfs};

    TypeScriptLib::side_module_init(rt).await?;
    TypeScriptVfs::side_module_init(rt).await?;

    let example = Example::main_module_init(rt).await?;

Prepare the VFS. This is very similar to the example in @typescript/vfs:

    inject_lib_dts(rt)?;

    let file = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("src/lib.ts")
        .pipe(std::fs::read_to_string)?;

    let root = {
        let mut map = HashMap::new();
        map.insert("src/lib.ts".into(), file);
        map
    };

Now, type check:

    let program = example.create_program(root, rt)?;

    let errors = program.print_diagnostics(true, rt)?;
    println!("{errors}");

    let errors = program.print_diagnostics(false, rt)?;
    insta::assert_snapshot!(errors, @r"
    src/lib.ts(1,16): error TS2307: Cannot find module 'npm:typescript' or its corresponding type declarations.
    src/lib.ts(6,8): error TS2307: Cannot find module 'npm:@typescript/vfs' or its corresponding type declarations.
    ");

    Ok(())
}
Additional setup code
use std::{collections::HashMap, path::Path};

use anyhow::Result;

use example_runtime::deno;
use example_ts::Compiler;
use tap::Pipe;

build.ts

build.ts compiles TypeScript files to JavaScript using esbuild at the time of cargo build.

Compiling is necessary because:

  • deno_core itself does not run TypeScript files.

  • The typescript lib is distributed in CommonJS. esbuild transforms it into ESM so that it can be imported.

File build.ts
import { build, relpath } from "../_runtime/src/lib.ts";

await build({
  entryPoints: [relpath("src/lib.ts", import.meta)],
  outdir: "dist",
  // since we are bundling for ESM, this is required for esbuild to consider
  // `typescript`, which is in CJS, to be importable
  mainFields: ["module", "main"],
  // these modules are made available at runtime
  external: ["npm:typescript", "npm:@typescript/vfs"],
  bundle: true,
});

await build({
  entryPoints: [
    relpath("src/deps/typescript.ts", import.meta),
    relpath("src/deps/typescript-vfs.ts", import.meta),
  ],
  outdir: "dist",
  external: ["fs", "path", "os", "inspector"],
  mainFields: ["module", "main"],
  bundle: true,
  minify: false,
  treeShaking: true,
});

build.rs

build.rs does a few things:

  • Run build.ts via deno to compile the .ts files used in this example.

  • Generate Rust code that will embed TypeScript's lib declarations into the program.

    These files contain definitions for ECMAScript language APIs, such as essential types like Promise, without which typescript will not know how to type check.

    The generated Rust functions are emitted under OUT_DIR and then included in lib.rs using include!.

File build.rs
use std::{
    path::Path,
    process::{Command, Stdio},
};

use anyhow::{bail, Context, Result};
use proc_macro2::TokenStream;
use quote::quote;

fn main() -> Result<()> {
    println!("cargo::rerun-if-changed=build.ts");

    let built = Command::new("deno")
        .args(["run", "--allow-all", "build.ts"])
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()
        .context("failed to bundle JavaScript resources, is Deno installed?")?
        .wait()?
        .success();

    if !built {
        bail!("failed to bundle JavaScript resources")
    }

    let lib_dts = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../node_modules/typescript/lib")
        .read_dir()?
        .filter_map(|entry| -> Option<TokenStream> {
            let path = entry.ok()?.path();
            let name = path.file_name()?.to_str()?;
            if !name.ends_with(".d.ts") {
                return None;
            }
            let text = std::fs::read_to_string(&path).ok()?;
            let path = path.to_string_lossy();
            println!("cargo::rerun-if-changed={path}");
            if text.is_ascii() {
                Some(quote! {
                    #[allow(long_running_const_eval)]
                    (ascii_str!(#name), FastString::from(ascii_str_include!(#path)))
                })
            } else {
                Some(quote! {
                    (ascii_str!(#name), FastString::from_static(include_str!(#path)))
                })
            }
        });

    let lib_dts = quote! {
        use ::example_runtime::deno_core::{
            anyhow::Result, v8,
            FastString, ascii_str, ascii_str_include,
        };

        pub fn dts(scope: &mut v8::HandleScope) -> Result<v8::Global<v8::Object>> {
            let obj = v8::Object::new(scope);
            let files = [ #(#lib_dts),* ];
            for (lib, dts) in files {
                let lib = lib.v8_string(scope)?;
                let dts = dts.v8_string(scope)?;
                obj.set(scope, lib.into(), dts.into()).unwrap();
            }
            Ok(v8::Global::new(scope, obj))
        }
    };

    std::fs::write(
        Path::new(&std::env::var("OUT_DIR")?).join("lib.dts.rs"),
        lib_dts.to_string(),
    )?;

    Ok(())
}

src/deps/*

These files re-export the respective libs for bundling.

File src/deps/typescript.ts
export { default } from "typescript";
File src/deps/typescript-vfs.ts
export * from "@typescript/vfs";

Example: ts-blank-space

ts-blank-space is a cool type-stripping TypeScript compiler. This example builds upon the ts example to run ts-blank-space.

tip

"Type-stripping" means erasing TypeScript specific syntax and features from the source code so that it can be directly executed as JavaScript.

To run this example, run:

cargo run --package example-ts-blank-space

important

This example requires the deno CLI to build.

screenshot of the example

Output of this example. Notice the extra whitespace in declarations.


Embed ts-blank-space

use ferrosaur::js;

#[js(module("../dist/main.js", fast))]
struct Main;

#[js(interface)]
impl Main {
    #[js(func(name = "default"))]
    fn blank_space<S: serde::Serialize>(&self, src: serde<S>) -> serde<String> {}
    // import { default as blank_space } from "../dist/main.js";
}

The file ../dist/main.js is emitted by esbuild during cargo build.

See build.ts which slightly processes the ts-blank-space library so that it can be used in this example.

Setup the runtime

#[tokio::main]
async fn main() -> Result<()> {
    let rt = &mut deno()?;

Initialize typescript

    use example_ts::{inject_lib_dts, TypeScriptLib, TypeScriptVfs};

    TypeScriptLib::side_module_init(rt).await?;

    TypeScriptVfs::side_module_init(rt).await?;

TypeScriptLib and TypeScriptVfs are provided by the ts example.

    inject_lib_dts(rt)?;

inject_lib_dts sets up some data that typescript requires in order to run. See build.rs in the ts example for more info.

Initialize ts-blank-space

    let ts = Main::main_module_init(rt).await?;

Run ts-blank-space on examples/ts/src/lib.ts

    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ts/src/lib.ts");

    let file = std::fs::read_to_string(&path)?;

    let js = ts.blank_space(&file, rt)?;

Evaluate the type-stripped result

    #[js(value(of_type(v8::Object)))]
    struct Example;

    let module: Example = {
        let url = Url::from_directory_path(env!("CARGO_MANIFEST_DIR"))
            .unwrap()
            .join("ad-hoc.js")?;
        let id = rt.load_side_es_module_from_code(&url, js.clone()).await?;
        rt.mod_evaluate(id).await?;
        rt.get_module_namespace(id)?.into()
    };

    use example_ts::Compiler;

    impl Compiler for Example {}

example_ts::Compiler describes the JavaScript APIs exported by lib.ts.

Here we are saying Example, our ad-hoc ES module produced by ts-blank-space, comforms to the interface as described by the Compiler trait, which is correct.

Pretty-print the type-stripped result

    use bat::PrettyPrinter;

    PrettyPrinter::new()
        .input_from_bytes(js.as_bytes())
        .language("javascript")
        .theme("GitHub")
        .print()?;

    println!();

PrettyPrinter courtesy of bat.

Use lib.ts to type check itself

    let root = HashMap::new().tap_mut(|map| drop(map.insert("src/lib.ts".into(), file)));

    let errors = module
        .create_program(root, rt)?
        .print_diagnostics(true, rt)?;

    println!("{errors}");

    {
        let mut settings = insta::Settings::clone_current();
        settings.set_description("script compiled with ts-blank-space");
        settings.set_prepend_module_to_snapshot(false);
        settings.set_snapshot_path("../tests/snapshots");
        settings.bind(|| insta::assert_snapshot!(js));
    }

    Ok(())
}
Additional setup code
use std::{collections::HashMap, path::Path};

use anyhow::Result;
use tap::Tap;

use example_runtime::{
    deno,
    deno_core::{self, serde, url::Url},
};

Reference

Entrypoints

Newtypes

  • js(value), give arbitrary JavaScript values a Rust type.

Interfaces

#[js(global_this)]

Use #[js(global_this)] for access to the JavaScript globalThis object:

use ferrosaur::js;
// use it on a unit struct:
#[js(global_this)]
struct Global;
// (struct name does not need to be `Global`)

Call the new method to initialize it:

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;

let rt = &mut fixture::deno()?;

// let rt: &mut JsRuntime;
let global = Global::new(rt);

Ok::<_, anyhow::Error>(())

After this, you can use #[js(interface)] to further derive access to properties, functions, and more, on globalThis:

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;

#[js(interface)]
impl Global {
    #[js(func)]
    fn atob(&self, to_decode: String) -> String {}
}

Ok::<_, anyhow::Error>(())

The atob function.

Derived APIs

Methods

pub fn new(rt: &mut JsRuntime) -> Self

Create a handle to the globalThis object from the given JsRuntime.

Trait implementations

impl AsRef<v8::Global<v8::Object>> for Global

impl<'a> ToV8<'a> for Global

impl<'a> ToV8<'a> for &'_ Global

#[js(module)]

Use #[js(module)] to embed an ECMAScript module in the program.

use ferrosaur::js;
#[js(module("../examples/js/mod.js"))]
pub struct Module;

The path is relative to the current file (it has the same usage as the include_str! macro).

Call the main_module_init or side_module_init method to initialize it as a main module or side module in the given JsRuntime.

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::modules::Main as MainModule;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;

let main = MainModule::main_module_init(rt).await?;

Ok(())
}

note

For the difference between a main module and a side module, see documentation for the corresponding JsRuntime methods:

After this, you can use #[js(interface)] to further derive access to items exported from your module. For example, if you have:

export const answer = "42";

Then you can write:

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::modules::{Main as MainModule};

#[js(interface)]
impl MainModule {
    #[js(prop)]
    fn answer(&self) -> String {}
}

Ok::<_, anyhow::Error>(())
Sections

Option fast

use ferrosaur::js;
#[js(module("../examples/js/mod.js", fast))]
pub struct Module;

Without the fast option, JavaScript source code is embedded using include_str!.

With the fast option, JavaScript source code is embedded using deno_core::ascii_str_include! instead.

The JS source file must be in 7-bit ASCII. It is a compile-time error if this does not hold.

note

For what it means for the string to be "fast," from deno_core::FastStaticString:

A static string that is compile-time checked to be ASCII and is stored in the most efficient possible way to create V8 strings.

fast(unsafe_debug)

use ferrosaur::js;
#[js(module("../examples/js/mod.js", fast(unsafe_debug)))]
pub struct Module;

Like fast, except for debug builds, at compile time, unsafely embed JS code as FastStaticStrings without checking it is in 7-bit ASCII.

For release builds, this behaves the same as fast. Under the hood, this uses the #[cfg(debug_assertions)] condition.

The behavior is undefined if the file is not actually in ASCII.

This could be useful if the source file you are trying to embed is very large, in which case the compile-time checking could take a very long time.

Option url(...)

Control the value of import.meta.url within the module:

url(preserve)

use ferrosaur::js;
#[js(module("../examples/js/mod.js", url(preserve)))]
pub struct Module;

import.meta.url will be file:/// followed by the relative path from CARGO_MANIFEST_DIR to the embedded JS file. This is the default behavior if the url(...) option is not specified.

Example
JavaScript file<CARGO_MANIFEST_DIR>/src/js/index.js
import.meta.url"file:///src/js/index.js"

url(cwd)

use ferrosaur::js;
#[js(module("../examples/js/mod.js", url(cwd)))]
pub struct Module;

import.meta.url will be file:// + std::env::current_dir() at runtime + a name generated from the file's relative path.

This essentially gives import.meta.url the same real working directory as the program itself.

Example
JavaScript file<CARGO_MANIFEST_DIR>/src/js/index.js
current_dir()/path/to/cwd
import.meta.url"file:///path/to/cwd/-src-js-index.js"

url("...")

use ferrosaur::js;
#[js(module("../examples/js/mod.js", url("...")))]
pub struct Module;

Use a custom import.meta.url.

The string must be parsable by url::Url. It is a runtime error if the URL is not parsable. Notably, this means you cannot use a bare identifier like "package" as you would with Node.

For example, url("npm:lodash") sets import.meta.url to "npm:lodash". Other modules in the same JsRuntime will then be able to import this module using import ... from "npm:lodash".

Derived APIs

Methods

pub async fn main_module_init(rt: &mut JsRuntime) -> anyhow::Result<Self>

Initialize the embedded ES module as a main module in the given JsRuntime.

pub async fn side_module_init(rt: &mut JsRuntime) -> anyhow::Result<Self>

Initialize the embedded ES module as a side module in the given JsRuntime.

pub fn module_url() -> anyhow::Result<ModuleSpecifier>

Get the import.meta.url within the module (controllable through the url(...) option).

Associated items

pub const MODULE_SRC: &str or FastStaticString

The embedded JS source code as a constant.

Trait implementations

impl AsRef<v8::Global<v8::Object>> for Module

impl<'a> ToV8<'a> for Module

impl<'a> ToV8<'a> for &'_ Module

#[js(value)]

Use #[js(value)] to represent arbitrary JavaScript values as types in Rust's type system.

use ferrosaur::js;
#[js(value)]
struct Lorem;

The derived types are not intended to be instantiated directly. Instead, you can return them from APIs that you declare on js(global_this), a js(module), or another js(value). To declare APIs, use js(interface).

Illustrative example: The To-do List
use ferrosaur::js;

#[js(module("../examples/js/mod.js"))]
struct Module;

#[js(interface)]
impl Module {
    #[js(prop)]
    fn todos(&self) -> TodoList {}
}

#[js(value)]
struct TodoList;

#[js(interface)]
impl TodoList {
    #[js(func)]
    fn create(&self) -> Todo {}
}

#[js(value)]
struct Todo;

#[js(interface)]
impl Todo {
    #[js(prop(with_setter))]
    fn done(&self) -> bool {}
}

tip

Types derived with js(value), js(module), and js(global_this) are essentially newtypes around V8 types.

Sections

Option of_type(T)

By default, js(value) generates a struct that is:

use deno_core::v8;
struct Lorem(v8::Global<v8::Value>);
//                          ^ inner type

By using the of_type option, you can use some other V8 data types for the inner type. For example:

use ferrosaur::js;
#[js(value(of_type(v8::Promise)))]
struct Response;
// struct Response(v8::Global<v8::Promise>);

It should make sense for the data type T to be placed in a v8::Global. In particular, this means v8::Local<v8::Value> implements TryInto<v8::Local<T>>.

This could be useful if you want to have simple runtime type checking for your types. For example, given the Response type above, if a JS function is supposed to return a Response, i.e. a Promise, but it returns undefined, then the corresponding Rust function returns Err(...) instead of Ok(Response).

Note that this is "type checking" only in so far as v8 can try-convert between different V8 types; this is not TypeScript-style structural typing.

note

See Specifying types for more info on how you can specify types when using this crate.

Derived APIs

In the signatures below,

  • Type is the type that you apply js(value) to;
  • <T> is the one of the v8::* data types. By default, this is v8::Value, but you can control it using the of_type option.

Trait implementations

impl AsRef<v8::Global<T>> for Type

impl From<v8::Global<T>> for Type

impl From<Type> for v8::Global<T>

impl<'a> FromV8<'a> for Type

impl<'a> ToV8<'a> for Type

impl<'a> ToV8<'a> for &'_ Type

#[js(interface)]

Use #[js(interface)] to declare:

You can use js(interface) on any type derived using this crate, such as a js(value) or a js(module). You can even use it on traits, see the ts example.

use ferrosaur::js;
// First, declare a type:
#[js(value)]
struct CowSay;

// Then, declare its APIs:
#[js(interface)]
impl CowSay {
    #[js(prop)]
    fn moo(&self) -> String {}
}

Example: The To-do List

Let's say you have the following JavaScript:

export const todos = todoList();

function todoList() {
  const items = [];

  const create = () => {
    const todo = new Todo();
    items.push(todo);
    return todo;
  };

  return { create };
}

class Todo {
  done = false;
}
../examples/js/mod.js

Expressed in TypeScript declarations, this is:

export declare const todos: TodoList;

interface TodoList {
  create: () => Todo;
}

interface Todo {
  done: boolean;
}
../examples/js/mod.d.ts

You can then express this in Rust as:

use ferrosaur::js;

#[js(module("../examples/js/mod.js"))]
struct Module;

#[js(interface)]
impl Module {
    #[js(prop)]
    fn todos(&self) -> TodoList {}
}

#[js(value)]
struct TodoList;

#[js(interface)]
impl TodoList {
    #[js(func)]
    fn create(&self) -> Todo {}
}

#[js(value)]
struct Todo;

#[js(interface)]
impl Todo {
    #[js(prop(with_setter))]
    fn done(&self) -> bool {}
}

#[js(prop)]

Use #[js(prop)] for access to JavaScript properties.

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;

#[js(value)]
struct Foo;

#[js(interface)]
impl Foo {
    #[js(prop)]
    fn bar(&self) -> serde<f64> {}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;

let foo: Foo = fixture::eval_value("({ bar: 42 })", rt)?;
// let foo: Foo;

assert_eq!(foo.bar(rt)?, 42.0);

Ok(())
}
// Expressed in TypeScript:
interface Foo {
  bar: number;
}
declare let foo: Foo;
assert(foo.bar === 42);

The generated function has the signature:

The return type indicates the expected type of the property, which must implement either FromV8 (the default) or DeserializeOwned (if written as serde<...>).

note

See Specifying types for more info on how you can specify types when using this crate.

Implicitly, the property name is the Rust function name converted to camelCase, but you can override this using the name or Symbol option.

Sections

Option name = "..."

Use the specified string as key when accessing the JS property.

You can also write name(propertyKey) if the key is identifier-like.

use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
    #[js(prop(name = "some bar"))]
    fn some_bar(&self) -> serde<Option<u32>> {}
}
// Expressed in TypeScript:
interface Foo {
  "some bar": number | null;
}

Option Symbol(...)

Use the specified well-known Symbol when accessing the JS property. The Symbol should be in camel case (i.e. the same as in JS).

use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
    #[js(prop(Symbol(toStringTag)))]
    fn to_string_tag(&self) -> serde<String> {}
}
// Expressed in TypeScript:
interface Foo {
  [Symbol.toStringTag]: string;
}

Option with_setter

Generate a setter function in addition to a getter function.

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;

#[js(value)]
struct Foo;

#[js(interface)]
impl Foo {
    #[js(prop(with_setter))]
    fn bar(&self) -> serde<f64> {}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;

let foo: Foo = fixture::eval_value("({ bar: 41 })", rt)?;
// let foo: Foo;

assert_eq!(foo.bar(rt)?, 41.0);
foo.set_bar(42.0, rt)?;
assert_eq!(foo.bar(rt)?, 42.0);

Ok(())
}
// Expressed in TypeScript:
interface Foo {
  bar: number;
}
declare let foo: Foo;
assert(foo.bar === 41);
foo.bar = 42;
assert(foo.bar === 42);

The generated function has the signature:

where value has the same type as the getter's declared return type.

#[js(func)]

Use #[js(func)] for calling JavaScript functions.

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;

#[js(value)]
struct Console;

#[js(interface)]
impl Console {
    #[js(func)]
    fn log(&self, message: serde<&str>) {}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;

let console: Console = fixture::eval_value("({ log: () => {} })", rt)?;
// let console: Console;

console.log("🦀 + 🦕", rt)?;

Ok(())
}
// Expressed in TypeScript:
interface Console {
  log(message: string): void;
}
declare let console: Console;
console.log("🦀 + 🦕");

The generated function has the signature:

Argument types must implement either ToV8 (the default) or Serialize (if written as serde<T>). The return type must implement either FromV8 or DeserializeOwned.

note

See Specifying types for more info on how you can specify types when using this crate.

Implicitly, the function name is the Rust function name converted to camelCase, but you can override this using the name or Symbol option.

Sections

async functions

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;

#[js(interface)]
impl Global {
    #[js(prop(name(Promise)))]
    fn promise_constructor(&self) -> PromiseConstructor {}
}

#[js(value)]
struct PromiseConstructor;

#[js(interface)]
impl PromiseConstructor {
    #[js(func)]
    async fn resolve(&self, value: serde<u64>) -> serde<u64> {}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;

let global = Global::new(rt);
#[allow(non_snake_case)]
let Promise = global.promise_constructor(rt)?;
// let Promise: PromiseConstructor;

assert_eq!(Promise.resolve(42, rt).await?, 42);

Ok(())
}
// Expressed in TypeScript:
interface PromiseConstructor {
  resolve(value: number): Promise<number>;
}
declare let Promise: PromiseConstructor;
assert((await Promise.resolve(42)) === 42);

The generated function will be an async fn. The returned Future will be ready once the underlying JS value fulfills.

Internally, this calls JsRuntime::with_event_loop_promise, which means you don't need to drive the event loop separately.

this argument

By default, the JS function will receive the object from which the function is accessed (i.e. &self) as its this value. Expressed in TypeScript, the way your function is invoked is roughly:

interface Foo {
  bar: () => void;
}
declare const foo: Foo;
const bar = foo.bar;
bar.call(foo);

See Function.prototype.call

Alternatively, you can explicitly declare the type of this using the second argument:

this: undefined

use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
    #[js(func)]
    fn bar(&self, this: undefined) {}
}
// Expressed in TypeScript:
const bar = foo.bar;
bar.call(undefined);

The JS function will receive a this value of undefined when called.

The resulting Rust function will not have a this argument.

this: [SomeType]

use ferrosaur::js;
#[js(value)]
struct Foo;
#[js(interface)]
impl Foo {
    #[js(func)]
    fn bar(&self, this: Baz) {}
}
#[js(value)]
struct Baz;
// Expressed in TypeScript:
interface Foo {
  bar: (this: Baz) => void;
}
const bar = foo.bar;
declare const baz: Baz;
bar.call(baz);

The resulting Rust function will have an explicit this argument, for which you will supply a value at call time; the argument will be subject to the same type conversion rules as other arguments.

Spread arguments

To indicate an argument should be flattened using the spread syntax at call time, prefix the argument name with .. (2 dots):

use deno_core::v8;
use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;
use fixture::items::global::Global;

#[js(value)]
struct Console;

#[js(interface)]
impl Console {
    #[js(func(name(log)))]
    pub fn log(&self, ..values: Vec<String>) {}
    //                ^
}
// let rt: &mut JsRuntime;
let rt = &mut fixture::deno()?;
// let console: Console;
let console: Console = fixture::eval_value("({ log: () => {} })", rt)?;
console.log(vec!["🦀".into(), "🦕".into()], rt)?;

Ok::<_, anyhow::Error>(())
// Expressed in TypeScript:
interface Console {
  log: (...values: string[]) => void;
}
declare const console: Console;
console.log(...["🦀", "🦕"]);

On the Rust side, a spread argument of type A must implement Iterator<Item = T>, where T must implement either ToV8 (the default) or Serialize (if written as serde<T>). When calling the function, pass the argument using normal syntax.

note

See Specifying types for more info on how you can specify types when using this crate.

tip

The syntax ..args: A is abusing the range pattern syntax, which is syntactically valid in function arguments.

Option name = "..."

Use the specified string as key when accessing the function. This has the same usage as js(prop(name)).

You can also write name(propertyKey) if the key is identifier-like.

use ferrosaur::js;
#[js(value)]
struct Date;
#[js(interface)]
impl Date {
    #[js(func(name(toISOString)))]
    fn to_iso_string(&self) -> serde<String> {}
}

Option Symbol(...)

Use the specified well-known Symbol when accessing the function. This has the same usage as js(prop(Symbol)).

use ferrosaur::js;
use deno_core::serde_json;
#[js(value)]
struct Date;
#[js(interface)]
impl Date {
    #[js(func(Symbol(toPrimitive)))]
    fn to_primitive(&self, hint: serde<&str>) -> serde<serde_json::Value> {}
}

#[js(new)]

Use #[js(func)] for invoking JavaScript constructors.

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;

#[js(global_this)]
struct Global;

#[js(interface)]
impl Global {
    #[js(new)]
    fn date(&self, timestamp: serde<f64>) -> Date {}
}

#[js(value)]
struct Date;
#[js(interface)]
impl Date {
    #[js(func(name(toISOString)))]
    fn to_iso_string(&self) -> serde<String> {}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;

let global = Global::new(rt);
// let global: Global;

let date = global.date(0.0, rt)?;
assert_eq!(date.to_iso_string(rt)?, "1970-01-01T00:00:00.000Z");
// struct Date;
// impl Date { ... }

Ok(())
}
// Expressed in TypeScript:
let date = new Date(0);
assert(date.toISOString() === "1970-01-01T00:00:00.000Z");

The generated function has the signature:

js(new) accepts the same function signature format as js(func), except constructors cannot be async.

Implicitly, the class name is the name of the return type (with case preserved). If the return type name cannot be used, such as if it is not a simple identifier, or if you would like to override it, you can use the class option.

Sections

Note on return type

Note that the return type of the method is the JavaScript type that will be constructed, whereas Self represents the JavaScript object from which the constructor is accessed (such as a module or globalThis).

In other words, the following usage is almost never what you want:

use ferrosaur::js;
// 🔴 these are almost never what you want
#[js(interface)]
impl Rectangle {
    #[js(new)]
    fn new() -> Self {}
    // or
    #[js(new)]
    fn new(&self) -> Self {}
}

Instead, you likely want to write:

use ferrosaur::js;
#[js(value)]
struct Shapes;
#[js(interface)]
impl Shapes {
    #[js(new)]
    fn rectangle(&self) -> Rectangle {}
}
#[js(value)]
struct Rectangle;
// Expressed in TypeScript:
declare const shapes: Shapes;
const Rectangle = shapes.Rectangle;
new Rectangle();

Option class(...)

Use the specified string as key when accessing the constructor, instead of using the name of the return type. This has the same usage as js(prop(name)).

use ferrosaur::js;
#[js(value)]
struct Window;
#[js(interface)]
impl Window {
    #[js(new(class(XMLHttpRequest)))]
    fn xml_http_request(&self) -> XmlHttpRequest {}
}
#[js(value)]
struct XmlHttpRequest;

#[js(get_index)], #[js(set_index)]

Use #[js(get_index)] and #[js(set_index)] for dynamic property access and update (i.e. the obj[prop] bracket notation).

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;

#[js(value)]
struct Record;

#[js(interface)]
impl Record {
    #[js(get_index)]
    fn get(&self, k: serde<&str>) -> String {}
    #[js(set_index)]
    fn set(&self, k: serde<&str>, v: serde<&str>) {}
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;
// let rt: &mut JsRuntime;

let record: Record = fixture::eval_value("({})", rt)?;
// let record: Record;

let key = "foo";
record.set(key, "bar", rt)?;
assert_eq!(record.get(key, rt)?, "bar");

Ok(())
}
// Expressed in TypeScript:
declare const record: Record<string, string>;
const key = "foo";
record[key] = "bar";
assert(record[key] === "bar");

Functions decorated with js(get_index) must have 2 arguments: &self and the key to get, as well as a return type: the type of the value.

Functions decorated with js(set_index) must have 3 arguments: &self, the key, and the value to set.

Argument types must implement either ToV8 (the default) or Serialize (if written as serde<T>). The return type must implement either FromV8 or DeserializeOwned.

note

See Specifying types for more info on how you can specify types when using this crate.

#[js(callable)]

Use #[js(callable)] to store JavaScript functions as values while declaring their signatures.

This could be used to describe functions that return other functions:

use ferrosaur::js;

#[js(value)]
struct Logger;

#[js(callable)]
impl Logger {
    fn call(&self, message: serde<&str>) {}
}

#[js(value)]
struct Logging;

#[js(interface)]
impl Logging {
    #[js(func)]
    fn with_prefix(&self, prefix: serde<&str>) -> Logger {}
}
// Expressed in TypeScript:

interface Logger {
  (message: string): void;
}

interface Logging {
  with_prefix: (prefix: string) => Logging;
  //           (prefix: string) => (message: string) => void
}

Use js(callable) on an impl block, which must contain a single item, a function named call, whose signature follows the same usage as js(func).

tip

JavaScript does not have a proper "callable interface." This is named callable to distinguish it from js(func), which is for describing named functions accessible from an object.

#[js(iterator)]

Use #[js(iterator)] to represent and interact with objects conforming to the iterator protocol.

use ferrosaur::js;

#[path = "../../../crates/ferrosaur/tests/fixture/mod.rs"]
mod fixture;

#[js(value)]
struct MapEntries;

#[js(iterator)]
impl MapEntries {
    type Item = serde<(String, String)>;
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = &mut fixture::deno()?;

// let rt: &mut JsRuntime;
let mut entries: MapEntries = rt
    .execute_script("eval", "new Map([['foo', 'bar']]).entries()")?
    .into();

let Ok(Some((k, v))) = entries.next(rt) else { panic!() };
assert_eq!(k, "foo");
assert_eq!(v, "bar");

Ok(())
}
// Expressed in TypeScript:
const entries = new Map([["foo", "bar"]]).entries();
const [k, v] = entries.next().value!;
assert(k === "foo");
assert(v === "bar");

Use js(callable) on an impl block. The impl must contain a single item, type Item = T, where T must implement either FromV8 (the default) or DeserializeOwned (if written as serde<T>).

note

See Specifying types for more info on how you can specify types when using this crate.

Derived APIs

note

Note that js(iterator) types do not implement the Iterator trait: both of the below provided APIs require passing in a JsRuntime at call time.

Methods

pub fn next(&mut self, &mut JsRuntime) -> anyhow::Result<Option<T>>

Receive the next value from the iterator:

  • Ok(Some(T)) if the next value is successfully returned;
  • Ok(None) if the iterator has been exhausted;
  • Err(...) if there was an error while advancing the iterator.

More specifically, the returned value depends on the value returned by the underlying next() function:

JavaScript valueRust value
{ done?: false, value: T }Ok(Some(T))
{ done: true, value: T }Ok(Some(T))
{ done: true, value?: undefined }Ok(None)
Exception caughtErr(...)

pub fn into_iter<'a>(self, rt: &'a mut JsRuntime)
-> impl Iterator<Item = anyhow::Result<T>> + use<'a>

Get a proper Rust Iterator, which produces anyhow::Result<T>.

This enables you to use all the capabilities of a Rust iterator, such as collect(), as well as using it in a for loop.

note

Due to lifetime restrictions, the returned iterator mutably borrows the JsRuntime for the entire duration of the iteration. This will prevent you from using it on the produced items until the iterator is dropped.

To be able to use the runtime during iteration, manually call next(&mut JsRuntime).

Specifying types

To be able to interact with JavaScript, code generated by this crate must convert between Rust types and V8 data types. Function arguments and return types must implement specific conversion traits.

Sections

ToV8 and FromV8

ToV8 and FromV8 are deno_core's builtin conversion traits.

By default:

  • Function arguments must implement ToV8;

  • Function return types, property accessor return types, and iterator item types must implement FromV8.

In addition to existing implementors,

Serialize and DeserializeOwned

Alternatively, you can opt in to data conversion using serde_v8. To do so, wrap the type in serde<...>:

use ferrosaur::js;

#[js(value)]
struct Foo;

use deno_core::serde::{Serialize, de::DeserializeOwned};

#[js(interface)]
impl Foo {
    #[js(func)]
    fn bar<T, U>(&self, baz: serde<T>) -> serde<U>
    where
        T: Serialize,
        U: DeserializeOwned,
    {}
}

In this case:

Common pitfalls

the trait bound ToV8<'_>/FromV8<'_> is not satisfied

Function argument and return types for js(func), js(prop), etc., must implement specific traits for data to be able to pass to/from JavaScript.

By default, arguments must implement ToV8, and return types must implement FromV8.

If you would like to serialize data using serde instead, you can opt into this behavior by rewriting a type T as serde<T>:

  #[js(interface)]
  impl Foo {
      #[js(func)]
-     fn bar(&self, baz: Baz) {}
+     fn bar(&self, baz: serde<Baz>) {}
  }

See Specifying types for additional information on data conversion.

use of undeclared crate or module deno_core

The #[js] macro generates code that references the deno_core crate, but it does not generate a use deno_core statement. Instead, it assume that deno_core is in scope where the macro is used.

If your crate directly depends on deno_core, then the macro will work without extra steps. If your crate does not directly depend on deno_core, for example, if you are using deno_runtime instead, then you can manually introduce deno_core into scope:

+ use deno_runtime::deno_core;

  #[js(global_this)]
  struct Global;

CHANGELOG

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

0.1.1

Other changes

Documentation

0.1.0 - 2025-05-07

Initial release!

note

This page is included only for link validation during build.

ferrosaur

crates.io documentation MIT/Apache-2.0 licensed

So you use deno_core, and you want to call JavaScript from Rust.

// If you have: lib.js
export const slowFib = (n) =>
  n === 0 ? 0 : n === 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
// and you write: lib.rs
use ferrosaur::js;

#[js(module("lib.js"))]
struct Math;

#[js(interface)]
impl Math {
    #[js(func)]
    fn slow_fib(&self, n: serde<usize>) -> serde<usize> {}
}
// Then you get:
// let rt: &mut JsRuntime;
let lib = Math::main_module_init(rt).await?;
let fib = lib.slow_fib(42, rt)?;
assert_eq!(fib, 267914296);

ferrosaur derives types and implementations, à la wasm-bindgen, that you can use with your favorite JsRuntime.

Documentation

Read the book!

You may be looking for:

License

This project is released under the Apache 2.0 License and the MIT License.

note

This page is included only for link validation during build.

Example: calculator

See documentation at https://tonywu6.github.io/ferrosaur/examples/calculator.

note

This page is included only for link validation during build.

Example: console.log

See documentation at https://tonywu6.github.io/ferrosaur/examples/console.

note

This page is included only for link validation during build.

Example: Fibonacci

This is the example from crate README, presented in full.

See documentation at https://tonywu6.github.io/ferrosaur/examples/fibonacci.