feat: copied from github.com/seddonm1/quickjs

This commit is contained in:
2023-02-01 00:08:34 +08:00
parent 03d006025f
commit 78db15fc04
16 changed files with 5112 additions and 2 deletions

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[workspace]
members = [
"crates/quickjs",
"crates/quickjs-wasm",
]
[profile.release]
lto = true
opt-level = 3

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Mike Seddon 2023
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +1,56 @@
# seddonm1-quickjs
This repository demonstrates how to use [quickjs-wasm-rs](https://github.com/Shopify/javy/tree/main/crates/quickjs-wasm-rs) with [wasmtime](https://github.com/bytecodealliance/wasmtime) to easily build a safe and isolated plugin system for Rust.
From: https://github.com/seddonm1/quickjs
Code to accompany blog post: https://reorchestrate.com/posts/plugins-for-rust
First `build-wasm.sh` script which will download and build the `quickjs.wasm` module.
# Examples
Run a sequential executor:
```bash
cargo run --example iter --release
```
Run a parallel executor:
```bash
cargo run --example par_iter --release
```
Both accept additional arguments like:
```bash
cargo run --release --example iter -- \
--module ./quickjs.wasm \
--script ./track_points.js \
--data ./track_points.json \
--iterations 1000 \
--inherit-stdout \
--inherit-stderr
```
# Build
```bash
cargo build --package quickjs --release
```
# Test
```bash
cargo test --package quickjs --release
```
# Bench
```bash
cargo bench --package quickjs
```
# Credits
- Peter Malmgren https://github.com/pmalmgren/wasi-data-sharing
- Shopify https://github.com/Shopify/javy
- Bytecode Alliance https://github.com/bytecodealliance/wasmtime
- Bytecode Alliance https://github.com/bytecodealliance/wizer

17
build-wasm.sh Executable file
View File

@@ -0,0 +1,17 @@
export QUICKJS_WASM_SYS_WASI_SDK_PATH=$HOME/opt/wasi-sdk
# Check that something is present where the user says the wasi-sdk is located
if [ ! -d "$QUICKJS_WASM_SYS_WASI_SDK_PATH" ]; then
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-19/wasi-sdk-19.0-linux.tar.gz
mkdir -p $QUICKJS_WASM_SYS_WASI_SDK_PATH
tar xvf wasi-sdk-19.0-linux.tar.gz --strip-components=1 -C $QUICKJS_WASM_SYS_WASI_SDK_PATH
rm wasi-sdk-19.0-linux.tar.gz
fi
# Build the base package
cargo build --release --package quickjs-wasm --target wasm32-wasi
# If wizer is not installed then install it
if [ -z $(which wizer) ]
then
cargo install wizer --all-features
fi
# apply wizer optimisation
wizer --allow-wasi target/wasm32-wasi/release/quickjs-wasm.wasm --wasm-bulk-memory true -o quickjs.wasm

View File

@@ -0,0 +1,17 @@
[package]
name = "quickjs-wasm"
version = "0.1.0"
authors = [""]
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.68"
once_cell = "1.17.0"
quickjs-wasm-rs = { version = "0.1.3", features = ["json"] }
[features]
default = []
console = []

View File

@@ -0,0 +1,36 @@
use anyhow::Result;
use quickjs_wasm_rs::{Context, Value};
use std::io::Write;
/// set quickjs globals
pub fn set_quickjs_globals(context: &Context) -> anyhow::Result<()> {
let global = context.global_object()?;
let console_log_callback = context.wrap_callback(console_log_to(std::io::stdout()))?;
let console_error_callback = context.wrap_callback(console_log_to(std::io::stderr()))?;
let console_object = context.object_value()?;
console_object.set_property("log", console_log_callback)?;
console_object.set_property("error", console_error_callback)?;
global.set_property("console", console_object)?;
Ok(())
}
/// console_log_to is used to allow the javascript functions console.log and console.error to
/// log to the stdout and stderr respectively.
fn console_log_to<T>(mut stream: T) -> impl FnMut(&Context, &Value, &[Value]) -> Result<Value>
where
T: Write + 'static,
{
move |ctx: &Context, _this: &Value, args: &[Value]| {
for (i, arg) in args.iter().enumerate() {
if i != 0 {
write!(stream, " ")?;
}
stream.write_all(arg.as_str()?.as_bytes())?;
}
writeln!(stream)?;
ctx.undefined_value()
}
}

View File

@@ -0,0 +1,68 @@
use anyhow::Result;
use quickjs_wasm_rs::{json, Context, Value};
#[link(wasm_import_module = "host")]
extern "C" {
fn get_input(ptr: i32);
fn get_input_size() -> i32;
fn get_data(ptr: i32);
fn get_data_size() -> i32;
fn set_output(ptr: i32, size: i32);
}
/// gets the input from the host as a string
pub fn get_input_string() -> Result<Option<String>> {
let input_size = unsafe { get_input_size() } as usize;
if input_size == 0 {
Ok(None)
} else {
let mut buf: Vec<u8> = Vec::with_capacity(input_size);
let ptr = buf.as_mut_ptr();
unsafe { get_input(ptr as i32) };
let input_buf = unsafe { Vec::from_raw_parts(ptr, input_size, input_size) };
Ok(Some(String::from_utf8(input_buf.to_vec())?))
}
}
/// gets the input from the host as a string
pub fn get_input_value(context: &Context) -> Result<Option<Value>> {
let input_size = unsafe { get_data_size() } as usize;
if input_size == 0 {
Ok(None)
} else {
let mut buf: Vec<u8> = Vec::with_capacity(input_size);
let ptr = buf.as_mut_ptr();
unsafe { get_data(ptr as i32) };
let input_buf = unsafe { Vec::from_raw_parts(ptr, input_size, input_size) };
Ok(Some(json::transcode_input(context, &input_buf)?))
}
}
/// sets the output value on the host
pub fn set_output_value(output: Option<Value>) -> Result<()> {
match output {
Some(output) if !output.is_undefined() => {
let output = json::transcode_output(output)?;
let size = output.len() as i32;
let ptr = output.as_ptr();
unsafe {
set_output(ptr as i32, size);
};
}
_ => {
unsafe {
set_output(0, 0);
};
}
}
Ok(())
}

View File

@@ -0,0 +1,43 @@
#[cfg(feature = "console")]
mod context;
mod io;
use anyhow::Result;
use once_cell::sync::OnceCell;
use quickjs_wasm_rs::Context;
static mut JS_CONTEXT: OnceCell<Context> = OnceCell::new();
static SCRIPT_NAME: &str = "script.js";
/// init() is executed by wizer to create a snapshot after the quickjs context has been initialized.
///
/// it also binds the console.log and console.error functions so they can be used for debugging in the
/// user script.
#[export_name = "wizer.initialize"]
pub extern "C" fn init() {
unsafe {
let context = Context::default();
// add globals to the quickjs instance if enabled
#[cfg(feature = "console")]
context::set_quickjs_globals(&context).unwrap();
JS_CONTEXT.set(context).unwrap();
}
}
fn main() -> Result<()> {
match io::get_input_string()? {
Some(input) => {
let context = unsafe { JS_CONTEXT.get_or_init(Context::default) };
if let Some(value) = io::get_input_value(context)? {
context.global_object()?.set_property("data", value)?;
}
let output = context.eval_global(SCRIPT_NAME, &input)?;
io::set_output_value(Some(output))
}
None => io::set_output_value(None),
}
}

20
crates/quickjs/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
[package]
name = "quickjs"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.68"
wasi-common = "5.0.0"
wasmtime = "5.0.0"
wasmtime-wasi = "5.0.0"
[dev-dependencies]
clap = { version = "4.1.2", features = ["derive"] }
num_cpus = "1.15.0"
rayon = "1.6.1"
criterion = "0.4.0"
[[bench]]
name = "benchmark"
harness = false

View File

@@ -0,0 +1,20 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use quickjs::QuickJS;
pub fn criterion_benchmark(c: &mut Criterion) {
let quickjs = QuickJS::default();
let script = include_str!("../../../track_points.js");
let data = include_str!("../../../track_points.json");
c.bench_function("try_execute", |b| {
b.iter(|| {
black_box(
quickjs
.try_execute(script, Some(data), false, false)
.unwrap(),
)
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -0,0 +1,74 @@
extern crate quickjs;
use anyhow::Result;
use clap::Parser;
use quickjs::QuickJS;
use std::{path::PathBuf, time::Instant};
/// Simple program to demonstr
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Path to the wasm module
#[arg(short, long)]
module: Option<PathBuf>,
/// Path to the input script
#[arg(short, long)]
script: Option<PathBuf>,
/// Path to the data json object
#[arg(short, long)]
data: Option<PathBuf>,
/// Number of iterations to execute
#[arg(short, long, default_value_t = 1000)]
iterations: usize,
/// Enable stdout (i.e. console.log) defualt false
#[arg(short, long)]
inherit_stdout: bool,
/// Enable stderr (i.e. console.error) default false
#[arg(short, long)]
inherit_stderr: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let quickjs = match args.module {
Some(path) => QuickJS::try_from(path)?,
None => QuickJS::default(),
};
let script = match args.script {
Some(path) => std::fs::read_to_string(path)?,
None => include_str!("../../../track_points.js").to_string(),
};
let data = match args.data {
Some(path) => std::fs::read_to_string(path)?,
None => include_str!("../../../track_points.json").to_string(),
};
let start = Instant::now();
for i in 0..args.iterations {
let output = quickjs.try_execute(
&script,
Some(&data),
args.inherit_stdout,
args.inherit_stderr,
)?;
println!("{i} {}", output.unwrap_or_else(|| "None".to_string()));
}
let duration = start.elapsed();
println!(
"elapsed: {:?}\niteration: {:?}",
duration,
duration.div_f32(args.iterations as f32)
);
Ok(())
}

View File

@@ -0,0 +1,88 @@
extern crate quickjs;
use anyhow::Result;
use clap::Parser;
use quickjs::QuickJS;
use rayon::prelude::*;
use std::{path::PathBuf, time::Instant};
/// Simple program to demonstr
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Path to the wasm module
#[arg(short, long)]
module: Option<PathBuf>,
/// Path to the input script
#[arg(short, long)]
script: Option<PathBuf>,
/// Path to the data json object
#[arg(short, long)]
data: Option<PathBuf>,
/// Number of iterations to execute
#[arg(short, long, default_value_t = 1000)]
iterations: usize,
/// Enable stdout (i.e. console.log) default false
#[arg(short, long)]
inherit_stdout: bool,
/// Enable stderr (i.e. console.error) default false
#[arg(short, long)]
inherit_stderr: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let quickjs = match args.module {
Some(path) => QuickJS::try_from(path)?,
None => QuickJS::default(),
};
let script = match args.script {
Some(path) => std::fs::read_to_string(path)?,
None => include_str!("../../../track_points.js").to_string(),
};
let data = match args.data {
Some(path) => std::fs::read_to_string(path)?,
None => include_str!("../../../track_points.json").to_string(),
};
let start = Instant::now();
(0..args.iterations)
.collect::<Vec<_>>()
.chunks(args.iterations / num_cpus::get())
.collect::<Vec<_>>()
.into_par_iter()
.map(|chunk| {
chunk
.iter()
.map(|i| {
let output = quickjs.try_execute(
&script,
Some(&data),
args.inherit_stdout,
args.inherit_stderr,
)?;
println!("{i} {}", output.unwrap_or_else(|| "None".to_string()));
Ok(())
})
.collect::<Result<Vec<_>>>()
})
.collect::<Result<Vec<_>>>()?;
let duration = start.elapsed();
println!(
"elapsed: {:?}\niteration: {:?}",
duration,
duration.div_f32(args.iterations as f32)
);
Ok(())
}

173
crates/quickjs/src/lib.rs Normal file
View File

@@ -0,0 +1,173 @@
use anyhow::{anyhow, Result};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use wasi_common::WasiCtx;
use wasmtime::*;
use wasmtime_wasi::sync::WasiCtxBuilder;
pub struct QuickJS {
engine: Engine,
module: Module,
}
impl Default for QuickJS {
fn default() -> Self {
let engine = Engine::default();
let module = Module::from_binary(&engine, include_bytes!("../../../quickjs.wasm")).unwrap();
Self { engine, module }
}
}
impl TryFrom<PathBuf> for QuickJS {
type Error = anyhow::Error;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
let engine = Engine::default();
let module = Module::from_file(&engine, path)?;
Ok(Self { engine, module })
}
}
impl QuickJS {
pub fn try_execute(
&self,
script: &str,
data: Option<&str>,
inherit_stdout: bool,
inherit_stderr: bool,
) -> Result<Option<String>> {
let input = script.as_bytes().to_vec();
let input_size = input.len() as i32;
let data = data
.map(|data| data.as_bytes().to_vec())
.unwrap_or_default();
let data_size = data.len() as i32;
let output = Arc::new(Mutex::new(None));
let mut linker = Linker::new(&self.engine);
wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;
let mut wasi_ctx_builder = WasiCtxBuilder::new();
if inherit_stdout {
wasi_ctx_builder = wasi_ctx_builder.inherit_stdout();
};
if inherit_stderr {
wasi_ctx_builder = wasi_ctx_builder.inherit_stderr();
};
let wasi = wasi_ctx_builder.build();
let mut store = Store::new(&self.engine, wasi);
let memory_type = MemoryType::new(1, None);
Memory::new(&mut store, memory_type)?;
linker.func_wrap(
"host",
"get_input_size",
move |_: Caller<'_, WasiCtx>| -> Result<i32> { Ok(input_size) },
)?;
linker.func_wrap(
"host",
"get_input",
move |mut caller: Caller<'_, WasiCtx>, ptr: i32| -> Result<()> {
let memory = match caller.get_export("memory") {
Some(Extern::Memory(memory)) => memory,
_ => return Err(anyhow!("failed to find host memory")),
};
let offset = ptr as u32 as usize;
Ok(memory.write(&mut caller, offset, &input)?)
},
)?;
linker.func_wrap(
"host",
"get_data_size",
move |_: Caller<'_, WasiCtx>| -> Result<i32> { Ok(data_size) },
)?;
linker.func_wrap(
"host",
"get_data",
move |mut caller: Caller<'_, WasiCtx>, ptr: i32| -> Result<()> {
let memory = match caller.get_export("memory") {
Some(Extern::Memory(memory)) => memory,
_ => return Err(anyhow!("failed to find host memory")),
};
let offset = ptr as u32 as usize;
Ok(memory.write(&mut caller, offset, &data)?)
},
)?;
let output_clone = output.clone();
linker.func_wrap(
"host",
"set_output",
move |mut caller: Caller<'_, WasiCtx>, ptr: i32, capacity: i32| -> Result<()> {
let mut output = output_clone.lock().unwrap();
*output = if capacity == 0 {
None
} else {
let memory = match caller.get_export("memory") {
Some(Extern::Memory(memory)) => memory,
_ => return Err(anyhow!("failed to find host memory")),
};
let offset = ptr as u32 as usize;
let mut buffer: Vec<u8> = vec![0; capacity as usize];
memory.read(&caller, offset, &mut buffer)?;
Some(String::from_utf8(buffer)?)
};
Ok(())
},
)?;
linker.module(&mut store, "", &self.module)?;
// call the default function i.e. main()
linker
.get_default(&mut store, "")?
.typed::<(), ()>(&store)?
.call(&mut store, ())?;
let output = output.lock().unwrap();
Ok(output.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_execute() {
let quickjs = QuickJS::default();
let script = r#"
'quickjs' + 'wasm'
"#;
let result = quickjs.try_execute(script, None, false, false).unwrap();
assert_eq!(result, Some("\"quickjswasm\"".to_string()));
}
#[test]
fn try_execute_data() {
let quickjs = QuickJS::default();
let script = r#"
'quickjs' + data.input
"#;
let data = r#"{"input": "wasm"}"#;
let result = quickjs
.try_execute(script, Some(data), false, false)
.unwrap();
assert_eq!(result, Some("\"quickjswasm\"".to_string()));
}
}

BIN
quickjs.wasm Normal file

Binary file not shown.

39
track_points.js Normal file
View File

@@ -0,0 +1,39 @@
// simple approximate distance function returning distance between two points in meters
function distance(lat0, lon0, lat1, lon1) {
if ((lat0 == lat1) && (lon0 == lon1)) {
return 0;
} else {
const radlat0 = Math.PI * lat0 / 180;
const radlat1 = Math.PI * lat1 / 180;
const theta = lon0 - lon1;
const radtheta = Math.PI * theta / 180;
let dist = Math.sin(radlat0) * Math.sin(radlat1) + Math.cos(radlat0) * Math.cos(radlat1) * Math.cos(radtheta);
if (dist > 1) {
dist = 1;
}
dist = Math.acos(dist);
dist = dist * 180 / Math.PI;
return dist * 60 * 1853.159;
}
}
// calculate the total length of a set of input features in canadian football fields
function calculate(data) {
// canadian football fields are 140 meters
const candadian_football_field = 140;
return data.features.reduce(
(accumulator, currentValue, currentIndex, array) => {
if (currentIndex == 0) {
return 0
} else {
const previousValue = array[currentIndex - 1];
const dist = distance(currentValue.geometry.coordinates[1], currentValue.geometry.coordinates[0], previousValue.geometry.coordinates[1], previousValue.geometry.coordinates[0]);
return accumulator + dist / candadian_football_field
}
},
0
)
}
calculate(data)

4432
track_points.json Normal file

File diff suppressed because it is too large Load Diff