commit 0219b4c2b7ab65eb1d601f91198acea6e0af9d06 Author: NGnius (Graham) Date: Tue Jun 7 20:05:04 2022 -0400 Initial functionality diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..506bba5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,270 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + +[[package]] +name = "usdpl" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "usdpl-core", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", + "wee_alloc", +] + +[[package]] +name = "usdpl-back" +version = "0.1.0" +dependencies = [ + "usdpl-core", +] + +[[package]] +name = "usdpl-core" +version = "0.1.0" + +[[package]] +name = "usdpl-rs" +version = "0.1.0" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4464b3f74729a25f42b1a0cd9e6a515d2f25001f3535a6cfaf35d34a4de3bab" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c5a6f82cc6093a321ca5fb3dc9327fe51675d477b3799b4a9375bac3b7b4c" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..32988bb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "usdpl-rs" +version = "0.1.0" +authors = ["NGnius (Graham) "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[workspace] +members = [ + "usdpl-core", + "usdpl-front", + "usdpl-back" +] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7eef3a9 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# usdpl-rs + +Universal Steam Deck Plugin Library + +A faster, lighter way to write plugins + +### Goals +- [ ] Minimum viable plugin +- [ ] Call back-end API from front-end UI +- [ ] Async support +- [ ] PluginLoader/Decky support +- [ ] Crankshaft support +- [ ] Unnamed plugin system support +- [ ] Cross-framework tooling + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a60daf1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,16 @@ +//! Universal Steam Deck Plugin Library +//! +//! A faster, lighter way to write plugins +//! +//! ## Goals +//! - [ ] Minimum viable plugin +//! - [ ] Call back-end API from front-end UI +//! - [ ] Async support +//! - [ ] PluginLoader/Decky support +//! - [ ] Crankshaft support +//! - [ ] Unnamed plugin system support +//! - [ ] Cross-framework tooling +//! +fn main() { + println!("Hello, USDPL!"); +} diff --git a/usdpl-back/Cargo.toml b/usdpl-back/Cargo.toml new file mode 100644 index 0000000..5d8e307 --- /dev/null +++ b/usdpl-back/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "usdpl-back" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +usdpl-core = { version = "0.1.0", path = "../usdpl-core" } diff --git a/usdpl-back/src/instance.rs b/usdpl-back/src/instance.rs new file mode 100644 index 0000000..7c6982e --- /dev/null +++ b/usdpl-back/src/instance.rs @@ -0,0 +1,194 @@ +use std::net::TcpListener; +use std::collections::HashMap; +use std::io::{Read, Write}; + +use usdpl_core::serdes::{Dumpable, Loadable, Primitive}; +use usdpl_core::{RemoteCallResponse, socket}; + +/// Instance for interacting with the front-end +pub struct Instance<'a> { + calls: HashMap) -> Vec>, +} + +impl<'a> Instance<'a> { + #[inline] + pub fn new() -> Self { + Instance { + calls: HashMap::new(), + } + } + + /// Register a function which can be invoked by the front-end + pub fn register) -> Vec) + Send + Sync>(&mut self, name: String, f: &'a mut F) -> &mut Self { + self.calls.insert(name, f); + self + } + + /// Receive and execute callbacks forever + pub fn serve(&mut self) -> std::io::Result<()> { + let listener = TcpListener::bind(socket::socket_addr())?; + for incoming in listener.incoming() { + let mut incoming = incoming?; + let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; + let len = incoming.read(&mut buffer)?; + let (obj_maybe, _) = socket::Packet::load(&buffer[..len]); + if let Some(packet) = obj_maybe { + match packet { + socket::Packet::Call(obj) => { + if let Some(target_func) = self.calls.get_mut(&obj.function) { + // TODO: multithread this + let result = target_func(obj.parameters); + let response = socket::Packet::CallResponse(RemoteCallResponse { + id: obj.id, + response: result, + }); + let (ok, len) = response.dump(&mut buffer); + if !ok && ERROR { + return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Cannot dump return value of function `{}`", &obj.function))); + } + if ERROR { + incoming.write(&buffer[..len])?; + } else { + incoming.write(&buffer[..len]).unwrap_or_default(); + } + } else { + if ERROR { + return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Invalid remote call `{}` received from {}", obj.function, incoming.peer_addr()?))); + } else { + eprintln!("Invalid remote call `{}` received from {}", obj.function, incoming.peer_addr()?); + } + + } + }, + _ => { + let (ok, len) = socket::Packet::Unsupported.dump(&mut buffer); + if !ok && ERROR { + return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Cannot dump unsupported packet"))); + } + if ERROR { + incoming.write(&buffer[..len])?; + } else { + incoming.write(&buffer[..len]).unwrap_or_default(); + } + } + } + } else { + if ERROR { + return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Invalid packet received from {}", incoming.peer_addr()?))); + } else { + eprintln!("Invalid packet received from {}", incoming.peer_addr()?); + } + } + incoming.shutdown(std::net::Shutdown::Both)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::net::TcpStream; + use super::*; + + #[test] + fn serve_full_test() -> std::io::Result<()> { + let _server = std::thread::spawn(|| { + Instance::new() + .register("echo".to_string(), &mut |params| params) + .register("hello".to_string(), &mut |params| { + if let Some(Primitive::String(name)) = params.get(0) { + vec![Primitive::String(format!("Hello {}", name))] + } else { + vec![] + } + }) + .serve::() + }); + std::thread::sleep(std::time::Duration::from_millis(10)); + let mut front = TcpStream::connect(socket::socket_addr()).unwrap(); + let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; + let call = socket::Packet::Call(usdpl_core::RemoteCall { + id: 42, + function: "hello".to_string(), + parameters: vec![Primitive::String("USDPL".to_string())] + }); + let (ok, len) = call.dump(&mut buffer); + assert!(ok, "Packet dump failed"); + assert_eq!(len, 32, "Packet dumped wrong amount of data"); + front.write(&buffer[..len])?; + let len = front.read(&mut buffer)?; + let (response, len) = socket::Packet::load(&buffer[..len]); + assert!(response.is_some(), "Response load failed"); + assert_eq!(len, 29, "Response loaded wrong amount of data"); + let response = response.unwrap(); + if let socket::Packet::CallResponse(resp) = response { + assert_eq!(resp.id, 42); + if let Some(Primitive::String(s)) = resp.response.get(0) { + assert_eq!(s, "Hello USDPL"); + } else { + panic!("Wrong response data"); + } + } else { + panic!("Wrong response packet type"); + } + + Ok(()) + } + + #[test] + #[should_panic] + fn serve_err_test() { + let _client = std::thread::spawn(|| { + std::thread::sleep(std::time::Duration::from_millis(100)); + let mut front = TcpStream::connect(socket::socket_addr()).unwrap(); + let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; + let (_, len) = socket::Packet::Bad.dump(&mut buffer); + front.write(&buffer[..len]).unwrap(); + let _ = front.read(&mut buffer).unwrap(); + }); + Instance::new() + .register("echo".to_string(), &mut |params| params) + .register("hello".to_string(), &mut |params| { + if let Some(Primitive::String(name)) = params.get(0) { + vec![Primitive::String(format!("Hello {}", name))] + } else { + vec![] + } + }) + .serve::() + .unwrap(); + } + + #[test] + #[should_panic] + fn serve_unsupported_test() { + let _server = std::thread::spawn(|| { + Instance::new() + .register("echo".to_string(), &mut |params| params) + .register("hello".to_string(), &mut |params| { + if let Some(Primitive::String(name)) = params.get(0) { + vec![Primitive::String(format!("Hello {}", name))] + } else { + vec![] + } + }) + .serve::() + }); + std::thread::sleep(std::time::Duration::from_millis(10)); + let mut front = TcpStream::connect(socket::socket_addr()).unwrap(); + let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; + let (ok, len) = socket::Packet::Unsupported.dump(&mut buffer); + assert!(ok, "Packet dump failed"); + assert_eq!(len, 32, "Packet dumped wrong amount of data"); + front.write(&buffer[..len]).unwrap(); + let len = front.read(&mut buffer).unwrap(); + let (response, len) = socket::Packet::load(&buffer[..len]); + assert!(response.is_some(), "Response load failed"); + assert_eq!(len, 29, "Response loaded wrong amount of data"); + let response = response.unwrap(); + if let socket::Packet::Unsupported = response { + } else { + panic!("Wrong response packet type"); + } + } +} diff --git a/usdpl-back/src/lib.rs b/usdpl-back/src/lib.rs new file mode 100644 index 0000000..989ac21 --- /dev/null +++ b/usdpl-back/src/lib.rs @@ -0,0 +1,9 @@ +//! Back-end library for plugins. +//! Targets x86_64 (native Steam Deck ISA). +//! +//! This is a minimalist TCP server for handling events from the front-end. +//! + +mod instance; + +pub use instance::Instance; diff --git a/usdpl-core/Cargo.toml b/usdpl-core/Cargo.toml new file mode 100644 index 0000000..f5be185 --- /dev/null +++ b/usdpl-core/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "usdpl-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/usdpl-core/src/lib.rs b/usdpl-core/src/lib.rs new file mode 100644 index 0000000..8769231 --- /dev/null +++ b/usdpl-core/src/lib.rs @@ -0,0 +1,8 @@ +//! Datatypes and constants core the back-end and front-end libraries' operation. +//! This contains serialization functionality and networking datatypes. +mod remote_call; + +pub mod socket; +pub mod serdes; + +pub use remote_call::{RemoteCall, RemoteCallResponse}; diff --git a/usdpl-core/src/remote_call.rs b/usdpl-core/src/remote_call.rs new file mode 100644 index 0000000..539dd55 --- /dev/null +++ b/usdpl-core/src/remote_call.rs @@ -0,0 +1,84 @@ +use crate::serdes::{Primitive, Loadable, Dumpable}; + +pub struct RemoteCall { + pub id: u64, + pub function: String, + pub parameters: Vec, +} + +impl Loadable for RemoteCall { + fn load(buffer: &[u8]) -> (Option, usize) { + let (id_num, len0) = u64::load(buffer); + if id_num.is_none() { + return (None, len0); + } + let (function_name, len1) = String::load(&buffer[len0..]); + if function_name.is_none() { + return (None, len1); + } + let (params, len2) = Vec::::load(&buffer[len0+len1..]); + if params.is_none() { + return (None, len1 + len2); + } + ( + Some(Self { + id: id_num.unwrap(), + function: function_name.unwrap(), + parameters: params.unwrap(), + }), + len0 + len1 + len2 + ) + } +} + +impl Dumpable for RemoteCall { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + let (ok0, len0) = self.id.dump(buffer); + if !ok0 { + return (ok0, len0); + } + let (ok1, len1) = self.function.dump(&mut buffer[len0..]); + if !ok1 { + return (ok1, len1); + } + let (ok2, len2) = self.parameters.dump(&mut buffer[len0+len1..]); + (ok2, len0 + len1 + len2) + } +} + +pub struct RemoteCallResponse { + pub id: u64, + pub response: Vec, +} + +impl Loadable for RemoteCallResponse { + fn load(buffer: &[u8]) -> (Option, usize) { + let (id_num, len0) = u64::load(buffer); + if id_num.is_none() { + return (None, len0); + } + let (response_var, len1) = Vec::::load(&buffer[len0..]); + if response_var.is_none() { + return (None, len1); + } + ( + Some(Self { + id: id_num.unwrap(), + response: response_var.unwrap(), + }), + len0 + len1 + ) + } +} + +impl Dumpable for RemoteCallResponse { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + let (ok0, len0) = self.id.dump(buffer); + if !ok0 { + return (ok0, len0); + } + let (ok1, len1) = self.response.dump(&mut buffer[len0..]); + (ok1, len0 + len1) + } +} + diff --git a/usdpl-core/src/serdes/dump_impl.rs b/usdpl-core/src/serdes/dump_impl.rs new file mode 100644 index 0000000..bb6b96e --- /dev/null +++ b/usdpl-core/src/serdes/dump_impl.rs @@ -0,0 +1,140 @@ +use super::Dumpable; + +impl Dumpable for String { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + let str_bytes = self.as_bytes(); + let len_bytes = (str_bytes.len() as u32).to_le_bytes(); + let total_len = str_bytes.len() + 4; + if buffer.len() < total_len { + return (false, 0); + } + (&mut buffer[..4]).copy_from_slice(&len_bytes); + (&mut buffer[4..total_len]).copy_from_slice(str_bytes); + (true, total_len) + } +} + +impl Dumpable for Vec { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + let len_bytes = (self.len() as u32).to_le_bytes(); + (&mut buffer[..4]).copy_from_slice(&len_bytes); + let mut cursor = 4; + for obj in self.iter() { + let (ok, len) = obj.dump(&mut buffer[cursor..]); + cursor += len; + if !ok { + return (false, cursor); + } + } + (true, cursor) + } +} + +impl Dumpable for bool { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + if buffer.len() < 1 { + return (false, 0); + } + buffer[0] = *self as u8; + (true, 1) + } +} + +impl Dumpable for u8 { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + if buffer.len() < 1 { + return (false, 0); + } + buffer[0] = *self; + (true, 1) + } +} + +impl Dumpable for i8 { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + if buffer.len() < 1 { + return (false, 0); + } + buffer[0] = self.to_le_bytes()[0]; + (true, 1) + } +} + +macro_rules! int_impl { + ($type:ty, $size:literal) => { + impl Dumpable for $type { + fn dump(&self, buffer: &mut [u8]) -> (bool, usize) { + if buffer.len() < $size { + return (false, 0); + } + (&mut buffer[..$size]).copy_from_slice(&self.to_le_bytes()); + (true, $size) + } + } + } +} + +int_impl!{u16, 2} +int_impl!{u32, 4} +int_impl!{u64, 8} +int_impl!{u128, 16} + +int_impl!{i16, 2} +int_impl!{i32, 4} +int_impl!{i64, 8} +int_impl!{i128, 16} + +int_impl!{f32, 4} +int_impl!{f64, 8} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_impl { + ($fn_name:ident, $data:expr, $expected_len:literal, $expected_dump:expr) => { + #[test] + fn $fn_name() { + let data = $data; + let mut buffer = [0u8; 128]; + let (ok, write_len) = data.dump(&mut buffer); + assert!(ok, "Dump not ok"); + assert_eq!(write_len, $expected_len, "Wrong amount written"); + assert_eq!(&buffer[..write_len], $expected_dump); + } + } + } + + test_impl!{string_dump_test, "test".to_string(), 8, &[4, 0, 0, 0, 116, 101, 115, 116]} + + test_impl!{ + vec_dump_test, + vec![ + "".to_string(), + "test1".to_string(), + "test2".to_string() + ], + 26, + &[3, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 49, 5, 0, 0, 0, 116, 101, 115, 116, 50] + } + + test_impl!{bool_true_dump_test, true, 1, &[1]} + test_impl!{bool_false_dump_test, false, 1, &[0]} + + // testing macro-generated code isn't particularly useful, but do it anyway + + test_impl!{u8_dump_test, 42u8, 1, &[42]} + test_impl!{u16_dump_test, 42u16, 2, &[42, 0]} + test_impl!{u32_dump_test, 42u32, 4, &[42, 0, 0, 0]} + test_impl!{u64_dump_test, 42u64, 8, &[42, 0, 0, 0, 0, 0, 0, 0]} + test_impl!{u128_dump_test, 42u128, 16, &[42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]} + + test_impl!{i8_dump_test, 42i8, 1, &[42]} + test_impl!{i16_dump_test, 42i16, 2, &[42, 0]} + test_impl!{i32_dump_test, 42i32, 4, &[42, 0, 0, 0]} + test_impl!{i64_dump_test, 42i64, 8, &[42, 0, 0, 0, 0, 0, 0, 0]} + test_impl!{i128_dump_test, 42i128, 16, &[42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]} + + test_impl!{f32_dump_test, 42f32, 4, &[0, 0, 40, 66]} + test_impl!{f64_dump_test, 42f64, 8, &[0, 0, 0, 0, 0, 0, 69, 64]} +} diff --git a/usdpl-core/src/serdes/load_impl.rs b/usdpl-core/src/serdes/load_impl.rs new file mode 100644 index 0000000..2428b99 --- /dev/null +++ b/usdpl-core/src/serdes/load_impl.rs @@ -0,0 +1,143 @@ +use super::Loadable; + +impl Loadable for String { + fn load(buffer: &[u8]) -> (Option, usize) { + if buffer.len() < 4 { + return (None, 0); + } + let mut u32_bytes: [u8; 4] = [u8::MAX; 4]; + u32_bytes.copy_from_slice(&buffer[..4]); + let str_size = u32::from_le_bytes(u32_bytes) as usize; + (Some(Self::from_utf8_lossy(&buffer[4..str_size + 4]).into_owned()), str_size + 4) + } +} + +impl Loadable for Vec { + fn load(buffer: &[u8]) -> (Option, usize) { + if buffer.len() < 4 { + return (None, 0); + } + let mut u32_bytes: [u8; 4] = [u8::MAX; 4]; + u32_bytes.copy_from_slice(&buffer[..4]); + let count = u32::from_le_bytes(u32_bytes) as usize; + let mut cursor = 4; + let mut items = Vec::with_capacity(count); + for _ in 0..count { + let (obj, len) = T::load(&buffer[cursor..]); + cursor += len; + if let Some(obj) = obj { + items.push(obj); + } else { + return (None, cursor); + } + } + (Some(items), cursor) + } +} + +impl Loadable for bool { + fn load(buffer: &[u8]) -> (Option, usize) { + if buffer.len() < 1 { + return (None, 0); + } + (Some(buffer[0] != 0), 1) + } +} + +impl Loadable for u8 { + fn load(buffer: &[u8]) -> (Option, usize) { + if buffer.len() < 1 { + return (None, 0); + } + (Some(buffer[0]), 1) + } +} + +impl Loadable for i8 { + fn load(buffer: &[u8]) -> (Option, usize) { + if buffer.len() < 1 { + return (None, 0); + } + (Some(i8::from_le_bytes([buffer[0]])), 1) + } +} + +macro_rules! int_impl { + ($type:ty, $size:literal) => { + impl Loadable for $type { + fn load(buffer: &[u8]) -> (Option, usize) { + if buffer.len() < $size { + return (None, 0); + } + let mut bytes: [u8; $size] = [u8::MAX; $size]; + bytes.copy_from_slice(&buffer[..$size]); + let i = <$type>::from_le_bytes(bytes); + (Some(i), $size) + } + } + } +} + +int_impl!{u16, 2} +int_impl!{u32, 4} +int_impl!{u64, 8} +int_impl!{u128, 16} + +int_impl!{i16, 2} +int_impl!{i32, 4} +int_impl!{i64, 8} +int_impl!{i128, 16} + +int_impl!{f32, 4} +int_impl!{f64, 8} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_impl { + ($fn_name:ident, $data:expr, $type:ty, $expected_len:literal, $expected_load:expr) => { + #[test] + fn $fn_name() { + let buffer = $data; + let (obj, read_len) = <$type>::load(&buffer); + assert!(obj.is_some(), "Load not ok"); + assert_eq!(read_len, $expected_len, "Wrong amount read"); + assert_eq!(obj.unwrap(), $expected_load, "Loaded value not as expected"); + } + } + } + + test_impl!{string_load_test, [4u8, 0, 0, 0, 116, 101, 115, 116, 0, 128], String, 8, "test"} + test_impl!{ + vec_load_test, + [3u8, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 49, 5, 0, 0, 0, 116, 101, 115, 116, 50], + Vec, + 26, + vec![ + "".to_string(), + "test1".to_string(), + "test2".to_string() + ] + } + + test_impl!{bool_true_load_test, [1], bool, 1, true} + test_impl!{bool_false_load_test, [0], bool, 1, false} + + // testing macro-generated code isn't particularly useful, but do it anyway + + test_impl!{u8_load_test, [42], u8, 1, 42u8} + test_impl!{u16_load_test, [42, 0], u16, 2, 42u16} + test_impl!{u32_load_test, [42, 0, 0, 0], u32, 4, 42u32} + test_impl!{u64_load_test, [42, 0, 0, 0, 0, 0, 0, 0], u64, 8, 42u64} + test_impl!{u128_load_test, [42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], u128, 16, 42u128} + + test_impl!{i8_load_test, [42], i8, 1, 42i8} + test_impl!{i16_load_test, [42, 0], i16, 2, 42i16} + test_impl!{i32_load_test, [42, 0, 0, 0], i32, 4, 42i32} + test_impl!{i64_load_test, [42, 0, 0, 0, 0, 0, 0, 0], i64, 8, 42i64} + test_impl!{i128_load_test, [42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], i128, 16, 42i128} + + test_impl!{f32_load_test, [0, 0, 40, 66], f32, 4, 42f32} + test_impl!{f64_load_test, [0, 0, 0, 0, 0, 0, 69, 64], f64, 8, 42f64} +} diff --git a/usdpl-core/src/serdes/mod.rs b/usdpl-core/src/serdes/mod.rs new file mode 100644 index 0000000..8490da0 --- /dev/null +++ b/usdpl-core/src/serdes/mod.rs @@ -0,0 +1,10 @@ +//! Serialization and deserialization functionality. +//! Little endian is preferred. + +mod dump_impl; +mod load_impl; +mod primitive; +mod traits; + +pub use traits::{Dumpable, Loadable}; +pub use primitive::Primitive; diff --git a/usdpl-core/src/serdes/primitive.rs b/usdpl-core/src/serdes/primitive.rs new file mode 100644 index 0000000..4ccbdca --- /dev/null +++ b/usdpl-core/src/serdes/primitive.rs @@ -0,0 +1,156 @@ +use super::{Loadable, Dumpable}; + +pub enum Primitive { + Empty, + String(String), + F32(f32), + F64(f64), + U32(u32), + U64(u64), + I32(i32), + I64(i64), + Bool(bool), + Json(String), +} + +impl Primitive { + const fn discriminant(&self) -> u8 { + match self { + Self::Empty => 1, + Self::String(_) => 2, + Self::F32(_)=> 3, + Self::F64(_)=> 4, + Self::U32(_)=> 5, + Self::U64(_)=> 6, + Self::I32(_)=> 7, + Self::I64(_)=> 8, + Self::Bool(_) => 9, + Self::Json(_) => 10, + } + } +} + +impl Loadable for Primitive { + fn load(buf: &[u8]) -> (Option, usize) { + if buf.len() == 0 { + return (None, 1); + } + let mut result: (Option, usize) = match buf[0] { + //0 => (None, 0), + 1 => (Some(Self::Empty), 0), + 2 => { + let (obj, len) = String::load(&buf[1..]); + (obj.map(Self::String), len) + }, + 3 => { + let (obj, len) = f32::load(&buf[1..]); + (obj.map(Self::F32), len) + }, + 4 => { + let (obj, len) = f64::load(&buf[1..]); + (obj.map(Self::F64), len) + }, + 5 => { + let (obj, len) = u32::load(&buf[1..]); + (obj.map(Self::U32), len) + }, + 6 => { + let (obj, len) = u64::load(&buf[1..]); + (obj.map(Self::U64), len) + }, + 7 => { + let (obj, len) = i32::load(&buf[1..]); + (obj.map(Self::I32), len) + }, + 8 => { + let (obj, len) = i64::load(&buf[1..]); + (obj.map(Self::I64), len) + }, + 9 => { + let (obj, len) = bool::load(&buf[1..]); + (obj.map(Self::Bool), len) + }, + 10 => { + let (obj, len) = String::load(&buf[1..]); + (obj.map(Self::Json), len) + } + _ => (None, 0) + }; + result.1 += 1; + result + } +} + + +impl Dumpable for Primitive { + fn dump(&self, buf: &mut [u8]) -> (bool, usize) { + if buf.len() == 0 { + return (false, 0); + } + buf[0] = self.discriminant(); + let mut result = match self { + Self::Empty => (true, 0), + Self::String(s) => s.dump(&mut buf[1..]), + Self::F32(x)=> x.dump(&mut buf[1..]), + Self::F64(x)=> x.dump(&mut buf[1..]), + Self::U32(x)=> x.dump(&mut buf[1..]), + Self::U64(x)=> x.dump(&mut buf[1..]), + Self::I32(x)=> x.dump(&mut buf[1..]), + Self::I64(x)=> x.dump(&mut buf[1..]), + Self::Bool(x)=> x.dump(&mut buf[1..]), + Self::Json(x) => x.dump(&mut buf[1..]), + }; + result.1 += 1; + result + } +} + +impl std::convert::Into for String { + fn into(self) -> Primitive { + Primitive::String(self) + } +} + +impl std::convert::Into for () { + fn into(self) -> Primitive { + Primitive::Empty + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn string_idempotence_test() { + let data = "Test"; + let primitive = Primitive::String(data.to_string()); + let mut buffer = [0u8; 128]; + let (ok, write_len) = primitive.dump(&mut buffer); + assert!(ok, "Dump not ok"); + let (obj, read_len) = Primitive::load(&buffer); + assert_eq!(write_len, read_len, "Amount written and amount read do not match"); + assert!(obj.is_some(), "Load not ok"); + if let Some(Primitive::String(result)) = obj { + assert_eq!(data, result, "Data written and read does not match"); + } else { + panic!("Read non-string primitive"); + } + } + + #[test] + fn empty_idempotence_test() { + let primitive = Primitive::Empty; + let mut buffer = [0u8; 128]; + let (ok, write_len) = primitive.dump(&mut buffer); + assert!(ok, "Dump not ok"); + let (obj, read_len) = Primitive::load(&buffer); + assert_eq!(write_len, read_len, "Amount written and amount read do not match"); + assert!(obj.is_some(), "Load not ok"); + if let Some(Primitive::Empty) = obj { + //assert_eq!(data, result, "Data written and read does not match"); + } else { + panic!("Read non-string primitive"); + } + } +} diff --git a/usdpl-core/src/serdes/traits.rs b/usdpl-core/src/serdes/traits.rs new file mode 100644 index 0000000..2df0901 --- /dev/null +++ b/usdpl-core/src/serdes/traits.rs @@ -0,0 +1,13 @@ +/// Load an object from the buffer +pub trait Loadable: Sized { + /// Read the buffer, building the object and returning the amount of bytes read. + /// If anything is wrong with the buffer, None should be returned. + fn load(buffer: &[u8]) -> (Option, usize); +} + +/// Dump an object into the buffer +pub trait Dumpable { + /// Write the object to the buffer, returning the amount of bytes written. + /// If anything is wrong, false should be returned. + fn dump(&self, buffer: &mut [u8]) -> (bool, usize); +} diff --git a/usdpl-core/src/socket.rs b/usdpl-core/src/socket.rs new file mode 100644 index 0000000..d163440 --- /dev/null +++ b/usdpl-core/src/socket.rs @@ -0,0 +1,91 @@ +use std::net::{SocketAddrV4, SocketAddr, Ipv4Addr}; + +use crate::serdes::{Loadable, Dumpable}; +use crate::{RemoteCall, RemoteCallResponse}; + +pub const PORT: u16 = 31337; +pub const HTTP_PORT: u16 = 31338; +pub const HOST_STR: &str = "127.0.0.1"; +pub const HOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); +pub const SOCKET_ADDR_STR: &str = "127.0.0.1:31337"; +//pub const SOCKET_ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(HOST, PORT)); + +pub const PACKET_BUFFER_SIZE: usize = 1024; + +pub fn socket_addr() -> SocketAddr { + SocketAddr::V4(SocketAddrV4::new(HOST, PORT)) +} + +pub enum Packet { + Call(RemoteCall), + CallResponse(RemoteCallResponse), + KeepAlive, + Invalid, + Message(String), + Unsupported, + Bad, +} + +impl Packet { + const fn discriminant(&self) -> u8 { + match self { + Self::Call(_) => 1, + Self::CallResponse(_) => 2, + Self::KeepAlive => 3, + Self::Invalid => 4, + Self::Message(_) => 5, + Self::Unsupported => 6, + Self::Bad => 7, + } + } +} + +impl Loadable for Packet { + fn load(buf: &[u8]) -> (Option, usize) { + if buf.len() == 0 { + return (None, 1); + } + let mut result: (Option, usize) = match buf[0] { + //0 => (None, 0), + 1 => { + let (obj, len) = RemoteCall::load(&buf[1..]); + (obj.map(Self::Call), len) + }, + 2 => { + let (obj, len) = RemoteCallResponse::load(&buf[1..]); + (obj.map(Self::CallResponse), len) + }, + 3 => (Some(Self::KeepAlive), 0), + 4 => (Some(Self::Invalid), 0), + 5 => { + let (obj, len) = String::load(&buf[1..]); + (obj.map(Self::Message), len) + }, + 6 => (Some(Self::Unsupported), 0), + 7 => (None, 0), + _ => (None, 0) + }; + result.1 += 1; + result + } +} + +impl Dumpable for Packet { + fn dump(&self, buf: &mut [u8]) -> (bool, usize) { + if buf.len() == 0 { + return (false, 0); + } + buf[0] = self.discriminant(); + let mut result = match self { + Self::Call(c) => c.dump(&mut buf[1..]), + Self::CallResponse(c) => c.dump(&mut buf[1..]), + Self::KeepAlive => (true, 0), + Self::Invalid => (true, 0), + Self::Message(s) => s.dump(&mut buf[1..]), + Self::Unsupported => (true, 0), + Self::Bad => (false, 0), + }; + result.1 += 1; + result + } +} diff --git a/usdpl-front/Cargo.toml b/usdpl-front/Cargo.toml new file mode 100644 index 0000000..0e7a396 --- /dev/null +++ b/usdpl-front/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "usdpl" +description = "WASM front-end library for USDPL" +version = "0.1.0" +authors = ["NGnius (Graham) "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.63" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.5", optional = true } + +web-sys = { version = "0.3", features = ["TcpSocket"] } +js-sys = { version = "0.3" } + +usdpl-core = { version = "0.1.0", path = "../usdpl-core" } + +[dev-dependencies] +wasm-bindgen-test = "0.3.13" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/usdpl-front/build.sh b/usdpl-front/build.sh new file mode 100755 index 0000000..a435b30 --- /dev/null +++ b/usdpl-front/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +wasm-pack build --target web diff --git a/usdpl-front/src/connection.rs b/usdpl-front/src/connection.rs new file mode 100644 index 0000000..7ea4c31 --- /dev/null +++ b/usdpl-front/src/connection.rs @@ -0,0 +1,52 @@ +use std::net::TcpStream; +use std::io::{Read, Write}; + +use web_sys::TcpSocket; +use js_sys::{ArrayBuffer, DataView}; + +use usdpl_core::socket; +use usdpl_core::serdes::{Dumpable, Loadable}; + +#[allow(dead_code)] +pub(crate) fn send(packet: socket::Packet) -> bool { + let socket = match TcpSocket::new(socket::HOST_STR, socket::PORT) { + Ok(s) => s, + Err(_) => return false, + }; + let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; + let (ok, len) = packet.dump(&mut buffer); + if !ok { + return false; + } + // copy to JS buffer + let array_buffer = ArrayBuffer::new(len as u32); + let dataview = DataView::new(&array_buffer, 0, len); + for i in 0..len { + dataview.set_uint8(i, buffer[i]); + } + match socket.send_with_array_buffer(&array_buffer) { + Ok(b) => b, + Err(_) => false + } +} + +pub(crate) fn send_native(packet: socket::Packet) -> Option { + let mut socket = match TcpStream::connect(socket::socket_addr()) { + Ok(s) => s, + Err(_) => return None, + }; + let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; + let (ok, len) = packet.dump(&mut buffer); + if !ok { + return None; + } + match socket.write(&buffer[..len]) { + Ok(_) => {}, + Err(_) => return None + } + let len = match socket.read(&mut buffer) { + Ok(len) => len, + Err(_) => return None + }; + socket::Packet::load(&buffer[..len]).0 +} diff --git a/usdpl-front/src/lib.rs b/usdpl-front/src/lib.rs new file mode 100644 index 0000000..a69dddb --- /dev/null +++ b/usdpl-front/src/lib.rs @@ -0,0 +1,85 @@ +//! Front-end library to be called from Javascript. +//! Targets WASM. +//! +//! In true Javascript tradition, this part of the library does not support error handling. +//! + +mod connection; + +use wasm_bindgen::prelude::*; +use js_sys::JSON::{stringify, parse}; + +use usdpl_core::{socket::Packet, RemoteCall, serdes::Primitive}; +const REMOTE_CALL_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern { + //fn alert(s: &str); +} + +/// Initialize the front-end library +#[wasm_bindgen] +pub fn init() -> bool { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + true +} + +/// Get the targeted plugin framework, or "any" if unknown +#[wasm_bindgen] +pub fn target() -> String { + "any".to_string() +} + +/// Call a function on the back-end. +/// Returns null (None) if this fails for any reason. +#[wasm_bindgen] +pub fn call_backend(name: String, parameters: Vec) -> Option> { + let next_id = REMOTE_CALL_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let mut params = Vec::with_capacity(parameters.len()); + for val in parameters { + if let Some(b) = val.as_bool() { + params.push(Primitive::Bool(b)); + } else if let Some(f) = val.as_f64() { + params.push(Primitive::F64(f)); + } else if let Some(s) = val.as_string() { + params.push(Primitive::String(s)); + } else if val.is_null() || val.is_undefined() { + params.push(Primitive::Empty); + } else if let Ok(s) = stringify(&val) { + params.push(Primitive::Json(s.as_string().unwrap())); + } else { + return None; + } + } + let results = match connection::send_native(Packet::Call(RemoteCall { + id: next_id, + function: name, + parameters: params, + })) { + Some(Packet::CallResponse(resp)) => resp, + _ => return None, + }; + let mut js_results = Vec::with_capacity(results.response.len()); + for val in results.response { + let js_val = match val { + Primitive::Empty => JsValue::null(), + Primitive::String(s) => JsValue::from_str(&s), + Primitive::F32(f)=> JsValue::from_f64(f as _), + Primitive::F64(f)=> JsValue::from_f64(f), + Primitive::U32(f)=> JsValue::from_f64(f as _), + Primitive::U64(f)=> JsValue::from_f64(f as _), + Primitive::I32(f)=> JsValue::from_f64(f as _), + Primitive::I64(f)=> JsValue::from_f64(f as _), + Primitive::Bool(b) => JsValue::from_bool(b), + Primitive::Json(s) => parse(&s).ok().unwrap_or(JsValue::from_str(&s)), + }; + js_results.push(js_val); + } + Some(js_results) +}