Initial functionality

This commit is contained in:
NGnius (Graham) 2022-06-07 20:05:04 -04:00
commit 0219b4c2b7
21 changed files with 1361 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

270
Cargo.lock generated Normal file
View file

@ -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"

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "usdpl-rs"
version = "0.1.0"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
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"
]

15
README.md Normal file
View file

@ -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

16
src/main.rs Normal file
View file

@ -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!");
}

9
usdpl-back/Cargo.toml Normal file
View file

@ -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" }

194
usdpl-back/src/instance.rs Normal file
View file

@ -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<String, &'a mut dyn FnMut(Vec<Primitive>) -> Vec<Primitive>>,
}
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<F: (FnMut(Vec<Primitive>) -> Vec<Primitive>) + 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<const ERROR: bool>(&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::<true>()
});
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::<true>()
.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::<true>()
});
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");
}
}
}

9
usdpl-back/src/lib.rs Normal file
View file

@ -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;

8
usdpl-core/Cargo.toml Normal file
View file

@ -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]

8
usdpl-core/src/lib.rs Normal file
View file

@ -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};

View file

@ -0,0 +1,84 @@
use crate::serdes::{Primitive, Loadable, Dumpable};
pub struct RemoteCall {
pub id: u64,
pub function: String,
pub parameters: Vec<Primitive>,
}
impl Loadable for RemoteCall {
fn load(buffer: &[u8]) -> (Option<Self>, 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::<Primitive>::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<Primitive>,
}
impl Loadable for RemoteCallResponse {
fn load(buffer: &[u8]) -> (Option<Self>, usize) {
let (id_num, len0) = u64::load(buffer);
if id_num.is_none() {
return (None, len0);
}
let (response_var, len1) = Vec::<Primitive>::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)
}
}

View file

@ -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<T: Dumpable> Dumpable for Vec<T> {
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]}
}

View file

@ -0,0 +1,143 @@
use super::Loadable;
impl Loadable for String {
fn load(buffer: &[u8]) -> (Option<Self>, 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<T: Loadable> Loadable for Vec<T> {
fn load(buffer: &[u8]) -> (Option<Self>, 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<Self>, usize) {
if buffer.len() < 1 {
return (None, 0);
}
(Some(buffer[0] != 0), 1)
}
}
impl Loadable for u8 {
fn load(buffer: &[u8]) -> (Option<Self>, usize) {
if buffer.len() < 1 {
return (None, 0);
}
(Some(buffer[0]), 1)
}
}
impl Loadable for i8 {
fn load(buffer: &[u8]) -> (Option<Self>, 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<Self>, 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<String>,
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}
}

View file

@ -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;

View file

@ -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<Self>, usize) {
if buf.len() == 0 {
return (None, 1);
}
let mut result: (Option<Self>, 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<Primitive> for String {
fn into(self) -> Primitive {
Primitive::String(self)
}
}
impl std::convert::Into<Primitive> 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");
}
}
}

View file

@ -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<Self>, 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);
}

91
usdpl-core/src/socket.rs Normal file
View file

@ -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<Self>, usize) {
if buf.len() == 0 {
return (None, 1);
}
let mut result: (Option<Self>, 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
}
}

40
usdpl-front/Cargo.toml Normal file
View file

@ -0,0 +1,40 @@
[package]
name = "usdpl"
description = "WASM front-end library for USDPL"
version = "0.1.0"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
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"

3
usdpl-front/build.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
wasm-pack build --target web

View file

@ -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<socket::Packet> {
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
}

85
usdpl-front/src/lib.rs Normal file
View file

@ -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<JsValue>) -> Option<Vec<JsValue>> {
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)
}