Macros for frontend nRPC service generation

This commit is contained in:
NGnius (Graham) 2023-04-16 22:57:12 -04:00
parent 79a8e7e128
commit 570c194e82
25 changed files with 1612 additions and 262 deletions

896
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,14 @@
[package] [workspace]
name = "usdpl" members = [
version = "0.10.0" "usdpl-core",
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] "usdpl-front",
edition = "2021" "usdpl-back",
license = "GPL-3.0-only" "usdpl-build",
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 exclude = [
"templates/decky/backend"
]
[profile.release] [profile.release]
# Tell `rustc` to optimize for small code size. # Tell `rustc` to optimize for small code size.
@ -16,14 +17,3 @@ debug = false
strip = true strip = true
lto = true lto = true
codegen-units = 4 codegen-units = 4
[workspace]
members = [
"usdpl-core",
"usdpl-front",
"usdpl-back",
]
exclude = [
"templates/decky/backend"
]

View file

@ -1,25 +1,26 @@
[package] [package]
name = "usdpl-back" name = "usdpl-back"
version = "0.10.1" version = "0.11.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs" repository = "https://github.com/NGnius/usdpl-rs"
readme = "README.md" readme = "../README.md"
description = "Universal Steam Deck Plugin Library back-end" description = "Universal Steam Deck Plugin Library back-end"
[features] [features]
default = ["blocking", "translate"] default = ["blocking"]
decky = ["usdpl-core/decky"] decky = ["usdpl-core/decky"]
crankshaft = ["usdpl-core/crankshaft"]
blocking = ["tokio", "tokio/rt", "tokio/rt-multi-thread"] # synchronous API for async functionality, using tokio blocking = ["tokio", "tokio/rt", "tokio/rt-multi-thread"] # synchronous API for async functionality, using tokio
encrypt = ["usdpl-core/encrypt", "obfstr", "hex"] encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
translate = ["usdpl-core/translate", "gettext-ng"]
[dependencies] [dependencies]
usdpl-core = { version = "0.10", path = "../usdpl-core"} usdpl-core = { version = "0.11", path = "../usdpl-core"}
log = "0.4" log = "0.4"
# gRPC/protobuf
nrpc = "0.2"
# HTTP web framework # HTTP web framework
warp = { version = "0.3" } warp = { version = "0.3" }
bytes = { version = "1.1" } bytes = { version = "1.1" }
@ -34,4 +35,7 @@ obfstr = { version = "0.3", optional = true }
hex = { version = "0.4", optional = true } hex = { version = "0.4", optional = true }
# translations # translations
gettext-ng = { version = "0.4.1", optional = true } gettext-ng = { version = "0.4.1" }
[build-dependencies]
usdpl-build = { version = "0.11", path = "../usdpl-build" }

3
usdpl-back/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
usdpl_build::back::build()
}

View file

@ -6,8 +6,6 @@ use std::path::PathBuf;
pub fn home() -> Option<PathBuf> { pub fn home() -> Option<PathBuf> {
#[cfg(not(any(feature = "decky", feature = "crankshaft")))] #[cfg(not(any(feature = "decky", feature = "crankshaft")))]
let result = crate::api_any::dirs::home(); let result = crate::api_any::dirs::home();
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
let result = None; // TODO
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))] #[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
let result = crate::api_decky::home().ok() let result = crate::api_decky::home().ok()
.map(|x| PathBuf::from(x) .map(|x| PathBuf::from(x)
@ -23,8 +21,6 @@ pub fn home() -> Option<PathBuf> {
pub fn plugin() -> Option<PathBuf> { pub fn plugin() -> Option<PathBuf> {
#[cfg(not(any(feature = "decky", feature = "crankshaft")))] #[cfg(not(any(feature = "decky", feature = "crankshaft")))]
let result = None; // TODO let result = None; // TODO
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
let result = None; // TODO
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))] #[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
let result = crate::api_decky::plugin_dir().ok().map(|x| x.into()); let result = crate::api_decky::plugin_dir().ok().map(|x| x.into());
@ -35,8 +31,6 @@ pub fn plugin() -> Option<PathBuf> {
pub fn log() -> Option<PathBuf> { pub fn log() -> Option<PathBuf> {
#[cfg(not(any(feature = "decky", feature = "crankshaft")))] #[cfg(not(any(feature = "decky", feature = "crankshaft")))]
let result = crate::api_any::dirs::log(); let result = crate::api_any::dirs::log();
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
let result = None; // TODO
#[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))] #[cfg(all(feature = "decky", not(any(feature = "crankshaft"))))]
let result = crate::api_decky::log_dir().ok().map(|x| x.into()); let result = crate::api_decky::log_dir().ok().map(|x| x.into());

View file

@ -1 +0,0 @@
compile_error!("Crankshaft unsupported (project no longer maintained)");

View file

18
usdpl-build/Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "usdpl-build"
version = "0.11.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nrpc-build = "0.5"
prost-build = "0.11"
prost-types = "0.11"
# code gen
prettyplease = "0.2"
quote = "1.0"
syn = "2.0"
proc-macro2 = "1.0"

View file

@ -0,0 +1,25 @@
syntax = "proto3";
package usdpl;
// The translation service
service DevTools {
// Retrieves all translations for the provided 4-letter code
rpc Log (LogMessage) returns (Empty);
}
enum LogLevel {
Trace = 0;
Debug = 1;
Info = 2;
Warn = 3;
Error = 4;
}
// The request message containing the log message
message LogMessage {
LogLevel level = 1;
string msg = 2;
}
message Empty {}

View file

@ -0,0 +1,19 @@
syntax = "proto3";
package usdpl;
// The translation service
service Translations {
// Retrieves all translations for the provided 4-letter code
rpc GetLanguage (LanguageRequest) returns (TranslationsReply) {}
}
// The request message containing the language code
message LanguageRequest {
string lang = 1;
}
// The response message containing all translations for the language
message TranslationsReply {
map<string, string> translations = 1;
}

View file

@ -0,0 +1,7 @@
pub fn build() {
crate::dump_protos_out().unwrap();
nrpc_build::compile_servers(
crate::all_proto_filenames().map(|n| crate::proto_out_path().clone().join(n)),
[crate::proto_out_path()]
)
}

View file

@ -0,0 +1,22 @@
mod preprocessor;
pub use preprocessor::WasmProtoPreprocessor;
mod service_generator;
pub use service_generator::WasmServiceGenerator;
mod shared_state;
pub(crate) use shared_state::SharedState;
pub fn build() {
let shared_state = SharedState::new();
crate::dump_protos_out().unwrap();
nrpc_build::Transpiler::new(
crate::all_proto_filenames().map(|n| crate::proto_out_path().clone().join(n)),
[crate::proto_out_path()]
).unwrap()
.generate_client()
.with_preprocessor(nrpc_build::AbstractImpl::outer(WasmProtoPreprocessor::with_state(&shared_state)))
.with_service_generator(nrpc_build::AbstractImpl::outer(WasmServiceGenerator::with_state(&shared_state)))
.transpile()
.unwrap()
}

View file

@ -0,0 +1,27 @@
use nrpc_build::IPreprocessor;
//use prost_build::{Service, ServiceGenerator};
use prost_types::FileDescriptorSet;
use super::SharedState;
pub struct WasmProtoPreprocessor {
shared: SharedState,
}
impl WasmProtoPreprocessor {
pub fn with_state(state: &SharedState) -> Self {
Self {
shared: state.clone(),
}
}
}
impl IPreprocessor for WasmProtoPreprocessor {
fn process(&mut self, fds: &mut FileDescriptorSet) -> proc_macro2::TokenStream {
self.shared.lock()
.expect("Cannot lock shared state")
.fds = Some(fds.clone());
quote::quote!{}
}
}

View file

@ -0,0 +1,592 @@
use std::collections::HashSet;
use prost_build::Service;
use prost_types::{FileDescriptorSet, DescriptorProto, EnumDescriptorProto, FieldDescriptorProto};
use nrpc_build::IServiceGenerator;
use super::SharedState;
pub struct WasmServiceGenerator {
shared: SharedState,
}
impl WasmServiceGenerator {
pub fn with_state(state: &SharedState) -> Self {
Self {
shared: state.clone(),
}
}
}
fn generate_service_methods(service: &Service, fds: &FileDescriptorSet) -> proc_macro2::TokenStream {
let mut gen_methods = Vec::with_capacity(service.methods.len());
for method in &service.methods {
let method_name = quote::format_ident!("{}", method.name);
let method_input = quote::format_ident!("{}{}", &service.name, method.input_type);
let method_output = quote::format_ident!("{}{}", &service.name, method.output_type);
let input_type = find_message_type(&method.input_type, &service.package, fds).expect("Protobuf message is used but not found");
let mut input_params = Vec::with_capacity(input_type.field.len());
let mut params_to_fields = Vec::with_capacity(input_type.field.len());
for field in &input_type.field {
//let param_name = quote::format_ident!("val{}", i.to_string());
let type_name = translate_type(field, &service.name);
let field_name = quote::format_ident!("{}", field.name.as_ref().expect("Protobuf message field needs a name"));
input_params.push(quote::quote!{
#field_name: #type_name,
});
params_to_fields.push(quote::quote!{
#field_name,//: #field_name,
});
}
let params_to_fields_transformer = if input_type.field.len() == 1 {
let field_name = quote::format_ident!("{}", input_type.field[0].name.as_ref().expect("Protobuf message field needs a name"));
quote::quote!{
let val = #field_name;
}
} else if input_type.field.is_empty() {
quote::quote!{
let val = #method_input {};
}
} else {
quote::quote!{
let val = #method_input {
#(#params_to_fields)*
};
}
};
let special_fn_into_input = quote::format_ident!("{}_convert_into", method.input_type.split('.').last().unwrap().to_lowercase());
let special_fn_from_output = quote::format_ident!("{}_convert_from", method.output_type.split('.').last().unwrap().to_lowercase());
gen_methods.push(
quote::quote!{
#[wasm_bindgen]
pub async fn #method_name(&mut self, #(#input_params)*) -> Option<#method_output> {
#params_to_fields_transformer
match self.service.#method_name(#special_fn_into_input(val)).await {
Ok(x) => Some(#special_fn_from_output(x)),
Err(_e) => {
// TODO log error
None
}
}
}
}
);
}
quote::quote!{
#(#gen_methods)*
}
}
fn find_message_type<'a>(want_type: &str, want_package: &str, fds: &'a FileDescriptorSet) -> Option<&'a DescriptorProto> {
for file in &fds.file {
for message_type in &file.message_type {
if let Some(name) = &message_type.name {
if let Some(pkg) = &file.package {
if name == want_type && pkg == want_package {
return Some(message_type);
}
}
}
}
}
None
}
fn find_enum_type<'a>(want_type: &str, want_package: &str, fds: &'a FileDescriptorSet) -> Option<&'a EnumDescriptorProto> {
for file in &fds.file {
for enum_type in &file.enum_type {
if let Some(name) = &enum_type.name {
if let Some(pkg) = &file.package {
if name == want_type && pkg == want_package {
return Some(enum_type);
}
}
}
}
}
None
}
fn find_field<'a>(want_field: &str, descriptor: &'a DescriptorProto) -> Option<&'a FieldDescriptorProto> {
for field in &descriptor.field {
if let Some(name) = &field.name {
if name == want_field {
return Some(field);
}
}
}
None
}
fn translate_type(field: &FieldDescriptorProto, service: &str) -> proc_macro2::TokenStream {
if let Some(type_name) = &field.type_name {
translate_type_name(type_name, service)
} else {
let number = field.r#type.unwrap();
translate_type_known(number)
}
}
fn generate_wasm_struct_interop(descriptor: &DescriptorProto, handled_enums: &mut HashSet<String>, handled_types: &mut HashSet<String>, is_response_msg: bool, service: &str) -> proc_macro2::TokenStream {
let msg_name = quote::format_ident!("{}{}", service, descriptor.name.as_ref().expect("Protobuf message needs a name"));
let super_msg_name = quote::format_ident!("{}", descriptor.name.as_ref().expect("Protobuf message needs a name"));
let mut gen_fields = Vec::with_capacity(descriptor.field.len());
let mut gen_into_fields = Vec::with_capacity(descriptor.field.len());
let mut gen_from_fields = Vec::with_capacity(descriptor.field.len());
let mut gen_nested_types = Vec::with_capacity(descriptor.nested_type.len());
let mut gen_enums = Vec::with_capacity(descriptor.enum_type.len());
if let Some(options) = &descriptor.options {
if let Some(map_entry) = options.map_entry {
// TODO deal with options when necessary
if map_entry {
let name = descriptor.name.clone().expect("Protobuf message needs a name");
let special_fn_from = quote::format_ident!("{}_convert_from", name.split('.').last().unwrap().to_lowercase());
let special_fn_into = quote::format_ident!("{}_convert_into", name.split('.').last().unwrap().to_lowercase());
let key_field = find_field("key", descriptor).expect("Protobuf map entry has no key field");
let key_type = translate_type(&key_field, service);
let value_field = find_field("value", descriptor).expect("Protobuf map entry has no value field");
let value_type = translate_type(&value_field, service);
return quote::quote!{
pub type #msg_name = ::js_sys::Map;
#[inline]
#[allow(dead_code)]
fn #special_fn_from(other: ::std::collections::HashMap<#key_type, #value_type>) -> #msg_name {
let map = #msg_name::new();
for (key, val) in other.iter() {
map.set(&key.into(), &val.into());
}
map
}
#[inline]
#[allow(dead_code)]
fn #special_fn_into(this: #msg_name) -> ::std::collections::HashMap<#key_type, #value_type> {
let mut output = ::std::collections::HashMap::<#key_type, #value_type>::new();
this.for_each(&mut |key: ::wasm_bindgen::JsValue, val: ::wasm_bindgen::JsValue| {
if let Some(key) = key.as_string() {
if let Some(val) = val.as_string() {
output.insert(key, val);
}
}
});
output
}
}
}
} else {
todo!("Deal with message options when necessary");
}
}
for n_type in &descriptor.nested_type {
let type_name = n_type.name.clone().expect("Protobuf nested message needs a name");
if !handled_types.contains(&type_name) {
handled_types.insert(type_name);
gen_nested_types.push(generate_wasm_struct_interop(n_type, handled_enums, handled_types, is_response_msg, service));
}
}
for e_type in &descriptor.enum_type {
let type_name = e_type.name.clone().expect("Protobuf enum needs a name");
if !handled_enums.contains(&type_name) {
handled_enums.insert(type_name);
gen_enums.push(generate_wasm_enum_interop(e_type, service));
}
}
if descriptor.field.len() == 1 {
let field = &descriptor.field[0];
let field_name = quote::format_ident!("{}", field.name.as_ref().expect("Protobuf message field needs a name"));
let type_name = translate_type(field, service);
gen_fields.push(quote::quote!{
pub #field_name: #type_name,
});
if let Some(name) = &field.type_name {
let special_fn_from = quote::format_ident!("{}_convert_from", name.split('.').last().unwrap().to_lowercase());
let special_fn_into = quote::format_ident!("{}_convert_into", name.split('.').last().unwrap().to_lowercase());
gen_into_fields.push(
quote::quote!{
#field_name: #special_fn_into(this)
}
);
gen_from_fields.push(
quote::quote!{
#special_fn_from(other.#field_name)
}
);
} else {
gen_into_fields.push(
quote::quote!{
#field_name: this
}
);
gen_from_fields.push(
quote::quote!{
other.#field_name
}
);
}
let name = descriptor.name.clone().expect("Protobuf message needs a name");
let special_fn_from = quote::format_ident!("{}_convert_from", name.split('.').last().unwrap().to_lowercase());
let special_fn_into = quote::format_ident!("{}_convert_into", name.split('.').last().unwrap().to_lowercase());
quote::quote!{
pub type #msg_name = #type_name;
#[inline]
#[allow(dead_code)]
fn #special_fn_from(other: super::#super_msg_name) -> #msg_name {
#(#gen_from_fields)*
}
#[inline]
#[allow(dead_code)]
fn #special_fn_into(this: #msg_name) -> super::#super_msg_name {
super::#super_msg_name {
#(#gen_into_fields)*
}
}
#(#gen_nested_types)*
#(#gen_enums)*
}
} else {
for field in &descriptor.field {
let field_name = quote::format_ident!("{}", field.name.as_ref().expect("Protobuf message field needs a name"));
let type_name = translate_type(field, service);
gen_fields.push(quote::quote!{
pub #field_name: #type_name,
});
if let Some(name) = &field.type_name {
let special_fn_from = quote::format_ident!("{}_convert_from", name.split('.').last().unwrap().to_lowercase());
let special_fn_into = quote::format_ident!("{}_convert_into", name.split('.').last().unwrap().to_lowercase());
gen_into_fields.push(
quote::quote!{
#field_name: #special_fn_into(self.#field_name),
}
);
gen_from_fields.push(
quote::quote!{
#field_name: #special_fn_from(other.#field_name),
}
);
} else {
gen_into_fields.push(
quote::quote!{
#field_name: self.#field_name,
}
);
gen_from_fields.push(
quote::quote!{
#field_name: other.#field_name,
}
);
}
}
let name = descriptor.name.clone().expect("Protobuf message needs a name");
let special_fn_from = quote::format_ident!("{}_convert_from", name.split('.').last().unwrap().to_lowercase());
let special_fn_into = quote::format_ident!("{}_convert_into", name.split('.').last().unwrap().to_lowercase());
let wasm_attribute_maybe = if descriptor.field.len() == 1 || !is_response_msg {
quote::quote!{}
} else {
quote::quote!{
#[wasm_bindgen]
}
};
quote::quote!{
#wasm_attribute_maybe
pub struct #msg_name {
#(#gen_fields)*
}
impl std::convert::Into<super::#super_msg_name> for #msg_name {
#[inline]
fn into(self) -> super::#super_msg_name {
super::#super_msg_name {
#(#gen_into_fields)*
}
}
}
impl std::convert::From<super::#super_msg_name> for #msg_name {
#[inline]
#[allow(unused_variables)]
fn from(other: super::#super_msg_name) -> Self {
#msg_name {
#(#gen_from_fields)*
}
}
}
#[inline]
#[allow(dead_code)]
fn #special_fn_from(other: super::#super_msg_name) -> #msg_name {
#msg_name::from(other)
}
#[inline]
#[allow(dead_code)]
fn #special_fn_into(this: #msg_name) -> super::#super_msg_name {
this.into()
}
#(#gen_nested_types)*
#(#gen_enums)*
}
}
}
fn translate_type_name(name: &str, service: &str) -> proc_macro2::TokenStream {
match name {
"double" => quote::quote!{f64},
"float" => quote::quote!{f32},
"int32" => quote::quote!{i32},
"int64" => quote::quote!{i64},
"uint32" => quote::quote!{u32},
"uint64" => quote::quote!{u64},
"sint32" => quote::quote!{i32},
"sint64" => quote::quote!{i64},
"fixed32" => quote::quote!{u32},
"fixed64" => quote::quote!{u64},
"sfixed32" => quote::quote!{i32},
"sfixed64" => quote::quote!{i64},
"bool" => quote::quote!{bool},
"string" => quote::quote!{String},
"bytes" => quote::quote!{Vec<u8>},
t => {
let ident = quote::format_ident!("{}{}", service, t.split('.').last().unwrap());
quote::quote!{#ident}
},
}
}
fn translate_type_known(id: i32) -> proc_macro2::TokenStream {
match id {
//"double" => quote::quote!{f64},
//"float" => quote::quote!{f32},
//"int32" => quote::quote!{i32},
//"int64" => quote::quote!{i64},
//"uint32" => quote::quote!{u32},
//"uint64" => quote::quote!{u64},
//"sint32" => quote::quote!{i32},
//"sint64" => quote::quote!{i64},
//"fixed32" => quote::quote!{u32},
//"fixed64" => quote::quote!{u64},
//"sfixed32" => quote::quote!{i32},
//"sfixed64" => quote::quote!{i64},
//"bool" => quote::quote!{bool},
9 => quote::quote!{String},
//"bytes" => quote::quote!{Vec<u8>},
t => {
let ident = quote::format_ident!("UnknownType{}", t.to_string());
quote::quote!{#ident}
},
}
}
fn generate_wasm_enum_interop(descriptor: &EnumDescriptorProto, service: &str) -> proc_macro2::TokenStream {
let enum_name = quote::format_ident!("{}{}", service, descriptor.name.as_ref().expect("Protobuf enum needs a name"));
let super_enum_name = quote::format_ident!("{}", descriptor.name.as_ref().expect("Protobuf enum needs a name"));
let mut gen_values = Vec::with_capacity(descriptor.value.len());
let mut gen_into_values = Vec::with_capacity(descriptor.value.len());
let mut gen_from_values = Vec::with_capacity(descriptor.value.len());
if let Some(_options) = &descriptor.options {
// TODO deal with options when necessary
todo!("Deal with enum options when necessary");
}
for value in &descriptor.value {
let val_name = quote::format_ident!("{}", value.name.as_ref().expect("Protobuf enum value needs a name"));
if let Some(_val_options) = &value.options {
// TODO deal with options when necessary
todo!("Deal with enum value options when necessary");
} else {
if let Some(number) = &value.number {
gen_values.push(
quote::quote!{
#val_name = #number,
}
);
} else {
gen_values.push(
quote::quote!{
#val_name,
}
);
}
gen_into_values.push(
quote::quote!{
Self::#val_name => super::#super_enum_name::#val_name,
}
);
gen_from_values.push(
quote::quote!{
super::#super_enum_name::#val_name => Self::#val_name,
}
);
}
}
let name = descriptor.name.clone().expect("Protobuf message needs a name");
let special_fn_from = quote::format_ident!("{}_convert_from", name.split('.').last().unwrap().to_lowercase());
let special_fn_into = quote::format_ident!("{}_convert_into", name.split('.').last().unwrap().to_lowercase());
quote::quote!{
#[wasm_bindgen]
#[repr(i32)]
#[derive(Clone, Copy)]
pub enum #enum_name {
#(#gen_values)*
}
impl std::convert::Into<super::#super_enum_name> for #enum_name {
fn into(self) -> super::#super_enum_name {
match self {
#(#gen_into_values)*
}
}
}
impl std::convert::From<super::#super_enum_name> for #enum_name {
fn from(other: super::#super_enum_name) -> Self {
match other {
#(#gen_from_values)*
}
}
}
#[inline]
#[allow(dead_code)]
fn #special_fn_from(other: i32) -> #enum_name {
#enum_name::from(super::#super_enum_name::from_i32(other).unwrap())
}
#[inline]
#[allow(dead_code)]
fn #special_fn_into(this: #enum_name) -> i32 {
this as i32
}
}
}
fn generate_service_io_types(service: &Service, fds: &FileDescriptorSet) -> proc_macro2::TokenStream {
let mut gen_types = Vec::with_capacity(service.methods.len() * 2);
let mut gen_enums = Vec::new();
let mut handled_enums = HashSet::new();
let mut handled_types = HashSet::new();
for method in &service.methods {
if let Some(input_message) = find_message_type(&method.input_type, &service.package, fds) {
let msg_name = input_message.name.clone().expect("Protobuf message name required");
if !handled_types.contains(&msg_name) {
handled_types.insert(msg_name);
gen_types.push(generate_wasm_struct_interop(input_message, &mut handled_enums, &mut handled_types, false, &service.name));
}
} else if let Some(input_enum) = find_enum_type(&method.input_type, &service.package, fds) {
let enum_name = input_enum.name.clone().expect("Protobuf enum name required");
if !handled_enums.contains(&enum_name) {
handled_enums.insert(enum_name);
gen_types.push(generate_wasm_enum_interop(input_enum, &service.name));
}
} else {
panic!("Cannot find proto type {}/{}", service.package, method.input_type);
}
if let Some(output_message) = find_message_type(&method.output_type, &service.package, fds) {
let msg_name = output_message.name.clone().expect("Protobuf message name required");
if !handled_types.contains(&msg_name) {
handled_types.insert(msg_name);
gen_types.push(generate_wasm_struct_interop(output_message, &mut handled_enums, &mut handled_types, true, &service.name));
}
} else if let Some(output_enum) = find_enum_type(&method.output_type, &service.package, fds) {
let enum_name = output_enum.name.clone().expect("Protobuf enum name required");
if !handled_enums.contains(&enum_name) {
handled_enums.insert(enum_name);
gen_types.push(generate_wasm_enum_interop(output_enum, &service.name));
}
} else {
panic!("Cannot find proto type {}/{}", service.package, method.input_type);
}
}
// always generate all enums, since they aren't encountered (ever, afaik) when generating message structs
for file in &fds.file {
for enum_type in &file.enum_type {
let enum_name = enum_type.name.clone().expect("Protobuf enum name required");
if !handled_enums.contains(&enum_name) {
handled_enums.insert(enum_name);
gen_enums.push(generate_wasm_enum_interop(enum_type, &service.name));
}
}
}
quote::quote! {
#(#gen_types)*
#(#gen_enums)*
}
}
impl IServiceGenerator for WasmServiceGenerator {
fn generate(&mut self, service: Service) -> proc_macro2::TokenStream {
let lock = self.shared.lock()
.expect("Cannot lock shared state");
let fds = lock.fds
.as_ref()
.expect("FileDescriptorSet required for WASM service generator");
let service_struct_name = quote::format_ident!("{}Client", service.name);
let service_js_name = quote::format_ident!("{}", service.name);
let service_methods = generate_service_methods(&service, fds);
let service_types = generate_service_io_types(&service, fds);
let mod_name = quote::format_ident!("js_{}", service.name.to_lowercase());
quote::quote!{
mod #mod_name {
use wasm_bindgen::prelude::*;
use crate::client_handler::WebSocketHandler;
#service_types
/// WASM/JS-compatible wrapper of the Rust nRPC service
#[wasm_bindgen]
pub struct #service_js_name {
//#[wasm_bindgen(skip)]
service: super::#service_struct_name<WebSocketHandler>,
}
#[wasm_bindgen]
impl #service_js_name {
#[wasm_bindgen(constructor)]
pub fn new(port: u16) -> Self {
let implementation = super::#service_struct_name::new(
WebSocketHandler::new(port)
);
Self {
service: implementation,
}
}
#service_methods
}
}
}
}
}

View file

@ -0,0 +1,26 @@
use std::sync::{Arc, Mutex};
use prost_types::FileDescriptorSet;
#[derive(Clone)]
pub struct SharedState(Arc<Mutex<SharedProtoData>>);
impl SharedState {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(SharedProtoData {
fds: None,
})))
}
}
impl std::ops::Deref for SharedState {
type Target = Arc<Mutex<SharedProtoData>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct SharedProtoData {
pub fds: Option<FileDescriptorSet>,
}

5
usdpl-build/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod back;
pub mod front;
mod proto_files;
pub use proto_files::{dump_protos, dump_protos_out, proto_out_path, all_proto_filenames};

View file

@ -0,0 +1,44 @@
use std::path::{Path, PathBuf};
struct IncludedFileStr<'a> {
filename: &'a str,
contents: &'a str,
}
const DEBUG_PROTO: IncludedFileStr<'static> = IncludedFileStr {
filename: "debug.proto",
contents: include_str!("../protos/debug.proto"),
};
const TRANSLATIONS_PROTO: IncludedFileStr<'static> = IncludedFileStr {
filename: "translations.proto",
contents: include_str!("../protos/translations.proto"),
};
const ALL_PROTOS: [IncludedFileStr<'static>; 2] = [
DEBUG_PROTO,
TRANSLATIONS_PROTO,
];
pub fn proto_out_path() -> PathBuf {
PathBuf::from(std::env::var("OUT_DIR").expect("Not in a build.rs context (missing $OUT_DIR)")).join("protos")
}
pub fn all_proto_filenames() -> impl Iterator<Item = &'static str> {
ALL_PROTOS.iter().map(|x| x.filename)
}
pub fn dump_protos(p: impl AsRef<Path>) -> std::io::Result<()> {
let p = p.as_ref();
for f in ALL_PROTOS {
let fullpath = p.join(f.filename);
std::fs::write(fullpath, f.contents)?;
}
Ok(())
}
pub fn dump_protos_out() -> std::io::Result<()> {
let path = proto_out_path();
std::fs::create_dir_all(&path)?;
dump_protos(&path)
}

View file

@ -1,22 +1,21 @@
[package] [package]
name = "usdpl-core" name = "usdpl-core"
version = "0.10.0" version = "0.11.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs" repository = "https://github.com/NGnius/usdpl-rs"
readme = "README.md" readme = "../README.md"
description = "Universal Steam Deck Plugin Library core" description = "Universal Steam Deck Plugin Library core"
[features] [features]
default = [] default = []
decky = [] decky = []
crankshaft = []
encrypt = ["aes-gcm-siv"] encrypt = ["aes-gcm-siv"]
translate = []
[dependencies] [dependencies]
base64 = "0.13" base64 = "0.13"
aes-gcm-siv = { version = "0.10", optional = true, default-features = false, features = ["alloc", "aes"] } aes-gcm-siv = { version = "0.10", optional = true, default-features = false, features = ["alloc", "aes"] }
# nrpc = "0.2"
[dev-dependencies] [dev-dependencies]
hex-literal = "0.3.4" hex-literal = "0.3.4"

View file

@ -4,8 +4,6 @@ pub enum Platform {
Any, Any,
/// Decky aka PluginLoader platform /// Decky aka PluginLoader platform
Decky, Decky,
/// Crankshaft platform
Crankshaft,
} }
impl Platform { impl Platform {
@ -16,10 +14,6 @@ impl Platform {
{ {
Self::Decky Self::Decky
} }
#[cfg(all(feature = "crankshaft", not(any(feature = "decky"))))]
{
Self::Crankshaft
}
#[cfg(not(any(feature = "decky", feature = "crankshaft")))] #[cfg(not(any(feature = "decky", feature = "crankshaft")))]
{ {
Self::Any Self::Any
@ -32,7 +26,6 @@ impl std::fmt::Display for Platform {
match self { match self {
Self::Any => write!(f, "any"), Self::Any => write!(f, "any"),
Self::Decky => write!(f, "decky"), Self::Decky => write!(f, "decky"),
Self::Crankshaft => write!(f, "crankshaft"),
} }
} }
} }

View file

@ -1 +0,0 @@

View file

@ -40,10 +40,8 @@ pub enum Packet {
/// Many packets merged into one /// Many packets merged into one
Many(Vec<Packet>), Many(Vec<Packet>),
/// Translation data dump /// Translation data dump
#[cfg(feature = "translate")]
Translations(Vec<(String, Vec<String>)>), Translations(Vec<(String, Vec<String>)>),
/// Request translations for language /// Request translations for language
#[cfg(feature = "translate")]
Language(String), Language(String),
} }
@ -59,9 +57,7 @@ impl Packet {
Self::Unsupported => 6, Self::Unsupported => 6,
Self::Bad => 7, Self::Bad => 7,
Self::Many(_) => 8, Self::Many(_) => 8,
#[cfg(feature = "translate")]
Self::Translations(_) => 9, Self::Translations(_) => 9,
#[cfg(feature = "translate")]
Self::Language(_) => 10, Self::Language(_) => 10,
} }
} }
@ -93,12 +89,10 @@ impl Loadable for Packet {
let (obj, len) = <_>::load(buf)?; let (obj, len) = <_>::load(buf)?;
(Self::Many(obj), len) (Self::Many(obj), len)
}, },
#[cfg(feature = "translate")]
9 => { 9 => {
let (obj, len) = <_>::load(buf)?; let (obj, len) = <_>::load(buf)?;
(Self::Translations(obj), len) (Self::Translations(obj), len)
}, },
#[cfg(feature = "translate")]
10 => { 10 => {
let (obj, len) = <_>::load(buf)?; let (obj, len) = <_>::load(buf)?;
(Self::Language(obj), len) (Self::Language(obj), len)
@ -122,9 +116,7 @@ impl Dumpable for Packet {
Self::Unsupported => Ok(0), Self::Unsupported => Ok(0),
Self::Bad => return Err(DumpError::Unsupported), Self::Bad => return Err(DumpError::Unsupported),
Self::Many(v) => v.dump(buf), Self::Many(v) => v.dump(buf),
#[cfg(feature = "translate")]
Self::Translations(tr) => tr.dump(buf), Self::Translations(tr) => tr.dump(buf),
#[cfg(feature = "translate")]
Self::Language(l) => l.dump(buf), Self::Language(l) => l.dump(buf),
}?; }?;
Ok(size1 + result) Ok(size1 + result)

View file

@ -1,27 +1,27 @@
[package] [package]
name = "usdpl-front" name = "usdpl-front"
version = "0.10.1" version = "0.11.0"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
edition = "2021" edition = "2021"
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/NGnius/usdpl-rs" repository = "https://github.com/NGnius/usdpl-rs"
readme = "README.md" readme = "../README.md"
description = "Universal Steam Deck Plugin Library front-end designed for WASM" description = "Universal Steam Deck Plugin Library front-end designed for WASM"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [features]
default = ["translate"] default = []
decky = ["usdpl-core/decky"] decky = ["usdpl-core/decky"]
crankshaft = ["usdpl-core/crankshaft"]
debug = ["console_error_panic_hook"] debug = ["console_error_panic_hook"]
encrypt = ["usdpl-core/encrypt", "obfstr", "hex"] encrypt = ["usdpl-core/encrypt", "obfstr", "hex"]
translate = ["usdpl-core/translate"]
[dependencies] [dependencies]
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
gloo-net = { version = "0.2", features = ["websocket"] }
futures = "0.3"
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires # logging them with `console.error`. This is great for development, but requires
@ -39,10 +39,17 @@ web-sys = { version = "0.3", features = [
]} ]}
js-sys = { version = "0.3" } js-sys = { version = "0.3" }
async-channel = "1.8"
obfstr = { version = "0.3", optional = true } obfstr = { version = "0.3", optional = true }
hex = { version = "0.4", optional = true } hex = { version = "0.4", optional = true }
usdpl-core = { version = "0.10", path = "../usdpl-core" } nrpc = "0.2"
usdpl-core = { version = "0.11", path = "../usdpl-core" }
prost = "0.11"
[dev-dependencies] [dev-dependencies]
wasm-bindgen-test = { version = "0.3.13" } wasm-bindgen-test = { version = "0.3.13" }
[build-dependencies]
usdpl-build = { version = "0.11", path = "../usdpl-build" }

3
usdpl-front/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
usdpl_build::front::build()
}

View file

@ -0,0 +1,81 @@
use std::sync::atomic::{AtomicU64, Ordering};
use nrpc::{ClientHandler, ServiceError, _helpers::bytes, _helpers::async_trait};
use gloo_net::websocket::{Message, futures::WebSocket};
use wasm_bindgen_futures::spawn_local;
use futures::{SinkExt, StreamExt};
static LAST_ID: AtomicU64 = AtomicU64::new(0);
pub struct WebSocketHandler {
// TODO
port: u16,
}
async fn send_recv_ws(url: String, input: bytes::Bytes) -> Result<Vec<u8>, String> {
let mut ws = WebSocket::open(&url).map_err(|e| e.to_string())?;
ws.send(Message::Bytes(input.into())).await.map_err(|e| e.to_string())?;
read_next_incoming(ws).await
}
async fn read_next_incoming(mut ws: WebSocket) -> Result<Vec<u8>, String> {
if let Some(msg) = ws.next().await {
match msg.map_err(|e| e.to_string())? {
Message::Bytes(b) => Ok(b),
Message::Text(_) => Err("Message::Text not allowed".into()),
}
} else {
Err("No response received".into())
}
}
#[derive(Debug)]
struct ErrorStr(String);
impl std::fmt::Display for ErrorStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error message: {}", self.0)
}
}
impl std::error::Error for ErrorStr {}
impl WebSocketHandler {
#[allow(dead_code)]
pub fn new(port: u16) -> Self {
Self { port }
}
}
#[async_trait::async_trait]
impl ClientHandler for WebSocketHandler {
async fn call(&mut self,
service: &str,
method: &str,
input: bytes::Bytes,
output: &mut bytes::BytesMut) -> Result<(), ServiceError> {
let id = LAST_ID.fetch_add(1, Ordering::SeqCst);
let url = format!(
"ws://usdpl-ws-{}.localhost:{}/{}/{}",
id,
self.port,
service,
method,
);
let (tx, rx) = async_channel::bounded(1);
spawn_local(async move {
tx.send(send_recv_ws(
url,
input
).await).await.unwrap_or(());
});
output.extend_from_slice(
&rx.recv().await
.map_err(|e| ServiceError::Method(Box::new(e)))?
.map_err(|e| ServiceError::Method(Box::new(ErrorStr(e))))?
);
Ok(())
}
}

View file

@ -5,10 +5,16 @@
//! //!
#![warn(missing_docs)] #![warn(missing_docs)]
mod client_handler;
mod connection; mod connection;
mod convert; mod convert;
mod imports; mod imports;
#[allow(missing_docs)] // existence is pain otherwise
pub mod _nrpc_js_interop {
include!(concat!(env!("OUT_DIR"), "/usdpl.rs"));
}
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use js_sys::Array; use js_sys::Array;
@ -19,7 +25,7 @@ use usdpl_core::{socket::Packet, RemoteCall};
//const REMOTE_PORT: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(31337); //const REMOTE_PORT: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(31337);
static mut CTX: UsdplContext = UsdplContext { static mut CTX: UsdplContext = UsdplContext {
port: 31337, port: 0,
id: AtomicU64::new(0), id: AtomicU64::new(0),
#[cfg(feature = "encrypt")] #[cfg(feature = "encrypt")]
key: Vec::new(), key: Vec::new(),
@ -27,7 +33,6 @@ static mut CTX: UsdplContext = UsdplContext {
static mut CACHE: Option<std::collections::HashMap<String, JsValue>> = None; static mut CACHE: Option<std::collections::HashMap<String, JsValue>> = None;
#[cfg(feature = "translate")]
static mut TRANSLATIONS: Option<std::collections::HashMap<String, Vec<String>>> = None; static mut TRANSLATIONS: Option<std::collections::HashMap<String, Vec<String>>> = None;
#[cfg(feature = "encrypt")] #[cfg(feature = "encrypt")]