diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1821e5a..b8660b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,22 @@ jobs: toolchain: ${{ matrix.rust }} - name: Run cargo check - run: cargo check --features="rpc grpc-server" + run: cargo check --features="rpc grpc-service" + + build-wasm: + name: Build wasm32 + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Run cargo build (wasm) + run: cargo build --target wasm32-unknown-unknown --features="rpc" test: name: Test suite @@ -41,7 +56,7 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Run cargo test - run: cargo test --features="rpc grpc-server" + run: cargo test --features="rpc grpc-service" loom-dry-run: name: Loom dry run @@ -54,7 +69,7 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Dry-run cargo test (Loom) - run: cargo test --no-run --tests --features="rpc grpc-server" + run: cargo test --no-run --tests --features="rpc grpc-service" env: RUSTFLAGS: --cfg asynchronix_loom @@ -71,12 +86,12 @@ jobs: components: miri - name: Run cargo miri tests (single-threaded executor) - run: cargo miri test --tests --lib --features="rpc grpc-server" + run: cargo miri test --tests --lib --features="rpc grpc-service" env: MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-num-cpus=1 - name: Run cargo miri tests (multi-threaded executor) - run: cargo miri test --tests --lib --features="rpc grpc-server" + run: cargo miri test --tests --lib --features="rpc grpc-service" env: MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-num-cpus=4 @@ -134,7 +149,7 @@ jobs: run: cargo fmt --all -- --check - name: Run cargo clippy - run: cargo clippy --features="rpc grpc-server" + run: cargo clippy --features="rpc grpc-service" docs: name: Docs @@ -147,4 +162,4 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Run cargo doc - run: cargo doc --no-deps --features="rpc grpc-server" --document-private-items + run: cargo doc --no-deps --features="rpc grpc-service" --document-private-items diff --git a/asynchronix/Cargo.toml b/asynchronix/Cargo.toml index 981ce35..2b978dd 100644 --- a/asynchronix/Cargo.toml +++ b/asynchronix/Cargo.toml @@ -26,8 +26,10 @@ autotests = false rpc = ["dep:rmp-serde", "dep:serde", "dep:tonic", "dep:prost", "dep:prost-types", "dep:bytes"] # This feature forces protobuf/gRPC code (re-)generation. rpc-codegen = ["dep:tonic-build"] -# gRPC server. -grpc-server = ["rpc", "dep:tokio"] +# gRPC service. +grpc-service = ["rpc", "dep:tokio" , "tonic/transport"] +# wasm service. +wasm-service = ["rpc", "dep:wasm-bindgen"] # API-unstable public exports meant for external test/benchmarking; development only. dev-hooks = [] # Logging of performance-related statistics; development only. @@ -58,10 +60,12 @@ prost-types = { version = "0.12", optional = true } rmp-serde = { version = "1.1", optional = true } serde = { version = "1", optional = true } -# gRPC dependencies. +# gRPC service dependencies. tokio = { version = "1.0", features=["net"], optional = true } -tonic = { version = "0.11", optional = true } +tonic = { version = "0.11", default-features = false, features=["codegen", "prost"], optional = true } +# WASM service dependencies. +wasm-bindgen = { version = "0.2", optional = true } [target.'cfg(asynchronix_loom)'.dependencies] loom = "0.5" diff --git a/asynchronix/build.rs b/asynchronix/build.rs index fb7492c..d2bb66b 100644 --- a/asynchronix/build.rs +++ b/asynchronix/build.rs @@ -7,14 +7,11 @@ fn main() -> Result<(), Box> { .build_client(false) .out_dir("src/rpc/codegen/"); - #[cfg(all(feature = "rpc-codegen", not(feature = "grpc-server")))] + #[cfg(all(feature = "rpc-codegen", not(feature = "grpc-service")))] let builder = builder.build_server(false); #[cfg(feature = "rpc-codegen")] - builder.compile( - &["simulation.proto", "custom_transport.proto"], - &["src/rpc/api/"], - )?; + builder.compile(&["simulation.proto"], &["src/rpc/api/"])?; Ok(()) } diff --git a/asynchronix/src/rpc.rs b/asynchronix/src/rpc.rs index d67e3d0..9506bfa 100644 --- a/asynchronix/src/rpc.rs +++ b/asynchronix/src/rpc.rs @@ -2,9 +2,12 @@ mod codegen; mod endpoint_registry; -mod generic_server; -#[cfg(feature = "grpc-server")] +#[cfg(feature = "grpc-service")] pub mod grpc; mod key_registry; +mod simulation_service; +#[cfg(feature = "wasm-service")] +pub mod wasm; pub use endpoint_registry::EndpointRegistry; +pub use simulation_service::SimulationService; diff --git a/asynchronix/src/rpc/api/custom_transport.proto b/asynchronix/src/rpc/api/custom_transport.proto deleted file mode 100644 index 46aefb4..0000000 --- a/asynchronix/src/rpc/api/custom_transport.proto +++ /dev/null @@ -1,50 +0,0 @@ -// Additional types for transport implementations which, unlike gRPC, do not -// support auto-generation from the `Simulation` service description. - -syntax = "proto3"; -package custom_transport; - -import "simulation.proto"; - -enum ServerErrorCode { - UNKNOWN_REQUEST = 0; - EMPTY_REQUEST = 1; -} - -message ServerError { - ServerErrorCode code = 1; - string message = 2; -} - -message AnyRequest { - oneof request { // Expects exactly 1 variant. - simulation.InitRequest init_request = 1; - simulation.TimeRequest time_request = 2; - simulation.StepRequest step_request = 3; - simulation.StepUntilRequest step_until_request = 4; - simulation.ScheduleEventRequest schedule_event_request = 5; - simulation.CancelEventRequest cancel_event_request = 6; - simulation.ProcessEventRequest process_event_request = 7; - simulation.ProcessQueryRequest process_query_request = 8; - simulation.ReadEventsRequest read_events_request = 9; - simulation.OpenSinkRequest open_sink_request = 10; - simulation.CloseSinkRequest close_sink_request = 11; - } -} - -message AnyReply { - oneof reply { // Contains exactly 1 variant. - simulation.InitReply init_reply = 1; - simulation.TimeReply time_reply = 2; - simulation.StepReply step_reply = 3; - simulation.StepUntilReply step_until_reply = 4; - simulation.ScheduleEventReply schedule_event_reply = 5; - simulation.CancelEventReply cancel_event_reply = 6; - simulation.ProcessEventReply process_event_reply = 7; - simulation.ProcessQueryReply process_query_reply = 8; - simulation.ReadEventsReply read_events_reply = 9; - simulation.OpenSinkReply open_sink_reply = 10; - simulation.CloseSinkReply close_sink_reply = 11; - ServerError error = 100; - } -} diff --git a/asynchronix/src/rpc/api/simulation.proto b/asynchronix/src/rpc/api/simulation.proto index b12d593..8aa9f68 100644 --- a/asynchronix/src/rpc/api/simulation.proto +++ b/asynchronix/src/rpc/api/simulation.proto @@ -146,6 +146,23 @@ message CloseSinkReply { } } +// A convenience message type for custom transport implementation. +message AnyRequest { + oneof request { // Expects exactly 1 variant. + InitRequest init_request = 1; + TimeRequest time_request = 2; + StepRequest step_request = 3; + StepUntilRequest step_until_request = 4; + ScheduleEventRequest schedule_event_request = 5; + CancelEventRequest cancel_event_request = 6; + ProcessEventRequest process_event_request = 7; + ProcessQueryRequest process_query_request = 8; + ReadEventsRequest read_events_request = 9; + OpenSinkRequest open_sink_request = 10; + CloseSinkRequest close_sink_request = 11; + } +} + service Simulation { rpc Init(InitRequest) returns (InitReply); rpc Time(TimeRequest) returns (TimeReply); diff --git a/asynchronix/src/rpc/codegen.rs b/asynchronix/src/rpc/codegen.rs index 3221cbc..c98125f 100644 --- a/asynchronix/src/rpc/codegen.rs +++ b/asynchronix/src/rpc/codegen.rs @@ -1,7 +1,6 @@ #![allow(unreachable_pub)] #![allow(clippy::enum_variant_names)] +#![allow(missing_docs)] -#[rustfmt::skip] -pub(crate) mod custom_transport; #[rustfmt::skip] pub(crate) mod simulation; diff --git a/asynchronix/src/rpc/codegen/custom_transport.rs b/asynchronix/src/rpc/codegen/custom_transport.rs deleted file mode 100644 index 61eac9d..0000000 --- a/asynchronix/src/rpc/codegen/custom_transport.rs +++ /dev/null @@ -1,111 +0,0 @@ -// This file is @generated by prost-build. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ServerError { - #[prost(enumeration = "ServerErrorCode", tag = "1")] - pub code: i32, - #[prost(string, tag = "2")] - pub message: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AnyRequest { - /// Expects exactly 1 variant. - #[prost(oneof = "any_request::Request", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11")] - pub request: ::core::option::Option, -} -/// Nested message and enum types in `AnyRequest`. -pub mod any_request { - /// Expects exactly 1 variant. - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Request { - #[prost(message, tag = "1")] - InitRequest(super::super::simulation::InitRequest), - #[prost(message, tag = "2")] - TimeRequest(super::super::simulation::TimeRequest), - #[prost(message, tag = "3")] - StepRequest(super::super::simulation::StepRequest), - #[prost(message, tag = "4")] - StepUntilRequest(super::super::simulation::StepUntilRequest), - #[prost(message, tag = "5")] - ScheduleEventRequest(super::super::simulation::ScheduleEventRequest), - #[prost(message, tag = "6")] - CancelEventRequest(super::super::simulation::CancelEventRequest), - #[prost(message, tag = "7")] - ProcessEventRequest(super::super::simulation::ProcessEventRequest), - #[prost(message, tag = "8")] - ProcessQueryRequest(super::super::simulation::ProcessQueryRequest), - #[prost(message, tag = "9")] - ReadEventsRequest(super::super::simulation::ReadEventsRequest), - #[prost(message, tag = "10")] - OpenSinkRequest(super::super::simulation::OpenSinkRequest), - #[prost(message, tag = "11")] - CloseSinkRequest(super::super::simulation::CloseSinkRequest), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AnyReply { - /// Contains exactly 1 variant. - #[prost(oneof = "any_reply::Reply", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 100")] - pub reply: ::core::option::Option, -} -/// Nested message and enum types in `AnyReply`. -pub mod any_reply { - /// Contains exactly 1 variant. - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Reply { - #[prost(message, tag = "1")] - InitReply(super::super::simulation::InitReply), - #[prost(message, tag = "2")] - TimeReply(super::super::simulation::TimeReply), - #[prost(message, tag = "3")] - StepReply(super::super::simulation::StepReply), - #[prost(message, tag = "4")] - StepUntilReply(super::super::simulation::StepUntilReply), - #[prost(message, tag = "5")] - ScheduleEventReply(super::super::simulation::ScheduleEventReply), - #[prost(message, tag = "6")] - CancelEventReply(super::super::simulation::CancelEventReply), - #[prost(message, tag = "7")] - ProcessEventReply(super::super::simulation::ProcessEventReply), - #[prost(message, tag = "8")] - ProcessQueryReply(super::super::simulation::ProcessQueryReply), - #[prost(message, tag = "9")] - ReadEventsReply(super::super::simulation::ReadEventsReply), - #[prost(message, tag = "10")] - OpenSinkReply(super::super::simulation::OpenSinkReply), - #[prost(message, tag = "11")] - CloseSinkReply(super::super::simulation::CloseSinkReply), - #[prost(message, tag = "100")] - Error(super::ServerError), - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum ServerErrorCode { - UnknownRequest = 0, - EmptyRequest = 1, -} -impl ServerErrorCode { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - ServerErrorCode::UnknownRequest => "UNKNOWN_REQUEST", - ServerErrorCode::EmptyRequest => "EMPTY_REQUEST", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "UNKNOWN_REQUEST" => Some(Self::UnknownRequest), - "EMPTY_REQUEST" => Some(Self::EmptyRequest), - _ => None, - } - } -} diff --git a/asynchronix/src/rpc/codegen/simulation.rs b/asynchronix/src/rpc/codegen/simulation.rs index aefb660..672aed1 100644 --- a/asynchronix/src/rpc/codegen/simulation.rs +++ b/asynchronix/src/rpc/codegen/simulation.rs @@ -332,6 +332,44 @@ pub mod close_sink_reply { Error(super::Error), } } +/// A convenience message type for custom transport implementation. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AnyRequest { + /// Expects exactly 1 variant. + #[prost(oneof = "any_request::Request", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11")] + pub request: ::core::option::Option, +} +/// Nested message and enum types in `AnyRequest`. +pub mod any_request { + /// Expects exactly 1 variant. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Request { + #[prost(message, tag = "1")] + InitRequest(super::InitRequest), + #[prost(message, tag = "2")] + TimeRequest(super::TimeRequest), + #[prost(message, tag = "3")] + StepRequest(super::StepRequest), + #[prost(message, tag = "4")] + StepUntilRequest(super::StepUntilRequest), + #[prost(message, tag = "5")] + ScheduleEventRequest(super::ScheduleEventRequest), + #[prost(message, tag = "6")] + CancelEventRequest(super::CancelEventRequest), + #[prost(message, tag = "7")] + ProcessEventRequest(super::ProcessEventRequest), + #[prost(message, tag = "8")] + ProcessQueryRequest(super::ProcessQueryRequest), + #[prost(message, tag = "9")] + ReadEventsRequest(super::ReadEventsRequest), + #[prost(message, tag = "10")] + OpenSinkRequest(super::OpenSinkRequest), + #[prost(message, tag = "11")] + CloseSinkRequest(super::CloseSinkRequest), + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum ErrorCode { diff --git a/asynchronix/src/rpc/grpc.rs b/asynchronix/src/rpc/grpc.rs index 94809e9..02b4bf5 100644 --- a/asynchronix/src/rpc/grpc.rs +++ b/asynchronix/src/rpc/grpc.rs @@ -1,4 +1,4 @@ -//! GRPC simulation server. +//! gRPC simulation service. use std::net::SocketAddr; use std::sync::Mutex; @@ -10,12 +10,12 @@ use crate::rpc::EndpointRegistry; use crate::simulation::SimInit; use super::codegen::simulation::*; -use super::generic_server::GenericServer; +use super::simulation_service::SimulationService; -/// Runs a GRPC simulation server. +/// Runs a gRPC simulation server. /// /// The first argument is a closure that is called every time the simulation is -/// started by the remote client. It must create a new `SimInit` object +/// (re)started by the remote client. It must create a new `SimInit` object /// complemented by a registry that exposes the public event and query /// interface. pub fn run(sim_gen: F, addr: SocketAddr) -> Result<(), Box> @@ -27,7 +27,7 @@ where .enable_io() .build()?; - let sim_manager = GrpcServer::new(sim_gen); + let sim_manager = GrpcSimulationService::new(sim_gen); rt.block_on(async move { Server::builder() @@ -39,33 +39,27 @@ where }) } -struct GrpcServer -where - F: FnMut() -> (SimInit, EndpointRegistry) + Send + 'static, -{ - inner: Mutex>, +struct GrpcSimulationService { + inner: Mutex, } -impl GrpcServer -where - F: FnMut() -> (SimInit, EndpointRegistry) + Send + 'static, -{ - fn new(sim_gen: F) -> Self { +impl GrpcSimulationService { + fn new(sim_gen: F) -> Self + where + F: FnMut() -> (SimInit, EndpointRegistry) + Send + 'static, + { Self { - inner: Mutex::new(GenericServer::new(sim_gen)), + inner: Mutex::new(SimulationService::new(sim_gen)), } } - fn inner(&self) -> MutexGuard<'_, GenericServer> { + fn inner(&self) -> MutexGuard<'_, SimulationService> { self.inner.lock().unwrap() } } #[tonic::async_trait] -impl simulation_server::Simulation for GrpcServer -where - F: FnMut() -> (SimInit, EndpointRegistry) + Send + 'static, -{ +impl simulation_server::Simulation for GrpcSimulationService { async fn init(&self, request: Request) -> Result, Status> { let request = request.into_inner(); diff --git a/asynchronix/src/rpc/generic_server.rs b/asynchronix/src/rpc/simulation_service.rs similarity index 89% rename from asynchronix/src/rpc/generic_server.rs rename to asynchronix/src/rpc/simulation_service.rs index 70032c4..9c53001 100644 --- a/asynchronix/src/rpc/generic_server.rs +++ b/asynchronix/src/rpc/simulation_service.rs @@ -1,3 +1,5 @@ +use std::error; +use std::fmt; use std::time::Duration; use bytes::Buf; @@ -9,86 +11,87 @@ use crate::rpc::key_registry::{KeyRegistry, KeyRegistryId}; use crate::rpc::EndpointRegistry; use crate::simulation::{SimInit, Simulation}; -use super::codegen::custom_transport::*; use super::codegen::simulation::*; -/// Transport-independent server implementation. +/// Protobuf-based simulation manager. /// -/// This implements the protobuf services without any transport-specific -/// management. -pub(crate) struct GenericServer { - sim_gen: F, +/// A `SimulationService` enables the management of the lifecycle of a +/// simulation, including creating a +/// [`Simulation`](crate::simulation::Simulation), invoking its methods and +/// instantiating a new simulation. +/// +/// Its methods map the various RPC service methods defined in +/// `simulation.proto`. +pub struct SimulationService { + sim_gen: Box (SimInit, EndpointRegistry) + Send + 'static>, sim_context: Option<(Simulation, EndpointRegistry, KeyRegistry)>, } -impl GenericServer -where - F: FnMut() -> (SimInit, EndpointRegistry) + Send + 'static, -{ - /// Creates a new `GenericServer` without any active simulation. - pub(crate) fn new(sim_gen: F) -> Self { +impl SimulationService { + /// Creates a new `SimulationService` without any active simulation. + /// + /// The argument is a closure that is called every time the simulation is + /// (re)started by the remote client. It must create a new `SimInit` object + /// complemented by a registry that exposes the public event and query + /// interface. + pub fn new(sim_gen: F) -> Self + where + F: FnMut() -> (SimInit, EndpointRegistry) + Send + 'static, + { Self { - sim_gen, + sim_gen: Box::new(sim_gen), sim_context: None, } } - /// Processes an encoded `AnyRequest` message and returns an encoded - /// `AnyReply`. - #[allow(dead_code)] - pub(crate) fn service_request(&mut self, request_buf: B) -> Vec + /// Processes an encoded `AnyRequest` message and returns an encoded reply. + pub fn process_request(&mut self, request_buf: B) -> Result, InvalidRequest> where B: Buf, { - let reply = match AnyRequest::decode(request_buf) { + match AnyRequest::decode(request_buf) { Ok(AnyRequest { request: Some(req) }) => match req { any_request::Request::InitRequest(request) => { - any_reply::Reply::InitReply(self.init(request)) + Ok(self.init(request).encode_to_vec()) } any_request::Request::TimeRequest(request) => { - any_reply::Reply::TimeReply(self.time(request)) + Ok(self.time(request).encode_to_vec()) } any_request::Request::StepRequest(request) => { - any_reply::Reply::StepReply(self.step(request)) + Ok(self.step(request).encode_to_vec()) } any_request::Request::StepUntilRequest(request) => { - any_reply::Reply::StepUntilReply(self.step_until(request)) + Ok(self.step_until(request).encode_to_vec()) } any_request::Request::ScheduleEventRequest(request) => { - any_reply::Reply::ScheduleEventReply(self.schedule_event(request)) + Ok(self.schedule_event(request).encode_to_vec()) } any_request::Request::CancelEventRequest(request) => { - any_reply::Reply::CancelEventReply(self.cancel_event(request)) + Ok(self.cancel_event(request).encode_to_vec()) } any_request::Request::ProcessEventRequest(request) => { - any_reply::Reply::ProcessEventReply(self.process_event(request)) + Ok(self.process_event(request).encode_to_vec()) } any_request::Request::ProcessQueryRequest(request) => { - any_reply::Reply::ProcessQueryReply(self.process_query(request)) + Ok(self.process_query(request).encode_to_vec()) } any_request::Request::ReadEventsRequest(request) => { - any_reply::Reply::ReadEventsReply(self.read_events(request)) + Ok(self.read_events(request).encode_to_vec()) } any_request::Request::OpenSinkRequest(request) => { - any_reply::Reply::OpenSinkReply(self.open_sink(request)) + Ok(self.open_sink(request).encode_to_vec()) } any_request::Request::CloseSinkRequest(request) => { - any_reply::Reply::CloseSinkReply(self.close_sink(request)) + Ok(self.close_sink(request).encode_to_vec()) } }, - Ok(AnyRequest { request: None }) => any_reply::Reply::Error(ServerError { - code: ServerErrorCode::EmptyRequest as i32, - message: "the message did not contain any request".to_string(), + Ok(AnyRequest { request: None }) => Err(InvalidRequest { + description: "the message did not contain any request".to_string(), }), - Err(err) => any_reply::Reply::Error(ServerError { - code: ServerErrorCode::UnknownRequest as i32, - message: format!("bad request: {}", err), + Err(err) => Err(InvalidRequest { + description: format!("bad request: {}", err), }), - }; - - let reply = AnyReply { reply: Some(reply) }; - - reply.encode_to_vec() + } } /// Initialize a simulation with the provided time. @@ -606,6 +609,25 @@ where } } +impl fmt::Debug for SimulationService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SimulationService").finish_non_exhaustive() + } +} + +#[derive(Clone, Debug)] +pub struct InvalidRequest { + description: String, +} + +impl fmt::Display for InvalidRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.description) + } +} + +impl error::Error for InvalidRequest {} + /// Attempts a cast from a `MonotonicTime` to a protobuf `Timestamp`. /// /// This will fail if the time is outside the protobuf-specified range for diff --git a/asynchronix/src/rpc/wasm.rs b/asynchronix/src/rpc/wasm.rs new file mode 100644 index 0000000..5526e6e --- /dev/null +++ b/asynchronix/src/rpc/wasm.rs @@ -0,0 +1,82 @@ +//! WASM simulation service. +//! +//! This module provides [`WasmSimulationService`], a thin wrapper over a +//! [`SimulationService`] that can be use from JavaScript. +//! +//! Although it is readily possible to use a +//! [`Simulation`](crate::simulation::Simulation) object from WASM, +//! [`WasmSimulationService`] goes further by exposing the complete simulation +//! API to JavaScript through protobuf. +//! +//! Keep in mind that WASM only supports single-threaded execution and therefore +//! any simulation bench compiled to WASM should instantiate simulations with +//! either [`SimInit::new()`](crate::simulation::SimInit::new) or +//! [`SimInit::with_num_threads(1)`](crate::simulation::SimInit::with_num_threads), +//! failing which the simulation will panic upon initialization. +//! +//! [`WasmSimulationService`] is exported to the JavaScript namespace as +//! `SimulationService`, and [`WasmSimulationService::process_request`] as +//! `SimulationService.processRequest`. + +use wasm_bindgen::prelude::*; + +use super::{EndpointRegistry, SimulationService}; +use crate::simulation::SimInit; + +/// A simulation service that can be used from JavaScript. +/// +/// This would typically be used by implementing a `run` function in Rust and +/// export it to WASM: +/// +/// ```no_run +/// #[wasm_bindgen] +/// pub fn run() -> WasmSimulationService { +/// WasmSimulationService::new(my_custom_bench_generator) +/// } +/// ``` +/// +/// which can then be used on the JS side to create a `SimulationService` as a +/// JS object, e.g. with: +/// +/// ```js +/// const simu = run(); +/// +/// // ...build a protobuf request and encode it as a `Uint8Array`... +/// +/// const reply = simu.processRequest(myRequest); +/// +/// // ...decode the protobuf reply... +/// ``` +#[wasm_bindgen(js_name = SimulationService)] +#[derive(Debug)] +pub struct WasmSimulationService(SimulationService); + +#[wasm_bindgen(js_class = SimulationService)] +impl WasmSimulationService { + /// Processes a protobuf-encoded `AnyRequest` message and returns a + /// protobuf-encoded reply. + /// + /// For the Protocol Buffer definitions, see the `simulation.proto` file. + #[wasm_bindgen(js_name = processRequest)] + pub fn process_request(&mut self, request: &[u8]) -> Result, JsError> { + self.0 + .process_request(request) + .map(|reply| reply.into_boxed_slice()) + .map_err(|e| JsError::new(&e.to_string())) + } +} + +impl WasmSimulationService { + /// Creates a new `SimulationService` without any active simulation. + /// + /// The argument is a closure that is called every time the simulation is + /// (re)started by the remote client. It must create a new `SimInit` object + /// complemented by a registry that exposes the public event and query + /// interface. + pub fn new(sim_gen: F) -> Self + where + F: FnMut() -> (SimInit, EndpointRegistry) + Send + 'static, + { + Self(SimulationService::new(sim_gen)) + } +}