From fa3bd879d2b027c9b9c63997a18f2cc96f5f2ffe Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Fri, 8 Aug 2025 16:01:56 -0400 Subject: [PATCH] Support in-memory I/O APIs --- Cargo.lock | 44 +++++- Cargo.toml | 1 + src/engine.rs | 86 +++-------- src/io.rs | 215 ++++++++++++++++++++++++++ src/lib.rs | 1 + test-utils/src/lib.rs | 29 +++- tests/fixtures/README.md | 2 + tests/fixtures/build/wasm_api_v2.wasm | Bin 0 -> 75061 bytes tests/fixtures/wasm_api_v2/Cargo.toml | 12 ++ tests/fixtures/wasm_api_v2/src/lib.rs | 26 ++++ tests/integration_tests.rs | 28 ++++ 11 files changed, 371 insertions(+), 73 deletions(-) create mode 100644 src/io.rs create mode 100644 tests/fixtures/build/wasm_api_v2.wasm create mode 100644 tests/fixtures/wasm_api_v2/Cargo.toml create mode 100644 tests/fixtures/wasm_api_v2/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4c65b7d3..8bc4c508 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2456,7 +2456,16 @@ checksum = "1eb4e60eb2f8c6e02b1f1e7634ef91738b1104b5bc2fa30458d10cd00917dbbf" dependencies = [ "bumpalo", "rmp", - "shopify_function_wasm_api_core", + "shopify_function_wasm_api_core 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "shopify_function_provider" +version = "2.0.0" +dependencies = [ + "bumpalo", + "rmp", + "shopify_function_wasm_api_core 0.1.0", ] [[package]] @@ -2467,11 +2476,30 @@ checksum = "a57a2e64ef7d28cbe26bf591fd084093327d9d359e38355010720d818cd92ba9" dependencies = [ "rmp-serde", "serde_json", - "shopify_function_provider", - "shopify_function_wasm_api_core", + "shopify_function_provider 1.0.1", + "shopify_function_wasm_api_core 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 2.0.14", +] + +[[package]] +name = "shopify_function_wasm_api" +version = "0.2.0" +dependencies = [ + "rmp-serde", + "seq-macro", + "serde_json", + "shopify_function_provider 2.0.0", + "shopify_function_wasm_api_core 0.1.0", "thiserror 2.0.14", ] +[[package]] +name = "shopify_function_wasm_api_core" +version = "0.1.0" +dependencies = [ + "strum", +] + [[package]] name = "shopify_function_wasm_api_core" version = "0.1.0" @@ -3129,7 +3157,15 @@ name = "wasm_api_v1" version = "0.1.0" dependencies = [ "anyhow", - "shopify_function_wasm_api", + "shopify_function_wasm_api 0.1.0", +] + +[[package]] +name = "wasm_api_v2" +version = "0.1.0" +dependencies = [ + "anyhow", + "shopify_function_wasm_api 0.2.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1cd7213b..46daff68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "tests/fixtures/messagepack-valid", "tests/fixtures/messagepack-invalid", "tests/fixtures/wasm_api_v1", + "tests/fixtures/wasm_api_v2", ] [package] diff --git a/src/engine.rs b/src/engine.rs index 064fe9db..a206c5bc 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,13 +1,12 @@ use anyhow::{anyhow, Result}; -use rust_embed::RustEmbed; +use std::path::PathBuf; use std::string::String; -use std::{collections::HashSet, path::PathBuf}; use wasmtime::{AsContextMut, Config, Engine, Linker, Module, ResourceLimiter, Store}; -use wasmtime_wasi::pipe::{MemoryInputPipe, MemoryOutputPipe}; use wasmtime_wasi::preview1::WasiP1Ctx; -use wasmtime_wasi::{I32Exit, WasiCtxBuilder}; +use wasmtime_wasi::I32Exit; use crate::function_run_result::FunctionRunResult; +use crate::io::{IOHandler, OutputAndLogs}; use crate::{BytesContainer, BytesContainerType}; #[derive(Clone)] @@ -16,44 +15,12 @@ pub struct ProfileOpts { pub out: PathBuf, } -#[derive(RustEmbed)] -#[folder = "providers/"] -struct StandardProviders; - pub fn uses_msgpack_provider(module: &Module) -> bool { module.imports().map(|i| i.module()).any(|module| { module.starts_with("shopify_function_v") || module == "shopify_functions_javy_v2" }) } -fn import_modules( - module: &Module, - engine: &Engine, - linker: &mut Linker, - mut store: &mut Store, -) { - let imported_modules: HashSet = - module.imports().map(|i| i.module().to_string()).collect(); - - imported_modules.iter().for_each(|module_name| { - let provider_path = format!("{module_name}.wasm"); - let imported_module_bytes = StandardProviders::get(&provider_path); - - if let Some(bytes) = imported_module_bytes { - let imported_module = Module::from_binary(engine, &bytes.data) - .unwrap_or_else(|_| panic!("Failed to load module {module_name}")); - - let imported_module_instance = linker - .instantiate(&mut store, &imported_module) - .expect("Failed to instantiate imported instance"); - - linker - .instance(&mut store, module_name, imported_module_instance) - .expect("Failed to import module"); - } - }); -} - pub struct FunctionRunParams<'a> { pub function_path: PathBuf, pub input: BytesContainer, @@ -68,12 +35,12 @@ const STARTING_FUEL: u64 = u64::MAX; const MAXIMUM_MEMORIES: usize = 2; // 1 for the module, 1 for Javy's provider struct FunctionContext { - wasi: WasiP1Ctx, + wasi: Option, limiter: MemoryLimiter, } impl FunctionContext { - fn new(wasi: WasiP1Ctx) -> Self { + fn new(wasi: Option) -> Self { Self { wasi, limiter: Default::default(), @@ -128,33 +95,29 @@ pub fn run(params: FunctionRunParams) -> Result { module, } = params; - let input_stream = MemoryInputPipe::new(input.raw.clone()); - let output_stream = MemoryOutputPipe::new(usize::MAX); - let error_stream = MemoryOutputPipe::new(usize::MAX); + let mut io_handler = IOHandler::new(module, input.clone()); let mut error_logs: String = String::new(); let mut linker = Linker::new(&engine); - wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |ctx: &mut FunctionContext| { - &mut ctx.wasi - })?; - deterministic_wasi_ctx::replace_scheduling_functions(&mut linker)?; - let mut wasi_builder = WasiCtxBuilder::new(); - wasi_builder.stdin(input_stream); - wasi_builder.stdout(output_stream.clone()); - wasi_builder.stderr(error_stream.clone()); - deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut wasi_builder); - let wasi = wasi_builder.build_p1(); + let wasi = io_handler.wasi(); + if wasi.is_some() { + wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |ctx: &mut FunctionContext| { + ctx.wasi.as_mut().expect("Should have WASI context") + })?; + deterministic_wasi_ctx::replace_scheduling_functions(&mut linker)?; + } + let function_context = FunctionContext::new(wasi); let mut store = Store::new(&engine, function_context); store.limiter(|s| &mut s.limiter); + + io_handler.initialize(&engine, &mut linker, &mut store)?; + store.set_fuel(STARTING_FUEL)?; store.set_epoch_deadline(1); - import_modules(&module, &engine, &mut linker, &mut store); - - linker.module(&mut store, "Function", &module)?; - let instance = linker.instantiate(&mut store, &module)?; + let instance = linker.instantiate(&mut store, io_handler.module())?; let func = instance.get_typed_func::<(), ()>(store.as_context_mut(), export)?; @@ -163,7 +126,6 @@ pub fn run(params: FunctionRunParams) -> Result { .frequency(profile_opts.interval) .weight_unit(wasmprof::WeightUnit::Fuel) .profile(|store| func.call(store.as_context_mut(), ())); - ( result, Some(profile_data.into_collapsed_stacks().to_string()), @@ -191,18 +153,14 @@ pub fn run(params: FunctionRunParams) -> Result { } } - drop(store); - - let mut logs = error_stream - .try_into_inner() - .expect("Log stream reference still exists"); + let OutputAndLogs { + output: raw_output, + mut logs, + } = io_handler.finalize(store)?; logs.extend_from_slice(error_logs.as_bytes()); let output_codec = input.codec; - let raw_output = output_stream - .try_into_inner() - .expect("Output stream reference still exists"); let output = BytesContainer::new( BytesContainerType::Output, output_codec, diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 00000000..35035f65 --- /dev/null +++ b/src/io.rs @@ -0,0 +1,215 @@ +use std::collections::HashSet; + +use anyhow::{anyhow, Result}; +use rust_embed::RustEmbed; +use wasmtime::{AsContext, AsContextMut, Engine, Instance, Linker, Module, Store}; +use wasmtime_wasi::{ + pipe::{MemoryInputPipe, MemoryOutputPipe}, + preview1::WasiP1Ctx, + WasiCtxBuilder, +}; + +use crate::BytesContainer; + +#[derive(RustEmbed)] +#[folder = "providers/"] +struct StandardProviders; + +pub(crate) struct OutputAndLogs { + pub output: Vec, + pub logs: Vec, +} + +struct WasiIO { + output: MemoryOutputPipe, + logs: MemoryOutputPipe, +} + +enum IOStrategy { + Wasi(WasiIO), + Memory(Option), +} + +pub(crate) struct IOHandler { + strategy: IOStrategy, + module: Module, + input: BytesContainer, +} + +impl IOHandler { + pub(crate) fn new(module: Module, input: BytesContainer) -> Self { + Self { + strategy: if uses_mem_io(&module) { + IOStrategy::Memory(None) + } else { + IOStrategy::Wasi(WasiIO { + output: MemoryOutputPipe::new(usize::MAX), + logs: MemoryOutputPipe::new(usize::MAX), + }) + }, + module, + input, + } + } + + pub(crate) fn module(&self) -> &Module { + &self.module + } + + pub(crate) fn wasi(&self) -> Option { + match &self.strategy { + IOStrategy::Wasi(WasiIO { output, logs }) => { + let input_stream = MemoryInputPipe::new(self.input.raw.clone()); + let mut wasi_builder = WasiCtxBuilder::new(); + wasi_builder.stdin(input_stream); + wasi_builder.stdout(output.clone()); + wasi_builder.stderr(logs.clone()); + deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut wasi_builder); + Some(wasi_builder.build_p1()) + } + IOStrategy::Memory(_instance) => None, + } + } + + pub(crate) fn initialize( + &mut self, + engine: &Engine, + linker: &mut Linker, + store: &mut Store, + ) -> Result<()> { + store.set_epoch_deadline(1); // Need to make sure we don't timeout during initialization. + store.set_fuel(u64::MAX)?; // Make sure we have fuel for initialization. + let mem_io_instance = instantiate_imports(&self.module, engine, linker, store); + if let IOStrategy::Memory(ref mut instance) = self.strategy { + *instance = mem_io_instance; + } + + if let Some(instance) = mem_io_instance { + let input_offset = instance + .get_typed_func::(store.as_context_mut(), "initialize")? + .call(store.as_context_mut(), self.input.raw.len() as _)?; + instance + .get_memory(store.as_context_mut(), "memory") + .ok_or_else(|| anyhow!("Missing memory export named memory"))? + .write(store.as_context_mut(), input_offset as _, &self.input.raw)?; + } + Ok(()) + } + + pub(crate) fn finalize(self, mut store: Store) -> Result { + match self.strategy { + IOStrategy::Memory(instance) => { + let instance = instance.expect("Should have been defined in initialize"); + store.set_epoch_deadline(1); // Make sure we don't timeout while finalizing. + store.set_fuel(u64::MAX)?; // Make sure we don't run out of fuel finalizing. + let results_offset = instance + .get_typed_func::<(), i32>(store.as_context_mut(), "finalize")? + .call(store.as_context_mut(), ())? + as usize; + + let memory = instance + .get_memory(store.as_context_mut(), "memory") + .ok_or_else(|| anyhow!("Missing memory export named memory"))?; + + let mut buf = [0; 24]; + memory.read(store.as_context(), results_offset, &mut buf)?; + + let output_offset = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize; + let output_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize; + let log_offset1 = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize; + let log_len1 = u32::from_le_bytes(buf[12..16].try_into().unwrap()) as usize; + let log_offset2 = u32::from_le_bytes(buf[16..20].try_into().unwrap()) as usize; + let log_len2 = u32::from_le_bytes(buf[20..24].try_into().unwrap()) as usize; + + let mut output = vec![0; output_len]; + memory.read(store.as_context(), output_offset, &mut output)?; + + let mut logs = vec![0; log_len1]; + memory.read(store.as_context(), log_offset1, &mut logs)?; + + let mut logs2 = vec![0; log_len2]; + memory.read(store.as_context(), log_offset2, &mut logs2)?; + + logs.append(&mut logs2); + + Ok(OutputAndLogs { output, logs }) + } + IOStrategy::Wasi(WasiIO { output, logs }) => { + // Need to drop store to have only one reference to output and error streams. + drop(store); + + let output = output + .try_into_inner() + .expect("Should have only one reference to output stream at this point") + .to_vec(); + let logs = logs + .try_into_inner() + .expect("Should have only one reference to error stream at this point") + .to_vec(); + Ok(OutputAndLogs { output, logs }) + } + } + } +} + +// Whether a module imports a provider that uses in-memory buffers for I/O. +fn uses_mem_io(module: &Module) -> bool { + module.imports().map(|i| i.module()).any(is_mem_io_provider) +} + +// Whether a provider exports functions for working with in-memory buffers for I/O. +fn is_mem_io_provider(module: &str) -> bool { + let javy_plugin_version = module + .strip_prefix("shopify_functions_javy_v") + .map(|s| s.parse::()) + .and_then(|result| result.ok()); + if javy_plugin_version.is_some_and(|version| version >= 3) { + return true; + } + + let functions_provider_version = module + .strip_prefix("shopify_function_v") + .map(|s| s.parse::()) + .and_then(|result| result.ok()); + if functions_provider_version.is_some_and(|version| version >= 2) { + return true; + } + + false +} + +fn instantiate_imports( + module: &Module, + engine: &Engine, + linker: &mut Linker, + mut store: &mut Store, +) -> Option { + let imported_modules: HashSet = + module.imports().map(|i| i.module().to_string()).collect(); + + let mut mem_io_instance = None; + + imported_modules.iter().for_each(|module_name| { + let provider_path = format!("{module_name}.wasm"); + let imported_module_bytes = StandardProviders::get(&provider_path); + + if let Some(bytes) = imported_module_bytes { + let imported_module = Module::from_binary(engine, &bytes.data) + .unwrap_or_else(|_| panic!("Failed to load module {module_name}")); + + let imported_module_instance = linker + .instantiate(&mut store, &imported_module) + .expect("Failed to instantiate imported instance"); + + if is_mem_io_provider(module_name) { + mem_io_instance = Some(imported_module_instance); + } + + linker + .instance(&mut store, module_name, imported_module_instance) + .expect("Failed to import module"); + } + }); + + mem_io_instance +} diff --git a/src/lib.rs b/src/lib.rs index ecde0ba7..d28e3cde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod bluejay_schema_analyzer; pub mod container; pub mod engine; pub mod function_run_result; +mod io; pub mod scale_limits_analyzer; use clap::ValueEnum; diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index f9dd2915..7e04a1c7 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -9,7 +9,22 @@ pub fn process_with_v1_trampoline, Q: AsRef>( wasm_path: P, trampolined_path: Q, ) -> Result<()> { - let trampoline_path = TRAMPOLINE_1_0_PATH + process_with_trampoline(&TRAMPOLINE_1_0_PATH, wasm_path, trampolined_path) +} + +pub fn process_with_v2_trampoline, Q: AsRef>( + wasm_path: P, + trampolined_path: Q, +) -> Result<()> { + process_with_trampoline(&TRAMPOLINE_2_0_PATH, wasm_path, trampolined_path) +} + +fn process_with_trampoline, Q: AsRef>( + trampoline_path: &LazyLock>, + wasm_path: P, + trampolined_path: Q, +) -> Result<()> { + let trampoline_path = trampoline_path .as_ref() .map_err(|e| anyhow!("Failed to download trampoline: {e}"))?; let status = Command::new(trampoline_path) @@ -24,15 +39,19 @@ pub fn process_with_v1_trampoline, Q: AsRef>( Ok(()) } -static TRAMPOLINE_1_0_PATH: LazyLock> = LazyLock::new(|| { +static TRAMPOLINE_1_0_PATH: LazyLock> = LazyLock::new(|| trampoline_path("1.0.2")); + +static TRAMPOLINE_2_0_PATH: LazyLock> = LazyLock::new(|| trampoline_path("2.0.0")); + +fn trampoline_path(version: &str) -> Result { let binaries_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tmp"); - let path = binaries_path.join("trampoline-1.0.2"); + let path = binaries_path.join(format!("trampoline-{version}")); if !path.exists() { std::fs::create_dir_all(binaries_path)?; - download_trampoline(&path, "1.0.2")?; + download_trampoline(&path, version)?; } Ok(path) -}); +} fn download_trampoline(destination: &Path, version: &str) -> Result<()> { let target_os = if cfg!(target_os = "macos") { diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 6b177745..25b685fe 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -13,6 +13,8 @@ Example Functions used as test fixtures. ``` cargo build --target wasm32-wasip1 --profile=wasm -p exit_code -p exports -p log_truncation_function -p noop -p wasm_api_v1 && find target/wasm32-wasip1/wasm/{exit_code.wasm,exports.wasm,log_truncation_function.wasm,noop.wasm,wasm_api_v1.wasm} | xargs -I {} sh -c 'name=$(basename {}); wasm-opt {} -Oz --enable-bulk-memory --strip-debug -o "tests/fixtures/build/$name"' +cargo build --target wasm32-unknown-unknown --profile=wasm -p wasm_api_v2 && + find target/wasm32-unknown-unknown/wasm/wasm_api_v2.wasm | xargs -I {} sh -c 'name=$(basename {}); wasm-opt {} -Oz --enable-bulk-memory --strip-debug -o "tests/fixtures/build/$name"' ``` **JS examples:** diff --git a/tests/fixtures/build/wasm_api_v2.wasm b/tests/fixtures/build/wasm_api_v2.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5be3260d0847e2b4d1278ba45a7266b542232210 GIT binary patch literal 75061 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 r2sQheQU>P5i%p6cfvumwg?A39gj4{O) Result<()> { + let mut ctx = shopify_function_wasm_api::Context::new(); + let input = ctx.input_get()?; + let str = input + .get_obj_prop("hello") + .as_string() + .ok_or_else(|| anyhow!("Should be string"))?; + ctx.write_object( + |ctx| { + ctx.write_utf8_str("bye")?; + ctx.write_utf8_str(&str)?; + Ok(()) + }, + 1, + )?; + ctx.log(&"a".repeat(1025)); + ctx.log(&"b".repeat(10)); + Ok(()) +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index a12e95bc..e9dba6fd 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -403,4 +403,32 @@ mod tests { Ok(()) } + + #[test] + fn run_wasm_api_v2_function() -> Result<()> { + let trampolined_module = assert_fs::NamedTempFile::new("wasm_api_v2.trampolined.wasm")?; + test_utils::process_with_v2_trampoline( + "tests/fixtures/build/wasm_api_v2.wasm", + &trampolined_module, + )?; + + let mut cmd = Command::cargo_bin("function-runner")?; + let input_file = temp_input(json!({"hello": "world"}))?; + + cmd.arg("--function") + .arg(trampolined_module.as_os_str()) + .arg("--json") + .arg("--input") + .arg(input_file.as_os_str()) + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output()?; + + cmd.assert().success(); + cmd.assert().stdout(contains("world")); + cmd.assert() + .stdout(contains(format!("{}{}", "a".repeat(1015), "b".repeat(10)))); + + Ok(()) + } }