diff --git a/Cargo.toml b/Cargo.toml index 32988bb..f7964f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,16 @@ name = "usdpl-rs" version = "0.1.0" authors = ["NGnius (Graham) "] edition = "2021" +license = "GPL-3.0-only" +repository = "https://github.com/NGnius/usdpl-rs" +readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" + [workspace] members = [ "usdpl-core", diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..63cf029 --- /dev/null +++ b/build.sh @@ -0,0 +1,45 @@ +#!/bin/bash +if [ -n "$1" ]; then + if [ "$1" == "--help" ]; then + echo "Usage: +$0 [decky|crankshaft|]" + exit 0 + elif [ "$1" == "decky" ]; then + echo "Building back & front for decky framework" + # usdpl-back + cd ./usdpl-back + ./build.sh decky + # usdpl-front + cd ../usdpl-front + ./build.sh decky + cd .. + echo "Built usdpl back & front for decky" + elif [ "$1" == "crankshaft" ]; then + echo "WARNING: crankshaft is unimplemented" + echo "Building back & front for crankshaft framework" + # usdpl-back + cd ./usdpl-back + ./build.sh crankshaft + # usdpl-front + cd ../usdpl-front + ./build.sh crankshaft + cd .. + echo "Built usdpl back & front for crankshaft" + else + echo "Unsupported plugin framework \`$1\`" + exit 1 + fi +else + echo "WARNING: Building for any plugin framework, which may not work for every framework" + echo "Building back & front for any framework" + # usdpl-back + echo "...Running usdpl-back build..." + cd ./usdpl-back + cargo build --release + # usdpl-front + echo "...Running usdpl-front build..." + cd ../usdpl-front + ./build.sh crankshaft + cd .. + echo "Built usdpl back & front for any" +fi diff --git a/usdpl-back/Cargo.toml b/usdpl-back/Cargo.toml index 5d8e307..0380033 100644 --- a/usdpl-back/Cargo.toml +++ b/usdpl-back/Cargo.toml @@ -2,8 +2,14 @@ name = "usdpl-back" version = "0.1.0" edition = "2021" +license = "GPL-3.0-only" +repository = "https://github.com/NGnius/usdpl-rs" +readme = "../README.md" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +decky = [] +crankshaft = [] [dependencies] usdpl-core = { version = "0.1.0", path = "../usdpl-core" } diff --git a/usdpl-back/build.sh b/usdpl-back/build.sh new file mode 100755 index 0000000..fa8af3b --- /dev/null +++ b/usdpl-back/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +if [ -n "$1" ]; then + if [ "$1" == "--help" ]; then + echo "Usage: +$0 [decky|crankshaft|]" + exit 0 + elif [ "$1" == "decky" ]; then + echo "Building back-end module for decky framework" + cargo build --release --features decky + elif [ "$1" == "crankshaft" ]; then + echo "WARNING: crankshaft support is unimplemented" + cargo build --release --features crankshaft + else + echo "Unsupported plugin framework \`$1\`" + exit 1 + fi +else + echo "WARNING: Building for any plugin framework, which may not work for every framework" + cargo build --release +fi diff --git a/usdpl-back/src/instance.rs b/usdpl-back/src/instance.rs index 7c6982e..50b298d 100644 --- a/usdpl-back/src/instance.rs +++ b/usdpl-back/src/instance.rs @@ -1,4 +1,4 @@ -use std::net::TcpListener; +use std::net::{TcpListener, TcpStream}; use std::collections::HashMap; use std::io::{Read, Write}; @@ -8,13 +8,15 @@ use usdpl_core::{RemoteCallResponse, socket}; /// Instance for interacting with the front-end pub struct Instance<'a> { calls: HashMap) -> Vec>, + port: u16, } impl<'a> Instance<'a> { #[inline] - pub fn new() -> Self { + pub fn new(port_usdpl: u16) -> Self { Instance { calls: HashMap::new(), + port: port_usdpl, } } @@ -24,54 +26,79 @@ impl<'a> Instance<'a> { self } - /// Receive and execute callbacks forever + fn handle_packet(&mut self, packet: socket::Packet, buffer: &mut [u8], incoming: &mut TcpStream) -> std::io::Result<()> { + 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(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()?); + } + + } + }, + socket::Packet::Many(many) => { + for packet in many { + if let socket::Packet::Many(_) = packet { + // drop nested socket packets (prevents DoS and bad practices) + if ERROR { + return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Invalid nested Many packet received from {}", incoming.peer_addr()?))); + } else { + eprintln!("Invalid nested Many packet received from {}", incoming.peer_addr()?); + } + continue; + } + self.handle_packet::(packet, buffer, incoming)?; + } + }, + _ => { + let (ok, len) = socket::Packet::Unsupported.dump(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(); + } + } + } + Ok(()) + } + pub fn serve(&mut self) -> std::io::Result<()> { - let listener = TcpListener::bind(socket::socket_addr())?; + let result = self.serve_internal::(); + //println!("Stopping server due to serve_internal returning a result"); + result + } + + /// Receive and execute callbacks forever + pub fn serve_internal(&mut self) -> std::io::Result<()> { + let listener = TcpListener::bind(socket::socket_addr(self.port))?; 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(); - } - } - } + self.handle_packet::(packet, &mut buffer, &mut incoming)?; } else { if ERROR { return Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Invalid packet received from {}", incoming.peer_addr()?))); @@ -90,10 +117,12 @@ mod tests { use std::net::TcpStream; use super::*; + const PORT: u16 = 31337; + #[test] fn serve_full_test() -> std::io::Result<()> { let _server = std::thread::spawn(|| { - Instance::new() + Instance::new(PORT, PORT + 80) .register("echo".to_string(), &mut |params| params) .register("hello".to_string(), &mut |params| { if let Some(Primitive::String(name)) = params.get(0) { @@ -105,7 +134,7 @@ mod tests { .serve::() }); std::thread::sleep(std::time::Duration::from_millis(10)); - let mut front = TcpStream::connect(socket::socket_addr()).unwrap(); + let mut front = TcpStream::connect(socket::socket_addr(PORT)).unwrap(); let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; let call = socket::Packet::Call(usdpl_core::RemoteCall { id: 42, @@ -140,13 +169,13 @@ mod tests { 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 front = TcpStream::connect(socket::socket_addr(PORT+1)).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() + Instance::new(PORT+1, PORT+1+80) .register("echo".to_string(), &mut |params| params) .register("hello".to_string(), &mut |params| { if let Some(Primitive::String(name)) = params.get(0) { @@ -163,7 +192,7 @@ mod tests { #[should_panic] fn serve_unsupported_test() { let _server = std::thread::spawn(|| { - Instance::new() + Instance::new(PORT+2, PORT+2+80) .register("echo".to_string(), &mut |params| params) .register("hello".to_string(), &mut |params| { if let Some(Primitive::String(name)) = params.get(0) { @@ -175,7 +204,7 @@ mod tests { .serve::() }); std::thread::sleep(std::time::Duration::from_millis(10)); - let mut front = TcpStream::connect(socket::socket_addr()).unwrap(); + let mut front = TcpStream::connect(socket::socket_addr(PORT+2)).unwrap(); let mut buffer = [0u8; socket::PACKET_BUFFER_SIZE]; let (ok, len) = socket::Packet::Unsupported.dump(&mut buffer); assert!(ok, "Packet dump failed"); diff --git a/usdpl-back/src/lib.rs b/usdpl-back/src/lib.rs index 989ac21..8a4fbfd 100644 --- a/usdpl-back/src/lib.rs +++ b/usdpl-back/src/lib.rs @@ -7,3 +7,7 @@ mod instance; pub use instance::Instance; + +pub mod core { + pub use usdpl_core::*; +} diff --git a/usdpl-core/Cargo.toml b/usdpl-core/Cargo.toml index f5be185..890b0aa 100644 --- a/usdpl-core/Cargo.toml +++ b/usdpl-core/Cargo.toml @@ -2,6 +2,9 @@ name = "usdpl-core" version = "0.1.0" edition = "2021" +license = "GPL-3.0-only" +repository = "https://github.com/NGnius/usdpl-rs" +readme = "../README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/usdpl-core/src/socket.rs b/usdpl-core/src/socket.rs index d163440..7acd7f2 100644 --- a/usdpl-core/src/socket.rs +++ b/usdpl-core/src/socket.rs @@ -3,17 +3,14 @@ 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)) +#[inline] +pub fn socket_addr(port: u16) -> SocketAddr { + SocketAddr::V4(SocketAddrV4::new(HOST, port)) } pub enum Packet { @@ -24,6 +21,7 @@ pub enum Packet { Message(String), Unsupported, Bad, + Many(Vec), } impl Packet { @@ -36,6 +34,7 @@ impl Packet { Self::Message(_) => 5, Self::Unsupported => 6, Self::Bad => 7, + Self::Many(_) => 8, } } } @@ -63,6 +62,10 @@ impl Loadable for Packet { }, 6 => (Some(Self::Unsupported), 0), 7 => (None, 0), + 8 => { + let (obj, len) = <_>::load(&buf[1..]); + (obj.map(Self::Many), len) + } _ => (None, 0) }; result.1 += 1; @@ -84,6 +87,7 @@ impl Dumpable for Packet { Self::Message(s) => s.dump(&mut buf[1..]), Self::Unsupported => (true, 0), Self::Bad => (false, 0), + Self::Many(v) => v.dump(&mut buf[1..]), }; result.1 += 1; result diff --git a/usdpl-front/Cargo.toml b/usdpl-front/Cargo.toml index 0e7a396..56f28ae 100644 --- a/usdpl-front/Cargo.toml +++ b/usdpl-front/Cargo.toml @@ -4,12 +4,17 @@ description = "WASM front-end library for USDPL" version = "0.1.0" authors = ["NGnius (Graham) "] edition = "2021" +license = "GPL-3.0-only" +repository = "https://github.com/NGnius/usdpl-rs" +readme = "../README.md" [lib] crate-type = ["cdylib", "rlib"] [features] default = ["console_error_panic_hook"] +decky = [] +crankshaft = [] [dependencies] wasm-bindgen = "0.2.63" @@ -35,6 +40,6 @@ usdpl-core = { version = "0.1.0", path = "../usdpl-core" } [dev-dependencies] wasm-bindgen-test = "0.3.13" -[profile.release] +#[profile.release] # Tell `rustc` to optimize for small code size. -opt-level = "s" +#opt-level = "s" diff --git a/usdpl-front/build.sh b/usdpl-front/build.sh index a435b30..ba35fa9 100755 --- a/usdpl-front/build.sh +++ b/usdpl-front/build.sh @@ -1,3 +1,22 @@ #!/bin/bash +if [ -n "$1" ]; then + if [ "$1" == "--help" ]; then + echo "Usage: +$0 [decky|crankshaft|]" + exit 0 + elif [ "$1" == "decky" ]; then + echo "Building WASM module for decky framework" + wasm-pack build --target web --features decky + elif [ "$1" == "crankshaft" ]; then + echo "WARNING: crankshaft support is unimplemented" + wasm-pack build --target web --features crankshaft + else + echo "Unsupported plugin framework \`$1\`" + exit 1 + fi +else + echo "WARNING: Building for any plugin framework, which may not work for every framework" + wasm-pack build --target web +fi -wasm-pack build --target web +python3 ./scripts/generate_embedded_wasm.py diff --git a/usdpl-front/scripts/generate_embedded_wasm.py b/usdpl-front/scripts/generate_embedded_wasm.py new file mode 100644 index 0000000..a6699df --- /dev/null +++ b/usdpl-front/scripts/generate_embedded_wasm.py @@ -0,0 +1,37 @@ +import base64 + +if __name__ == "__main__": + print("Embedding WASM into udspl.js") + # assumption: current working directory (relative to this script) is ../ + # assumption: release wasm binary at ./pkg/usdpl_bg.wasm + with open("./pkg/usdpl_bg.wasm", mode="rb") as infile: + with open("./pkg/usdpl.js", mode="ab") as outfile: + outfile.write("\n\n// USDPL customization\nconst encoded = \"".encode()) + encoded = base64.b64encode(infile.read()) + outfile.write(encoded) + outfile.write("\";\n\n".encode()) + outfile.write( +"""function asciiToBinary(str) { + if (typeof atob === 'function') { + return atob(str) + } else { + return new Buffer(str, 'base64').toString('binary'); + } +} + +function decode() { + var binaryString = asciiToBinary(encoded); + var bytes = new Uint8Array(binaryString.length); + for (var i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return (async function() {return new Response(bytes.buffer);})(); +} + +export function init_embedded() { + return init(decode()) +} +""".encode()) + with open("./pkg/usdpl.d.ts", "a") as outfile: + outfile.write("\n\n// USDPL customization\nexport function init_embedded();\n") + print("Done: Embedded WASM into udspl.js") diff --git a/usdpl-front/src/connection.rs b/usdpl-front/src/connection.rs index 7ea4c31..4d02940 100644 --- a/usdpl-front/src/connection.rs +++ b/usdpl-front/src/connection.rs @@ -8,8 +8,8 @@ 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) { +pub(crate) fn send(packet: socket::Packet, port: u16) -> bool { + let socket = match TcpSocket::new(socket::HOST_STR, port) { Ok(s) => s, Err(_) => return false, }; @@ -30,8 +30,8 @@ pub(crate) fn send(packet: socket::Packet) -> bool { } } -pub(crate) fn send_native(packet: socket::Packet) -> Option { - let mut socket = match TcpStream::connect(socket::socket_addr()) { +pub(crate) fn send_native(packet: socket::Packet, port: u16) -> Option { + let mut socket = match TcpStream::connect(socket::socket_addr(port)) { Ok(s) => s, Err(_) => return None, }; diff --git a/usdpl-front/src/convert.rs b/usdpl-front/src/convert.rs new file mode 100644 index 0000000..847e976 --- /dev/null +++ b/usdpl-front/src/convert.rs @@ -0,0 +1,35 @@ +use wasm_bindgen::prelude::JsValue; +use js_sys::JSON::{stringify, parse}; + +use usdpl_core::serdes::Primitive; + +pub(crate) fn primitive_to_js(primitive: Primitive) -> JsValue { + match primitive { + 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)), + } +} + +pub(crate) fn js_to_primitive(val: JsValue) -> Primitive { + if let Some(b) = val.as_bool() { + Primitive::Bool(b) + } else if let Some(f) = val.as_f64() { + Primitive::F64(f) + } else if let Some(s) = val.as_string() { + Primitive::String(s) + } else if val.is_null() || val.is_undefined() { + Primitive::Empty + } else if let Ok(s) = stringify(&val) { + Primitive::Json(s.as_string().unwrap()) + } else { + Primitive::Empty + } +} diff --git a/usdpl-front/src/lib.rs b/usdpl-front/src/lib.rs index a69dddb..1cf3e8a 100644 --- a/usdpl-front/src/lib.rs +++ b/usdpl-front/src/lib.rs @@ -5,12 +5,13 @@ //! mod connection; +mod convert; use wasm_bindgen::prelude::*; -use js_sys::JSON::{stringify, parse}; -use usdpl_core::{socket::Packet, RemoteCall, serdes::Primitive}; +use usdpl_core::{socket::Packet, RemoteCall}; const REMOTE_CALL_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); +const REMOTE_PORT: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(31337); // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global allocator. #[cfg(feature = "wee_alloc")] @@ -24,16 +25,22 @@ extern { /// Initialize the front-end library #[wasm_bindgen] -pub fn init() -> bool { +pub fn init_usdpl(port: u16) -> bool { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); + REMOTE_PORT.store(port, std::sync::atomic::Ordering::Relaxed); true } /// Get the targeted plugin framework, or "any" if unknown #[wasm_bindgen] pub fn target() -> String { - "any".to_string() + #[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))] + {"decky".to_string()} + #[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))] + {"crankshaft".to_string()} + #[cfg(not(any(feature = "decky", feature = "crankshaft")))] + {"any".to_string()} } /// Call a function on the back-end. @@ -43,42 +50,19 @@ pub fn call_backend(name: String, parameters: Vec) -> Option 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)), - }; + let js_val = convert::primitive_to_js(val); js_results.push(js_val); } Some(js_results)