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

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()));
}
}