Improve build scripts and framework to get WASM loaded on Steam Deck

This commit is contained in:
NGnius (Graham) 2022-06-12 17:30:14 -04:00
parent eaf193a1b2
commit ccd3969185
14 changed files with 291 additions and 93 deletions

View file

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

45
build.sh Executable file
View file

@ -0,0 +1,45 @@
#!/bin/bash
if [ -n "$1" ]; then
if [ "$1" == "--help" ]; then
echo "Usage:
$0 [decky|crankshaft|<nothing>]"
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

View file

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

20
usdpl-back/build.sh Executable file
View file

@ -0,0 +1,20 @@
#!/bin/bash
if [ -n "$1" ]; then
if [ "$1" == "--help" ]; then
echo "Usage:
$0 [decky|crankshaft|<nothing>]"
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

View file

@ -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<String, &'a mut dyn FnMut(Vec<Primitive>) -> Vec<Primitive>>,
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<const ERROR: bool>(&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::<ERROR>(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<const ERROR: bool>(&mut self) -> std::io::Result<()> {
let listener = TcpListener::bind(socket::socket_addr())?;
let result = self.serve_internal::<ERROR>();
//println!("Stopping server due to serve_internal returning a result");
result
}
/// Receive and execute callbacks forever
pub fn serve_internal<const ERROR: bool>(&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::<ERROR>(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::<true>()
});
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::<true>()
});
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");

View file

@ -7,3 +7,7 @@
mod instance;
pub use instance::Instance;
pub mod core {
pub use usdpl_core::*;
}

View file

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

View file

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

View file

@ -4,12 +4,17 @@ description = "WASM front-end library for USDPL"
version = "0.1.0"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
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"

View file

@ -1,3 +1,22 @@
#!/bin/bash
if [ -n "$1" ]; then
if [ "$1" == "--help" ]; then
echo "Usage:
$0 [decky|crankshaft|<nothing>]"
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

View file

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

View file

@ -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<socket::Packet> {
let mut socket = match TcpStream::connect(socket::socket_addr()) {
pub(crate) fn send_native(packet: socket::Packet, port: u16) -> Option<socket::Packet> {
let mut socket = match TcpStream::connect(socket::socket_addr(port)) {
Ok(s) => s,
Err(_) => return None,
};

View file

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

View file

@ -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<JsValue>) -> Option<Vec<JsValu
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;
}
params.push(convert::js_to_primitive(val));
}
let results = match connection::send_native(Packet::Call(RemoteCall {
id: next_id,
function: name,
parameters: params,
})) {
}), REMOTE_PORT.load(std::sync::atomic::Ordering::Relaxed)) {
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)),
};
let js_val = convert::primitive_to_js(val);
js_results.push(js_val);
}
Some(js_results)