escouade_plugin_protocol/
turn.rs1use crate::Refusal;
16use serde_json::Value;
17use std::path::Path;
18
19#[cfg_attr(not(target_family = "wasm"), allow(dead_code))]
21fn whole_lines(partial: &mut Vec<u8>, read: &[u8]) -> Vec<String> {
22 let mut all = std::mem::take(partial);
23 all.extend_from_slice(read);
24 let end = all.iter().rposition(|b| *b == b'\n').map_or(0, |i| i + 1);
25 *partial = all.split_off(end);
26 String::from_utf8_lossy(&all)
27 .lines()
28 .map(str::to_string)
29 .collect()
30}
31
32#[macro_export]
36macro_rules! module {
37 ($run:path) => {
38 #[cfg(target_family = "wasm")]
39 #[export_name = "escouade-turn"]
40 pub extern "C" fn __escouade_turn() {
41 $crate::turn::enter($run);
42 }
43 };
44}
45
46#[cfg(not(target_family = "wasm"))]
49pub fn run(
50 name: &str,
51 capabilities: Value,
52 handler: impl FnMut(&str, Value) -> Result<Value, Refusal> + 'static,
53) -> i32 {
54 let stdin = std::io::stdin();
55 match crate::serve(
56 stdin.lock(),
57 std::io::stdout().lock(),
58 capabilities,
59 handler,
60 ) {
61 Ok(()) => 0,
62 Err(e) => {
63 eprintln!("escouade-plugins {name}: {e}");
64 1
65 }
66 }
67}
68
69#[cfg(not(target_family = "wasm"))]
71pub fn instance_id() -> u64 {
72 u64::from(std::process::id())
73}
74
75#[cfg(not(target_family = "wasm"))]
78pub fn read_file(path: &Path) -> Option<String> {
79 std::fs::read_to_string(path).ok()
80}
81
82#[cfg(target_family = "wasm")]
83pub use module::*;
84
85#[cfg(target_family = "wasm")]
86mod module {
87 use super::*;
88 use crate::serve;
89 use serde_json::json;
90 use std::cell::{Cell, RefCell};
91 use std::io::{Read, Write};
92
93 type Handler = Box<dyn FnMut(&str, Value) -> Result<Value, Refusal>>;
94
95 struct Machine {
96 capabilities: Value,
97 handler: Handler,
98 }
99
100 thread_local! {
101 static STARTED: Cell<bool> = const { Cell::new(false) };
102 static TURN: Cell<Option<fn()>> = const { Cell::new(None) };
103 static PARTIAL: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
104 static OUT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
105 static MACHINE: RefCell<Option<Machine>> = const { RefCell::new(None) };
106 }
107
108 pub fn lines() -> Vec<String> {
110 let mut read = Vec::new();
111 let mut buf = [0u8; 16 * 1024];
112 let mut input = std::io::stdin().lock();
113 while let Ok(n) = input.read(&mut buf) {
114 if n == 0 {
115 break;
116 }
117 read.extend_from_slice(&buf[..n]);
118 }
119 PARTIAL.with(|p| whole_lines(&mut p.borrow_mut(), &read))
120 }
121
122 pub fn write(line: &str) {
124 OUT.with(|o| {
125 let mut o = o.borrow_mut();
126 o.extend_from_slice(line.as_bytes());
127 o.push(b'\n');
128 });
129 }
130
131 pub fn flush() {
133 OUT.with(|o| {
134 let mut o = o.borrow_mut();
135 if !o.is_empty() {
136 let mut out = std::io::stdout().lock();
137 let _ = out.write_all(&o);
138 let _ = out.flush();
139 o.clear();
140 }
141 });
142 }
143
144 pub fn set_turn(turn: fn()) {
146 TURN.with(|t| t.set(Some(turn)));
147 }
148
149 #[doc(hidden)]
151 pub fn enter(start: impl FnOnce() -> i32) {
152 if !STARTED.with(|s| s.replace(true)) {
153 start();
154 }
155 if let Some(turn) = TURN.with(Cell::get) {
156 turn();
157 }
158 }
159
160 pub fn run(
162 _name: &str,
163 capabilities: Value,
164 handler: impl FnMut(&str, Value) -> Result<Value, Refusal> + 'static,
165 ) -> i32 {
166 let handler = Box::new(handler);
167 MACHINE.with(|m| {
168 *m.borrow_mut() = Some(Machine {
169 capabilities,
170 handler,
171 })
172 });
173 set_turn(machine_turn);
174 0
175 }
176
177 fn machine_turn() {
178 let input = lines().join("\n");
179 MACHINE.with(|m| {
180 let mut m = m.borrow_mut();
181 let Some(m) = m.as_mut() else { return };
182 let mut out = Vec::new();
183 let handler = &mut m.handler;
184 let capabilities = m.capabilities.clone();
185 let _ = serve(input.as_bytes(), &mut out, capabilities, |m, p| {
186 handler(m, p)
187 });
188 OUT.with(|o| o.borrow_mut().extend_from_slice(&out));
189 });
190 flush();
191 }
192
193 #[link(wasm_import_module = "escouade")]
194 extern "C" {
195 fn call(request: *const u8, len: usize, out: *mut u8, cap: usize) -> i64;
198 }
199
200 #[link(wasm_import_module = "wasi_snapshot_preview1")]
201 extern "C" {
202 fn random_get(buf: *mut u8, len: usize) -> i32;
203 }
204
205 pub fn host_call(method: &str, params: Value) -> Result<Value, String> {
208 let request = json!({ "method": method, "params": params }).to_string();
209 let mut out = vec![0u8; 64 * 1024];
210 loop {
211 let n = unsafe { call(request.as_ptr(), request.len(), out.as_mut_ptr(), out.len()) };
213 if n < 0 {
214 out.resize(n.unsigned_abs() as usize, 0);
215 continue;
216 }
217 let v: Value = serde_json::from_slice(&out[..n as usize]).map_err(|e| e.to_string())?;
218 return match v.get("error") {
219 Some(e) => Err(e.as_str().unwrap_or("refused").to_string()),
220 None => Ok(v["result"].clone()),
221 };
222 }
223 }
224
225 pub fn instance_id() -> u64 {
228 let mut bytes = [0u8; 4];
229 let failed = unsafe { random_get(bytes.as_mut_ptr(), bytes.len()) };
231 if failed != 0 {
232 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
233 return now.map_or(0, |d| u64::from(d.subsec_nanos()));
234 }
235 u64::from(u32::from_le_bytes(bytes))
236 }
237
238 pub fn read_file(path: &Path) -> Option<String> {
240 let v = host_call("fs/read", json!({ "path": path.to_string_lossy() })).ok()?;
241 v["text"].as_str().map(str::to_string)
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn a_line_cut_in_two_comes_whole_at_the_next_turn() {
251 let mut partial = Vec::new();
252 assert_eq!(whole_lines(&mut partial, b"one\ntw"), vec!["one"]);
253 assert_eq!(partial, b"tw");
254 assert!(whole_lines(&mut partial, b"o").is_empty());
255 assert_eq!(whole_lines(&mut partial, b"\nthree\n"), ["two", "three"]);
256 assert!(partial.is_empty());
257 }
258
259 #[test]
260 fn an_instance_has_the_id_of_its_process_natively() {
261 assert_eq!(instance_id(), u64::from(std::process::id()));
262 }
263
264 #[test]
265 fn a_file_is_read_from_the_disk_natively() {
266 let this = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
267 assert!(read_file(&this).is_some_and(|t| t.contains("escouade-plugin-protocol")));
268 assert_eq!(read_file(&this.join("nothing")), None);
269 }
270}