Skip to content

Commit

Permalink
Add support for Wasm plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
mrozycki committed Nov 23, 2024
1 parent 322d14f commit 2dd3637
Show file tree
Hide file tree
Showing 24 changed files with 2,263 additions and 357 deletions.
1,636 changes: 1,435 additions & 201 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"animation-macros",
"animation-template",
"animation-utils",
"animation-wasm-wrapper",
"animations",
"light-client",
"configurator",
Expand All @@ -15,6 +16,7 @@ members = [
"webui",
"visualizer",
"events",
"test-wasm-animation",
]

resolver = "2"
26 changes: 22 additions & 4 deletions animation-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,31 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro_attribute]
pub fn wasm_plugin(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast: syn::ItemStruct = syn::parse2(item.into()).unwrap();
generate_wasm_plugin(ast).into()
}

fn generate_wasm_plugin(ast: syn::ItemStruct) -> proc_macro2::TokenStream {
let name = &ast.ident;
quote! {
#ast

type WrappedGuestPlugin = animation_wasm_wrapper::guest::GuestPluginWrapper<#name>;
animation_wasm_wrapper::guest::export!(WrappedGuestPlugin with_types_in animation_wasm_wrapper::guest);
}
}

#[proc_macro_attribute]
pub fn plugin(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast: syn::ItemStruct = syn::parse2(item.into()).unwrap();
generate_native_plugin(ast).into()
}

fn generate_native_plugin(ast: syn::ItemStruct) -> proc_macro2::TokenStream {
let name = &ast.ident;
let output = quote! {
quote! {
#ast

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
Expand Down Expand Up @@ -130,9 +150,7 @@ pub fn plugin(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
Ok(())
}
};

output.into()
}
}

#[derive(Debug, Clone, FromMeta)]
Expand Down
2 changes: 1 addition & 1 deletion animation-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ pub mod decorators;

use std::f64::consts::TAU;

pub use animation_macros::{plugin, EnumSchema, Schema};
pub use animation_macros::{plugin, wasm_plugin, EnumSchema, Schema};
use nalgebra::{Rotation3, Unit, Vector3};
use rand::Rng;

Expand Down
25 changes: 25 additions & 0 deletions animation-wasm-wrapper/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "animation-wasm-wrapper"
version = "0.1.0"
edition = "2021"

[dependencies]
animation-api = { path = "../animation-api" }
animation-utils = { path = "../animation-utils" }
lightfx = { path = "../lightfx" }

itertools = "0.13.0"
serde = "1.0.215"
serde_json = "1.0.132"
wit-bindgen = { version = "0.35.0", optional = true }
wasmtime = { version = "27.0.0", optional = true }
wasmtime-wasi = { version = "27.0.0", optional = true }
thiserror = "2.0.3"
log = "0.4.22"
tokio = { version = "1.41.1", optional = true }


[features]
default = ["guest", "host"]
guest = ["wit-bindgen"]
host = ["wasmtime", "wasmtime-wasi", "tokio"]
74 changes: 74 additions & 0 deletions animation-wasm-wrapper/src/guest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::{cell::RefCell, marker::PhantomData};

use animation_api::schema::GetSchema;
use exports::guest::animation::plugin::{Color, Guest, Position};

wit_bindgen::generate!({
world: "animation",
pub_export_macro: true,
});

pub struct GuestPluginWrapper<T: animation_api::Animation> {
_phantom: PhantomData<T>,
}

impl<T: animation_api::Animation + 'static> Guest for GuestPluginWrapper<T> {
type Animation = GuestAnimationWrapper<<T as animation_api::Animation>::Wrapped>;
}

pub struct GuestAnimationWrapper<T: animation_api::Animation<Parameters: GetSchema>> {
inner: RefCell<T>,
}

impl<T: animation_api::Animation + 'static> exports::guest::animation::plugin::GuestAnimation
for GuestAnimationWrapper<T>
{
fn new(points: Vec<Position>) -> Self {
Self {
inner: RefCell::new(T::new(
points.into_iter().map(|p| (p.x, p.y, p.z)).collect(),
)),
}
}

fn update(&self, time_delta: f64) {
self.inner.borrow_mut().update(time_delta);
}

fn render(&self) -> Vec<Color> {
self.inner
.borrow()
.render()
.pixels_iter()
.map(|p| Color {
r: p.r,
g: p.g,
b: p.b,
})
.collect()
}

fn get_schema(&self) -> String {
serde_json::to_string(&self.inner.borrow().get_schema()).unwrap()
}

fn get_parameters(&self) -> String {
serde_json::to_string(&self.inner.borrow().get_parameters()).unwrap()
}

fn set_parameters(&self, values: String) {
if let Ok(values) = serde_json::from_str(&values) {
self.inner.borrow_mut().set_parameters(values);
}
}

fn get_fps(&self) -> f64 {
self.inner.borrow().get_fps()
}

fn on_event(&self, event: String) {
if let Ok(event) = serde_json::from_str(&event) {
self.inner.borrow_mut().on_event(event);
}
}
}
164 changes: 164 additions & 0 deletions animation-wasm-wrapper/src/host.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use std::{collections::HashMap, path::Path};

use animation_api::{event::Event, schema};
use exports::guest::animation::plugin::{Color, Position};
use itertools::Itertools;
use tokio::sync::Mutex;
use wasmtime::{
component::{bindgen, Component, Linker, ResourceAny},
AsContextMut, Config, Engine, Store,
};
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView};

bindgen!({
world: "animation",
async: true,
});

struct State {
ctx: WasiCtx,
table: ResourceTable,
}

impl WasiView for State {
fn ctx(&mut self) -> &mut WasiCtx {
&mut self.ctx
}
fn table(&mut self) -> &mut ResourceTable {
&mut self.table
}
}

#[derive(Debug, thiserror::Error)]
pub enum HostedPluginError {
#[error("wasmtime returned error: {0}")]
WasmtimeError(#[from] wasmtime::Error),
}
type Result<T> = std::result::Result<T, HostedPluginError>;

pub struct HostedPlugin {
store: Mutex<Store<State>>,
bindings: Animation,
handle: ResourceAny,
}

impl HostedPlugin {
pub async fn new(executable_path: &Path, points: Vec<(f64, f64, f64)>) -> Result<Self> {
let mut config = Config::new();
config.async_support(true);
let engine = Engine::new(&config)?;
let component = Component::from_file(&engine, executable_path)?;

let mut linker = Linker::new(&engine);
wasmtime_wasi::add_to_linker_async(&mut linker)?;

let mut store = Store::new(
&engine,
State {
ctx: WasiCtxBuilder::new().build(),
table: ResourceTable::new(),
},
);

let bindings = Animation::instantiate_async(&mut store, &component, &linker)
.await
.unwrap();
let guest = bindings.guest_animation_plugin();
let animation = guest.animation();
let points = points
.into_iter()
.map(|(x, y, z)| Position { x, y, z })
.collect_vec();
let handle = animation
.call_constructor(&mut store, &points)
.await
.unwrap();

Ok(Self {
store: Mutex::new(store),
bindings,
handle,
})
}

pub async fn update(&self, time_delta: f64) -> Result<()> {
let mut store = self.store.lock().await;
self.bindings
.guest_animation_plugin()
.animation()
.call_update(store.as_context_mut(), self.handle, time_delta)
.await
.map_err(Into::into)
}

pub async fn render(&self) -> Result<Vec<Color>> {
let mut store = self.store.lock().await;
self.bindings
.guest_animation_plugin()
.animation()
.call_render(store.as_context_mut(), self.handle)
.await
.map_err(Into::into)
}

pub async fn get_schema(&self) -> Result<schema::ConfigurationSchema> {
let mut store = self.store.lock().await;
let schema = self
.bindings
.guest_animation_plugin()
.animation()
.call_get_schema(store.as_context_mut(), self.handle)
.await?;

Ok(serde_json::from_str(&schema).unwrap())
}

pub async fn set_parameters(
&mut self,
values: &HashMap<String, schema::ParameterValue>,
) -> Result<()> {
let mut store = self.store.lock().await;
let values = serde_json::to_string(values).unwrap();

self.bindings
.guest_animation_plugin()
.animation()
.call_set_parameters(store.as_context_mut(), self.handle, &values)
.await
.map_err(Into::into)
}

pub async fn get_parameters(&self) -> Result<HashMap<String, schema::ParameterValue>> {
let mut store = self.store.lock().await;
let values = self
.bindings
.guest_animation_plugin()
.animation()
.call_get_parameters(store.as_context_mut(), self.handle)
.await?;

Ok(serde_json::from_str(&values).unwrap_or_default())
}

pub async fn get_fps(&self) -> Result<f64> {
let mut store = self.store.lock().await;
self.bindings
.guest_animation_plugin()
.animation()
.call_get_fps(store.as_context_mut(), self.handle)
.await
.map_err(Into::into)
}

pub async fn send_event(&self, event: Event) -> Result<()> {
let mut store = self.store.lock().await;
let event = serde_json::to_string(&event).unwrap();

self.bindings
.guest_animation_plugin()
.animation()
.call_on_event(store.as_context_mut(), self.handle, &event)
.await
.map_err(Into::into)
}
}
5 changes: 5 additions & 0 deletions animation-wasm-wrapper/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(feature = "guest")]
pub mod guest;

#[cfg(feature = "host")]
pub mod host;
31 changes: 31 additions & 0 deletions animation-wasm-wrapper/wit/animation.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package guest:animation;

interface plugin {
record position {
x: f64,
y: f64,
z: f64,
}

record color {
r: u8,
g: u8,
b: u8,
}

resource animation {
constructor(points: list<position>);

update: func(time-delta: f64);
render: func() -> list<color>;
get-schema: func() -> string;
get-parameters: func() -> string;
set-parameters: func(values: string);
get-fps: func() -> f64;
on-event: func(event: string);
}
}

world animation {
export plugin;
}
7 changes: 7 additions & 0 deletions animator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ tokio = { version = "1", features = ["full"] }
log = "0.4.17"
env_logger = "0.10.0"
thiserror = "1.0.60"
itertools = "0.13.0"
async-trait = "0.1.83"

[dependencies.animation-wasm-wrapper]
path = "../animation-wasm-wrapper"
default-features = false
features = ["host"]

[features]
default = ["midi", "audio"]
Expand Down
Loading

0 comments on commit 2dd3637

Please sign in to comment.