use crate::Refusal;
use serde_json::Value;
use std::path::Path;
#[cfg_attr(not(target_family = "wasm"), allow(dead_code))]
fn whole_lines(partial: &mut Vec<u8>, read: &[u8]) -> Vec<String> {
let mut all = std::mem::take(partial);
all.extend_from_slice(read);
let end = all.iter().rposition(|b| *b == b'\n').map_or(0, |i| i + 1);
*partial = all.split_off(end);
String::from_utf8_lossy(&all)
.lines()
.map(str::to_string)
.collect()
}
#[macro_export]
macro_rules! module {
($run:path) => {
#[cfg(target_family = "wasm")]
#[export_name = "escouade-turn"]
pub extern "C" fn __escouade_turn() {
$crate::turn::enter($run);
}
};
}
#[cfg(not(target_family = "wasm"))]
pub fn run(
name: &str,
capabilities: Value,
handler: impl FnMut(&str, Value) -> Result<Value, Refusal> + 'static,
) -> i32 {
let stdin = std::io::stdin();
match crate::serve(
stdin.lock(),
std::io::stdout().lock(),
capabilities,
handler,
) {
Ok(()) => 0,
Err(e) => {
eprintln!("escouade-plugins {name}: {e}");
1
}
}
}
#[cfg(not(target_family = "wasm"))]
pub fn instance_id() -> u64 {
u64::from(std::process::id())
}
#[cfg(not(target_family = "wasm"))]
pub fn read_file(path: &Path) -> Option<String> {
std::fs::read_to_string(path).ok()
}
#[cfg(target_family = "wasm")]
pub use module::*;
#[cfg(target_family = "wasm")]
mod module {
use super::*;
use crate::serve;
use serde_json::json;
use std::cell::{Cell, RefCell};
use std::io::{Read, Write};
type Handler = Box<dyn FnMut(&str, Value) -> Result<Value, Refusal>>;
struct Machine {
capabilities: Value,
handler: Handler,
}
thread_local! {
static STARTED: Cell<bool> = const { Cell::new(false) };
static TURN: Cell<Option<fn()>> = const { Cell::new(None) };
static PARTIAL: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
static OUT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
static MACHINE: RefCell<Option<Machine>> = const { RefCell::new(None) };
}
pub fn lines() -> Vec<String> {
let mut read = Vec::new();
let mut buf = [0u8; 16 * 1024];
let mut input = std::io::stdin().lock();
while let Ok(n) = input.read(&mut buf) {
if n == 0 {
break;
}
read.extend_from_slice(&buf[..n]);
}
PARTIAL.with(|p| whole_lines(&mut p.borrow_mut(), &read))
}
pub fn write(line: &str) {
OUT.with(|o| {
let mut o = o.borrow_mut();
o.extend_from_slice(line.as_bytes());
o.push(b'\n');
});
}
pub fn flush() {
OUT.with(|o| {
let mut o = o.borrow_mut();
if !o.is_empty() {
let mut out = std::io::stdout().lock();
let _ = out.write_all(&o);
let _ = out.flush();
o.clear();
}
});
}
pub fn set_turn(turn: fn()) {
TURN.with(|t| t.set(Some(turn)));
}
#[doc(hidden)]
pub fn enter(start: impl FnOnce() -> i32) {
if !STARTED.with(|s| s.replace(true)) {
start();
}
if let Some(turn) = TURN.with(Cell::get) {
turn();
}
}
pub fn run(
_name: &str,
capabilities: Value,
handler: impl FnMut(&str, Value) -> Result<Value, Refusal> + 'static,
) -> i32 {
let handler = Box::new(handler);
MACHINE.with(|m| {
*m.borrow_mut() = Some(Machine {
capabilities,
handler,
})
});
set_turn(machine_turn);
0
}
fn machine_turn() {
let input = lines().join("\n");
MACHINE.with(|m| {
let mut m = m.borrow_mut();
let Some(m) = m.as_mut() else { return };
let mut out = Vec::new();
let handler = &mut m.handler;
let capabilities = m.capabilities.clone();
let _ = serve(input.as_bytes(), &mut out, capabilities, |m, p| {
handler(m, p)
});
OUT.with(|o| o.borrow_mut().extend_from_slice(&out));
});
flush();
}
#[link(wasm_import_module = "escouade")]
extern "C" {
fn call(request: *const u8, len: usize, out: *mut u8, cap: usize) -> i64;
}
#[link(wasm_import_module = "wasi_snapshot_preview1")]
extern "C" {
fn random_get(buf: *mut u8, len: usize) -> i32;
}
pub fn host_call(method: &str, params: Value) -> Result<Value, String> {
let request = json!({ "method": method, "params": params }).to_string();
let mut out = vec![0u8; 64 * 1024];
loop {
let n = unsafe { call(request.as_ptr(), request.len(), out.as_mut_ptr(), out.len()) };
if n < 0 {
out.resize(n.unsigned_abs() as usize, 0);
continue;
}
let v: Value = serde_json::from_slice(&out[..n as usize]).map_err(|e| e.to_string())?;
return match v.get("error") {
Some(e) => Err(e.as_str().unwrap_or("refused").to_string()),
None => Ok(v["result"].clone()),
};
}
}
pub fn instance_id() -> u64 {
let mut bytes = [0u8; 4];
let failed = unsafe { random_get(bytes.as_mut_ptr(), bytes.len()) };
if failed != 0 {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
return now.map_or(0, |d| u64::from(d.subsec_nanos()));
}
u64::from(u32::from_le_bytes(bytes))
}
pub fn read_file(path: &Path) -> Option<String> {
let v = host_call("fs/read", json!({ "path": path.to_string_lossy() })).ok()?;
v["text"].as_str().map(str::to_string)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_cut_in_two_comes_whole_at_the_next_turn() {
let mut partial = Vec::new();
assert_eq!(whole_lines(&mut partial, b"one\ntw"), vec!["one"]);
assert_eq!(partial, b"tw");
assert!(whole_lines(&mut partial, b"o").is_empty());
assert_eq!(whole_lines(&mut partial, b"\nthree\n"), ["two", "three"]);
assert!(partial.is_empty());
}
#[test]
fn an_instance_has_the_id_of_its_process_natively() {
assert_eq!(instance_id(), u64::from(std::process::id()));
}
#[test]
fn a_file_is_read_from_the_disk_natively() {
let this = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
assert!(read_file(&this).is_some_and(|t| t.contains("escouade-plugin-protocol")));
assert_eq!(read_file(&this.join("nothing")), None);
}
}