diff --git a/Cargo.lock b/Cargo.lock index 44ee6a47e..bd76ddee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,6 +247,14 @@ dependencies = [ "generic-array", ] +[[package]] +name = "build-your-own-js-snapshot" +version = "0.1.0" +dependencies = [ + "deno_core", + "tokio", +] + [[package]] name = "bumpalo" version = "3.16.0" diff --git a/Cargo.toml b/Cargo.toml index c0c8c30b3..62a1cccca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "core", + "core/examples/snapshot", "dcore", "ops", "ops/compile_test_runner", diff --git a/core/examples/snapshot/Cargo.toml b/core/examples/snapshot/Cargo.toml new file mode 100644 index 000000000..03a76ce86 --- /dev/null +++ b/core/examples/snapshot/Cargo.toml @@ -0,0 +1,19 @@ +# Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +# Note: Since Cargo "example" targets don't discover/use `build.rs` files, this +# example is a member of the root `deno_core` workspace. That means it will +# compile with `cargo build` in the root, so that this example/documentation +# stays in-sync with development. + +[package] +name = "build-your-own-js-snapshot" +version = "0.1.0" +edition = "2021" +build = "build.rs" + + +[dependencies] +deno_core.workspace = true +tokio.workspace = true + +[build-dependencies] +deno_core.workspace = true diff --git a/core/examples/snapshot/README.md b/core/examples/snapshot/README.md new file mode 100644 index 000000000..af7653259 --- /dev/null +++ b/core/examples/snapshot/README.md @@ -0,0 +1,32 @@ +# Snapshot Example + +This example roughly follows the blog post +[Roll Your Own JavaScript Runtime: Part 3][blog] to create a `JsRuntime` with an +embedded startup snapshot. + +That blog post and the two that preceded it were no longer accurate. By +including this example in the repository, it will continually be built, so it +will hopefully stay up-to-date. + +## Running + +The example can be run by changing to the `core/examples/snapshot` directory and +running `cargo run`. + +## Differences + +Differences from those blog posts: + +- The `create_snapshot()` API has changed in various ways. +- New API features for extensions: + - `#[op2]` ([read more][op2]) + - `extension!(...)` macro replaces `Extension::builder()` + - ESM-based extensions. + +Missing features vs. those blog posts: + +- Does not implement [TsModuleLoader], to keep this example more concise. + +[blog]: https://deno.com/blog/roll-your-own-javascript-runtime-pt3#creating-a-snapshot-in-buildrs +[op2]: https://github.com/denoland/deno_core/tree/main/ops/op2#readme +[TsModuleLoader]: https://deno.com/blog/roll-your-own-javascript-runtime-pt2#supporting-typescript diff --git a/core/examples/snapshot/build.rs b/core/examples/snapshot/build.rs new file mode 100644 index 000000000..1258f2a69 --- /dev/null +++ b/core/examples/snapshot/build.rs @@ -0,0 +1,42 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_core::{ + extension, + snapshot::{create_snapshot, CreateSnapshotOptions}, +}; +use std::path::PathBuf; +use std::{env, fs}; + +fn main() { + extension!( + runjs_extension, + // Must specify an entrypoint so that our module gets loaded while snapshotting: + esm_entry_point = "my:runtime", + esm = [ + dir "src", + "my:runtime" = "runtime.js", + ], + ); + + let options = CreateSnapshotOptions { + cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), + startup_snapshot: None, + extensions: vec![runjs_extension::init_ops_and_esm()], + with_runtime_cb: None, + skip_op_registration: false, + extension_transpiler: None, + }; + let warmup_script = None; + + let snapshot = + create_snapshot(options, warmup_script).expect("Error creating snapshot"); + + // Save the snapshot for use by our source code: + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let file_path = out_dir.join("RUNJS_SNAPSHOT.bin"); + fs::write(file_path, snapshot.output).expect("Failed to write snapshot"); + + // Let cargo know that builds depend on these files: + for path in snapshot.files_loaded_during_snapshot { + println!("cargo:rerun-if-changed={}", path.display()); + } +} diff --git a/core/examples/snapshot/example.js b/core/examples/snapshot/example.js new file mode 100644 index 000000000..800fd3eba --- /dev/null +++ b/core/examples/snapshot/example.js @@ -0,0 +1,6 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Run this script with `cargo run`. + +import { callRust } from "my:runtime"; + +callRust("Hello from example.js"); diff --git a/core/examples/snapshot/src/main.rs b/core/examples/snapshot/src/main.rs new file mode 100644 index 000000000..83f865d0c --- /dev/null +++ b/core/examples/snapshot/src/main.rs @@ -0,0 +1,47 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::{env::current_dir, rc::Rc}; + +use deno_core::{ + error::AnyError, extension, op2, FsModuleLoader, JsRuntime, + PollEventLoopOptions, RuntimeOptions, +}; + +fn main() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + if let Err(error) = runtime.block_on(run_js("./example.js")) { + eprintln!("error: {}", error); + } +} + +#[op2(fast)] +fn op_call_rust(#[string] value: String) { + println!("Received this value from JS: {value}"); +} + +extension!(runjs_extension, ops = [op_call_rust,],); + +async fn run_js(file_path: &str) -> Result<(), AnyError> { + let cwd = current_dir()?; + let main_module = deno_core::resolve_path(file_path, &cwd)?; + + let mut js_runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(Rc::new(FsModuleLoader)), + startup_snapshot: Some(RUNTIME_SNAPSHOT), + extensions: vec![runjs_extension::init_ops()], + ..Default::default() + }); + + let mod_id = js_runtime.load_main_es_module(&main_module).await?; + let result = js_runtime.mod_evaluate(mod_id); + js_runtime + .run_event_loop(PollEventLoopOptions::default()) + .await?; + result.await +} + +// Load the snapshot generated by build.rs: +static RUNTIME_SNAPSHOT: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/RUNJS_SNAPSHOT.bin")); diff --git a/core/examples/snapshot/src/runtime.js b/core/examples/snapshot/src/runtime.js new file mode 100644 index 000000000..f5179e112 --- /dev/null +++ b/core/examples/snapshot/src/runtime.js @@ -0,0 +1,10 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +/** + * This module provides the JavaScript interface atop calls to the Rust ops. + */ + +// Minimal example, just passes arguments through to Rust: +export function callRust(stringValue) { + const { op_call_rust } = Deno.core.ops; + op_call_rust(stringValue); +} diff --git a/core/examples/snapshot/tests/output.rs b/core/examples/snapshot/tests/output.rs new file mode 100644 index 000000000..0a092d063 --- /dev/null +++ b/core/examples/snapshot/tests/output.rs @@ -0,0 +1,33 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use core::str; +use std::process::{Command, Output}; + +#[test] +fn check_output() -> Result<(), Box> { + let output = capture_output()?; + + let err = str::from_utf8(&output.stderr)?; + assert_eq!(err, ""); + assert!(output.status.success()); + + let out = str::from_utf8(&output.stdout)?; + assert_eq!(out, "Received this value from JS: Hello from example.js\n"); + + Ok(()) +} + +/// NOTE!: This is NOT the preferred pattern to follow for testing binary crates! +/// See: +/// +/// However, we want to keep this example simple, so we're not going to create separate main.rs & lib.rs, or +/// add injectable outputs. We'll just run the binary and then capture its output. +fn capture_output() -> Result { + Command::new("cargo") + .args([ + "run", + "--release", // CI runs in --release mode, so re-use its cache. + "--quiet", // only capture the command's output. + ]) + .output() +} diff --git a/core/runtime/snapshot.rs b/core/runtime/snapshot.rs index 7a834af9a..d9935db6d 100644 --- a/core/runtime/snapshot.rs +++ b/core/runtime/snapshot.rs @@ -110,22 +110,72 @@ impl SnapshotStoreDataStore { } } +/// Options for [`create_snapshot`]. +/// +/// See: [example][1]. +/// +/// [1]: https://github.com/denoland/deno_core/tree/main/core/examples/snapshot pub struct CreateSnapshotOptions { + /// The directory which Cargo will compile everything into. + /// + /// This should always be the CARGO_MANIFEST_DIR environment variable. pub cargo_manifest_dir: &'static str, + + /// An optional starting snapshot atop which to build this snapshot. + /// + /// Passed to: [`RuntimeOptions::startup_snapshot`] pub startup_snapshot: Option<&'static [u8]>, + + /// Passed to [`RuntimeOptions::skip_op_registration`] while initializing the snapshot runtime. pub skip_op_registration: bool, + + /// Extensions to include within the generated snapshot. + /// + /// Passed to [`RuntimeOptions::extensions`] pub extensions: Vec, + + /// An optional transpiler to modify the module source before inclusion in the snapshot. + /// + /// For example, this might transpile from TypeScript to JavaScript. + /// + /// Passed to: [`RuntimeOptions::extension_transpiler`] pub extension_transpiler: Option>, + + /// An optional callback to perform further modification of the runtime before + /// taking the snapshot. pub with_runtime_cb: Option>, } +/// See [`create_snapshot`] for usage overview. pub struct CreateSnapshotOutput { /// Any files marked as LoadedFromFsDuringSnapshot are collected here and should be /// printed as 'cargo:rerun-if-changed' lines from your build script. pub files_loaded_during_snapshot: Vec, + + /// The resulting snapshot file's bytes. pub output: Box<[u8]>, } +/// Create a snapshot of a JavaScript runtime, which may yield better startup +/// time. +/// +/// At a high level, the steps are: +/// +/// * In your project's `build.rs` file: +/// * Call `create_snapshot()` from your `build.rs` file. +/// * Output the resulting snapshot to a path, preferably in [OUT_DIR]. +/// * Make sure to print a `cargo:rerun-if-changed` line for each +/// [`CreateSnapshotOutput::files_loaded_during_snapshot`]. +/// * In your project's source: +/// * Load the bytes of the generated snapshot file +/// ([`include_bytes`] is useful here) +/// * Pass those bytes to [`deno_core::JsRuntime::new`] via +/// [`RuntimeOptions::startup_snapshot`] +/// +/// For a concrete example, see [core/examples/snapshot/][example]. +/// +/// [example]: https://github.com/denoland/deno_core/tree/main/core/examples/snapshot +/// [OUT_DIR]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts #[must_use = "The files listed by create_snapshot should be printed as 'cargo:rerun-if-changed' lines"] pub fn create_snapshot( create_snapshot_options: CreateSnapshotOptions,