diff --git a/crates/wasm-host/examples/wasm_call.rs b/crates/wasm-host/examples/wasm_call.rs new file mode 100644 index 00000000..79dc64ac --- /dev/null +++ b/crates/wasm-host/examples/wasm_call.rs @@ -0,0 +1,125 @@ +//! Call one function of an app's WebAssembly file from the command line, as +//! the shell's `wasm` service would, with no shell and no device: +//! +//! ```sh +//! cargo run -q -p octosense-wasm-host --example wasm_call -- \ +//! bundle/fns/text-tools.wasm count '{"text": "two words"}' +//! ``` +//! +//! For a component (ADR 0014) the arguments are the JSON an app's script +//! passes to `wasm.`: a bare value for one parameter, an object by +//! name or an array in order; the answer prints as JSON. For a core module +//! (ADR 0011) the arguments are the function's input as text, and its output +//! prints as text when it is UTF-8. +//! +//! `--storage DIR` gives a component `DIR` as its app's storage folder +//! (`wasi:filesystem`); without it there is none. Its `wasi:http` requests +//! go out as in the shell. Its `octosense:host` calls are answered with an +//! error: there are no host services here. Each call runs in a fresh +//! instance; the shell keeps a component's instance between calls. + +use std::path::PathBuf; +use std::process::ExitCode; +use std::sync::Arc; +use std::time::Instant; + +use octosense_wasm_host::component::{self, Grants, HostCalls}; +use octosense_wasm_host::{Limits, Runtime}; +use serde_json::Value; + +/// `octosense:host` without a shell. +struct NoHost; + +impl HostCalls for NoHost { + fn request(&self, service: &str, _args: &str, _deadline: Instant) -> Result { + Err(format!( + "no host services in wasm_call: {service} needs a shell" + )) + } +} + +fn usage() -> ExitCode { + eprintln!("usage: wasm_call FILE.wasm FUNCTION [ARGS] [--storage DIR]"); + eprintln!( + " ARGS: JSON for a component (default null), text for a core module (default empty)" + ); + ExitCode::from(2) +} + +fn main() -> ExitCode { + let mut positional = Vec::new(); + let mut storage = None; + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--storage" => match args.next() { + Some(dir) => storage = Some(PathBuf::from(dir)), + None => return usage(), + }, + "-h" | "--help" => return usage(), + _ => positional.push(arg), + } + } + let (file, function, input) = match positional.as_slice() { + [file, function] => (file, function, None), + [file, function, input] => (file, function, Some(input.as_str())), + _ => return usage(), + }; + match run(file, function, input, storage) { + Ok(answer) => { + println!("{answer}"); + ExitCode::SUCCESS + } + Err(error) => { + eprintln!("wasm_call: {error}"); + ExitCode::FAILURE + } + } +} + +fn run( + file: &str, + function: &str, + input: Option<&str>, + storage: Option, +) -> Result { + let bytes = std::fs::read(file).map_err(|e| format!("{file}: {e}"))?; + let runtime = Runtime::new(Limits::default(), None)?; + if component::is_component(&bytes) { + let program = runtime + .load_component(&bytes) + .map_err(|e| format!("{file}: {e}"))?; + let args: Value = match input { + None => Value::Null, + Some(text) => serde_json::from_str(text) + .map_err(|e| format!("the arguments are not JSON: {e}"))?, + }; + let grants = Grants { + storage_dir: storage, + read_only: false, + }; + let mut instance = runtime + .instantiate_component(&program, &grants, None) + .map_err(|e| e.to_string())?; + instance.set_host_calls(Some(Arc::new(NoHost))); + let result = instance.call_json(&component::snake(function), &args); + for line in instance.take_logs() { + eprintln!("log: {line}"); + } + let answer = result.map_err(|e| e.to_string())?; + serde_json::to_string_pretty(&answer).map_err(|e| e.to_string()) + } else { + if storage.is_some() { + return Err("a core module has no storage; --storage is for components".into()); + } + let program = runtime.load(&bytes).map_err(|e| format!("{file}: {e}"))?; + let mut instance = runtime.instantiate(&program).map_err(|e| e.to_string())?; + let output = instance + .call(function, input.unwrap_or("").as_bytes()) + .map_err(|e| e.to_string())?; + Ok(match String::from_utf8(output) { + Ok(text) => text, + Err(bytes) => format!("{:?}", bytes.into_bytes()), + }) + } +} diff --git a/docs/wasm.md b/docs/wasm.md index 73fc41a3..f112c0ad 100644 --- a/docs/wasm.md +++ b/docs/wasm.md @@ -318,6 +318,30 @@ The test components rebuild byte for byte: with rustc 1.97.1, and Home includes the service, and `tools/check-shell-graph.sh` checks that its runtime is linked exactly on macOS, Linux and Android. +### Calling a function from the command line + +`crates/wasm-host/examples/wasm_call.rs` calls one function of a `.wasm` +file with the same runtime and limits as the `wasm` service, with no shell +and no device. Run from this repository: + +```sh +cargo run -q -p octosense-wasm-host --example wasm_call -- \ + crates/wasm-host/tests/fixtures/notes.component.wasm to-html '"# Hi"' +``` + +It prints `"

Hi

\n"`. For a component, the arguments are the JSON a +script passes to `wasm.`: a bare value for one parameter, an object +by name, or an array in order. The answer prints as JSON, and a record prints +as an object. For a core module, the arguments are the function's input as +text: `wasm_call apps/wasmlab/bundle/fns/wasmlab.wasm md_to_html '# Hello *core*'` +prints `

Hello core

`. + +`--storage DIR` gives a component `DIR` as its app's storage folder. Its +`wasi:http` requests go out as in the shell. Its `octosense:host` calls fail +with `no host services in wasm_call: needs a shell`. Each call runs +in a fresh instance, where the shell keeps a component's instance between +calls. + ### Phone acceptance [`tools/fixtures/wasm-phone-lab`](../tools/fixtures/wasm-phone-lab/README.md) diff --git a/docs/wasm.zh-CN.md b/docs/wasm.zh-CN.md index 1c043552..47d3f0cb 100644 --- a/docs/wasm.zh-CN.md +++ b/docs/wasm.zh-CN.md @@ -249,6 +249,25 @@ cd phone && cargo test --locked --features mobile-apps -p octosense-shell wasm_s 两者。CI 中的每个桌面版和 Home 构建都包含这个服务,`tools/check-shell-graph.sh` 检查它的 运行时恰好在 macOS、Linux 和 Android 上链接。 +### 在命令行中调用函数 + +`crates/wasm-host/examples/wasm_call.rs` 用与 `wasm` 服务相同的运行时和限制调用 `.wasm` 文件中的一个函数, +不需要 Shell,也不需要设备。在本仓库中运行: + +```sh +cargo run -q -p octosense-wasm-host --example wasm_call -- \ + crates/wasm-host/tests/fixtures/notes.component.wasm to-html '"# Hi"' +``` + +它输出 `"

Hi

\n"`。对组件来说,参数就是脚本传给 `wasm.<函数>` 的 JSON:单个参数可直接给值,也可以按名称 +传对象,或按顺序传数组;结果以 JSON 输出,记录输出为对象。对核心模块来说,参数是函数的文本输入: +`wasm_call apps/wasmlab/bundle/fns/wasmlab.wasm md_to_html '# Hello *core*'` 输出 +`

Hello core

`。 + +`--storage DIR` 把 `DIR` 作为组件所在应用的存储文件夹。组件的 `wasi:http` 请求与在 Shell 中一样发出; +`octosense:host` 调用会失败,并返回 `no host services in wasm_call: needs a shell`。每次调用都使用 +新的实例,而 Shell 会在两次调用之间保留组件的实例。 + ### 手机验收 [`tools/fixtures/wasm-phone-lab`](../tools/fixtures/wasm-phone-lab/README.zh-CN.md)