Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions crates/wasm-host/examples/wasm_call.rs
Original file line number Diff line number Diff line change
@@ -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.<function>`: 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<String, String> {
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<PathBuf>,
) -> Result<String, String> {
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()),
})
}
}
24 changes: 24 additions & 0 deletions docs/wasm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `"<h1>Hi</h1>\n"`. For a component, the arguments are the JSON a
script passes to `wasm.<function>`: 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 `<h1>Hello <em>core</em></h1>`.

`--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: <service> 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)
Expand Down
19 changes: 19 additions & 0 deletions docs/wasm.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"'
```

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

`--storage DIR` 把 `DIR` 作为组件所在应用的存储文件夹。组件的 `wasi:http` 请求与在 Shell 中一样发出;
`octosense:host` 调用会失败,并返回 `no host services in wasm_call: <service> needs a shell`。每次调用都使用
新的实例,而 Shell 会在两次调用之间保留组件的实例。

### 手机验收

[`tools/fixtures/wasm-phone-lab`](../tools/fixtures/wasm-phone-lab/README.zh-CN.md)
Expand Down
Loading