From ff84022ef6182333bf393afd1c5c0b8a2a1c5732 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Fri, 20 Jun 2025 16:44:59 -0400 Subject: [PATCH 01/20] Stop using stdio --- README.md | 3 +- .../cart-checkout-validation-wasm-api.rs | 3 +- api/examples/echo.rs | 2 +- api/examples/log.rs | 13 ++ api/examples/panic.rs | 8 + api/src/lib.rs | 36 +++- api/src/log.rs | 14 ++ api/src/shopify_function.h | 27 ++- api/src/shopify_function.wat | 31 ++-- api/src/test_data/header_test.c | 5 +- api/src/test_data/header_test.wasm | Bin 2642 -> 2558 bytes api/src/write.rs | 5 - core/src/lib.rs | 1 + core/src/log.rs | 6 + integration_tests/tests/integration_test.rs | 175 ++++++++++++++---- provider/src/lib.rs | 65 ++++--- provider/src/log.rs | 21 +++ provider/src/write.rs | 25 --- trampoline/src/lib.rs | 65 ++++++- ...__disassemble_trampoline@consumer.wat.snap | 100 +++++----- trampoline/src/test_data/consumer.wat | 5 +- 21 files changed, 415 insertions(+), 195 deletions(-) create mode 100644 api/examples/log.rs create mode 100644 api/examples/panic.rs create mode 100644 api/src/log.rs create mode 100644 core/src/log.rs create mode 100644 provider/src/log.rs diff --git a/README.md b/README.md index f3e083c..b62d0c5 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,11 @@ Here's a simple example of how to use the API: ```rust fn main(context: &mut Context) -> Result<()> { + shopify_function_wasm_api::init_panic_handler(); let input = context.input_get()?; // Function logic - context.finalize_output()?; - Ok(()) } ``` diff --git a/api/examples/cart-checkout-validation-wasm-api.rs b/api/examples/cart-checkout-validation-wasm-api.rs index 6307d76..ea5c91c 100644 --- a/api/examples/cart-checkout-validation-wasm-api.rs +++ b/api/examples/cart-checkout-validation-wasm-api.rs @@ -2,6 +2,7 @@ use shopify_function_wasm_api::{Context, Value}; use std::error::Error; fn main() -> Result<(), Box> { + shopify_function_wasm_api::init_panic_handler(); let mut context = Context::new(); let input = context.input_get()?; @@ -38,8 +39,6 @@ fn main() -> Result<(), Box> { 1, )?; - context.finalize_output()?; - Ok(()) } diff --git a/api/examples/echo.rs b/api/examples/echo.rs index 94c7303..2f69fc2 100644 --- a/api/examples/echo.rs +++ b/api/examples/echo.rs @@ -5,13 +5,13 @@ use shopify_function_wasm_api::{ use std::error::Error; fn main() -> Result<(), Box> { + shopify_function_wasm_api::init_panic_handler(); let mut context = Context::new(); let input = context.input_get()?; let value = Value::deserialize(&input)?; let result = echo(value); result.serialize(&mut context)?; - context.finalize_output()?; Ok(()) } diff --git a/api/examples/log.rs b/api/examples/log.rs new file mode 100644 index 0000000..5be3de1 --- /dev/null +++ b/api/examples/log.rs @@ -0,0 +1,13 @@ +use std::error::Error; + +use shopify_function_wasm_api::Context; + +fn main() -> Result<(), Box> { + shopify_function_wasm_api::init_panic_handler(); + let mut context = Context::new(); + context.log("Hi!\n"); + context.log("Hello\n"); + context.log("Here's a third string\n"); + context.log("✌️\n"); + Ok(()) +} diff --git a/api/examples/panic.rs b/api/examples/panic.rs new file mode 100644 index 0000000..91512af --- /dev/null +++ b/api/examples/panic.rs @@ -0,0 +1,8 @@ +use std::error::Error; + +use shopify_function_wasm_api::init_panic_handler; + +fn main() -> Result<(), Box> { + init_panic_handler(); + panic!("at the disco"); +} diff --git a/api/src/lib.rs b/api/src/lib.rs index d5e6f0e..cf385e6 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -9,11 +9,11 @@ //! use std::error::Error; //! //! fn main() -> Result<(), Box> { +//! shopify_function_wasm_api::init_panic_handler(); //! let mut context = Context::new(); //! let input = context.input_get()?; //! let value: i32 = Deserialize::deserialize(&input)?; //! value.serialize(&mut context)?; -//! context.finalize_output()?; //! Ok(()) //! } //! ``` @@ -23,6 +23,7 @@ use shopify_function_wasm_api_core::read::{ErrorCode, NanBox, Val, ValueRef}; use std::sync::atomic::{AtomicUsize, Ordering}; +pub mod log; pub mod read; pub mod write; @@ -32,9 +33,6 @@ pub use write::Serialize; #[cfg(target_family = "wasm")] #[link(wasm_import_module = "shopify_function_v1")] extern "C" { - // Common API. - fn shopify_function_context_new(); - // Read API. fn shopify_function_input_get() -> Val; fn shopify_function_input_get_val_len(scope: Val) -> usize; @@ -50,7 +48,6 @@ extern "C" { // Write API. fn shopify_function_output_new_bool(bool: u32) -> usize; fn shopify_function_output_new_null() -> usize; - fn shopify_function_output_finalize() -> usize; fn shopify_function_output_new_i32(int: i32) -> usize; fn shopify_function_output_new_f64(float: f64) -> usize; fn shopify_function_output_new_utf8_str(ptr: *const u8, len: usize) -> usize; @@ -62,6 +59,9 @@ extern "C" { fn shopify_function_output_new_array(len: usize) -> usize; fn shopify_function_output_finish_array() -> usize; + // Log API. + fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize; + // Other. fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize; } @@ -119,9 +119,6 @@ mod provider_fallback { pub(crate) unsafe fn shopify_function_output_new_null() -> usize { shopify_function_provider::write::shopify_function_output_new_null() as usize } - pub(crate) unsafe fn shopify_function_output_finalize() -> usize { - shopify_function_provider::write::shopify_function_output_finalize() as usize - } pub(crate) unsafe fn shopify_function_output_new_i32(int: i32) -> usize { shopify_function_provider::write::shopify_function_output_new_i32(int) as usize } @@ -155,6 +152,17 @@ mod provider_fallback { shopify_function_provider::write::shopify_function_output_finish_array() as usize } + // Logging. + pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize { + let result = shopify_function_provider::log::shopify_function_log_new_utf8_str(len); + let write_result = (result >> usize::BITS) as usize; + let dst = result as usize; + if write_result == WriteResult::Ok as usize { + std::ptr::copy(ptr as _, dst as _, len); + } + write_result + } + // Other. pub(crate) unsafe fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize { let result = shopify_function_provider::shopify_function_intern_utf8_str(len); @@ -408,7 +416,6 @@ impl Context { #[cfg(target_family = "wasm")] { - unsafe { shopify_function_context_new() }; Self } } @@ -419,7 +426,7 @@ impl Context { #[cfg(not(target_family = "wasm"))] pub fn new_with_input(input: serde_json::Value) -> Self { let bytes = rmp_serde::to_vec(&input).unwrap(); - shopify_function_provider::shopify_function_context_new_from_msgpack_bytes(bytes); + shopify_function_provider::initialize_from_msgpack_bytes(bytes); Self } @@ -448,6 +455,15 @@ impl Default for Context { } } +/// Configures panics to write to the logging API. +pub fn init_panic_handler() { + #[cfg(target_family = "wasm")] + std::panic::set_hook(Box::new(|info| { + let message = format!("{info}"); + log::log_utf8_str(&message); + })); +} + #[cfg(test)] mod tests { use super::*; diff --git a/api/src/log.rs b/api/src/log.rs new file mode 100644 index 0000000..08a2c99 --- /dev/null +++ b/api/src/log.rs @@ -0,0 +1,14 @@ +//! The log API for the Shopify Function Wasm API. + +use crate::Context; + +pub(super) fn log_utf8_str(message: &str) { + unsafe { crate::shopify_function_log_new_utf8_str(message.as_ptr(), message.len()) }; +} + +impl Context { + /// Log `message`. + pub fn log(&mut self, message: &str) { + log_utf8_str(message) + } +} diff --git a/api/src/shopify_function.h b/api/src/shopify_function.h index 1159c73..0d32001 100644 --- a/api/src/shopify_function.h +++ b/api/src/shopify_function.h @@ -7,6 +7,7 @@ // Type definitions typedef int64_t Val; typedef int32_t WriteResult; +typedef int32_t LogResult; typedef size_t InternedStringId; // Constants for WriteResult @@ -16,14 +17,6 @@ typedef size_t InternedStringId; // Import module declaration #define SHOPIFY_FUNCTION_IMPORT_MODULE "shopify_function_v1" -// Common API -/** - * Creates a new context for the Shopify Function execution - */ -__attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) -__attribute__((import_name("shopify_function_context_new"))) -extern void shopify_function_context_new(void); - // Read API /** * Gets the input value from the context @@ -111,14 +104,6 @@ __attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) __attribute__((import_name("shopify_function_output_new_null"))) extern WriteResult shopify_function_output_new_null(); -/** - * Finalizes the output and returns the result - * @return WriteResult indicating success or failure - */ -__attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) -__attribute__((import_name("shopify_function_output_finalize"))) -extern WriteResult shopify_function_output_finalize(); - /** * Creates a new 32-bit integer output value * @param value The integer value @@ -201,4 +186,14 @@ __attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) __attribute__((import_name("shopify_function_intern_utf8_str"))) extern InternedStringId shopify_function_intern_utf8_str(const uint8_t* ptr, size_t len); +/** + * Logs a new UTF-8 string output value + * @param ptr The string data + * @param len The length of the string + * @return LogResult indicating success or failure + */ +__attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) +__attribute__((import_name("shopify_function_log_new_utf8_str"))) +extern LogResult shopify_function_log_new_utf8_str(const uint8_t* ptr, size_t len); + #endif // SHOPIFY_FUNCTION_H diff --git a/api/src/shopify_function.wat b/api/src/shopify_function.wat index 62c6308..46ed13b 100644 --- a/api/src/shopify_function.wat +++ b/api/src/shopify_function.wat @@ -7,15 +7,6 @@ ;; host and guest. (module - ;; Creates and returns a new Context handle. - ;; - ;; The context represents an isolated scope in which values and state - ;; are bound. Every API call operates within this context, ensuring - ;; that the execution environment remains distinct and independent. - (import "shopify_function_v1" "shopify_function_context_new" - (func) - ) - ;; Read API Functions - Used to access input data. ;; Retrieves the root input value from the context. @@ -133,16 +124,6 @@ (func (result i32)) ) - ;; Finalizes the output, making it available to the host. - ;; Must be called after output construction is complete. - ;; This is typically the last API call made before function returns. - ;; Signals that the response is complete and ready to be used. - ;; Returns: - ;; - i32 status code indicating success or failure - (import "shopify_function_v1" "shopify_function_output_finalize" - (func (result i32)) - ) - ;; Writes a new integer output value. ;; Used for numeric values that fit within 32 bits. ;; More efficient than f64 for integral values. @@ -246,4 +227,16 @@ (import "shopify_function_v1" "shopify_function_intern_utf8_str" (func (param $ptr i32) (param $len i32) (result i32)) ) + + ;; Logs a new string output value. + ;; Used for text values in the logs. + ;; The string data is copied from WebAssembly memory. + ;; Parameters: + ;; - ptr: i32 pointer to string data in WebAssembly memory. + ;; - len: i32 length of string in bytes. + ;; Returns: + ;; - i32 status code indicating success or failure + (import "shopify_function_v1" "shopify_function_log_new_utf8_str" + (func (param $len i32) (result i32)) + ) ) diff --git a/api/src/test_data/header_test.c b/api/src/test_data/header_test.c index 41960c8..a5d54cb 100644 --- a/api/src/test_data/header_test.c +++ b/api/src/test_data/header_test.c @@ -8,7 +8,6 @@ // `/opt/homebrew/opt/llvm/bin/clang --target=wasm32-wasip1 -I .. -nostdlib -Wl,--no-entry -Wl,--export-all -Wl,--allow-undefined -o header_test.wasm header_test.c` volatile void* imports[] = { - (void*)shopify_function_context_new, (void*)shopify_function_input_get, (void*)shopify_function_input_get_val_len, (void*)shopify_function_input_read_utf8_str, @@ -18,7 +17,6 @@ volatile void* imports[] = { (void*)shopify_function_input_get_obj_key_at_index, (void*)shopify_function_output_new_bool, (void*)shopify_function_output_new_null, - (void*)shopify_function_output_finalize, (void*)shopify_function_output_new_i32, (void*)shopify_function_output_new_f64, (void*)shopify_function_output_new_utf8_str, @@ -27,5 +25,6 @@ volatile void* imports[] = { (void*)shopify_function_output_finish_object, (void*)shopify_function_output_new_array, (void*)shopify_function_output_finish_array, - (void*)shopify_function_intern_utf8_str + (void*)shopify_function_intern_utf8_str, + (void*)shopify_function_log_new_utf8_str }; diff --git a/api/src/test_data/header_test.wasm b/api/src/test_data/header_test.wasm index 057e77e7109619385b9c5239273ad035461efa2a..3b69e693bcd528ae4204c1ea6cd475b19f6702b3 100755 GIT binary patch delta 579 zcmZ9IyKWOf6o%)VnZ1s^$zB|YNia4`P=w>45Fv{y=|h!{X0s$VFH8A=@+FH$Pk=E7oA$)HZR|0IA5EV= zLoj){1gr?>AwV9e%{|%zog_Gb#iRu{fEVcyTC@ulYQd5nXHhRb=sagap=}U!f@`#e z7J0ZK(s7c|TfRUSSXYCJI+wNX-A!gp|5yRDNF%8qeGpgSg1(9S z;L|F-3O6yQX zXsF|g^)ybRq?2N(+9l<@a~qo_Mh_p@F64HV%7{>3tJKy;uP4i(_5FFA}O+XX^YD;&-N+eid3~uVEMyc&4u1Z;eh24D@ zBnD;}8CeMbKtZew@E0IPB&5QfQ!z5#`#kTXdrrSQt$%ULB$)02Ap|zXM;FA%=ee_`#yk9VxQIUgRt?bkd#nu<+Cm> zgO~sA;1E2#2|e6rO8V$a6H~T?@8FuYaE&(kc9M*-#V_M`>WKJYJSGy`ysM9M=&$J=& zHIp)!OoF4Leye0L3WOVwg6H8Yei1(7d)`-%FpalY8bEG=;}W2Z8$z0v?UWYEimBMZ_-T0Lzrdk^ARCmf zsN;er4J@B^fFDcWPPQKn(n*pUSBvEjQ3Y`Yl9 Lqp3ekSik-P|3IOj diff --git a/api/src/write.rs b/api/src/write.rs index 147cf25..31784a2 100644 --- a/api/src/write.rs +++ b/api/src/write.rs @@ -113,11 +113,6 @@ impl Context { map_result(unsafe { crate::shopify_function_output_finish_array() }) } - /// Finalize the output. This must be called exactly once, and must be called after all other writes. - pub fn finalize_output(self) -> Result<(), Error> { - map_result(unsafe { crate::shopify_function_output_finalize() }) - } - #[cfg(not(target_family = "wasm"))] /// Finalize the output and return the serialized value as a `serde_json::Value`. /// This is only available in non-Wasm targets, and therefore only recommended for use in tests. diff --git a/core/src/lib.rs b/core/src/lib.rs index 26e4673..e0c0ec0 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod log; pub mod read; pub mod write; diff --git a/core/src/log.rs b/core/src/log.rs new file mode 100644 index 0000000..bf6f4a8 --- /dev/null +++ b/core/src/log.rs @@ -0,0 +1,6 @@ +#[repr(usize)] +#[derive(Debug, strum::FromRepr, PartialEq, Eq)] +pub enum LogResult { + /// The log operation was successful. + Ok = 0, +} diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 4183cd9..80324d5 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -1,6 +1,6 @@ -use anyhow::Result; +use anyhow::{Error, Result}; use integration_tests::prepare_example; -use std::sync::LazyLock; +use std::{fmt::Display, sync::LazyLock}; use wasmtime::{Config, Engine, Linker, Module, Store}; use wasmtime_wasi::{ pipe::{MemoryInputPipe, MemoryOutputPipe}, @@ -43,7 +43,28 @@ fn assert_fuel_consumed_within_threshold(target_fuel: u64, fuel_consumed: u64) { } } -fn run_example(example: &str, input_bytes: Vec) -> Result<(Vec, u64)> { +enum Api { + Wasi, + Wasm, +} + +impl Api { + fn is_wasi(&self) -> bool { + match self { + Self::Wasi => true, + Self::Wasm => false, + } + } + + fn is_wasm(&self) -> bool { + match self { + Self::Wasi => false, + Self::Wasm => true, + } + } +} + +fn run_example(example: &str, input_bytes: Vec, api: Api) -> Result<(Vec, String, u64)> { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace_root = std::path::PathBuf::from(manifest_dir).join(".."); let engine = Engine::new(Config::new().consume_fuel(true))?; @@ -69,26 +90,37 @@ fn run_example(example: &str, input_bytes: Vec) -> Result<(Vec, u64)> { deterministic_wasi_ctx::replace_scheduling_functions_for_wasi_preview_0(&mut linker) .expect("Failed to replace scheduling functions in wasi-ctx"); - let stdin = MemoryInputPipe::new(input_bytes); - let stderr = MemoryOutputPipe::new(usize::MAX); - let stdout = MemoryOutputPipe::new(usize::MAX); let mut wasi_builder = WasiCtxBuilder::new(); - wasi_builder - .stdin(stdin) - .stdout(stdout.clone()) - .stderr(stderr.clone()); + let stdout = MemoryOutputPipe::new(usize::MAX); + if api.is_wasi() { + let stdin = MemoryInputPipe::new(input_bytes.clone()); + let stderr = MemoryOutputPipe::new(usize::MAX); + wasi_builder + .stdin(stdin) + .stdout(stdout.clone()) + .stderr(stderr); + } deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut wasi_builder); let wasi = wasi_builder.build_p1(); let mut store = Store::new(&engine, wasi); - store.set_fuel(STARTING_FUEL)?; let provider_instance = linker.instantiate(&mut store, &provider)?; + if api.is_wasm() { + store.set_fuel(STARTING_FUEL)?; + let init_func = provider_instance.get_typed_func::(&mut store, "initialize")?; + let input_buffer_offset = init_func.call(&mut store, input_bytes.len() as i32)?; + provider_instance + .get_memory(&mut store, "memory") + .unwrap() + .write(&mut store, input_buffer_offset as usize, &input_bytes)?; + } linker.instance( &mut store, shopify_function_provider::PROVIDER_MODULE_NAME, provider_instance, )?; + store.set_fuel(STARTING_FUEL)?; let instance = linker.instantiate(&mut store, &module)?; let func = instance.get_typed_func::<(), ()>(&mut store, "_start")?; @@ -97,19 +129,41 @@ fn run_example(example: &str, input_bytes: Vec) -> Result<(Vec, u64)> { let instructions = STARTING_FUEL.saturating_sub(store.get_fuel().unwrap_or_default()); + let mut output = Vec::new(); + let mut logs = Vec::new(); + if api.is_wasm() { + let results_offset = provider_instance + .get_typed_func::<(), u32>(&mut store, "finalize")? + .call(&mut store, ())?; + let memory = provider_instance.get_memory(&mut store, "memory").unwrap(); + let mut buf = [0; 16]; + memory.read(&store, results_offset as usize, &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 logs_offset = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize; + let logs_len = u32::from_le_bytes(buf[12..16].try_into().unwrap()) as usize; + output = vec![0; output_len]; + memory.read(&store, output_offset, &mut output)?; + logs = vec![0; logs_len]; + memory.read(&store, logs_offset, &mut logs)?; + } + drop(store); + let logs = String::from_utf8_lossy(&logs).to_string(); if let Err(e) = result { - let error = stderr.contents().to_vec(); - return Err(anyhow::anyhow!( - "{}\n\nSTDERR:\n{}", - e, - String::from_utf8(error)? - )); + return Err(anyhow::anyhow!(CallFuncError { + trap_error: e, + logs, + })); } - let output = stdout.contents().to_vec(); - Ok((output, instructions)) + if api.is_wasi() { + output = stdout.contents().to_vec(); + } + + Ok((output, logs, instructions)) } fn decode_msgpack_output(output: Vec) -> Result { @@ -151,15 +205,29 @@ fn prepare_wasi_json_input(input: serde_json::Value) -> Result> { fn run_wasm_api_example(example: &str, input: serde_json::Value) -> Result { let input_bytes = prepare_wasm_api_input(input)?; - let (output, _fuel) = run_example(example, input_bytes)?; + let (output, _logs, _fuel) = run_example(example, input_bytes, Api::Wasm)?; decode_msgpack_output(output) } +#[derive(Debug)] +struct CallFuncError { + trap_error: Error, + logs: String, +} + +impl Display for CallFuncError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}\n\nLogs: {}", self.trap_error, self.logs) + } +} + static ECHO_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("echo")); static BENCHMARK_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("cart-checkout-validation-wasm-api")); static BENCHMARK_NON_WASM_API_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("cart-checkout-validation-wasi-json")); +static LOG_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("log")); +static PANIC_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("panic")); #[test] fn test_echo_with_bool_input() -> Result<()> { @@ -341,10 +409,14 @@ fn test_fuel_consumption_within_threshold() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {}", e))?; let input = generate_cart_with_size(2, true); let wasm_api_input = prepare_wasm_api_input(input.clone())?; - let (_, wasm_api_fuel) = run_example("cart-checkout-validation-wasm-api", wasm_api_input)?; + let (_, _, wasm_api_fuel) = run_example( + "cart-checkout-validation-wasm-api", + wasm_api_input, + Api::Wasm, + )?; eprintln!("WASM API fuel: {}", wasm_api_fuel); // Using a target fuel value as reference similar to the Javy example - assert_fuel_consumed_within_threshold(14828, wasm_api_fuel); + assert_fuel_consumed_within_threshold(11504, wasm_api_fuel); Ok(()) } @@ -360,13 +432,19 @@ fn test_benchmark_comparison_with_input() -> Result<()> { let input = generate_cart_with_size(2, true); let wasm_api_input = prepare_wasm_api_input(input.clone())?; - let (wasm_api_output, wasm_api_fuel) = - run_example("cart-checkout-validation-wasm-api", wasm_api_input)?; + let (wasm_api_output, _logs, wasm_api_fuel) = run_example( + "cart-checkout-validation-wasm-api", + wasm_api_input, + Api::Wasm, + )?; let wasm_api_value = decode_msgpack_output(wasm_api_output)?; let wasi_json_input = prepare_wasi_json_input(input)?; - let (non_wasm_api_output, non_wasm_api_fuel) = - run_example("cart-checkout-validation-wasi-json", wasi_json_input)?; + let (non_wasm_api_output, _logs, non_wasm_api_fuel) = run_example( + "cart-checkout-validation-wasi-json", + wasi_json_input, + Api::Wasi, + )?; let non_wasm_api_value = decode_json_output(non_wasm_api_output)?; assert_eq!(wasm_api_value, non_wasm_api_value); @@ -384,7 +462,7 @@ fn test_benchmark_comparison_with_input() -> Result<()> { wasm_api_fuel, non_wasm_api_fuel, improvement ); - assert_fuel_consumed_within_threshold(14828, wasm_api_fuel); + assert_fuel_consumed_within_threshold(11504, wasm_api_fuel); assert_fuel_consumed_within_threshold(23858, non_wasm_api_fuel); Ok(()) @@ -402,13 +480,19 @@ fn test_benchmark_comparison_with_input_early_exit() -> Result<()> { let input = generate_cart_with_size(100, false); let wasm_api_input = prepare_wasm_api_input(input.clone())?; - let (wasm_api_output, wasm_api_fuel) = - run_example("cart-checkout-validation-wasm-api", wasm_api_input)?; + let (wasm_api_output, _logs, wasm_api_fuel) = run_example( + "cart-checkout-validation-wasm-api", + wasm_api_input, + Api::Wasm, + )?; let wasm_api_value = decode_msgpack_output(wasm_api_output)?; let wasi_json_input = prepare_wasi_json_input(input)?; - let (non_wasm_api_output, non_wasm_api_fuel) = - run_example("cart-checkout-validation-wasi-json", wasi_json_input)?; + let (non_wasm_api_output, _logs, non_wasm_api_fuel) = run_example( + "cart-checkout-validation-wasi-json", + wasi_json_input, + Api::Wasi, + )?; let non_wasm_api_value = decode_json_output(non_wasm_api_output)?; assert_eq!(wasm_api_value, non_wasm_api_value); @@ -427,8 +511,35 @@ fn test_benchmark_comparison_with_input_early_exit() -> Result<()> { ); // Add fuel consumption threshold checks for both implementations - assert_fuel_consumed_within_threshold(16880, wasm_api_fuel); + assert_fuel_consumed_within_threshold(11440, wasm_api_fuel); assert_fuel_consumed_within_threshold(736695, non_wasm_api_fuel); Ok(()) } + +#[test] +fn test_log() -> Result<()> { + LOG_EXAMPLE_RESULT + .as_ref() + .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; + let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; + assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); + assert_fuel_consumed_within_threshold(1895, fuel); + Ok(()) +} + +#[test] +fn test_panic() -> Result<()> { + PANIC_EXAMPLE_RESULT + .as_ref() + .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; + let error = run_example("panic", vec![], Api::Wasm) + .unwrap_err() + .downcast::()?; + assert_eq!( + error.logs, + "panicked at api/examples/panic.rs:7:5: +at the disco" + ); + Ok(()) +} diff --git a/provider/src/lib.rs b/provider/src/lib.rs index be55bb0..6c2a0d1 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -1,4 +1,5 @@ mod alloc; +pub mod log; pub mod read; mod string_interner; pub mod write; @@ -21,6 +22,7 @@ struct Context { bump_allocator: bumpalo::Bump, input_bytes: Vec, output_bytes: ByteBuf, + logs: Vec, write_state: State, write_parent_state_stack: Vec, string_interner: StringInterner, @@ -30,12 +32,18 @@ thread_local! { static CONTEXT: RefCell = RefCell::new(Context::default()) } +#[cfg(target_family = "wasm")] +thread_local! { + static OUTPUT_AND_LOG_PTRS: RefCell<[usize; 4]> = RefCell::new([0; 4]); +} + impl Default for Context { fn default() -> Self { Self { bump_allocator: Bump::new(), input_bytes: Vec::new(), output_bytes: ByteBuf::new(), + logs: Vec::with_capacity(1024), write_state: State::Start, write_parent_state_stack: Vec::new(), string_interner: StringInterner::new(), @@ -44,29 +52,11 @@ impl Default for Context { } impl Context { + #[cfg(not(target_family = "wasm"))] fn new(input_bytes: Vec) -> Self { - let bump_allocator = bumpalo::Bump::new(); - Self { - bump_allocator, - input_bytes, - output_bytes: ByteBuf::new(), - write_state: State::Start, - write_parent_state_stack: Vec::new(), - string_interner: StringInterner::new(), - } - } - - #[cfg(target_family = "wasm")] - fn new_from_stdin() -> Self { - use std::io::Read; - let mut input_bytes: Vec = vec![]; - let mut stdin = std::io::stdin(); - // Temporary use of stdin, to copy data into the Wasm linear memory. - // Initial benchmarking doesn't seem to suggest that this represents - // a source of performance overhead. - stdin.read_to_end(&mut input_bytes).unwrap(); - - Self::new(input_bytes) + let mut context = Self::default(); + context.input_bytes = input_bytes; + context } fn with(f: F) -> T @@ -105,16 +95,39 @@ macro_rules! decorate_for_target { pub(crate) use decorate_for_target; #[cfg(target_family = "wasm")] -#[export_name = "_shopify_function_context_new"] -extern "C" fn shopify_function_context_new() { - CONTEXT.with_borrow_mut(|context| *context = Context::new_from_stdin()) +#[export_name = "initialize"] +extern "C" fn initialize(input_len: usize) -> *const u8 { + CONTEXT.with_borrow_mut(|context| { + *context = Context::default(); + context.input_bytes = vec![0; input_len]; + context.input_bytes.as_ptr() + }) } #[cfg(not(target_family = "wasm"))] -pub fn shopify_function_context_new_from_msgpack_bytes(bytes: Vec) { +pub fn initialize_from_msgpack_bytes(bytes: Vec) { CONTEXT.with_borrow_mut(|context| *context = Context::new(bytes)) } +#[cfg(target_family = "wasm")] +#[export_name = "finalize"] +extern "C" fn finalize() -> *const usize { + Context::with(|context| { + let output = context.output_bytes.as_vec(); + let output_ptr = output.as_ptr(); + let output_len = output.len(); + let logs_ptr = context.logs.as_ptr(); + let logs_len = context.logs.len(); + OUTPUT_AND_LOG_PTRS.with_borrow_mut(|output_and_log_ptrs| { + output_and_log_ptrs[0] = output_ptr as usize; + output_and_log_ptrs[1] = output_len; + output_and_log_ptrs[2] = logs_ptr as usize; + output_and_log_ptrs[3] = logs_len; + output_and_log_ptrs.as_ptr() + }) + }) +} + decorate_for_target! { fn shopify_function_intern_utf8_str(len: usize) -> DoubleUsize { Context::with_mut(|context| { diff --git a/provider/src/log.rs b/provider/src/log.rs new file mode 100644 index 0000000..2b2350d --- /dev/null +++ b/provider/src/log.rs @@ -0,0 +1,21 @@ +use crate::{decorate_for_target, Context, DoubleUsize}; +use shopify_function_wasm_api_core::log::LogResult; + +impl Context { + fn allocate_log(&mut self, len: usize) -> *const u8 { + let write_offset = self.logs.len(); + self.logs.append(&mut vec![0; len]); + unsafe { self.logs.as_ptr().add(write_offset) } + } +} + +decorate_for_target! { + /// The most significant 32 bits are the result, the least significant 32 bits are the pointer. + fn shopify_function_log_new_utf8_str(len: usize) -> DoubleUsize { + Context::with_mut(|context| { + let ptr = context.allocate_log(len); + let result = LogResult::Ok; + ((result as DoubleUsize) << usize::BITS) | ptr as DoubleUsize + }) + } +} diff --git a/provider/src/write.rs b/provider/src/write.rs index 9e262ec..e650244 100644 --- a/provider/src/write.rs +++ b/provider/src/write.rs @@ -1,7 +1,6 @@ use crate::{decorate_for_target, Context, DoubleUsize}; use rmp::encode; use shopify_function_wasm_api_core::write::WriteResult; -use std::io::Write; mod state; @@ -205,30 +204,6 @@ decorate_for_target! { } } -decorate_for_target! { - fn shopify_function_output_finalize() -> WriteResult { - Context::with_mut(|context| { - let Context { - output_bytes, - write_state, - .. - } = &context; - if *write_state != State::End { - return WriteResult::ValueNotFinished; - } - let mut stdout = std::io::stdout(); - if stdout.write_all(output_bytes.as_slice()).is_err() { - return WriteResult::IoError; - } - if stdout.flush().is_err() { - return WriteResult::IoError; - } - WriteResult::Ok - }) - } -} - -#[cfg(not(target_family = "wasm"))] pub fn shopify_function_output_finalize_and_return_msgpack_bytes() -> (WriteResult, Vec) { Context::with_mut(|context| { let Context { diff --git a/trampoline/src/lib.rs b/trampoline/src/lib.rs index 3046182..5cf1324 100644 --- a/trampoline/src/lib.rs +++ b/trampoline/src/lib.rs @@ -6,10 +6,6 @@ use walrus::{ }; static IMPORTS: &[(&str, &str)] = &[ - ( - "shopify_function_context_new", - "_shopify_function_context_new", - ), ("shopify_function_input_get", "_shopify_function_input_get"), ( "shopify_function_input_get_val_len", @@ -40,10 +36,6 @@ static IMPORTS: &[(&str, &str)] = &[ "shopify_function_output_new_null", "_shopify_function_output_new_null", ), - ( - "shopify_function_output_finalize", - "_shopify_function_output_finalize", - ), ( "shopify_function_output_new_i32", "_shopify_function_output_new_i32", @@ -80,6 +72,10 @@ static IMPORTS: &[(&str, &str)] = &[ "shopify_function_output_finish_array", "_shopify_function_output_finish_array", ), + ( + "shopify_function_log_new_utf8_str", + "_shopify_function_log_new_utf8_str", + ), ]; pub const PROVIDER_MODULE_NAME: &str = @@ -449,6 +445,56 @@ impl TrampolineCodegen { Ok(()) } + fn emit_shopify_function_log_new_utf8_str(&mut self) -> walrus::Result<()> { + let Ok(imported_shopify_function_log_new_utf8_str) = self + .module + .imports + .get_func(PROVIDER_MODULE_NAME, "shopify_function_log_new_utf8_str") + else { + return Ok(()); + }; + + let shopify_function_log_new_utf8_str_type = + self.module.types.add(&[ValType::I32], &[ValType::I64]); + + let (provider_shopify_function_log_new_utf8_str, _) = self.module.add_import_func( + PROVIDER_MODULE_NAME, + "_shopify_function_log_new_utf8_str", + shopify_function_log_new_utf8_str_type, + ); + + let memcpy_to_provider = self.emit_memcpy_to_provider(); + + let output = self.module.locals.add(ValType::I64); + + self.module.replace_imported_func( + imported_shopify_function_log_new_utf8_str, + |(builder, arg_locals)| { + let src_ptr = arg_locals[0]; + let len = arg_locals[1]; + + builder + .func_body() + .local_get(len) + // most significant 32 bits are the result, least significant 32 bits are the pointer + .call(provider_shopify_function_log_new_utf8_str) + .local_tee(output) + // extract the result with a bit shift and wrap it to i32 + .i64_const(32) + .binop(BinaryOp::I64ShrU) + .unop(UnaryOp::I32WrapI64) // result is on the stack now + // extract the pointer by wrapping the output to i32 + .local_get(output) + .unop(UnaryOp::I32WrapI64) // dst_ptr is on the stack now + .local_get(src_ptr) + .local_get(len) + .call(memcpy_to_provider); + }, + )?; + + Ok(()) + } + pub fn apply(mut self) -> walrus::Result { // If the module does not have a memory, we should no-op if self.guest_memory_id.is_none() { @@ -469,6 +515,9 @@ impl TrampolineCodegen { "shopify_function_intern_utf8_str" => { self.emit_shopify_function_intern_utf8_str()? } + "shopify_function_log_new_utf8_str" => { + self.emit_shopify_function_log_new_utf8_str()? + } original => self.rename_imported_func(original, new)?, }; } diff --git a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap index 78d19a1..97be59c 100644 --- a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap +++ b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap @@ -4,46 +4,58 @@ expression: actual input_file: trampoline/src/test_data/consumer.wat --- (module - (type (;0;) (func)) - (type (;1;) (func (result i32))) - (type (;2;) (func (result i64))) - (type (;3;) (func (param i32) (result i32))) - (type (;4;) (func (param i32) (result i64))) - (type (;5;) (func (param i32 i32) (result i32))) - (type (;6;) (func (param i32 i32 i32))) - (type (;7;) (func (param i32 i32 i32 i32) (result i32))) - (type (;8;) (func (param i64) (result i32))) - (type (;9;) (func (param i64 i32) (result i64))) - (type (;10;) (func (param i64 i32 i32) (result i64))) - (type (;11;) (func (param f64) (result i32))) - (import "shopify_function_v1" "_shopify_function_context_new" (func (;0;) (type 0))) - (import "shopify_function_v1" "_shopify_function_input_get" (func (;1;) (type 2))) - (import "shopify_function_v1" "_shopify_function_input_get_interned_obj_prop" (func (;2;) (type 9))) - (import "shopify_function_v1" "_shopify_function_input_get_at_index" (func (;3;) (type 9))) - (import "shopify_function_v1" "_shopify_function_input_get_obj_key_at_index" (func (;4;) (type 9))) - (import "shopify_function_v1" "_shopify_function_input_get_val_len" (func (;5;) (type 8))) - (import "shopify_function_v1" "_shopify_function_output_new_bool" (func (;6;) (type 3))) - (import "shopify_function_v1" "_shopify_function_output_new_null" (func (;7;) (type 1))) - (import "shopify_function_v1" "_shopify_function_output_new_i32" (func (;8;) (type 3))) - (import "shopify_function_v1" "_shopify_function_output_new_f64" (func (;9;) (type 11))) - (import "shopify_function_v1" "_shopify_function_output_new_object" (func (;10;) (type 3))) - (import "shopify_function_v1" "_shopify_function_output_finish_object" (func (;11;) (type 1))) - (import "shopify_function_v1" "_shopify_function_output_new_array" (func (;12;) (type 3))) - (import "shopify_function_v1" "_shopify_function_output_finish_array" (func (;13;) (type 1))) - (import "shopify_function_v1" "_shopify_function_output_new_interned_utf8_str" (func (;14;) (type 3))) - (import "shopify_function_v1" "_shopify_function_output_finalize" (func (;15;) (type 1))) - (import "shopify_function_v1" "_shopify_function_input_get_utf8_str_addr" (func (;16;) (type 3))) + (type (;0;) (func (result i32))) + (type (;1;) (func (result i64))) + (type (;2;) (func (param i32) (result i32))) + (type (;3;) (func (param i32) (result i64))) + (type (;4;) (func (param i32 i32) (result i32))) + (type (;5;) (func (param i32 i32 i32))) + (type (;6;) (func (param i32 i32 i32 i32) (result i32))) + (type (;7;) (func (param i64) (result i32))) + (type (;8;) (func (param i64 i32) (result i64))) + (type (;9;) (func (param i64 i32 i32) (result i64))) + (type (;10;) (func (param f64) (result i32))) + (import "shopify_function_v1" "_shopify_function_input_get" (func (;0;) (type 1))) + (import "shopify_function_v1" "_shopify_function_input_get_interned_obj_prop" (func (;1;) (type 8))) + (import "shopify_function_v1" "_shopify_function_input_get_at_index" (func (;2;) (type 8))) + (import "shopify_function_v1" "_shopify_function_input_get_obj_key_at_index" (func (;3;) (type 8))) + (import "shopify_function_v1" "_shopify_function_input_get_val_len" (func (;4;) (type 7))) + (import "shopify_function_v1" "_shopify_function_output_new_bool" (func (;5;) (type 2))) + (import "shopify_function_v1" "_shopify_function_output_new_null" (func (;6;) (type 0))) + (import "shopify_function_v1" "_shopify_function_output_new_i32" (func (;7;) (type 2))) + (import "shopify_function_v1" "_shopify_function_output_new_f64" (func (;8;) (type 10))) + (import "shopify_function_v1" "_shopify_function_output_new_object" (func (;9;) (type 2))) + (import "shopify_function_v1" "_shopify_function_output_finish_object" (func (;10;) (type 0))) + (import "shopify_function_v1" "_shopify_function_output_new_array" (func (;11;) (type 2))) + (import "shopify_function_v1" "_shopify_function_output_finish_array" (func (;12;) (type 0))) + (import "shopify_function_v1" "_shopify_function_output_new_interned_utf8_str" (func (;13;) (type 2))) + (import "shopify_function_v1" "_shopify_function_input_get_utf8_str_addr" (func (;14;) (type 2))) (import "shopify_function_v1" "memory" (memory (;0;) 1)) - (import "shopify_function_v1" "_shopify_function_input_get_obj_prop" (func (;17;) (type 10))) - (import "shopify_function_v1" "shopify_function_realloc" (func (;18;) (type 7))) - (import "shopify_function_v1" "_shopify_function_output_new_utf8_str" (func (;19;) (type 4))) - (import "shopify_function_v1" "_shopify_function_intern_utf8_str" (func (;20;) (type 4))) + (import "shopify_function_v1" "_shopify_function_input_get_obj_prop" (func (;15;) (type 9))) + (import "shopify_function_v1" "shopify_function_realloc" (func (;16;) (type 6))) + (import "shopify_function_v1" "_shopify_function_output_new_utf8_str" (func (;17;) (type 3))) + (import "shopify_function_v1" "_shopify_function_intern_utf8_str" (func (;18;) (type 3))) + (import "shopify_function_v1" "_shopify_function_log_new_utf8_str" (func (;19;) (type 3))) (memory (;1;) 1) (export "memory" (memory 1)) - (func (;21;) (type 5) (param i32 i32) (result i32) + (func (;20;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 - call 20 + call 18 + local.tee 2 + i64.const 32 + i64.shr_u + i32.wrap_i64 + local.get 2 + i32.wrap_i64 + local.get 0 + local.get 1 + call 27 + ) + (func (;21;) (type 4) (param i32 i32) (result i32) + (local i64) + local.get 1 + call 17 local.tee 2 i64.const 32 i64.shr_u @@ -54,7 +66,7 @@ input_file: trampoline/src/test_data/consumer.wat local.get 1 call 27 ) - (func (;22;) (type 5) (param i32 i32) (result i32) + (func (;22;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 call 19 @@ -68,7 +80,7 @@ input_file: trampoline/src/test_data/consumer.wat local.get 1 call 27 ) - (func (;23;) (type 10) (param i64 i32 i32) (result i64) + (func (;23;) (type 9) (param i64 i32 i32) (result i64) (local i32) local.get 2 call 25 @@ -79,29 +91,29 @@ input_file: trampoline/src/test_data/consumer.wat local.get 0 local.get 3 local.get 2 - call 17 + call 15 ) - (func (;24;) (type 6) (param i32 i32 i32) + (func (;24;) (type 5) (param i32 i32 i32) local.get 1 local.get 0 - call 16 + call 14 local.get 2 call 26 ) - (func (;25;) (type 3) (param i32) (result i32) + (func (;25;) (type 2) (param i32) (result i32) i32.const 0 i32.const 0 i32.const 1 local.get 0 - call 18 + call 16 ) - (func (;26;) (type 6) (param i32 i32 i32) + (func (;26;) (type 5) (param i32 i32 i32) local.get 0 local.get 1 local.get 2 memory.copy 1 0 ) - (func (;27;) (type 6) (param i32 i32 i32) + (func (;27;) (type 5) (param i32 i32 i32) local.get 0 local.get 1 local.get 2 diff --git a/trampoline/src/test_data/consumer.wat b/trampoline/src/test_data/consumer.wat index 7ff48e6..7f7a5a4 100644 --- a/trampoline/src/test_data/consumer.wat +++ b/trampoline/src/test_data/consumer.wat @@ -1,6 +1,5 @@ (module ;; General - (import "shopify_function_v1" "shopify_function_context_new" (func)) (import "shopify_function_v1" "shopify_function_intern_utf8_str" (func (param i32 i32) (result i32))) ;; Read. @@ -23,7 +22,9 @@ (import "shopify_function_v1" "shopify_function_output_new_array" (func (param i32) (result i32))) (import "shopify_function_v1" "shopify_function_output_finish_array" (func (result i32))) (import "shopify_function_v1" "shopify_function_output_new_interned_utf8_str" (func (param i32) (result i32))) - (import "shopify_function_v1" "shopify_function_output_finalize" (func (result i32))) + + ;; Log. + (import "shopify_function_v1" "shopify_function_log_new_utf8_str" (func (param i32 i32) (result i32))) ;; Memory (memory 1) From 805722002096b086d902936737d4611d350597f3 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 24 Jun 2025 10:12:18 -0400 Subject: [PATCH 02/20] Make finalize more efficient --- provider/src/lib.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 6c2a0d1..94b81e4 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -113,16 +113,12 @@ pub fn initialize_from_msgpack_bytes(bytes: Vec) { #[export_name = "finalize"] extern "C" fn finalize() -> *const usize { Context::with(|context| { - let output = context.output_bytes.as_vec(); - let output_ptr = output.as_ptr(); - let output_len = output.len(); - let logs_ptr = context.logs.as_ptr(); - let logs_len = context.logs.len(); OUTPUT_AND_LOG_PTRS.with_borrow_mut(|output_and_log_ptrs| { - output_and_log_ptrs[0] = output_ptr as usize; - output_and_log_ptrs[1] = output_len; - output_and_log_ptrs[2] = logs_ptr as usize; - output_and_log_ptrs[3] = logs_len; + let output = context.output_bytes.as_vec(); + output_and_log_ptrs[0] = output.as_ptr() as usize; + output_and_log_ptrs[1] = output.len(); + output_and_log_ptrs[2] = context.logs.as_ptr() as usize; + output_and_log_ptrs[3] = context.logs.len(); output_and_log_ptrs.as_ptr() }) }) From 0617c42f7a00610eca473afe0bbbb37f49cd8db1 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Wed, 25 Jun 2025 11:05:30 -0400 Subject: [PATCH 03/20] Make initial log capacity configurable --- integration_tests/tests/integration_test.rs | 7 ++++--- provider/src/lib.rs | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 80324d5..fe67ce2 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -107,8 +107,9 @@ fn run_example(example: &str, input_bytes: Vec, api: Api) -> Result<(Vec let provider_instance = linker.instantiate(&mut store, &provider)?; if api.is_wasm() { store.set_fuel(STARTING_FUEL)?; - let init_func = provider_instance.get_typed_func::(&mut store, "initialize")?; - let input_buffer_offset = init_func.call(&mut store, input_bytes.len() as i32)?; + let init_func = + provider_instance.get_typed_func::<(i32, i32), i32>(&mut store, "initialize")?; + let input_buffer_offset = init_func.call(&mut store, (input_bytes.len() as i32, 1024))?; provider_instance .get_memory(&mut store, "memory") .unwrap() @@ -524,7 +525,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(1895, fuel); + assert_fuel_consumed_within_threshold(1807, fuel); Ok(()) } diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 94b81e4..0da4269 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -96,10 +96,11 @@ pub(crate) use decorate_for_target; #[cfg(target_family = "wasm")] #[export_name = "initialize"] -extern "C" fn initialize(input_len: usize) -> *const u8 { +extern "C" fn initialize(input_len: usize, log_initial_capacity: usize) -> *const u8 { CONTEXT.with_borrow_mut(|context| { *context = Context::default(); context.input_bytes = vec![0; input_len]; + context.logs = Vec::with_capacity(log_initial_capacity); context.input_bytes.as_ptr() }) } From 6335851353a43fc5f36677b1078c8ca554c2d0a0 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Wed, 25 Jun 2025 11:22:13 -0400 Subject: [PATCH 04/20] Make initial output capacity configurable --- integration_tests/tests/integration_test.rs | 11 ++++++----- provider/src/lib.rs | 9 +++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index fe67ce2..ca33e89 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -108,8 +108,9 @@ fn run_example(example: &str, input_bytes: Vec, api: Api) -> Result<(Vec if api.is_wasm() { store.set_fuel(STARTING_FUEL)?; let init_func = - provider_instance.get_typed_func::<(i32, i32), i32>(&mut store, "initialize")?; - let input_buffer_offset = init_func.call(&mut store, (input_bytes.len() as i32, 1024))?; + provider_instance.get_typed_func::<(i32, i32, i32), i32>(&mut store, "initialize")?; + let input_buffer_offset = + init_func.call(&mut store, (input_bytes.len() as i32, 1024, 1024))?; provider_instance .get_memory(&mut store, "memory") .unwrap() @@ -417,7 +418,7 @@ fn test_fuel_consumption_within_threshold() -> Result<()> { )?; eprintln!("WASM API fuel: {}", wasm_api_fuel); // Using a target fuel value as reference similar to the Javy example - assert_fuel_consumed_within_threshold(11504, wasm_api_fuel); + assert_fuel_consumed_within_threshold(10830, wasm_api_fuel); Ok(()) } @@ -463,7 +464,7 @@ fn test_benchmark_comparison_with_input() -> Result<()> { wasm_api_fuel, non_wasm_api_fuel, improvement ); - assert_fuel_consumed_within_threshold(11504, wasm_api_fuel); + assert_fuel_consumed_within_threshold(10830, wasm_api_fuel); assert_fuel_consumed_within_threshold(23858, non_wasm_api_fuel); Ok(()) @@ -512,7 +513,7 @@ fn test_benchmark_comparison_with_input_early_exit() -> Result<()> { ); // Add fuel consumption threshold checks for both implementations - assert_fuel_consumed_within_threshold(11440, wasm_api_fuel); + assert_fuel_consumed_within_threshold(10021, wasm_api_fuel); assert_fuel_consumed_within_threshold(736695, non_wasm_api_fuel); Ok(()) diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 0da4269..6b69c75 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -42,7 +42,7 @@ impl Default for Context { Self { bump_allocator: Bump::new(), input_bytes: Vec::new(), - output_bytes: ByteBuf::new(), + output_bytes: ByteBuf::with_capacity(1024), logs: Vec::with_capacity(1024), write_state: State::Start, write_parent_state_stack: Vec::new(), @@ -96,10 +96,15 @@ pub(crate) use decorate_for_target; #[cfg(target_family = "wasm")] #[export_name = "initialize"] -extern "C" fn initialize(input_len: usize, log_initial_capacity: usize) -> *const u8 { +extern "C" fn initialize( + input_len: usize, + output_initial_capacity: usize, + log_initial_capacity: usize, +) -> *const u8 { CONTEXT.with_borrow_mut(|context| { *context = Context::default(); context.input_bytes = vec![0; input_len]; + context.output_bytes = ByteBuf::with_capacity(output_initial_capacity); context.logs = Vec::with_capacity(log_initial_capacity); context.input_bytes.as_ptr() }) From 07258cc10feca435767870f8b926c7c5a79edc30 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Wed, 25 Jun 2025 15:07:10 -0400 Subject: [PATCH 05/20] Reduce fuel used by initialization --- integration_tests/tests/integration_test.rs | 6 +++--- provider/src/lib.rs | 21 ++++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index ca33e89..525dab7 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -418,7 +418,7 @@ fn test_fuel_consumption_within_threshold() -> Result<()> { )?; eprintln!("WASM API fuel: {}", wasm_api_fuel); // Using a target fuel value as reference similar to the Javy example - assert_fuel_consumed_within_threshold(10830, wasm_api_fuel); + assert_fuel_consumed_within_threshold(10839, wasm_api_fuel); Ok(()) } @@ -464,7 +464,7 @@ fn test_benchmark_comparison_with_input() -> Result<()> { wasm_api_fuel, non_wasm_api_fuel, improvement ); - assert_fuel_consumed_within_threshold(10830, wasm_api_fuel); + assert_fuel_consumed_within_threshold(10839, wasm_api_fuel); assert_fuel_consumed_within_threshold(23858, non_wasm_api_fuel); Ok(()) @@ -526,7 +526,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(1807, fuel); + assert_fuel_consumed_within_threshold(1895, fuel); Ok(()) } diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 6b69c75..4c4aba2 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -59,6 +59,19 @@ impl Context { context } + #[cfg(target_family = "wasm")] + fn new(output_capacity: usize, log_capacity: usize) -> Self { + Self { + bump_allocator: Bump::new(), + input_bytes: Vec::with_capacity(0), + output_bytes: ByteBuf::with_capacity(output_capacity), + logs: Vec::with_capacity(log_capacity), + write_state: State::Start, + write_parent_state_stack: Vec::new(), + string_interner: StringInterner::new(), + } + } + fn with(f: F) -> T where F: FnOnce(&Context) -> T, @@ -98,14 +111,12 @@ pub(crate) use decorate_for_target; #[export_name = "initialize"] extern "C" fn initialize( input_len: usize, - output_initial_capacity: usize, - log_initial_capacity: usize, + output_capacity: usize, + log_capacity: usize, ) -> *const u8 { CONTEXT.with_borrow_mut(|context| { - *context = Context::default(); + *context = Context::new(output_capacity, log_capacity); context.input_bytes = vec![0; input_len]; - context.output_bytes = ByteBuf::with_capacity(output_initial_capacity); - context.logs = Vec::with_capacity(log_initial_capacity); context.input_bytes.as_ptr() }) } From 21595da136b02cb376cf04ffbf273f0d96cc5a91 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Mon, 21 Jul 2025 15:06:56 -0400 Subject: [PATCH 06/20] Add more fuel tests around logging --- api/examples/log-len.rs | 14 ++++++++++ integration_tests/tests/integration_test.rs | 31 +++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 api/examples/log-len.rs diff --git a/api/examples/log-len.rs b/api/examples/log-len.rs new file mode 100644 index 0000000..e6e6297 --- /dev/null +++ b/api/examples/log-len.rs @@ -0,0 +1,14 @@ +use std::error::Error; + +use shopify_function_wasm_api::Context; + +fn main() -> Result<(), Box> { + shopify_function_wasm_api::init_panic_handler(); + let mut context = Context::new(); + let input = context.input_get()?; + let len = input.as_number().unwrap() as usize; + for _ in 0..len / 100 { + context.log(&"a".repeat(100)); + } + Ok(()) +} diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 525dab7..28d8afd 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -99,6 +99,8 @@ fn run_example(example: &str, input_bytes: Vec, api: Api) -> Result<(Vec .stdin(stdin) .stdout(stdout.clone()) .stderr(stderr); + } else { + wasi_builder.inherit_stderr(); } deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut wasi_builder); let wasi = wasi_builder.build_p1(); @@ -230,6 +232,7 @@ static BENCHMARK_NON_WASM_API_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("cart-checkout-validation-wasi-json")); static LOG_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("log")); static PANIC_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("panic")); +static LOG_LEN_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("log-len")); #[test] fn test_echo_with_bool_input() -> Result<()> { @@ -530,6 +533,34 @@ fn test_log() -> Result<()> { Ok(()) } +#[test] +fn test_log_len() -> Result<()> { + LOG_LEN_EXAMPLE_RESULT + .as_ref() + .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; + let run = |len| -> Result { + Ok(run_example( + "log-len", + prepare_wasm_api_input(serde_json::json!(len))?, + Api::Wasm, + )? + .2) + }; + let fuel = run(1)?; + assert_fuel_consumed_within_threshold(766, fuel); + let fuel = run(500)?; + assert_fuel_consumed_within_threshold(4_308, fuel); + let fuel = run(1_000)?; + assert_fuel_consumed_within_threshold(7_243, fuel); + let fuel = run(5_000)?; + assert_fuel_consumed_within_threshold(31_419, fuel); + let fuel = run(10_000)?; + assert_fuel_consumed_within_threshold(61_001, fuel); + let fuel = run(100_000)?; + assert_fuel_consumed_within_threshold(591_022, fuel); + Ok(()) +} + #[test] fn test_panic() -> Result<()> { PANIC_EXAMPLE_RESULT From 10ad15d6c11b050a6a9640ccf5eb42af785a54a2 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Mon, 21 Jul 2025 16:48:41 -0400 Subject: [PATCH 07/20] Use fixed size array for logs --- api/examples/log-past-capacity.rs | 11 ++++ api/src/lib.rs | 11 ++-- api/src/shopify_function.h | 4 +- api/src/shopify_function.wat | 4 +- core/src/lib.rs | 1 - core/src/log.rs | 6 --- integration_tests/tests/integration_test.rs | 25 ++++++--- provider/src/lib.rs | 8 +-- provider/src/log.rs | 52 +++++++++++++++---- trampoline/src/lib.rs | 7 +-- ...__disassemble_trampoline@consumer.wat.snap | 7 +-- 11 files changed, 92 insertions(+), 44 deletions(-) create mode 100644 api/examples/log-past-capacity.rs delete mode 100644 core/src/log.rs diff --git a/api/examples/log-past-capacity.rs b/api/examples/log-past-capacity.rs new file mode 100644 index 0000000..9b1bc14 --- /dev/null +++ b/api/examples/log-past-capacity.rs @@ -0,0 +1,11 @@ +use std::error::Error; + +use shopify_function_wasm_api::Context; + +fn main() -> Result<(), Box> { + shopify_function_wasm_api::init_panic_handler(); + let mut context = Context::new(); + context.log(&"a".repeat(1020)); + context.log(&"b".repeat(10)); + Ok(()) +} diff --git a/api/src/lib.rs b/api/src/lib.rs index cf385e6..c44da1e 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -60,7 +60,7 @@ extern "C" { fn shopify_function_output_finish_array() -> usize; // Log API. - fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize; + fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize); // Other. fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize; @@ -153,14 +153,11 @@ mod provider_fallback { } // Logging. - pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize { + pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) { let result = shopify_function_provider::log::shopify_function_log_new_utf8_str(len); - let write_result = (result >> usize::BITS) as usize; + let len = (result >> usize::BITS) as usize; let dst = result as usize; - if write_result == WriteResult::Ok as usize { - std::ptr::copy(ptr as _, dst as _, len); - } - write_result + std::ptr::copy(ptr as _, dst as _, len); } // Other. diff --git a/api/src/shopify_function.h b/api/src/shopify_function.h index 0d32001..8822876 100644 --- a/api/src/shopify_function.h +++ b/api/src/shopify_function.h @@ -7,7 +7,6 @@ // Type definitions typedef int64_t Val; typedef int32_t WriteResult; -typedef int32_t LogResult; typedef size_t InternedStringId; // Constants for WriteResult @@ -190,10 +189,9 @@ extern InternedStringId shopify_function_intern_utf8_str(const uint8_t* ptr, siz * Logs a new UTF-8 string output value * @param ptr The string data * @param len The length of the string - * @return LogResult indicating success or failure */ __attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) __attribute__((import_name("shopify_function_log_new_utf8_str"))) -extern LogResult shopify_function_log_new_utf8_str(const uint8_t* ptr, size_t len); +extern void shopify_function_log_new_utf8_str(const uint8_t* ptr, size_t len); #endif // SHOPIFY_FUNCTION_H diff --git a/api/src/shopify_function.wat b/api/src/shopify_function.wat index 46ed13b..a18e60d 100644 --- a/api/src/shopify_function.wat +++ b/api/src/shopify_function.wat @@ -234,9 +234,7 @@ ;; Parameters: ;; - ptr: i32 pointer to string data in WebAssembly memory. ;; - len: i32 length of string in bytes. - ;; Returns: - ;; - i32 status code indicating success or failure (import "shopify_function_v1" "shopify_function_log_new_utf8_str" - (func (param $len i32) (result i32)) + (func (param $len i32)) ) ) diff --git a/core/src/lib.rs b/core/src/lib.rs index e0c0ec0..26e4673 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,3 @@ -pub mod log; pub mod read; pub mod write; diff --git a/core/src/log.rs b/core/src/log.rs deleted file mode 100644 index bf6f4a8..0000000 --- a/core/src/log.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[repr(usize)] -#[derive(Debug, strum::FromRepr, PartialEq, Eq)] -pub enum LogResult { - /// The log operation was successful. - Ok = 0, -} diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 28d8afd..ab99562 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -233,6 +233,8 @@ static BENCHMARK_NON_WASM_API_EXAMPLE_RESULT: LazyLock> = static LOG_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("log")); static PANIC_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("panic")); static LOG_LEN_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("log-len")); +static LOG_PAST_CAPACITY_EXAMPLE_RESULT: LazyLock> = + LazyLock::new(|| prepare_example("log-past-capacity")); #[test] fn test_echo_with_bool_input() -> Result<()> { @@ -529,7 +531,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(1895, fuel); + assert_fuel_consumed_within_threshold(466, fuel); Ok(()) } @@ -549,15 +551,26 @@ fn test_log_len() -> Result<()> { let fuel = run(1)?; assert_fuel_consumed_within_threshold(766, fuel); let fuel = run(500)?; - assert_fuel_consumed_within_threshold(4_308, fuel); + assert_fuel_consumed_within_threshold(2_878, fuel); let fuel = run(1_000)?; - assert_fuel_consumed_within_threshold(7_243, fuel); + assert_fuel_consumed_within_threshold(4_383, fuel); let fuel = run(5_000)?; - assert_fuel_consumed_within_threshold(31_419, fuel); + assert_fuel_consumed_within_threshold(16_423, fuel); let fuel = run(10_000)?; - assert_fuel_consumed_within_threshold(61_001, fuel); + assert_fuel_consumed_within_threshold(31_473, fuel); let fuel = run(100_000)?; - assert_fuel_consumed_within_threshold(591_022, fuel); + assert_fuel_consumed_within_threshold(302_403, fuel); + Ok(()) +} + +#[test] +fn test_log_past_capacity() -> Result<()> { + LOG_PAST_CAPACITY_EXAMPLE_RESULT + .as_ref() + .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; + let (_, logs, fuel) = run_example("log-past-capacity", vec![], Api::Wasm)?; + assert_eq!(logs, format!("{}bbbb", "a".repeat(1020))); + assert_fuel_consumed_within_threshold(1297, fuel); Ok(()) } diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 4c4aba2..2e0e12a 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -22,7 +22,7 @@ struct Context { bump_allocator: bumpalo::Bump, input_bytes: Vec, output_bytes: ByteBuf, - logs: Vec, + logs: Logs, write_state: State, write_parent_state_stack: Vec, string_interner: StringInterner, @@ -43,7 +43,7 @@ impl Default for Context { bump_allocator: Bump::new(), input_bytes: Vec::new(), output_bytes: ByteBuf::with_capacity(1024), - logs: Vec::with_capacity(1024), + logs: Logs::default(), write_state: State::Start, write_parent_state_stack: Vec::new(), string_interner: StringInterner::new(), @@ -65,7 +65,7 @@ impl Context { bump_allocator: Bump::new(), input_bytes: Vec::with_capacity(0), output_bytes: ByteBuf::with_capacity(output_capacity), - logs: Vec::with_capacity(log_capacity), + logs: Logs::default(), write_state: State::Start, write_parent_state_stack: Vec::new(), string_interner: StringInterner::new(), @@ -107,6 +107,8 @@ macro_rules! decorate_for_target { pub(crate) use decorate_for_target; +use crate::log::Logs; + #[cfg(target_family = "wasm")] #[export_name = "initialize"] extern "C" fn initialize( diff --git a/provider/src/log.rs b/provider/src/log.rs index 2b2350d..6156a24 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -1,21 +1,55 @@ use crate::{decorate_for_target, Context, DoubleUsize}; -use shopify_function_wasm_api_core::log::LogResult; + +const CAPACITY: usize = 1024; + +#[derive(Debug)] +pub(crate) struct Logs { + buffer: [u8; CAPACITY], + write_offset: usize, +} + +impl Default for Logs { + fn default() -> Self { + Self { + buffer: [0; CAPACITY], + write_offset: 0, + } + } +} + +impl Logs { + fn append(&mut self, len: usize) -> (*const u8, usize) { + let mut ret_len = len; + let remaining_capacity = CAPACITY - self.write_offset; + if len > remaining_capacity { + ret_len = remaining_capacity; + } + let write_offset = self.write_offset; + self.write_offset += ret_len; + (unsafe { self.buffer.as_ptr().add(write_offset) }, ret_len) + } + + pub(crate) fn as_ptr(&self) -> *const u8 { + self.buffer.as_ptr() + } + + pub(crate) fn len(&self) -> usize { + self.write_offset + } +} impl Context { - fn allocate_log(&mut self, len: usize) -> *const u8 { - let write_offset = self.logs.len(); - self.logs.append(&mut vec![0; len]); - unsafe { self.logs.as_ptr().add(write_offset) } + fn allocate_log(&mut self, len: usize) -> (*const u8, usize) { + self.logs.append(len) } } decorate_for_target! { - /// The most significant 32 bits are the result, the least significant 32 bits are the pointer. + /// The most significant 32 bits are the length, the least significant 32 bits are the pointer. fn shopify_function_log_new_utf8_str(len: usize) -> DoubleUsize { Context::with_mut(|context| { - let ptr = context.allocate_log(len); - let result = LogResult::Ok; - ((result as DoubleUsize) << usize::BITS) | ptr as DoubleUsize + let (ptr, len) = context.allocate_log(len); + ((len as DoubleUsize) << usize::BITS) | ptr as DoubleUsize }) } } diff --git a/trampoline/src/lib.rs b/trampoline/src/lib.rs index 5cf1324..cefe4f5 100644 --- a/trampoline/src/lib.rs +++ b/trampoline/src/lib.rs @@ -476,13 +476,14 @@ impl TrampolineCodegen { builder .func_body() .local_get(len) - // most significant 32 bits are the result, least significant 32 bits are the pointer + // most significant 32 bits are the length, least significant 32 bits are the pointer .call(provider_shopify_function_log_new_utf8_str) .local_tee(output) - // extract the result with a bit shift and wrap it to i32 + // extract the length with a bit shift and wrap it to i32 .i64_const(32) .binop(BinaryOp::I64ShrU) - .unop(UnaryOp::I32WrapI64) // result is on the stack now + .unop(UnaryOp::I32WrapI64) // length is on the stack now + .local_set(len) // extract the pointer by wrapping the output to i32 .local_get(output) .unop(UnaryOp::I32WrapI64) // dst_ptr is on the stack now diff --git a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap index 97be59c..51ecb86 100644 --- a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap +++ b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap @@ -41,11 +41,12 @@ input_file: trampoline/src/test_data/consumer.wat (func (;20;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 - call 18 + call 19 local.tee 2 i64.const 32 i64.shr_u i32.wrap_i64 + local.set 1 local.get 2 i32.wrap_i64 local.get 0 @@ -55,7 +56,7 @@ input_file: trampoline/src/test_data/consumer.wat (func (;21;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 - call 17 + call 18 local.tee 2 i64.const 32 i64.shr_u @@ -69,7 +70,7 @@ input_file: trampoline/src/test_data/consumer.wat (func (;22;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 - call 19 + call 17 local.tee 2 i64.const 32 i64.shr_u From a5953aaed91a86a0080bbb6111b2ffb68bb6a405 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 22 Jul 2025 13:22:51 -0400 Subject: [PATCH 08/20] Avoid one unnecessary allocation --- integration_tests/tests/integration_test.rs | 12 ++++++------ provider/src/log.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 28d8afd..167ece3 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -529,7 +529,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(1895, fuel); + assert_fuel_consumed_within_threshold(646, fuel); Ok(()) } @@ -549,15 +549,15 @@ fn test_log_len() -> Result<()> { let fuel = run(1)?; assert_fuel_consumed_within_threshold(766, fuel); let fuel = run(500)?; - assert_fuel_consumed_within_threshold(4_308, fuel); + assert_fuel_consumed_within_threshold(3_103, fuel); let fuel = run(1_000)?; - assert_fuel_consumed_within_threshold(7_243, fuel); + assert_fuel_consumed_within_threshold(4_833, fuel); let fuel = run(5_000)?; - assert_fuel_consumed_within_threshold(31_419, fuel); + assert_fuel_consumed_within_threshold(19_369, fuel); let fuel = run(10_000)?; - assert_fuel_consumed_within_threshold(61_001, fuel); + assert_fuel_consumed_within_threshold(36_901, fuel); let fuel = run(100_000)?; - assert_fuel_consumed_within_threshold(591_022, fuel); + assert_fuel_consumed_within_threshold(350_022, fuel); Ok(()) } diff --git a/provider/src/log.rs b/provider/src/log.rs index 2b2350d..a15d0a6 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -4,7 +4,7 @@ use shopify_function_wasm_api_core::log::LogResult; impl Context { fn allocate_log(&mut self, len: usize) -> *const u8 { let write_offset = self.logs.len(); - self.logs.append(&mut vec![0; len]); + self.logs.resize(write_offset + len, 0); unsafe { self.logs.as_ptr().add(write_offset) } } } From 0bc54233293b7abb15e0059913bd0ad6626db4eb Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 22 Jul 2025 13:11:41 -0400 Subject: [PATCH 09/20] Ring buffer for logging --- api/src/lib.rs | 14 ++- integration_tests/tests/integration_test.rs | 35 +++--- provider/src/lib.rs | 9 +- provider/src/log.rs | 91 ++++++++++++---- trampoline/src/lib.rs | 102 +++++++++++++++--- ...__disassemble_trampoline@consumer.wat.snap | 40 +++++-- 6 files changed, 228 insertions(+), 63 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index c44da1e..394dbb1 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -154,10 +154,16 @@ mod provider_fallback { // Logging. pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) { - let result = shopify_function_provider::log::shopify_function_log_new_utf8_str(len); - let len = (result >> usize::BITS) as usize; - let dst = result as usize; - std::ptr::copy(ptr as _, dst as _, len); + let addr = shopify_function_provider::log::shopify_function_log_new_utf8_str(len) + as *const [usize; 5]; + let array = *addr; + let source_offset = array[0]; + let dst_offset1 = array[1]; + let len1 = array[2]; + let dst_offset2 = array[3]; + let len2 = array[4]; + std::ptr::copy(ptr.add(source_offset) as _, dst_offset1 as _, len1); + std::ptr::copy(ptr.add(source_offset).add(len1), dst_offset2 as _, len2); } // Other. diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index ab99562..8edfee3 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -140,17 +140,24 @@ fn run_example(example: &str, input_bytes: Vec, api: Api) -> Result<(Vec .get_typed_func::<(), u32>(&mut store, "finalize")? .call(&mut store, ())?; let memory = provider_instance.get_memory(&mut store, "memory").unwrap(); - let mut buf = [0; 16]; + let mut buf = [0; 24]; memory.read(&store, results_offset as usize, &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 logs_offset = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize; - let logs_len = u32::from_le_bytes(buf[12..16].try_into().unwrap()) as usize; + let logs_offset1 = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize; + let logs_len1 = u32::from_le_bytes(buf[12..16].try_into().unwrap()) as usize; + let logs_offset2 = u32::from_le_bytes(buf[16..20].try_into().unwrap()) as usize; + let logs_len2 = u32::from_le_bytes(buf[20..24].try_into().unwrap()) as usize; output = vec![0; output_len]; memory.read(&store, output_offset, &mut output)?; - logs = vec![0; logs_len]; - memory.read(&store, logs_offset, &mut logs)?; + let mut logs1 = vec![0; logs_len1]; + memory.read(&store, logs_offset1, &mut logs1)?; + let mut logs2 = vec![0; logs_len2]; + memory.read(&store, logs_offset2, &mut logs2)?; + logs = Vec::with_capacity(logs_len1 + logs_len2); + logs.extend(logs1); + logs.extend(logs2); } drop(store); @@ -221,7 +228,7 @@ struct CallFuncError { impl Display for CallFuncError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}\n\nLogs: {}", self.trap_error, self.logs) + write!(f, "{:?}\n\nLogs: {}", self.trap_error, self.logs) } } @@ -531,7 +538,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(466, fuel); + assert_fuel_consumed_within_threshold(706, fuel); Ok(()) } @@ -551,15 +558,15 @@ fn test_log_len() -> Result<()> { let fuel = run(1)?; assert_fuel_consumed_within_threshold(766, fuel); let fuel = run(500)?; - assert_fuel_consumed_within_threshold(2_878, fuel); + assert_fuel_consumed_within_threshold(3_178, fuel); let fuel = run(1_000)?; - assert_fuel_consumed_within_threshold(4_383, fuel); + assert_fuel_consumed_within_threshold(4_983, fuel); let fuel = run(5_000)?; - assert_fuel_consumed_within_threshold(16_423, fuel); + assert_fuel_consumed_within_threshold(19_891, fuel); let fuel = run(10_000)?; - assert_fuel_consumed_within_threshold(31_473, fuel); + assert_fuel_consumed_within_threshold(38_526, fuel); let fuel = run(100_000)?; - assert_fuel_consumed_within_threshold(302_403, fuel); + assert_fuel_consumed_within_threshold(373_901, fuel); Ok(()) } @@ -569,8 +576,8 @@ fn test_log_past_capacity() -> Result<()> { .as_ref() .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log-past-capacity", vec![], Api::Wasm)?; - assert_eq!(logs, format!("{}bbbb", "a".repeat(1020))); - assert_fuel_consumed_within_threshold(1297, fuel); + assert_eq!(logs, format!("{}{}", "a".repeat(1014), "b".repeat(10))); + assert_fuel_consumed_within_threshold(1453, fuel); Ok(()) } diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 2e0e12a..50fd9e9 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -34,7 +34,7 @@ thread_local! { #[cfg(target_family = "wasm")] thread_local! { - static OUTPUT_AND_LOG_PTRS: RefCell<[usize; 4]> = RefCell::new([0; 4]); + static OUTPUT_AND_LOG_PTRS: RefCell<[usize; 6]> = RefCell::new([0; 6]); } impl Default for Context { @@ -136,8 +136,11 @@ extern "C" fn finalize() -> *const usize { let output = context.output_bytes.as_vec(); output_and_log_ptrs[0] = output.as_ptr() as usize; output_and_log_ptrs[1] = output.len(); - output_and_log_ptrs[2] = context.logs.as_ptr() as usize; - output_and_log_ptrs[3] = context.logs.len(); + let (log_offset1, log_len1, log_offset2, log_len2) = context.logs.read_ptrs(); + output_and_log_ptrs[2] = log_offset1 as _; + output_and_log_ptrs[3] = log_len1; + output_and_log_ptrs[4] = log_offset2 as _; + output_and_log_ptrs[5] = log_len2; output_and_log_ptrs.as_ptr() }) }) diff --git a/provider/src/log.rs b/provider/src/log.rs index 6156a24..eda3658 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -1,55 +1,108 @@ -use crate::{decorate_for_target, Context, DoubleUsize}; +use std::ptr; +use crate::{decorate_for_target, Context}; + +static mut LOG_RET_AREA: [usize; 5] = [0; 5]; const CAPACITY: usize = 1024; #[derive(Debug)] pub(crate) struct Logs { buffer: [u8; CAPACITY], + read_offset: usize, write_offset: usize, + len: usize, } impl Default for Logs { fn default() -> Self { Self { buffer: [0; CAPACITY], + read_offset: 0, write_offset: 0, + len: 0, } } } impl Logs { - fn append(&mut self, len: usize) -> (*const u8, usize) { - let mut ret_len = len; - let remaining_capacity = CAPACITY - self.write_offset; - if len > remaining_capacity { - ret_len = remaining_capacity; + fn append(&mut self, mut len: usize) -> (usize, *const u8, usize, *const u8, usize) { + let mut source_offset = 0; + let dst_offset1 = unsafe { self.buffer.as_ptr().add(self.write_offset) }; + let len1; + let mut dst_offset2 = ptr::null(); + let mut len2 = 0; + + // Need to strip off start of incoming buffer if the incoming buffer exceeds capacity. + if len > CAPACITY { + source_offset = len - CAPACITY; + len = CAPACITY; + } + + let space_to_end = CAPACITY - self.write_offset; + if len <= space_to_end { + // Incoming buffer fits in one block. + len1 = len; + } else { + // Incoming data wrap will wrap around. + len1 = space_to_end; + dst_offset2 = self.buffer.as_ptr(); + len2 = len - space_to_end; } - let write_offset = self.write_offset; - self.write_offset += ret_len; - (unsafe { self.buffer.as_ptr().add(write_offset) }, ret_len) - } - pub(crate) fn as_ptr(&self) -> *const u8 { - self.buffer.as_ptr() + self.write_offset = (self.write_offset + len) % CAPACITY; + + if self.len + len <= CAPACITY { + // No overwriting. + self.len += len; + } else { + // Overwriting. + let overwritten_bytes = self.len + len - CAPACITY; + self.read_offset = (self.read_offset + overwritten_bytes) % CAPACITY; + self.len = CAPACITY; + } + + (source_offset, dst_offset1, len1, dst_offset2, len2) } - pub(crate) fn len(&self) -> usize { - self.write_offset + pub(crate) fn read_ptrs(&self) -> (*const u8, usize, *const u8, usize) { + let data_to_end = CAPACITY - self.read_offset; + if self.len <= data_to_end { + ( + unsafe { self.buffer.as_ptr().add(self.read_offset) }, + self.len, + ptr::null(), + 0, + ) + } else { + ( + unsafe { self.buffer.as_ptr().add(self.read_offset) }, + data_to_end, + self.buffer.as_ptr(), + self.len - data_to_end, + ) + } } } impl Context { - fn allocate_log(&mut self, len: usize) -> (*const u8, usize) { + fn allocate_log(&mut self, len: usize) -> (usize, *const u8, usize, *const u8, usize) { self.logs.append(len) } } decorate_for_target! { - /// The most significant 32 bits are the length, the least significant 32 bits are the pointer. - fn shopify_function_log_new_utf8_str(len: usize) -> DoubleUsize { + fn shopify_function_log_new_utf8_str(len: usize) -> *const usize { Context::with_mut(|context| { - let (ptr, len) = context.allocate_log(len); - ((len as DoubleUsize) << usize::BITS) | ptr as DoubleUsize + let (src_offset, ptr1, len1, ptr2, len2) = context.allocate_log(len); + #[allow(static_mut_refs)] // This is _technically_ safe given this is single threaded. + unsafe { + LOG_RET_AREA[0] = src_offset; + LOG_RET_AREA[1] = ptr1 as usize; + LOG_RET_AREA[2] = len1; + LOG_RET_AREA[3] = ptr2 as usize; + LOG_RET_AREA[4] = len2; + LOG_RET_AREA.as_ptr() + } }) } } diff --git a/trampoline/src/lib.rs b/trampoline/src/lib.rs index cefe4f5..80c8d2c 100644 --- a/trampoline/src/lib.rs +++ b/trampoline/src/lib.rs @@ -1,7 +1,7 @@ use std::cell::OnceCell; use std::path::Path; use walrus::{ - ir::{BinaryOp, UnaryOp}, + ir::{BinaryOp, MemArg, UnaryOp}, FunctionBuilder, FunctionId, ImportKind, MemoryId, Module, ValType, }; @@ -455,7 +455,7 @@ impl TrampolineCodegen { }; let shopify_function_log_new_utf8_str_type = - self.module.types.add(&[ValType::I32], &[ValType::I64]); + self.module.types.add(&[ValType::I32], &[ValType::I32]); let (provider_shopify_function_log_new_utf8_str, _) = self.module.add_import_func( PROVIDER_MODULE_NAME, @@ -464,8 +464,13 @@ impl TrampolineCodegen { ); let memcpy_to_provider = self.emit_memcpy_to_provider(); - - let output = self.module.locals.add(ValType::I64); + let provider_memory = self.provider_memory_id(); + let array_addr = self.module.locals.add(ValType::I32); + let source_offset = self.module.locals.add(ValType::I32); + let dst_offset1 = self.module.locals.add(ValType::I32); + let len1 = self.module.locals.add(ValType::I32); + let dst_offset2 = self.module.locals.add(ValType::I32); + let len2 = self.module.locals.add(ValType::I32); self.module.replace_imported_func( imported_shopify_function_log_new_utf8_str, @@ -476,20 +481,87 @@ impl TrampolineCodegen { builder .func_body() .local_get(len) - // most significant 32 bits are the length, least significant 32 bits are the pointer + // return value is memory address for (src_offset, dst_offset1, len1, dst_offset2, len2) .call(provider_shopify_function_log_new_utf8_str) - .local_tee(output) - // extract the length with a bit shift and wrap it to i32 - .i64_const(32) - .binop(BinaryOp::I64ShrU) - .unop(UnaryOp::I32WrapI64) // length is on the stack now - .local_set(len) - // extract the pointer by wrapping the output to i32 - .local_get(output) - .unop(UnaryOp::I32WrapI64) // dst_ptr is on the stack now + .local_tee(array_addr) + .load( + provider_memory, + walrus::ir::LoadKind::I32 { atomic: false }, + MemArg { + offset: 0, + align: 4, + }, + ) + .local_set(source_offset) + .local_get(array_addr) + .load( + provider_memory, + walrus::ir::LoadKind::I32 { atomic: false }, + MemArg { + offset: 4, + align: 4, + }, + ) + .local_set(dst_offset1) + .local_get(array_addr) + .load( + provider_memory, + walrus::ir::LoadKind::I32 { atomic: false }, + MemArg { + offset: 8, + align: 4, + }, + ) + .local_set(len1) + // prep and call first memcpy + // need to get (dst1, src + src_offset, len1) on stack + .local_get(dst_offset1) .local_get(src_ptr) + .local_get(source_offset) + .binop(BinaryOp::I32Add) + .local_tee(src_ptr) + .local_get(len1) + .call(memcpy_to_provider) + // exit early if len1 == len which implies len2 will be 0 + .local_get(len1) .local_get(len) - .call(memcpy_to_provider); + .binop(BinaryOp::I32Ne) + .if_else( + None, + |then| { + // load locals for second memcpy + then.local_get(array_addr) + .load( + provider_memory, + walrus::ir::LoadKind::I32 { atomic: false }, + MemArg { + offset: 12, + align: 4, + }, + ) + .local_set(dst_offset2) + .local_get(array_addr) + .load( + provider_memory, + walrus::ir::LoadKind::I32 { atomic: false }, + MemArg { + offset: 16, + align: 4, + }, + ) + .local_set(len2) + // prep and call second memcpy + // need to get (dst2, src + src_offset + len1, len2) on stack + .local_get(dst_offset2) + // contains (src + src_offset) from last memcpy + .local_get(src_ptr) + .local_get(len1) + .binop(BinaryOp::I32Add) + .local_get(len2) + .call(memcpy_to_provider); + }, + |_else| {}, + ); }, )?; diff --git a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap index 51ecb86..1233b35 100644 --- a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap +++ b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap @@ -35,23 +35,47 @@ input_file: trampoline/src/test_data/consumer.wat (import "shopify_function_v1" "shopify_function_realloc" (func (;16;) (type 6))) (import "shopify_function_v1" "_shopify_function_output_new_utf8_str" (func (;17;) (type 3))) (import "shopify_function_v1" "_shopify_function_intern_utf8_str" (func (;18;) (type 3))) - (import "shopify_function_v1" "_shopify_function_log_new_utf8_str" (func (;19;) (type 3))) + (import "shopify_function_v1" "_shopify_function_log_new_utf8_str" (func (;19;) (type 2))) (memory (;1;) 1) (export "memory" (memory 1)) (func (;20;) (type 4) (param i32 i32) (result i32) - (local i64) + (local i32 i32 i32 i32 i32 i32) local.get 1 call 19 local.tee 2 - i64.const 32 - i64.shr_u - i32.wrap_i64 - local.set 1 + i32.load + local.set 3 local.get 2 - i32.wrap_i64 + i32.load offset=4 + local.set 4 + local.get 2 + i32.load offset=8 + local.set 5 + local.get 4 local.get 0 - local.get 1 + local.get 3 + i32.add + local.tee 0 + local.get 5 call 27 + local.get 5 + local.get 1 + i32.ne + if ;; label = @1 + local.get 2 + i32.load offset=12 + local.set 6 + local.get 2 + i32.load offset=16 + local.set 7 + local.get 6 + local.get 0 + local.get 5 + i32.add + local.get 7 + call 27 + else + end ) (func (;21;) (type 4) (param i32 i32) (result i32) (local i64) From f46bd73e8ce178cec75de5df4aa3ce28851b0267 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Mon, 21 Jul 2025 16:48:41 -0400 Subject: [PATCH 10/20] Use fixed size array for logs --- api/examples/log-past-capacity.rs | 11 ++++ api/src/lib.rs | 11 ++-- api/src/shopify_function.h | 4 +- api/src/shopify_function.wat | 4 +- core/src/lib.rs | 1 - core/src/log.rs | 6 --- integration_tests/tests/integration_test.rs | 25 ++++++--- provider/src/lib.rs | 8 +-- provider/src/log.rs | 52 +++++++++++++++---- trampoline/src/lib.rs | 29 +++++++---- ...__disassemble_trampoline@consumer.wat.snap | 22 +++++--- 11 files changed, 118 insertions(+), 55 deletions(-) create mode 100644 api/examples/log-past-capacity.rs delete mode 100644 core/src/log.rs diff --git a/api/examples/log-past-capacity.rs b/api/examples/log-past-capacity.rs new file mode 100644 index 0000000..9b1bc14 --- /dev/null +++ b/api/examples/log-past-capacity.rs @@ -0,0 +1,11 @@ +use std::error::Error; + +use shopify_function_wasm_api::Context; + +fn main() -> Result<(), Box> { + shopify_function_wasm_api::init_panic_handler(); + let mut context = Context::new(); + context.log(&"a".repeat(1020)); + context.log(&"b".repeat(10)); + Ok(()) +} diff --git a/api/src/lib.rs b/api/src/lib.rs index cf385e6..c44da1e 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -60,7 +60,7 @@ extern "C" { fn shopify_function_output_finish_array() -> usize; // Log API. - fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize; + fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize); // Other. fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize; @@ -153,14 +153,11 @@ mod provider_fallback { } // Logging. - pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize { + pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) { let result = shopify_function_provider::log::shopify_function_log_new_utf8_str(len); - let write_result = (result >> usize::BITS) as usize; + let len = (result >> usize::BITS) as usize; let dst = result as usize; - if write_result == WriteResult::Ok as usize { - std::ptr::copy(ptr as _, dst as _, len); - } - write_result + std::ptr::copy(ptr as _, dst as _, len); } // Other. diff --git a/api/src/shopify_function.h b/api/src/shopify_function.h index 0d32001..8822876 100644 --- a/api/src/shopify_function.h +++ b/api/src/shopify_function.h @@ -7,7 +7,6 @@ // Type definitions typedef int64_t Val; typedef int32_t WriteResult; -typedef int32_t LogResult; typedef size_t InternedStringId; // Constants for WriteResult @@ -190,10 +189,9 @@ extern InternedStringId shopify_function_intern_utf8_str(const uint8_t* ptr, siz * Logs a new UTF-8 string output value * @param ptr The string data * @param len The length of the string - * @return LogResult indicating success or failure */ __attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) __attribute__((import_name("shopify_function_log_new_utf8_str"))) -extern LogResult shopify_function_log_new_utf8_str(const uint8_t* ptr, size_t len); +extern void shopify_function_log_new_utf8_str(const uint8_t* ptr, size_t len); #endif // SHOPIFY_FUNCTION_H diff --git a/api/src/shopify_function.wat b/api/src/shopify_function.wat index 46ed13b..a18e60d 100644 --- a/api/src/shopify_function.wat +++ b/api/src/shopify_function.wat @@ -234,9 +234,7 @@ ;; Parameters: ;; - ptr: i32 pointer to string data in WebAssembly memory. ;; - len: i32 length of string in bytes. - ;; Returns: - ;; - i32 status code indicating success or failure (import "shopify_function_v1" "shopify_function_log_new_utf8_str" - (func (param $len i32) (result i32)) + (func (param $len i32)) ) ) diff --git a/core/src/lib.rs b/core/src/lib.rs index e0c0ec0..26e4673 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,3 @@ -pub mod log; pub mod read; pub mod write; diff --git a/core/src/log.rs b/core/src/log.rs deleted file mode 100644 index bf6f4a8..0000000 --- a/core/src/log.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[repr(usize)] -#[derive(Debug, strum::FromRepr, PartialEq, Eq)] -pub enum LogResult { - /// The log operation was successful. - Ok = 0, -} diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 28d8afd..4583c5b 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -233,6 +233,8 @@ static BENCHMARK_NON_WASM_API_EXAMPLE_RESULT: LazyLock> = static LOG_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("log")); static PANIC_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("panic")); static LOG_LEN_EXAMPLE_RESULT: LazyLock> = LazyLock::new(|| prepare_example("log-len")); +static LOG_PAST_CAPACITY_EXAMPLE_RESULT: LazyLock> = + LazyLock::new(|| prepare_example("log-past-capacity")); #[test] fn test_echo_with_bool_input() -> Result<()> { @@ -529,7 +531,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(1895, fuel); + assert_fuel_consumed_within_threshold(478, fuel); Ok(()) } @@ -549,15 +551,26 @@ fn test_log_len() -> Result<()> { let fuel = run(1)?; assert_fuel_consumed_within_threshold(766, fuel); let fuel = run(500)?; - assert_fuel_consumed_within_threshold(4_308, fuel); + assert_fuel_consumed_within_threshold(2_893, fuel); let fuel = run(1_000)?; - assert_fuel_consumed_within_threshold(7_243, fuel); + assert_fuel_consumed_within_threshold(4_413, fuel); let fuel = run(5_000)?; - assert_fuel_consumed_within_threshold(31_419, fuel); + assert_fuel_consumed_within_threshold(16_183, fuel); let fuel = run(10_000)?; - assert_fuel_consumed_within_threshold(61_001, fuel); + assert_fuel_consumed_within_threshold(30_883, fuel); let fuel = run(100_000)?; - assert_fuel_consumed_within_threshold(591_022, fuel); + assert_fuel_consumed_within_threshold(295_513, fuel); + Ok(()) +} + +#[test] +fn test_log_past_capacity() -> Result<()> { + LOG_PAST_CAPACITY_EXAMPLE_RESULT + .as_ref() + .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; + let (_, logs, fuel) = run_example("log-past-capacity", vec![], Api::Wasm)?; + assert_eq!(logs, format!("{}bbbb", "a".repeat(1020))); + assert_fuel_consumed_within_threshold(1297, fuel); Ok(()) } diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 4c4aba2..2e0e12a 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -22,7 +22,7 @@ struct Context { bump_allocator: bumpalo::Bump, input_bytes: Vec, output_bytes: ByteBuf, - logs: Vec, + logs: Logs, write_state: State, write_parent_state_stack: Vec, string_interner: StringInterner, @@ -43,7 +43,7 @@ impl Default for Context { bump_allocator: Bump::new(), input_bytes: Vec::new(), output_bytes: ByteBuf::with_capacity(1024), - logs: Vec::with_capacity(1024), + logs: Logs::default(), write_state: State::Start, write_parent_state_stack: Vec::new(), string_interner: StringInterner::new(), @@ -65,7 +65,7 @@ impl Context { bump_allocator: Bump::new(), input_bytes: Vec::with_capacity(0), output_bytes: ByteBuf::with_capacity(output_capacity), - logs: Vec::with_capacity(log_capacity), + logs: Logs::default(), write_state: State::Start, write_parent_state_stack: Vec::new(), string_interner: StringInterner::new(), @@ -107,6 +107,8 @@ macro_rules! decorate_for_target { pub(crate) use decorate_for_target; +use crate::log::Logs; + #[cfg(target_family = "wasm")] #[export_name = "initialize"] extern "C" fn initialize( diff --git a/provider/src/log.rs b/provider/src/log.rs index 2b2350d..6156a24 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -1,21 +1,55 @@ use crate::{decorate_for_target, Context, DoubleUsize}; -use shopify_function_wasm_api_core::log::LogResult; + +const CAPACITY: usize = 1024; + +#[derive(Debug)] +pub(crate) struct Logs { + buffer: [u8; CAPACITY], + write_offset: usize, +} + +impl Default for Logs { + fn default() -> Self { + Self { + buffer: [0; CAPACITY], + write_offset: 0, + } + } +} + +impl Logs { + fn append(&mut self, len: usize) -> (*const u8, usize) { + let mut ret_len = len; + let remaining_capacity = CAPACITY - self.write_offset; + if len > remaining_capacity { + ret_len = remaining_capacity; + } + let write_offset = self.write_offset; + self.write_offset += ret_len; + (unsafe { self.buffer.as_ptr().add(write_offset) }, ret_len) + } + + pub(crate) fn as_ptr(&self) -> *const u8 { + self.buffer.as_ptr() + } + + pub(crate) fn len(&self) -> usize { + self.write_offset + } +} impl Context { - fn allocate_log(&mut self, len: usize) -> *const u8 { - let write_offset = self.logs.len(); - self.logs.append(&mut vec![0; len]); - unsafe { self.logs.as_ptr().add(write_offset) } + fn allocate_log(&mut self, len: usize) -> (*const u8, usize) { + self.logs.append(len) } } decorate_for_target! { - /// The most significant 32 bits are the result, the least significant 32 bits are the pointer. + /// The most significant 32 bits are the length, the least significant 32 bits are the pointer. fn shopify_function_log_new_utf8_str(len: usize) -> DoubleUsize { Context::with_mut(|context| { - let ptr = context.allocate_log(len); - let result = LogResult::Ok; - ((result as DoubleUsize) << usize::BITS) | ptr as DoubleUsize + let (ptr, len) = context.allocate_log(len); + ((len as DoubleUsize) << usize::BITS) | ptr as DoubleUsize }) } } diff --git a/trampoline/src/lib.rs b/trampoline/src/lib.rs index 5cf1324..010e5ab 100644 --- a/trampoline/src/lib.rs +++ b/trampoline/src/lib.rs @@ -476,19 +476,30 @@ impl TrampolineCodegen { builder .func_body() .local_get(len) - // most significant 32 bits are the result, least significant 32 bits are the pointer + // most significant 32 bits are the length, least significant 32 bits are the pointer .call(provider_shopify_function_log_new_utf8_str) .local_tee(output) - // extract the result with a bit shift and wrap it to i32 + // extract the length with a bit shift and wrap it to i32 .i64_const(32) .binop(BinaryOp::I64ShrU) - .unop(UnaryOp::I32WrapI64) // result is on the stack now - // extract the pointer by wrapping the output to i32 - .local_get(output) - .unop(UnaryOp::I32WrapI64) // dst_ptr is on the stack now - .local_get(src_ptr) - .local_get(len) - .call(memcpy_to_provider); + .unop(UnaryOp::I32WrapI64) // length is on the stack now + .local_tee(len) + // exit early if len == 0 + .i32_const(0) + .binop(BinaryOp::I32Ne) + .if_else( + None, + |then| { + then + // extract the pointer by wrapping the output to i32 + .local_get(output) + .unop(UnaryOp::I32WrapI64) // dst_ptr is on the stack now + .local_get(src_ptr) + .local_get(len) + .call(memcpy_to_provider); + }, + |_else| {}, + ); }, )?; diff --git a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap index 97be59c..26f386c 100644 --- a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap +++ b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap @@ -41,21 +41,27 @@ input_file: trampoline/src/test_data/consumer.wat (func (;20;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 - call 18 + call 19 local.tee 2 i64.const 32 i64.shr_u i32.wrap_i64 - local.get 2 - i32.wrap_i64 - local.get 0 - local.get 1 - call 27 + local.tee 1 + i32.const 0 + i32.ne + if ;; label = @1 + local.get 2 + i32.wrap_i64 + local.get 0 + local.get 1 + call 27 + else + end ) (func (;21;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 - call 17 + call 18 local.tee 2 i64.const 32 i64.shr_u @@ -69,7 +75,7 @@ input_file: trampoline/src/test_data/consumer.wat (func (;22;) (type 4) (param i32 i32) (result i32) (local i64) local.get 1 - call 19 + call 17 local.tee 2 i64.const 32 i64.shr_u From f3be2b0af5b13dc3c6590541460a7c6b662237c1 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Wed, 23 Jul 2025 10:39:55 -0400 Subject: [PATCH 11/20] Remove LogResult --- api/src/lib.rs | 10 +++---- api/src/shopify_function.h | 2 +- api/src/shopify_function.wat | 2 +- integration_tests/tests/integration_test.rs | 12 ++++----- provider/src/log.rs | 8 +++--- trampoline/src/lib.rs | 14 ++-------- ...__disassemble_trampoline@consumer.wat.snap | 27 +++++++------------ 7 files changed, 26 insertions(+), 49 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index cf385e6..7467cff 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -60,7 +60,7 @@ extern "C" { fn shopify_function_output_finish_array() -> usize; // Log API. - fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize; + fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize); // Other. fn shopify_function_intern_utf8_str(ptr: *const u8, len: usize) -> usize; @@ -153,14 +153,10 @@ mod provider_fallback { } // Logging. - pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) -> usize { + pub(crate) unsafe fn shopify_function_log_new_utf8_str(ptr: *const u8, len: usize) { let result = shopify_function_provider::log::shopify_function_log_new_utf8_str(len); - let write_result = (result >> usize::BITS) as usize; let dst = result as usize; - if write_result == WriteResult::Ok as usize { - std::ptr::copy(ptr as _, dst as _, len); - } - write_result + std::ptr::copy(ptr as _, dst as _, len); } // Other. diff --git a/api/src/shopify_function.h b/api/src/shopify_function.h index 0d32001..df8279b 100644 --- a/api/src/shopify_function.h +++ b/api/src/shopify_function.h @@ -194,6 +194,6 @@ extern InternedStringId shopify_function_intern_utf8_str(const uint8_t* ptr, siz */ __attribute__((import_module(SHOPIFY_FUNCTION_IMPORT_MODULE))) __attribute__((import_name("shopify_function_log_new_utf8_str"))) -extern LogResult shopify_function_log_new_utf8_str(const uint8_t* ptr, size_t len); +extern void shopify_function_log_new_utf8_str(const uint8_t* ptr, size_t len); #endif // SHOPIFY_FUNCTION_H diff --git a/api/src/shopify_function.wat b/api/src/shopify_function.wat index 46ed13b..4a60fe1 100644 --- a/api/src/shopify_function.wat +++ b/api/src/shopify_function.wat @@ -237,6 +237,6 @@ ;; Returns: ;; - i32 status code indicating success or failure (import "shopify_function_v1" "shopify_function_log_new_utf8_str" - (func (param $len i32) (result i32)) + (func (param $len i32)) ) ) diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 167ece3..eb5f1ad 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -529,7 +529,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(646, fuel); + assert_fuel_consumed_within_threshold(618, fuel); Ok(()) } @@ -549,15 +549,15 @@ fn test_log_len() -> Result<()> { let fuel = run(1)?; assert_fuel_consumed_within_threshold(766, fuel); let fuel = run(500)?; - assert_fuel_consumed_within_threshold(3_103, fuel); + assert_fuel_consumed_within_threshold(3_068, fuel); let fuel = run(1_000)?; - assert_fuel_consumed_within_threshold(4_833, fuel); + assert_fuel_consumed_within_threshold(4_763, fuel); let fuel = run(5_000)?; - assert_fuel_consumed_within_threshold(19_369, fuel); + assert_fuel_consumed_within_threshold(19_019, fuel); let fuel = run(10_000)?; - assert_fuel_consumed_within_threshold(36_901, fuel); + assert_fuel_consumed_within_threshold(36_201, fuel); let fuel = run(100_000)?; - assert_fuel_consumed_within_threshold(350_022, fuel); + assert_fuel_consumed_within_threshold(343_022, fuel); Ok(()) } diff --git a/provider/src/log.rs b/provider/src/log.rs index a15d0a6..a646ef0 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -1,5 +1,4 @@ -use crate::{decorate_for_target, Context, DoubleUsize}; -use shopify_function_wasm_api_core::log::LogResult; +use crate::{decorate_for_target, Context}; impl Context { fn allocate_log(&mut self, len: usize) -> *const u8 { @@ -11,11 +10,10 @@ impl Context { decorate_for_target! { /// The most significant 32 bits are the result, the least significant 32 bits are the pointer. - fn shopify_function_log_new_utf8_str(len: usize) -> DoubleUsize { + fn shopify_function_log_new_utf8_str(len: usize) -> usize { Context::with_mut(|context| { let ptr = context.allocate_log(len); - let result = LogResult::Ok; - ((result as DoubleUsize) << usize::BITS) | ptr as DoubleUsize + ptr as usize }) } } diff --git a/trampoline/src/lib.rs b/trampoline/src/lib.rs index 5cf1324..91a6391 100644 --- a/trampoline/src/lib.rs +++ b/trampoline/src/lib.rs @@ -455,7 +455,7 @@ impl TrampolineCodegen { }; let shopify_function_log_new_utf8_str_type = - self.module.types.add(&[ValType::I32], &[ValType::I64]); + self.module.types.add(&[ValType::I32], &[ValType::I32]); let (provider_shopify_function_log_new_utf8_str, _) = self.module.add_import_func( PROVIDER_MODULE_NAME, @@ -465,8 +465,6 @@ impl TrampolineCodegen { let memcpy_to_provider = self.emit_memcpy_to_provider(); - let output = self.module.locals.add(ValType::I64); - self.module.replace_imported_func( imported_shopify_function_log_new_utf8_str, |(builder, arg_locals)| { @@ -476,16 +474,8 @@ impl TrampolineCodegen { builder .func_body() .local_get(len) - // most significant 32 bits are the result, least significant 32 bits are the pointer + // puts dst_ptr on to stack .call(provider_shopify_function_log_new_utf8_str) - .local_tee(output) - // extract the result with a bit shift and wrap it to i32 - .i64_const(32) - .binop(BinaryOp::I64ShrU) - .unop(UnaryOp::I32WrapI64) // result is on the stack now - // extract the pointer by wrapping the output to i32 - .local_get(output) - .unop(UnaryOp::I32WrapI64) // dst_ptr is on the stack now .local_get(src_ptr) .local_get(len) .call(memcpy_to_provider); diff --git a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap index 97be59c..99bb3ac 100644 --- a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap +++ b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap @@ -35,7 +35,7 @@ input_file: trampoline/src/test_data/consumer.wat (import "shopify_function_v1" "shopify_function_realloc" (func (;16;) (type 6))) (import "shopify_function_v1" "_shopify_function_output_new_utf8_str" (func (;17;) (type 3))) (import "shopify_function_v1" "_shopify_function_intern_utf8_str" (func (;18;) (type 3))) - (import "shopify_function_v1" "_shopify_function_log_new_utf8_str" (func (;19;) (type 3))) + (import "shopify_function_v1" "_shopify_function_log_new_utf8_str" (func (;19;) (type 2))) (memory (;1;) 1) (export "memory" (memory 1)) (func (;20;) (type 4) (param i32 i32) (result i32) @@ -66,21 +66,7 @@ input_file: trampoline/src/test_data/consumer.wat local.get 1 call 27 ) - (func (;22;) (type 4) (param i32 i32) (result i32) - (local i64) - local.get 1 - call 19 - local.tee 2 - i64.const 32 - i64.shr_u - i32.wrap_i64 - local.get 2 - i32.wrap_i64 - local.get 0 - local.get 1 - call 27 - ) - (func (;23;) (type 9) (param i64 i32 i32) (result i64) + (func (;22;) (type 9) (param i64 i32 i32) (result i64) (local i32) local.get 2 call 25 @@ -93,13 +79,20 @@ input_file: trampoline/src/test_data/consumer.wat local.get 2 call 15 ) - (func (;24;) (type 5) (param i32 i32 i32) + (func (;23;) (type 5) (param i32 i32 i32) local.get 1 local.get 0 call 14 local.get 2 call 26 ) + (func (;24;) (type 4) (param i32 i32) (result i32) + local.get 1 + call 19 + local.get 0 + local.get 1 + call 27 + ) (func (;25;) (type 2) (param i32) (result i32) i32.const 0 i32.const 0 From 6b1e355900b975e0edc63fb697f1b3092272fe11 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Wed, 23 Jul 2025 13:07:38 -0400 Subject: [PATCH 12/20] Remove more logresult stuff --- core/src/lib.rs | 1 - core/src/log.rs | 6 ------ 2 files changed, 7 deletions(-) delete mode 100644 core/src/log.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index e0c0ec0..26e4673 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,4 +1,3 @@ -pub mod log; pub mod read; pub mod write; diff --git a/core/src/log.rs b/core/src/log.rs deleted file mode 100644 index bf6f4a8..0000000 --- a/core/src/log.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[repr(usize)] -#[derive(Debug, strum::FromRepr, PartialEq, Eq)] -pub enum LogResult { - /// The log operation was successful. - Ok = 0, -} From 2701aa5b538edd51f512cbade4ff646c89d34d4e Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 12 Aug 2025 10:44:03 -0400 Subject: [PATCH 13/20] Fix consumer.wat logging API signature --- ...__disassemble_trampoline@consumer.wat.snap | 43 ++++++++++--------- trampoline/src/test_data/consumer.wat | 2 +- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap index 1233b35..74cc21a 100644 --- a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap +++ b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap @@ -8,22 +8,23 @@ input_file: trampoline/src/test_data/consumer.wat (type (;1;) (func (result i64))) (type (;2;) (func (param i32) (result i32))) (type (;3;) (func (param i32) (result i64))) - (type (;4;) (func (param i32 i32) (result i32))) - (type (;5;) (func (param i32 i32 i32))) - (type (;6;) (func (param i32 i32 i32 i32) (result i32))) - (type (;7;) (func (param i64) (result i32))) - (type (;8;) (func (param i64 i32) (result i64))) - (type (;9;) (func (param i64 i32 i32) (result i64))) - (type (;10;) (func (param f64) (result i32))) + (type (;4;) (func (param i32 i32))) + (type (;5;) (func (param i32 i32) (result i32))) + (type (;6;) (func (param i32 i32 i32))) + (type (;7;) (func (param i32 i32 i32 i32) (result i32))) + (type (;8;) (func (param i64) (result i32))) + (type (;9;) (func (param i64 i32) (result i64))) + (type (;10;) (func (param i64 i32 i32) (result i64))) + (type (;11;) (func (param f64) (result i32))) (import "shopify_function_v1" "_shopify_function_input_get" (func (;0;) (type 1))) - (import "shopify_function_v1" "_shopify_function_input_get_interned_obj_prop" (func (;1;) (type 8))) - (import "shopify_function_v1" "_shopify_function_input_get_at_index" (func (;2;) (type 8))) - (import "shopify_function_v1" "_shopify_function_input_get_obj_key_at_index" (func (;3;) (type 8))) - (import "shopify_function_v1" "_shopify_function_input_get_val_len" (func (;4;) (type 7))) + (import "shopify_function_v1" "_shopify_function_input_get_interned_obj_prop" (func (;1;) (type 9))) + (import "shopify_function_v1" "_shopify_function_input_get_at_index" (func (;2;) (type 9))) + (import "shopify_function_v1" "_shopify_function_input_get_obj_key_at_index" (func (;3;) (type 9))) + (import "shopify_function_v1" "_shopify_function_input_get_val_len" (func (;4;) (type 8))) (import "shopify_function_v1" "_shopify_function_output_new_bool" (func (;5;) (type 2))) (import "shopify_function_v1" "_shopify_function_output_new_null" (func (;6;) (type 0))) (import "shopify_function_v1" "_shopify_function_output_new_i32" (func (;7;) (type 2))) - (import "shopify_function_v1" "_shopify_function_output_new_f64" (func (;8;) (type 10))) + (import "shopify_function_v1" "_shopify_function_output_new_f64" (func (;8;) (type 11))) (import "shopify_function_v1" "_shopify_function_output_new_object" (func (;9;) (type 2))) (import "shopify_function_v1" "_shopify_function_output_finish_object" (func (;10;) (type 0))) (import "shopify_function_v1" "_shopify_function_output_new_array" (func (;11;) (type 2))) @@ -31,14 +32,14 @@ input_file: trampoline/src/test_data/consumer.wat (import "shopify_function_v1" "_shopify_function_output_new_interned_utf8_str" (func (;13;) (type 2))) (import "shopify_function_v1" "_shopify_function_input_get_utf8_str_addr" (func (;14;) (type 2))) (import "shopify_function_v1" "memory" (memory (;0;) 1)) - (import "shopify_function_v1" "_shopify_function_input_get_obj_prop" (func (;15;) (type 9))) - (import "shopify_function_v1" "shopify_function_realloc" (func (;16;) (type 6))) + (import "shopify_function_v1" "_shopify_function_input_get_obj_prop" (func (;15;) (type 10))) + (import "shopify_function_v1" "shopify_function_realloc" (func (;16;) (type 7))) (import "shopify_function_v1" "_shopify_function_output_new_utf8_str" (func (;17;) (type 3))) (import "shopify_function_v1" "_shopify_function_intern_utf8_str" (func (;18;) (type 3))) (import "shopify_function_v1" "_shopify_function_log_new_utf8_str" (func (;19;) (type 2))) (memory (;1;) 1) (export "memory" (memory 1)) - (func (;20;) (type 4) (param i32 i32) (result i32) + (func (;20;) (type 4) (param i32 i32) (local i32 i32 i32 i32 i32 i32) local.get 1 call 19 @@ -77,7 +78,7 @@ input_file: trampoline/src/test_data/consumer.wat else end ) - (func (;21;) (type 4) (param i32 i32) (result i32) + (func (;21;) (type 5) (param i32 i32) (result i32) (local i64) local.get 1 call 18 @@ -91,7 +92,7 @@ input_file: trampoline/src/test_data/consumer.wat local.get 1 call 27 ) - (func (;22;) (type 4) (param i32 i32) (result i32) + (func (;22;) (type 5) (param i32 i32) (result i32) (local i64) local.get 1 call 17 @@ -105,7 +106,7 @@ input_file: trampoline/src/test_data/consumer.wat local.get 1 call 27 ) - (func (;23;) (type 9) (param i64 i32 i32) (result i64) + (func (;23;) (type 10) (param i64 i32 i32) (result i64) (local i32) local.get 2 call 25 @@ -118,7 +119,7 @@ input_file: trampoline/src/test_data/consumer.wat local.get 2 call 15 ) - (func (;24;) (type 5) (param i32 i32 i32) + (func (;24;) (type 6) (param i32 i32 i32) local.get 1 local.get 0 call 14 @@ -132,13 +133,13 @@ input_file: trampoline/src/test_data/consumer.wat local.get 0 call 16 ) - (func (;26;) (type 5) (param i32 i32 i32) + (func (;26;) (type 6) (param i32 i32 i32) local.get 0 local.get 1 local.get 2 memory.copy 1 0 ) - (func (;27;) (type 5) (param i32 i32 i32) + (func (;27;) (type 6) (param i32 i32 i32) local.get 0 local.get 1 local.get 2 diff --git a/trampoline/src/test_data/consumer.wat b/trampoline/src/test_data/consumer.wat index 7f7a5a4..28b0c6d 100644 --- a/trampoline/src/test_data/consumer.wat +++ b/trampoline/src/test_data/consumer.wat @@ -24,7 +24,7 @@ (import "shopify_function_v1" "shopify_function_output_new_interned_utf8_str" (func (param i32) (result i32))) ;; Log. - (import "shopify_function_v1" "shopify_function_log_new_utf8_str" (func (param i32 i32) (result i32))) + (import "shopify_function_v1" "shopify_function_log_new_utf8_str" (func (param i32 i32))) ;; Memory (memory 1) From 480c2b0b608aff7967919eede22719b9b316c34d Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 12 Aug 2025 13:54:31 -0400 Subject: [PATCH 14/20] Use one byte over log limit to signal truncation --- integration_tests/tests/integration_test.rs | 2 +- provider/src/log.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 462a3c7..ea87a95 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -575,7 +575,7 @@ fn test_log_past_capacity() -> Result<()> { .as_ref() .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log-past-capacity", vec![], Api::Wasm)?; - assert_eq!(logs, format!("{}{}", "a".repeat(1014), "b".repeat(10))); + assert_eq!(logs, format!("{}{}", "a".repeat(1015), "b".repeat(10))); assert_fuel_consumed_within_threshold(1453, fuel); Ok(()) } diff --git a/provider/src/log.rs b/provider/src/log.rs index 3123a39..5039aae 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -3,7 +3,8 @@ use std::ptr; use crate::{decorate_for_target, Context}; static mut LOG_RET_AREA: [usize; 5] = [0; 5]; -const CAPACITY: usize = 1024; +// One more byte so we can check if we're truncating. +const CAPACITY: usize = 1025; #[derive(Debug)] pub(crate) struct Logs { From 42bf92d1c459f565be2e864c352ac63daaa8bfc3 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 12 Aug 2025 14:16:06 -0400 Subject: [PATCH 15/20] Add unit tests for ring buffer --- provider/src/log.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/provider/src/log.rs b/provider/src/log.rs index 5039aae..0d19715 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -108,3 +108,105 @@ decorate_for_target! { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_append_fits_in_buffer() { + let mut logs = Logs::default(); + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(100); + + assert_eq!(source_offset, 0); + assert_eq!(ptr1, logs.buffer.as_ptr()); + assert_eq!(len1, 100); + assert_eq!(len2, 0); + assert!(ptr2.is_null()); + assert_eq!(logs.len, 100); + assert_eq!(logs.write_offset, 100); + assert_eq!(logs.read_offset, 0); + } + + #[test] + fn test_append_exceeds_capacity() { + let mut logs = Logs::default(); + let large_len = CAPACITY + 100; + + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(large_len); + + assert_eq!(source_offset, 100); + assert_eq!(ptr1, logs.buffer.as_ptr()); + assert_eq!(len1, CAPACITY); + assert_eq!(len2, 0); + assert!(ptr2.is_null()); + assert_eq!(logs.len, CAPACITY); + assert_eq!(logs.write_offset, 0); + assert_eq!(logs.read_offset, 0); + } + + #[test] + fn test_append_zero_length() { + let mut logs = Logs::default(); + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(0); + + assert_eq!(source_offset, 0); + assert_eq!(ptr1, logs.buffer.as_ptr()); + assert_eq!(len1, 0); + assert_eq!(len2, 0); + assert!(ptr2.is_null()); + assert_eq!(logs.len, 0); + assert_eq!(logs.write_offset, 0); + assert_eq!(logs.read_offset, 0); + } + + #[test] + fn test_append_exact_capacity() { + let mut logs = Logs::default(); + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(CAPACITY); + + assert_eq!(source_offset, 0); + assert_eq!(ptr1, logs.buffer.as_ptr()); + assert_eq!(len1, CAPACITY); + assert_eq!(len2, 0); + assert!(ptr2.is_null()); + assert_eq!(logs.len, CAPACITY); + assert_eq!(logs.write_offset, 0); + assert_eq!(logs.read_offset, 0); + } + + #[test] + fn test_append_multiple_operations() { + let mut logs = Logs::default(); + + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(300); + assert_eq!(source_offset, 0); + assert_eq!(ptr1, logs.buffer.as_ptr()); + assert_eq!(len1, 300); + assert_eq!(ptr2, ptr::null()); + assert_eq!(len2, 0); + assert_eq!(logs.len, 300); + assert_eq!(logs.write_offset, 300); + assert_eq!(logs.read_offset, 0); + + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(200); + assert_eq!(source_offset, 0); + assert_eq!(ptr1, unsafe { logs.buffer.as_ptr().add(300) }); + assert_eq!(len1, 200); + assert_eq!(ptr2, ptr::null()); + assert_eq!(len2, 0); + assert_eq!(logs.len, 500); + assert_eq!(logs.write_offset, 500); + assert_eq!(logs.read_offset, 0); + + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(600); // Total would be 1100, exceeds CAPACITY (1025) + assert_eq!(source_offset, 0); + assert_eq!(ptr1, unsafe { logs.buffer.as_ptr().add(500) }); + assert_eq!(len1, 525); + assert_eq!(ptr2, logs.buffer.as_ptr()); + assert_eq!(len2, 75); + assert_eq!(logs.len, CAPACITY); + assert_eq!(logs.write_offset, 75); // (500 + 600) % CAPACITY + assert_eq!(logs.read_offset, 75); // Advanced by overflow amount + } +} From 1dba3619aef0b96d886a4477b99ec058cc0d0100 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 12 Aug 2025 17:05:51 -0400 Subject: [PATCH 16/20] Simplify ring buffer implementation --- provider/src/log.rs | 65 +++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/provider/src/log.rs b/provider/src/log.rs index 0d19715..67cf135 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -6,11 +6,13 @@ static mut LOG_RET_AREA: [usize; 5] = [0; 5]; // One more byte so we can check if we're truncating. const CAPACITY: usize = 1025; +// A kind of ring buffer implementation. Since all reads are guaranteed to +// start after all writes have finished, we can simplify the +// implementation by only using a single offset for reads and writes. #[derive(Debug)] pub(crate) struct Logs { buffer: [u8; CAPACITY], - read_offset: usize, - write_offset: usize, + offset: usize, len: usize, } @@ -18,8 +20,7 @@ impl Default for Logs { fn default() -> Self { Self { buffer: [0; CAPACITY], - read_offset: 0, - write_offset: 0, + offset: 0, len: 0, } } @@ -28,7 +29,7 @@ impl Default for Logs { impl Logs { fn append(&mut self, mut len: usize) -> (usize, *const u8, usize, *const u8, usize) { let mut source_offset = 0; - let dst_offset1 = unsafe { self.buffer.as_ptr().add(self.write_offset) }; + let dst_offset1 = unsafe { self.buffer.as_ptr().add(self.offset) }; let len1; let mut dst_offset2 = ptr::null(); let mut len2 = 0; @@ -39,45 +40,36 @@ impl Logs { len = CAPACITY; } - let space_to_end = CAPACITY - self.write_offset; + let space_to_end = CAPACITY - self.offset; if len <= space_to_end { // Incoming buffer fits in one block. len1 = len; + self.len += len; } else { // Incoming data wrap will wrap around. len1 = space_to_end; dst_offset2 = self.buffer.as_ptr(); len2 = len - space_to_end; - } - - self.write_offset = (self.write_offset + len) % CAPACITY; - - if self.len + len <= CAPACITY { - // No overwriting. - self.len += len; - } else { - // Overwriting. - let overwritten_bytes = self.len + len - CAPACITY; - self.read_offset = (self.read_offset + overwritten_bytes) % CAPACITY; self.len = CAPACITY; } + self.offset = (self.offset + len) % CAPACITY; + (source_offset, dst_offset1, len1, dst_offset2, len2) } #[cfg(target_family = "wasm")] pub(crate) fn read_ptrs(&self) -> (*const u8, usize, *const u8, usize) { - let data_to_end = CAPACITY - self.read_offset; - if self.len <= data_to_end { - ( - unsafe { self.buffer.as_ptr().add(self.read_offset) }, - self.len, - ptr::null(), - 0, - ) + // _After_ filling the buffer, the read offset will _always_ be the + // same as the write offset. + let read_offset = if self.len <= CAPACITY { 0 } else { self.offset }; + + if read_offset == 0 { + (self.buffer.as_ptr(), self.len, ptr::null(), 0) } else { + let data_to_end = CAPACITY - read_offset; ( - unsafe { self.buffer.as_ptr().add(self.read_offset) }, + unsafe { self.buffer.as_ptr().add(self.offset) }, data_to_end, self.buffer.as_ptr(), self.len - data_to_end, @@ -124,8 +116,7 @@ mod tests { assert_eq!(len2, 0); assert!(ptr2.is_null()); assert_eq!(logs.len, 100); - assert_eq!(logs.write_offset, 100); - assert_eq!(logs.read_offset, 0); + assert_eq!(logs.offset, 100); } #[test] @@ -141,8 +132,7 @@ mod tests { assert_eq!(len2, 0); assert!(ptr2.is_null()); assert_eq!(logs.len, CAPACITY); - assert_eq!(logs.write_offset, 0); - assert_eq!(logs.read_offset, 0); + assert_eq!(logs.offset, 0); } #[test] @@ -156,8 +146,7 @@ mod tests { assert_eq!(len2, 0); assert!(ptr2.is_null()); assert_eq!(logs.len, 0); - assert_eq!(logs.write_offset, 0); - assert_eq!(logs.read_offset, 0); + assert_eq!(logs.offset, 0); } #[test] @@ -171,8 +160,7 @@ mod tests { assert_eq!(len2, 0); assert!(ptr2.is_null()); assert_eq!(logs.len, CAPACITY); - assert_eq!(logs.write_offset, 0); - assert_eq!(logs.read_offset, 0); + assert_eq!(logs.offset, 0); } #[test] @@ -186,8 +174,7 @@ mod tests { assert_eq!(ptr2, ptr::null()); assert_eq!(len2, 0); assert_eq!(logs.len, 300); - assert_eq!(logs.write_offset, 300); - assert_eq!(logs.read_offset, 0); + assert_eq!(logs.offset, 300); let (source_offset, ptr1, len1, ptr2, len2) = logs.append(200); assert_eq!(source_offset, 0); @@ -196,8 +183,7 @@ mod tests { assert_eq!(ptr2, ptr::null()); assert_eq!(len2, 0); assert_eq!(logs.len, 500); - assert_eq!(logs.write_offset, 500); - assert_eq!(logs.read_offset, 0); + assert_eq!(logs.offset, 500); let (source_offset, ptr1, len1, ptr2, len2) = logs.append(600); // Total would be 1100, exceeds CAPACITY (1025) assert_eq!(source_offset, 0); @@ -206,7 +192,6 @@ mod tests { assert_eq!(ptr2, logs.buffer.as_ptr()); assert_eq!(len2, 75); assert_eq!(logs.len, CAPACITY); - assert_eq!(logs.write_offset, 75); // (500 + 600) % CAPACITY - assert_eq!(logs.read_offset, 75); // Advanced by overflow amount + assert_eq!(logs.offset, 75); // (500 + 600) % CAPACITY } } From ec7a4331aa1347ca437ee2c76c35c94fcc460b18 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Fri, 15 Aug 2025 16:17:20 -0400 Subject: [PATCH 17/20] Update import and versions --- Cargo.lock | 4 ++-- api/Cargo.toml | 3 +-- api/src/lib.rs | 2 +- provider/Cargo.toml | 2 +- provider/src/lib.rs | 17 ++--------------- trampoline/Cargo.toml | 2 +- 6 files changed, 8 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d8c789..c764c78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1810,7 +1810,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "shopify_function_provider" -version = "1.0.1" +version = "2.0.0" dependencies = [ "bumpalo", "paste", @@ -1822,7 +1822,7 @@ dependencies = [ [[package]] name = "shopify_function_trampoline" -version = "1.0.2" +version = "2.0.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/api/Cargo.toml b/api/Cargo.toml index c352c43..954f5a4 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -13,7 +13,7 @@ thiserror = "2.0" seq-macro = "0.3.5" [target.'cfg(not(target_family = "wasm"))'.dependencies] -shopify_function_provider = { path = "../provider", version = "1.0.1" } +shopify_function_provider = { path = "../provider", version = "2.0.0" } serde_json = "1.0" rmp-serde = "1.3" @@ -33,4 +33,3 @@ path = "examples/cart-checkout-validation-wasm-api.rs" [[example]] name = "cart-checkout-validation-wasi-json" path = "examples/cart-checkout-validation-wasi-json.rs" - diff --git a/api/src/lib.rs b/api/src/lib.rs index 394dbb1..77c9a4e 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -31,7 +31,7 @@ pub use read::Deserialize; pub use write::Serialize; #[cfg(target_family = "wasm")] -#[link(wasm_import_module = "shopify_function_v1")] +#[link(wasm_import_module = "shopify_function_v2")] extern "C" { // Read API. fn shopify_function_input_get() -> Val; diff --git a/provider/Cargo.toml b/provider/Cargo.toml index 0c4f846..963d5d2 100644 --- a/provider/Cargo.toml +++ b/provider/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shopify_function_provider" -version = "1.0.1" +version = "2.0.0" edition = "2021" license = "MIT" repository = "https://github.com/Shopify/shopify-function-wasm-api" diff --git a/provider/src/lib.rs b/provider/src/lib.rs index 3cc4268..16cf6b6 100644 --- a/provider/src/lib.rs +++ b/provider/src/lib.rs @@ -59,19 +59,6 @@ impl Context { context } - #[cfg(target_family = "wasm")] - fn new(output_capacity: usize) -> Self { - Self { - bump_allocator: Bump::new(), - input_bytes: Vec::with_capacity(0), - output_bytes: ByteBuf::with_capacity(output_capacity), - logs: Logs::default(), - write_state: State::Start, - write_parent_state_stack: Vec::new(), - string_interner: StringInterner::new(), - } - } - fn with(f: F) -> T where F: FnOnce(&Context) -> T, @@ -111,9 +98,9 @@ use crate::log::Logs; #[cfg(target_family = "wasm")] #[export_name = "initialize"] -extern "C" fn initialize(input_len: usize, output_capacity: usize) -> *const u8 { +extern "C" fn initialize(input_len: usize) -> *const u8 { CONTEXT.with_borrow_mut(|context| { - *context = Context::new(output_capacity); + *context = Context::default(); context.input_bytes = vec![0; input_len]; context.input_bytes.as_ptr() }) diff --git a/trampoline/Cargo.toml b/trampoline/Cargo.toml index dba4ca2..420a905 100644 --- a/trampoline/Cargo.toml +++ b/trampoline/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shopify_function_trampoline" -version = "1.0.2" +version = "2.0.0" edition = "2021" license = "MIT" repository = "https://github.com/Shopify/shopify-function-wasm-api" From 3c48b25d3e37116f8844158eed385365ba601b8b Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Mon, 18 Aug 2025 15:51:58 -0400 Subject: [PATCH 18/20] Fix tests --- api/src/shopify_function.h | 2 +- api/src/shopify_function.wat | 38 ++++++++-------- api/src/test_data/header_test.c | 2 +- api/src/test_data/header_test.wasm | Bin 2558 -> 2563 bytes integration_tests/tests/integration_test.rs | 17 ++++--- provider/src/log.rs | 2 +- trampoline/src/lib.rs | 5 ++- ...__disassemble_trampoline@consumer.wat.snap | 42 +++++++++--------- trampoline/src/test_data/consumer.wat | 38 ++++++++-------- 9 files changed, 74 insertions(+), 72 deletions(-) diff --git a/api/src/shopify_function.h b/api/src/shopify_function.h index 8822876..84e7698 100644 --- a/api/src/shopify_function.h +++ b/api/src/shopify_function.h @@ -14,7 +14,7 @@ typedef size_t InternedStringId; #define WRITE_RESULT_ERROR 1 // Import module declaration -#define SHOPIFY_FUNCTION_IMPORT_MODULE "shopify_function_v1" +#define SHOPIFY_FUNCTION_IMPORT_MODULE "shopify_function_v2" // Read API /** diff --git a/api/src/shopify_function.wat b/api/src/shopify_function.wat index a18e60d..ac8bb5b 100644 --- a/api/src/shopify_function.wat +++ b/api/src/shopify_function.wat @@ -15,7 +15,7 @@ ;; The resulting value can be traversed using the other input API functions. ;; Returns: ;; - NanBox value representing the root input value. - (import "shopify_function_v1" "shopify_function_input_get" + (import "shopify_function_v2" "shopify_function_input_get" (func (result i64)) ) @@ -35,7 +35,7 @@ ;; - scope: NaNBox encoded value. ;; Returns ;; - The value length. - (import "shopify_function_v1" "shopify_function_input_get_val_len" + (import "shopify_function_v2" "shopify_function_input_get_val_len" (func (param $scope i64) (result i32)) ) @@ -47,7 +47,7 @@ ;; - src: i32 memory address of the string. ;; - out: i32 pointer to the destination buffer. ;; - len: i32 length of the string in bytes. - (import "shopify_function_v1" "shopify_function_input_read_utf8_str" + (import "shopify_function_v2" "shopify_function_input_read_utf8_str" (func (param $src i32) (param $out i32) (param $len i32)) ) @@ -60,7 +60,7 @@ ;; - len: i32 length of the property name in bytes. ;; Returns: ;; - i64 NanBox value of the property. - (import "shopify_function_v1" "shopify_function_input_get_obj_prop" + (import "shopify_function_v2" "shopify_function_input_get_obj_prop" (func (param $scope i64) (param $ptr i32) (param $len i32) (result i64)) ) @@ -73,7 +73,7 @@ ;; - interned_string_id: i32 ID of the interned string. ;; Returns: ;; - i64 NanBox value of the property. - (import "shopify_function_v1" "shopify_function_input_get_interned_obj_prop" + (import "shopify_function_v2" "shopify_function_input_get_interned_obj_prop" (func (param $scope i64) (param $interned_string_id i32) (result i64)) ) @@ -85,7 +85,7 @@ ;; - i64 NanBox value at the index. ;; Errors: ;; - If index is out of bounds, returns a NanBox with ErrorCode::IndexOutOfBounds. - (import "shopify_function_v1" "shopify_function_input_get_at_index" + (import "shopify_function_v2" "shopify_function_input_get_at_index" (func (param $scope i64) (param $index i32) (result i64)) ) @@ -98,7 +98,7 @@ ;; - i64 NanBox string value of the key. ;; Errors: ;; - If index is out of bounds, returns a NanBox with ErrorCode::IndexOutOfBounds. - (import "shopify_function_v1" "shopify_function_input_get_obj_key_at_index" + (import "shopify_function_v2" "shopify_function_input_get_obj_key_at_index" (func (param $scope i64) (param $index i32) (result i64)) ) @@ -111,7 +111,7 @@ ;; - value: i32 boolean value (0 = false, 1 = true). ;; Returns: ;; - i32 status code indicating success or failure - (import "shopify_function_v1" "shopify_function_output_new_bool" + (import "shopify_function_v2" "shopify_function_output_new_bool" (func (param $value i32) (result i32)) ) @@ -120,7 +120,7 @@ ;; Different from omitting a property. ;; Returns: ;; - i32 status code indicating success or failure - (import "shopify_function_v1" "shopify_function_output_new_null" + (import "shopify_function_v2" "shopify_function_output_new_null" (func (result i32)) ) @@ -131,7 +131,7 @@ ;; - value: i32 integer value. ;; Returns: ;; - i32 status code indicating success or failure - (import "shopify_function_v1" "shopify_function_output_new_i32" + (import "shopify_function_v2" "shopify_function_output_new_i32" (func (param $value i32) (result i32)) ) @@ -142,7 +142,7 @@ ;; - value: f64 floating point value. ;; Returns: ;; - i32 status code indicating success or failure. - (import "shopify_function_v1" "shopify_function_output_new_f64" + (import "shopify_function_v2" "shopify_function_output_new_f64" (func (param $value f64) (result i32)) ) @@ -154,7 +154,7 @@ ;; - len: i32 length of string in bytes. ;; Returns: ;; - i32 status code indicating success or failure - (import "shopify_function_v1" "shopify_function_output_new_utf8_str" + (import "shopify_function_v2" "shopify_function_output_new_utf8_str" (func (param $ptr i32) (param $len i32) (result i32)) ) @@ -165,7 +165,7 @@ ;; - id: i32 ID of the interned string from shopify_function_intern_utf8_str. ;; Returns: ;; - i32 status code indicating success or failure. - (import "shopify_function_v1" "shopify_function_output_new_interned_utf8_str" + (import "shopify_function_v2" "shopify_function_output_new_interned_utf8_str" (func (param $id i32) (result i32)) ) @@ -177,7 +177,7 @@ ;; - len: i32 number of properties in the object (key-value pairs). ;; Returns: ;; - i32 status code indicating success or failure. - (import "shopify_function_v1" "shopify_function_output_new_object" + (import "shopify_function_v2" "shopify_function_output_new_object" (func (param $len i32) (result i32)) ) @@ -186,7 +186,7 @@ ;; Validates that the correct number of properties were added. ;; Returns: ;; - i32 status code indicating success or failure. - (import "shopify_function_v1" "shopify_function_output_finish_object" + (import "shopify_function_v2" "shopify_function_output_finish_object" (func (result i32)) ) @@ -198,7 +198,7 @@ ;; - len: i32 number of elements in the array. ;; Returns: ;; - i32 status code indicating success or failure. - (import "shopify_function_v1" "shopify_function_output_new_array" + (import "shopify_function_v2" "shopify_function_output_new_array" (func (param $len i32) (result i32)) ) @@ -207,7 +207,7 @@ ;; Validates that the correct number of elements were added. ;; Returns: ;; - i32 status code indicating success or failure. - (import "shopify_function_v1" "shopify_function_output_finish_array" + (import "shopify_function_v2" "shopify_function_output_finish_array" (func (result i32)) ) @@ -224,7 +224,7 @@ ;; - len: i32 length of string in bytes. ;; Returns: ;; - i32 ID of the interned string (to be used in other API calls). - (import "shopify_function_v1" "shopify_function_intern_utf8_str" + (import "shopify_function_v2" "shopify_function_intern_utf8_str" (func (param $ptr i32) (param $len i32) (result i32)) ) @@ -234,7 +234,7 @@ ;; Parameters: ;; - ptr: i32 pointer to string data in WebAssembly memory. ;; - len: i32 length of string in bytes. - (import "shopify_function_v1" "shopify_function_log_new_utf8_str" + (import "shopify_function_v2" "shopify_function_log_new_utf8_str" (func (param $len i32)) ) ) diff --git a/api/src/test_data/header_test.c b/api/src/test_data/header_test.c index a5d54cb..5654584 100644 --- a/api/src/test_data/header_test.c +++ b/api/src/test_data/header_test.c @@ -5,7 +5,7 @@ // To update this file you will need a compiler toolchain: // `brew install llvm lld` // On updating this file, regenerate the header_test.wasm file with the following command: -// `/opt/homebrew/opt/llvm/bin/clang --target=wasm32-wasip1 -I .. -nostdlib -Wl,--no-entry -Wl,--export-all -Wl,--allow-undefined -o header_test.wasm header_test.c` +// `/opt/homebrew/opt/llvm/bin/clang --target=wasm32-unknown-unknown -I .. -nostdlib -Wl,--no-entry -Wl,--export-all -Wl,--allow-undefined -o header_test.wasm header_test.c` volatile void* imports[] = { (void*)shopify_function_input_get, diff --git a/api/src/test_data/header_test.wasm b/api/src/test_data/header_test.wasm index 3b69e693bcd528ae4204c1ea6cd475b19f6702b3..70e98392c17d6ce2a79e921f7f99e4199c12410c 100755 GIT binary patch delta 239 zcmew-+$_S)kXW3{$iTqBXvsa1+mMB+zMf&CwG@+))MQ3>sfmT6Oh!sTmh|L$Huj0D zL>Y}H9B=DJ+**7%e8Jb4~{U Dkc~XG delta 233 zcmXwzy$%6U5QTSUuh>N+{@2R-SsElJ5hBrO6-vFHh)BEuFK|sEIy#9`=P~pu9>5cb zdvBuq=A1KgW}E(|;?``v0L!wV8Df vM5^=I3HYk9VnT1*Q)f?3okO9bW!0SGiBRA1O4K5bjo{vn;vt~VbNBEA#)UMe diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index ea87a95..3c2992c 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -109,9 +109,8 @@ fn run_example(example: &str, input_bytes: Vec, api: Api) -> Result<(Vec let provider_instance = linker.instantiate(&mut store, &provider)?; if api.is_wasm() { store.set_fuel(STARTING_FUEL)?; - let init_func = - provider_instance.get_typed_func::<(i32, i32), i32>(&mut store, "initialize")?; - let input_buffer_offset = init_func.call(&mut store, (input_bytes.len() as i32, 1024))?; + let init_func = provider_instance.get_typed_func::(&mut store, "initialize")?; + let input_buffer_offset = init_func.call(&mut store, input_bytes.len() as _)?; provider_instance .get_memory(&mut store, "memory") .unwrap() @@ -537,7 +536,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(706, fuel); + assert_fuel_consumed_within_threshold(670, fuel); Ok(()) } @@ -559,13 +558,13 @@ fn test_log_len() -> Result<()> { let fuel = run(500)?; assert_fuel_consumed_within_threshold(3_178, fuel); let fuel = run(1_000)?; - assert_fuel_consumed_within_threshold(4_983, fuel); + assert_fuel_consumed_within_threshold(4_873, fuel); let fuel = run(5_000)?; - assert_fuel_consumed_within_threshold(19_891, fuel); + assert_fuel_consumed_within_threshold(18_918, fuel); let fuel = run(10_000)?; - assert_fuel_consumed_within_threshold(38_526, fuel); + assert_fuel_consumed_within_threshold(36_478, fuel); let fuel = run(100_000)?; - assert_fuel_consumed_within_threshold(373_901, fuel); + assert_fuel_consumed_within_threshold(352_498, fuel); Ok(()) } @@ -576,7 +575,7 @@ fn test_log_past_capacity() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log-past-capacity", vec![], Api::Wasm)?; assert_eq!(logs, format!("{}{}", "a".repeat(1015), "b".repeat(10))); - assert_fuel_consumed_within_threshold(1453, fuel); + assert_fuel_consumed_within_threshold(1410, fuel); Ok(()) } diff --git a/provider/src/log.rs b/provider/src/log.rs index 67cf135..25bb7d7 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -62,7 +62,7 @@ impl Logs { pub(crate) fn read_ptrs(&self) -> (*const u8, usize, *const u8, usize) { // _After_ filling the buffer, the read offset will _always_ be the // same as the write offset. - let read_offset = if self.len <= CAPACITY { 0 } else { self.offset }; + let read_offset = if self.len < CAPACITY { 0 } else { self.offset }; if read_offset == 0 { (self.buffer.as_ptr(), self.len, ptr::null(), 0) diff --git a/trampoline/src/lib.rs b/trampoline/src/lib.rs index 80c8d2c..6f9b4c5 100644 --- a/trampoline/src/lib.rs +++ b/trampoline/src/lib.rs @@ -635,7 +635,10 @@ mod test { let buf = wat::parse_bytes(input).unwrap(); let module = Module::from_buffer(&buf).unwrap(); for (import, _) in IMPORTS { - assert!(module.imports.find(PROVIDER_MODULE_NAME, import).is_some()); + assert!( + module.imports.find(PROVIDER_MODULE_NAME, import).is_some(), + "{import} not found" + ); } } diff --git a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap index 74cc21a..a33a115 100644 --- a/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap +++ b/trampoline/src/snapshots/shopify_function_trampoline__test__disassemble_trampoline@consumer.wat.snap @@ -16,27 +16,27 @@ input_file: trampoline/src/test_data/consumer.wat (type (;9;) (func (param i64 i32) (result i64))) (type (;10;) (func (param i64 i32 i32) (result i64))) (type (;11;) (func (param f64) (result i32))) - (import "shopify_function_v1" "_shopify_function_input_get" (func (;0;) (type 1))) - (import "shopify_function_v1" "_shopify_function_input_get_interned_obj_prop" (func (;1;) (type 9))) - (import "shopify_function_v1" "_shopify_function_input_get_at_index" (func (;2;) (type 9))) - (import "shopify_function_v1" "_shopify_function_input_get_obj_key_at_index" (func (;3;) (type 9))) - (import "shopify_function_v1" "_shopify_function_input_get_val_len" (func (;4;) (type 8))) - (import "shopify_function_v1" "_shopify_function_output_new_bool" (func (;5;) (type 2))) - (import "shopify_function_v1" "_shopify_function_output_new_null" (func (;6;) (type 0))) - (import "shopify_function_v1" "_shopify_function_output_new_i32" (func (;7;) (type 2))) - (import "shopify_function_v1" "_shopify_function_output_new_f64" (func (;8;) (type 11))) - (import "shopify_function_v1" "_shopify_function_output_new_object" (func (;9;) (type 2))) - (import "shopify_function_v1" "_shopify_function_output_finish_object" (func (;10;) (type 0))) - (import "shopify_function_v1" "_shopify_function_output_new_array" (func (;11;) (type 2))) - (import "shopify_function_v1" "_shopify_function_output_finish_array" (func (;12;) (type 0))) - (import "shopify_function_v1" "_shopify_function_output_new_interned_utf8_str" (func (;13;) (type 2))) - (import "shopify_function_v1" "_shopify_function_input_get_utf8_str_addr" (func (;14;) (type 2))) - (import "shopify_function_v1" "memory" (memory (;0;) 1)) - (import "shopify_function_v1" "_shopify_function_input_get_obj_prop" (func (;15;) (type 10))) - (import "shopify_function_v1" "shopify_function_realloc" (func (;16;) (type 7))) - (import "shopify_function_v1" "_shopify_function_output_new_utf8_str" (func (;17;) (type 3))) - (import "shopify_function_v1" "_shopify_function_intern_utf8_str" (func (;18;) (type 3))) - (import "shopify_function_v1" "_shopify_function_log_new_utf8_str" (func (;19;) (type 2))) + (import "shopify_function_v2" "_shopify_function_input_get" (func (;0;) (type 1))) + (import "shopify_function_v2" "_shopify_function_input_get_interned_obj_prop" (func (;1;) (type 9))) + (import "shopify_function_v2" "_shopify_function_input_get_at_index" (func (;2;) (type 9))) + (import "shopify_function_v2" "_shopify_function_input_get_obj_key_at_index" (func (;3;) (type 9))) + (import "shopify_function_v2" "_shopify_function_input_get_val_len" (func (;4;) (type 8))) + (import "shopify_function_v2" "_shopify_function_output_new_bool" (func (;5;) (type 2))) + (import "shopify_function_v2" "_shopify_function_output_new_null" (func (;6;) (type 0))) + (import "shopify_function_v2" "_shopify_function_output_new_i32" (func (;7;) (type 2))) + (import "shopify_function_v2" "_shopify_function_output_new_f64" (func (;8;) (type 11))) + (import "shopify_function_v2" "_shopify_function_output_new_object" (func (;9;) (type 2))) + (import "shopify_function_v2" "_shopify_function_output_finish_object" (func (;10;) (type 0))) + (import "shopify_function_v2" "_shopify_function_output_new_array" (func (;11;) (type 2))) + (import "shopify_function_v2" "_shopify_function_output_finish_array" (func (;12;) (type 0))) + (import "shopify_function_v2" "_shopify_function_output_new_interned_utf8_str" (func (;13;) (type 2))) + (import "shopify_function_v2" "_shopify_function_input_get_utf8_str_addr" (func (;14;) (type 2))) + (import "shopify_function_v2" "memory" (memory (;0;) 1)) + (import "shopify_function_v2" "_shopify_function_input_get_obj_prop" (func (;15;) (type 10))) + (import "shopify_function_v2" "shopify_function_realloc" (func (;16;) (type 7))) + (import "shopify_function_v2" "_shopify_function_output_new_utf8_str" (func (;17;) (type 3))) + (import "shopify_function_v2" "_shopify_function_intern_utf8_str" (func (;18;) (type 3))) + (import "shopify_function_v2" "_shopify_function_log_new_utf8_str" (func (;19;) (type 2))) (memory (;1;) 1) (export "memory" (memory 1)) (func (;20;) (type 4) (param i32 i32) diff --git a/trampoline/src/test_data/consumer.wat b/trampoline/src/test_data/consumer.wat index 28b0c6d..09036aa 100644 --- a/trampoline/src/test_data/consumer.wat +++ b/trampoline/src/test_data/consumer.wat @@ -1,30 +1,30 @@ (module ;; General - (import "shopify_function_v1" "shopify_function_intern_utf8_str" (func (param i32 i32) (result i32))) + (import "shopify_function_v2" "shopify_function_intern_utf8_str" (func (param i32 i32) (result i32))) ;; Read. - (import "shopify_function_v1" "shopify_function_input_get" (func (result i64))) - (import "shopify_function_v1" "shopify_function_input_get_obj_prop" (func (param i64 i32 i32) (result i64))) - (import "shopify_function_v1" "shopify_function_input_get_interned_obj_prop" (func (param i64 i32) (result i64))) - (import "shopify_function_v1" "shopify_function_input_get_at_index" (func (param i64 i32) (result i64))) - (import "shopify_function_v1" "shopify_function_input_get_obj_key_at_index" (func (param i64 i32) (result i64))) - (import "shopify_function_v1" "shopify_function_input_get_val_len" (func (param i64) (result i32))) - (import "shopify_function_v1" "shopify_function_input_read_utf8_str" (func (param i32 i32 i32))) + (import "shopify_function_v2" "shopify_function_input_get" (func (result i64))) + (import "shopify_function_v2" "shopify_function_input_get_obj_prop" (func (param i64 i32 i32) (result i64))) + (import "shopify_function_v2" "shopify_function_input_get_interned_obj_prop" (func (param i64 i32) (result i64))) + (import "shopify_function_v2" "shopify_function_input_get_at_index" (func (param i64 i32) (result i64))) + (import "shopify_function_v2" "shopify_function_input_get_obj_key_at_index" (func (param i64 i32) (result i64))) + (import "shopify_function_v2" "shopify_function_input_get_val_len" (func (param i64) (result i32))) + (import "shopify_function_v2" "shopify_function_input_read_utf8_str" (func (param i32 i32 i32))) ;; Write. - (import "shopify_function_v1" "shopify_function_output_new_bool" (func (param i32) (result i32))) - (import "shopify_function_v1" "shopify_function_output_new_null" (func (result i32))) - (import "shopify_function_v1" "shopify_function_output_new_i32" (func (param i32) (result i32))) - (import "shopify_function_v1" "shopify_function_output_new_f64" (func (param f64) (result i32))) - (import "shopify_function_v1" "shopify_function_output_new_utf8_str" (func (param i32 i32) (result i32))) - (import "shopify_function_v1" "shopify_function_output_new_object" (func (param i32) (result i32))) - (import "shopify_function_v1" "shopify_function_output_finish_object" (func (result i32))) - (import "shopify_function_v1" "shopify_function_output_new_array" (func (param i32) (result i32))) - (import "shopify_function_v1" "shopify_function_output_finish_array" (func (result i32))) - (import "shopify_function_v1" "shopify_function_output_new_interned_utf8_str" (func (param i32) (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_bool" (func (param i32) (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_null" (func (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_i32" (func (param i32) (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_f64" (func (param f64) (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_utf8_str" (func (param i32 i32) (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_object" (func (param i32) (result i32))) + (import "shopify_function_v2" "shopify_function_output_finish_object" (func (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_array" (func (param i32) (result i32))) + (import "shopify_function_v2" "shopify_function_output_finish_array" (func (result i32))) + (import "shopify_function_v2" "shopify_function_output_new_interned_utf8_str" (func (param i32) (result i32))) ;; Log. - (import "shopify_function_v1" "shopify_function_log_new_utf8_str" (func (param i32 i32))) + (import "shopify_function_v2" "shopify_function_log_new_utf8_str" (func (param i32 i32))) ;; Memory (memory 1) From 29498b7bb72e379f9023acb3e8e079cf30066a52 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Tue, 26 Aug 2025 10:24:41 -0400 Subject: [PATCH 19/20] Update WAT param list --- api/src/shopify_function.wat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/shopify_function.wat b/api/src/shopify_function.wat index ac8bb5b..fc79624 100644 --- a/api/src/shopify_function.wat +++ b/api/src/shopify_function.wat @@ -235,6 +235,6 @@ ;; - ptr: i32 pointer to string data in WebAssembly memory. ;; - len: i32 length of string in bytes. (import "shopify_function_v2" "shopify_function_log_new_utf8_str" - (func (param $len i32)) + (func (param $ptr i32) (param $len i32)) ) ) From cc354c8b9d55890689bc163703e36e36050f2c05 Mon Sep 17 00:00:00 2001 From: Jeff Charles Date: Thu, 28 Aug 2025 15:17:44 -0400 Subject: [PATCH 20/20] Fix bug --- integration_tests/tests/integration_test.rs | 2 +- provider/src/log.rs | 39 +++++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 3c2992c..055bcad 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -536,7 +536,7 @@ fn test_log() -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to prepare example: {e}"))?; let (_, logs, fuel) = run_example("log", vec![], Api::Wasm)?; assert_eq!(logs, "Hi!\nHello\nHere's a third string\n✌️\n"); - assert_fuel_consumed_within_threshold(670, fuel); + assert_fuel_consumed_within_threshold(694, fuel); Ok(()) } diff --git a/provider/src/log.rs b/provider/src/log.rs index 25bb7d7..51a2719 100644 --- a/provider/src/log.rs +++ b/provider/src/log.rs @@ -44,7 +44,7 @@ impl Logs { if len <= space_to_end { // Incoming buffer fits in one block. len1 = len; - self.len += len; + self.len = (self.len + len).min(CAPACITY); } else { // Incoming data wrap will wrap around. len1 = space_to_end; @@ -111,12 +111,12 @@ mod tests { let (source_offset, ptr1, len1, ptr2, len2) = logs.append(100); assert_eq!(source_offset, 0); + assert_eq!(logs.len, 100); + assert_eq!(logs.offset, 100); assert_eq!(ptr1, logs.buffer.as_ptr()); assert_eq!(len1, 100); assert_eq!(len2, 0); assert!(ptr2.is_null()); - assert_eq!(logs.len, 100); - assert_eq!(logs.offset, 100); } #[test] @@ -127,12 +127,12 @@ mod tests { let (source_offset, ptr1, len1, ptr2, len2) = logs.append(large_len); assert_eq!(source_offset, 100); + assert_eq!(logs.len, CAPACITY); + assert_eq!(logs.offset, 0); assert_eq!(ptr1, logs.buffer.as_ptr()); assert_eq!(len1, CAPACITY); assert_eq!(len2, 0); assert!(ptr2.is_null()); - assert_eq!(logs.len, CAPACITY); - assert_eq!(logs.offset, 0); } #[test] @@ -141,12 +141,12 @@ mod tests { let (source_offset, ptr1, len1, ptr2, len2) = logs.append(0); assert_eq!(source_offset, 0); + assert_eq!(logs.len, 0); + assert_eq!(logs.offset, 0); assert_eq!(ptr1, logs.buffer.as_ptr()); assert_eq!(len1, 0); assert_eq!(len2, 0); assert!(ptr2.is_null()); - assert_eq!(logs.len, 0); - assert_eq!(logs.offset, 0); } #[test] @@ -155,12 +155,12 @@ mod tests { let (source_offset, ptr1, len1, ptr2, len2) = logs.append(CAPACITY); assert_eq!(source_offset, 0); + assert_eq!(logs.len, CAPACITY); + assert_eq!(logs.offset, 0); assert_eq!(ptr1, logs.buffer.as_ptr()); assert_eq!(len1, CAPACITY); assert_eq!(len2, 0); assert!(ptr2.is_null()); - assert_eq!(logs.len, CAPACITY); - assert_eq!(logs.offset, 0); } #[test] @@ -169,29 +169,38 @@ mod tests { let (source_offset, ptr1, len1, ptr2, len2) = logs.append(300); assert_eq!(source_offset, 0); + assert_eq!(logs.len, 300); + assert_eq!(logs.offset, 300); assert_eq!(ptr1, logs.buffer.as_ptr()); assert_eq!(len1, 300); assert_eq!(ptr2, ptr::null()); assert_eq!(len2, 0); - assert_eq!(logs.len, 300); - assert_eq!(logs.offset, 300); let (source_offset, ptr1, len1, ptr2, len2) = logs.append(200); assert_eq!(source_offset, 0); + assert_eq!(logs.len, 500); + assert_eq!(logs.offset, 500); assert_eq!(ptr1, unsafe { logs.buffer.as_ptr().add(300) }); assert_eq!(len1, 200); assert_eq!(ptr2, ptr::null()); assert_eq!(len2, 0); - assert_eq!(logs.len, 500); - assert_eq!(logs.offset, 500); - let (source_offset, ptr1, len1, ptr2, len2) = logs.append(600); // Total would be 1100, exceeds CAPACITY (1025) + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(600); // Total would be 1100, exceeds capacity (1025) assert_eq!(source_offset, 0); + assert_eq!(logs.len, CAPACITY); + assert_eq!(logs.offset, 75); // (500 + 600) % CAPACITY assert_eq!(ptr1, unsafe { logs.buffer.as_ptr().add(500) }); assert_eq!(len1, 525); assert_eq!(ptr2, logs.buffer.as_ptr()); assert_eq!(len2, 75); + + let (source_offset, ptr1, len1, ptr2, len2) = logs.append(100); // Total would be 1200 + assert_eq!(source_offset, 0); assert_eq!(logs.len, CAPACITY); - assert_eq!(logs.offset, 75); // (500 + 600) % CAPACITY + assert_eq!(logs.offset, 175); // (500 + 600 + 100) % CAPACITY + assert_eq!(ptr1, unsafe { logs.buffer.as_ptr().add(75) }); + assert_eq!(len1, 100); + assert_eq!(ptr2, ptr::null()); + assert_eq!(len2, 0); } }