From 4061af264d517be37f79f5242d941ec96109b051 Mon Sep 17 00:00:00 2001 From: mars Date: Wed, 2 Nov 2022 16:25:12 -0600 Subject: [PATCH 1/2] Add backend module --- src/backend/mod.rs | 28 ++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 29 insertions(+) create mode 100644 src/backend/mod.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 0000000..1e86768 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,28 @@ +//! This module defines backends for WebAssembly execution. +//! +//! Canary is designed to support multiple WebAssembly runtimes for different +//! purposes. Currently, [wasmtime](https://wasmtime.dev) is the only one +//! implemented, but in the future, [wasm3](https://github.com/wasm3/wasm3) +//! will also be provided. + +use super::*; + +/// A WebAssembly runtime backend. +pub trait Backend { + fn load_module(&self, module: &[u8]) -> Arc; +} + +/// An instance of a WebAssembly module. +/// +/// All self parameters to this trait's functions are immutable, so the +/// implementation must provide interior mutability. This allows instances +/// to intelligently optimize the execution of their scripts, for example, by +/// allowing the execution of multiple ABI calls from multiple threads when +/// a script supports the WebAssembly multithreading extension. +pub trait Instance { + fn bind_panel(&self, panel: PanelId, msg: Vec) -> u32; + fn update(&self, panel: PanelId, dt: f32); + fn draw(&self, panel: PanelId) -> Vec; + fn on_cursor_event(&self, panel: PanelId, kind: CursorEventKind, at: Vec2); + fn on_message(&self, panel: PanelId, msg: Vec); +} diff --git a/src/lib.rs b/src/lib.rs index 6e89aa5..5d37f67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use slab::Slab; use std::collections::HashMap; use std::sync::Arc; +pub mod backend; pub mod text; /// Proportion constant between pixels (at 96dpi) to millimeters (Canary's unit measurement). From 44024ccdef502db6d5fdb53613e4633712810216 Mon Sep 17 00:00:00 2001 From: mars Date: Wed, 2 Nov 2022 17:42:01 -0600 Subject: [PATCH 2/2] Refactor Canary API --- apps/magpie/src/service/window.rs | 32 ++-- apps/sandbox/src/main.rs | 82 ++++---- src/backend/mod.rs | 38 +++- src/backend/wasmtime.rs | 227 ++++++++++++++++++++++ src/lib.rs | 303 +++++++----------------------- 5 files changed, 379 insertions(+), 303 deletions(-) create mode 100644 src/backend/wasmtime.rs diff --git a/apps/magpie/src/service/window.rs b/apps/magpie/src/service/window.rs index 4fb3aff..ee19c99 100644 --- a/apps/magpie/src/service/window.rs +++ b/apps/magpie/src/service/window.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::time::Instant; -use canary::{PanelId, ScriptAbiImpl, ScriptInstance, WasmtimeRuntime, WasmtimeScript}; +use canary::{Panel, Runtime}; use glium::backend::glutin::DisplayCreationError; use glium::{glutin, Surface}; use glutin::event::{Event, WindowEvent}; @@ -23,15 +23,13 @@ pub type WindowMessageSender = EventLoopProxy; pub struct Window { pub graphics: Graphics, - pub script: WasmtimeScript, - pub panel: PanelId, + pub panel: Panel, pub last_update: Instant, } impl Window { pub fn new( - script: WasmtimeScript, - panel: PanelId, + panel: Panel, event_loop: &EventLoopWindowTarget, ) -> Result { let wb = glutin::window::WindowBuilder::new(); @@ -41,7 +39,6 @@ impl Window { let last_update = Instant::now(); Ok(Self { graphics, - script, panel, last_update, }) @@ -58,17 +55,16 @@ impl Window { pub fn update(&mut self) { let now = Instant::now(); let dt = now.duration_since(self.last_update).as_secs_f32(); - self.script.update(self.panel, dt); + self.panel.update(dt); } pub fn draw(&mut self) { - self.script.draw(self.panel, |commands| { - self.graphics.draw(commands); - }); + let commands = self.panel.draw(); + self.graphics.draw(&commands); } pub fn send_message(&mut self, msg: Vec) { - self.script.on_message(self.panel, msg); + self.panel.on_message(msg); } } @@ -76,16 +72,19 @@ pub struct WindowStore { pub ipc_sender: IpcMessageSender, pub ipc_to_window: HashMap, pub windows: HashMap, - pub runtime: WasmtimeRuntime, + pub runtime: Runtime, } impl WindowStore { pub fn new(ipc_sender: IpcMessageSender) -> Self { + let backend = canary::backend::make_default_backend().unwrap(); + let runtime = Runtime::new(backend).unwrap(); + Self { ipc_sender, ipc_to_window: Default::default(), windows: Default::default(), - runtime: WasmtimeRuntime::new().unwrap(), + runtime, } } @@ -120,11 +119,10 @@ impl WindowStore { Event::UserEvent(event) => match event { WindowMessage::OpenWindow { id, script } => { println!("Opening window {} with script {:?}", id, script); - let abi = Default::default(); let module = std::fs::read(script).unwrap(); - let mut script = self.runtime.load_module(abi, &module).unwrap(); - let panel = script.bind_panel(vec![]); - let window = Window::new(script, panel, &event_loop).unwrap(); + let mut script = self.runtime.load_module(&module).unwrap(); + let panel = script.create_panel(vec![]).unwrap(); + let window = Window::new(panel, &event_loop).unwrap(); let window_id = window.get_id(); self.windows.insert(window_id, window); self.ipc_to_window.insert(id, window_id); diff --git a/apps/sandbox/src/main.rs b/apps/sandbox/src/main.rs index 0ae7094..a6381eb 100644 --- a/apps/sandbox/src/main.rs +++ b/apps/sandbox/src/main.rs @@ -1,9 +1,8 @@ // Copyright (c) 2022 Marceline Cramer // SPDX-License-Identifier: AGPL-3.0-or-later -use canary::{CursorEventKind, ScriptInstance}; +use canary::{CursorEventKind, Panel, Runtime, Script}; use eframe::egui; -use std::sync::{Arc, RwLock}; use std::time::Instant; fn main() { @@ -27,11 +26,9 @@ fn main() { ); } -type Script = Arc>>; - struct App { script: Script, - panels: Vec, + panels: Vec, next_idx: usize, last_update: Instant, bind_message_buf: String, @@ -39,13 +36,13 @@ struct App { impl App { pub fn new(module_path: &str) -> Self { - let runtime = canary::WasmtimeRuntime::new().unwrap(); - let abi = canary::ScriptAbiImpl::default(); + let backend = canary::backend::make_default_backend().unwrap(); + let runtime = Runtime::new(backend).unwrap(); let module = std::fs::read(module_path).unwrap(); - let script = runtime.load_module(abi, &module).unwrap(); + let script = runtime.load_module(&module).unwrap(); Self { - script: Arc::new(RwLock::new(script)), + script, panels: vec![], next_idx: 0, last_update: Instant::now(), @@ -58,21 +55,18 @@ impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { ctx.request_repaint(); - let mut script = self.script.write().unwrap(); - egui::SidePanel::left("left_panel").show(ctx, |ui| { let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor(); ui.add(text_edit); if ui.button("Bind Panel").clicked() { let msg = self.bind_message_buf.as_bytes().to_vec(); - let id = script.bind_panel(msg); + let panel = self.script.create_panel(msg).unwrap(); let index = self.next_idx; self.next_idx += 1; - let panel = Panel { - script: self.script.to_owned(), - id, + let panel = PanelWindow { + panel, index, msg_buf: String::new(), show_msg: false, @@ -86,22 +80,21 @@ impl eframe::App for App { self.last_update = Instant::now(); for panel in self.panels.iter_mut() { - script.update(panel.id, dt); - panel.show(&mut *script, ctx); + panel.panel.update(dt); + panel.show(ctx); } } } -pub struct Panel { - pub script: Script, - pub id: canary::PanelId, +pub struct PanelWindow { + pub panel: Panel, pub index: usize, pub msg_buf: String, pub show_msg: bool, } -impl Panel { - pub fn show(&mut self, script: &mut impl canary::ScriptInstance, ctx: &egui::Context) { +impl PanelWindow { + pub fn show(&mut self, ctx: &egui::Context) { let window_id = egui::Id::new(format!("panel_{}", self.index)); egui::Window::new("Panel").id(window_id).show(ctx, |ui| { egui::menu::bar(ui, |ui| { @@ -134,38 +127,37 @@ impl Panel { CursorEventKind::Hover }; - script.on_cursor_event(self.id, kind, pos); + self.panel.on_cursor_event(kind, pos); } let texture = egui::TextureId::Managed(0); let uv = egui::pos2(0.0, 0.0); let mut mesh = egui::Mesh::with_texture(texture); - script.draw(self.id, |commands| { - for command in commands.iter() { - let voff = mesh.vertices.len() as u32; + let commands = self.panel.draw(); + for command in commands.into_iter() { + let voff = mesh.vertices.len() as u32; - match command { - canary::DrawCommand::Mesh { vertices, indices } => { - for v in vertices.iter() { - use egui::epaint::Vertex; - let pos = egui::pos2(v.position.x, -v.position.y); - let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5); - let pos = rect.left_top() + pos * rect.size(); - let (r, g, b, a) = v.color.to_rgba_unmultiplied(); - let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a); - let v = Vertex { pos, uv, color }; - mesh.vertices.push(v); - } - - for i in indices.iter() { - mesh.indices.push(i + voff); - } + match command { + canary::DrawCommand::Mesh { vertices, indices } => { + for v in vertices.iter() { + use egui::epaint::Vertex; + let pos = egui::pos2(v.position.x, -v.position.y); + let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5); + let pos = rect.left_top() + pos * rect.size(); + let (r, g, b, a) = v.color.to_rgba_unmultiplied(); + let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a); + let v = Vertex { pos, uv, color }; + mesh.vertices.push(v); + } + + for i in indices.iter() { + mesh.indices.push(i + voff); } - _ => unimplemented!(), } + _ => unimplemented!(), } - }); + } let painter = ui.painter_at(rect); let shape = egui::Shape::mesh(mesh); @@ -184,7 +176,7 @@ impl Panel { if ui.button("Send Message").clicked() { let msg = self.msg_buf.as_bytes().to_vec(); - script.on_message(self.id, msg); + self.panel.on_message(msg); } }); } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 1e86768..5b75ed8 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -7,9 +7,19 @@ use super::*; +pub mod wasmtime; + +/// Creates the default WebAssembly backend. +/// +/// Currently, only ever creates [wasmtime::WasmtimeBackend]. +pub fn make_default_backend() -> anyhow::Result> { + let backend = wasmtime::WasmtimeBackend::new()?; + Ok(Box::new(backend)) +} + /// A WebAssembly runtime backend. pub trait Backend { - fn load_module(&self, module: &[u8]) -> Arc; + fn load_module(&self, module: &[u8]) -> anyhow::Result>; } /// An instance of a WebAssembly module. @@ -20,9 +30,27 @@ pub trait Backend { /// allowing the execution of multiple ABI calls from multiple threads when /// a script supports the WebAssembly multithreading extension. pub trait Instance { + /// Binds script data to a Canary panel. + /// + /// To "bind" a Canary panel to a Canary script, this function must be + /// called. It passes the ID of a panel to the script, plus an + /// initialization message, and the script returns an integer as + /// userdata. All panel events will be identified to the script with this + /// userdata as the first argument. + /// + /// The intended usecase for this userdata is to contain a pointer. A + /// Canary script can allocate some high-level object in memory, and when + /// a panel is bound, the script will return a pointer to that object as the + /// userdata. Then, when the runtime calls back into the script, the + /// userdata will be reinterpreted as a pointer and a method can be called + /// on that object in memory. fn bind_panel(&self, panel: PanelId, msg: Vec) -> u32; - fn update(&self, panel: PanelId, dt: f32); - fn draw(&self, panel: PanelId) -> Vec; - fn on_cursor_event(&self, panel: PanelId, kind: CursorEventKind, at: Vec2); - fn on_message(&self, panel: PanelId, msg: Vec); + + fn update(&self, panel_ud: u32, dt: f32); + + fn draw(&self, panel_ud: u32) -> Vec; + + fn on_cursor_event(&self, panel_ud: u32, kind: CursorEventKind, at: Vec2); + + fn on_message(&self, panel_ud: u32, msg: Vec); } diff --git a/src/backend/wasmtime.rs b/src/backend/wasmtime.rs new file mode 100644 index 0000000..6551565 --- /dev/null +++ b/src/backend/wasmtime.rs @@ -0,0 +1,227 @@ +use std::ops::DerefMut; + +use super::{Arc, Backend, Instance, PanelId}; +use crate::{DrawCommand, ScriptAbi, ScriptAbiImpl}; + +use canary_script::{Color, CursorEventKind, Rect, Vec2}; +use parking_lot::Mutex; + +type Caller<'a> = wasmtime::Caller<'a, ScriptAbiImpl>; +type Store = wasmtime::Store; +type Linker = wasmtime::Linker; + +pub struct WasmtimeBackend { + engine: wasmtime::Engine, +} +impl WasmtimeBackend { + pub fn new() -> anyhow::Result { + let mut config = wasmtime::Config::new(); + config.wasm_simd(true); + config.wasm_bulk_memory(true); + config.cranelift_opt_level(wasmtime::OptLevel::Speed); + + let engine = wasmtime::Engine::new(&config)?; + + Ok(Self { engine }) + } +} + +impl Backend for WasmtimeBackend { + fn load_module(&self, module: &[u8]) -> anyhow::Result> { + let module = wasmtime::Module::new(&self.engine, module)?; + let abi = ScriptAbiImpl::default(); + let mut store = wasmtime::Store::new(&self.engine, abi); + let mut linker = Linker::new(&self.engine); + WasmtimeInstance::link(&mut linker)?; + let instance = linker.instantiate(&mut store, &module)?; + let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?; + let update = instance.get_typed_func(&mut store, "update")?; + let draw = instance.get_typed_func(&mut store, "draw")?; + let on_cursor_event = instance.get_typed_func(&mut store, "on_cursor_event")?; + let on_message = instance.get_typed_func(&mut store, "on_message")?; + + let instance = WasmtimeInstance { + store: Mutex::new(store), + bind_panel, + update, + draw, + on_cursor_event, + on_message, + }; + + let instance = Arc::new(instance); + + Ok(instance) + } +} + +pub struct WasmtimeInstance { + store: Mutex, + bind_panel: wasmtime::TypedFunc<(u32, u32), u32>, + update: wasmtime::TypedFunc<(u32, f32), ()>, + draw: wasmtime::TypedFunc, + on_cursor_event: wasmtime::TypedFunc<(u32, u32, f32, f32), ()>, + on_message: wasmtime::TypedFunc<(u32, u32), ()>, +} + +impl WasmtimeInstance { + pub fn link(linker: &mut Linker) -> anyhow::Result<()> { + let module = "env"; + + linker.func_wrap( + module, + "draw_indexed", + |mut caller: Caller<'_>, + vertices_ptr: u32, + vertices_num: u32, + indices_ptr: u32, + indices_num: u32| { + let vertices = Self::get_memory_slice(&mut caller, vertices_ptr, vertices_num); + let indices = Self::get_memory_slice(&mut caller, indices_ptr, indices_num); + caller.data().draw_indexed(vertices, indices); + }, + )?; + + linker.func_wrap( + module, + "draw_text_layout", + |caller: Caller<'_>, id: u32, xoff: f32, yoff: f32, scale: f32, color: u32| { + let offset = Vec2 { x: xoff, y: yoff }; + let color = Color(color); + caller.data().draw_text_layout(id, offset, scale, color); + }, + )?; + + linker.func_wrap( + module, + "font_load", + |mut caller: Caller<'_>, family_ptr: u32, family_len: u32| { + let family = Self::get_memory_slice_str(&mut caller, family_ptr, family_len); + caller.data().font_load(family) + }, + )?; + + linker.func_wrap( + module, + "text_layout_new", + |mut caller: Caller<'_>, font_id: u32, text_ptr: u32, text_len: u32| { + let text = Self::get_memory_slice_str(&mut caller, text_ptr, text_len); + caller.data().text_layout_new(font_id, text) + }, + )?; + + linker.func_wrap( + module, + "text_layout_delete", + |caller: Caller<'_>, id: u32| caller.data().text_layout_delete(id), + )?; + + linker.func_wrap( + module, + "text_layout_get_bounds", + |mut caller: Caller<'_>, id: u32, rect_ptr: u32| { + let rect: &mut Rect = Self::get_memory_ref(&mut caller, rect_ptr); + caller.data().text_layout_get_bounds(id, rect); + }, + )?; + + linker.func_wrap( + module, + "message_get_len", + |caller: Caller<'_>, id: u32| -> u32 { caller.data().message_get_len(id) }, + )?; + + linker.func_wrap( + module, + "message_get_data", + |mut caller: Caller<'_>, id: u32, ptr: u32| { + let ptr = ptr as usize; + let len = caller.data().message_get_len(id) as usize; + let dst = Self::get_memory_slice_bytes(&mut caller, ptr, len); + caller.data().message_get_data(id, dst) + }, + )?; + + Ok(()) + } + + fn get_memory_ref(caller: &mut Caller<'_>, ptr: u32) -> &'static mut D { + let len = std::mem::size_of::(); + let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len); + bytemuck::from_bytes_mut(bytes) + } + + fn get_memory_slice( + caller: &mut Caller<'_>, + ptr: u32, + num: u32, + ) -> &'static mut [D] { + let len = num as usize * std::mem::size_of::(); + let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len); + bytemuck::cast_slice_mut(bytes) + } + + fn get_memory_slice_str(caller: &mut Caller<'_>, ptr: u32, len: u32) -> &'static mut str { + let memory = Self::get_memory_slice_bytes(caller, ptr as usize, len as usize); + std::str::from_utf8_mut(memory).unwrap() + } + + fn get_memory_slice_bytes( + caller: &mut Caller<'_>, + ptr: usize, + len: usize, + ) -> &'static mut [u8] { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + if ptr + len > memory.data_size(&caller) { + panic!("Attempted wasm memory read is out-of-bounds!"); + } + + unsafe { + let ptr = memory.data_ptr(caller).add(ptr); + std::slice::from_raw_parts_mut(ptr, len) + } + } +} + +impl Instance for WasmtimeInstance { + fn bind_panel(&self, panel: PanelId, msg: Vec) -> u32 { + let mut store = self.store.lock(); + let msg = store.data().message_new(msg); + let args = (panel.0 as u32, msg); + let data = self.bind_panel.call(store.deref_mut(), args).unwrap(); + store.data().message_free(msg); + data + } + + fn update(&self, panel_ud: u32, dt: f32) { + let mut store = self.store.lock(); + self.update.call(store.deref_mut(), (panel_ud, dt)).unwrap(); + } + + fn draw(&self, panel_ud: u32) -> Vec { + let mut store = self.store.lock(); + store.data().start_draw(); + self.draw.call(store.deref_mut(), panel_ud).unwrap(); + let mut cmds = Vec::new(); + store + .data() + .with_draw_commands(|slice| cmds.extend_from_slice(slice)); + cmds + } + + fn on_cursor_event(&self, panel_ud: u32, kind: CursorEventKind, at: Vec2) { + let mut store = self.store.lock(); + self.on_cursor_event + .call(store.deref_mut(), (panel_ud, kind as u32, at.x, at.y)) + .unwrap(); + } + + fn on_message(&self, panel_ud: u32, msg: Vec) { + let mut store = self.store.lock(); + let msg = store.data().message_new(msg); + self.on_message + .call(store.deref_mut(), (panel_ud, msg)) + .unwrap(); + store.data().message_free(msg); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5d37f67..33ccf07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,72 @@ use std::sync::Arc; pub mod backend; pub mod text; +use backend::{Backend, Instance}; + +/// The main interface to Canary. +pub struct Runtime { + backend: Box, +} + +impl Runtime { + pub fn new(backend: Box) -> anyhow::Result { + Ok(Self { backend }) + } + + pub fn load_module(&self, module: &[u8]) -> anyhow::Result