Expand description
The web-rpc
create is a library for performing RPCs (remote proceedure calls) between
browsing contexts, web workers, and channels. It allows you to define an RPC using a trait
similar to Google’s tarpc and will transparently
handle the serialization and deserialization of the arguments. Moreover, it can post
anything that implements AsRef<JsValue>
and also supports transferring ownership.
§Quick start
To get started define a trait for your RPC service as follows. Annnotate this trait with the
service
procedural macro that is exported by this crate:
#[web_rpc::service]
pub trait Calculator {
fn add(left: u32, right: u32) -> u32;
}
This macro will generate the structs CalculatorClient
, CalculatorService
, and a new trait
Calculator
that you can use to implement the service as follows:
struct CalculatorServiceImpl;
impl Calculator for CalculatorServiceImpl {
fn add(&self, left: u32, right: u32) -> u32 {
left + right
}
}
Note that the version of the trait emitted from the macro adds a &self
receiver. Although not
used in this example, this is useful when we want the RPC to modify some state (via interior
mutability). Now that we have defined our RPC, let’s create a client and server for it! In this
example, we will use MessageChannel
since it is easy to construct and test, however, a more common case would be to construct the
channel from a Worker
or a
DedicatedWorkerGlobalScope
.
Let’s start by defining the server:
// create a MessageChannel
let channel = web_sys::MessageChannel::new();
// Create two interfaces from the ports
let (server_interface, client_interface) = futures_util::future::join(
web_rpc::Interface::new(channel.port1()),
web_rpc::Interface::new(channel.port2()),
).await;
// create a server with the first interface
let server = web_rpc::Builder::new(server_interface)
.with_service::<CalculatorService<_>>(CalculatorServiceImpl)
.build();
// spawn the server
wasm_bindgen_futures::spawn_local(server);
Interface::new
is async since there is no way to synchronously check whether a channel or
a worker is ready to receive messages. To workaround this, temporary listeners are attached to
determine when a channel is ready for communication. The server returned by the build method is
a future that can be added to the browser’s event loop using
wasm_bindgen_futures::spawn_local
, however, this will run the server indefinitely. For more
control, consider wrapping the server with futures_util::FutureExt::remote_handle
before
spawning it, which will shutdown the server once the handle has been dropped. Moving onto the
client:
// create a client using the second interface
let client = web_rpc::Builder::new(client_interface)
.with_client::<CalculatorClient>()
.build();
/* call `add` */
assert_eq!(client.add(41, 1).await, 42);
That is it! Underneath the hood, the client will serialize its arguments using bincode and
transfer the bytes to server. The server will deserialize those arguments and run
<CalculatorServiceImpl as Calculator>::add
before returning the result to the client. Note
that we are only awaiting the response of the call to add
, the request itself is sent
synchronously before we await anything.
§Advanced examples
Now that we have the basic idea of how define an RPC trait and set up a server and client, let’s dive into some of the more advanced features of this library!
§Synchronous and asynchronous RPC methods
Server methods can be asynchronous! That is, you can define the following RPC trait and service implementation:
#[web_rpc::service]
pub trait Sleep {
async fn sleep(interval: Duration);
}
struct SleepServiceImpl;
impl Sleep for SleepServiceImpl {
async fn sleep(&self, interval: Duration) -> bool {
gloo_timers::future::sleep(interval).await;
// sleep completed (was not cancelled)
true
}
}
Asynchronous RPC methods are run concurrently on the server and also support cancellation if the future on the client side is dropped. However, such a future is only returned from a client method if the RPC returns a value. Otherwise the RPC is considered a notification.
§Notifications
Notifications are RPCs that do not return anything. On the client side, the method is completely synchronous and also returns nothing. This setup is useful if you need to communicate with another part of your application but cannot yield to the event loop.
The implication of this, however, is that even if the server method is asynchronous, we are unable to cancel it from the client side since we do not have a future that can be dropped.
§Posting and transferring Javascript types
In the example above, we discussed how the client serializes its arguments before sending them
to the server. This approach is convenient, but how do send web types such as a
WebAssembly.Module
or an OffscreenCanvas
that have no serializable representation? Well, we
are in luck since this happens to be one of the key features of this crate. Consider the
following RPC trait:
#[web_rpc::service]
pub trait Concat {
#[post(left, right, return)]
fn concat_with_space(
left: js_sys::JsString,
right: js_sys::JsString
) -> js_sys::JsString;
}
All we have done is added the post
attribute to the method and listed the arguments that we
would like to be posted to the other side. Under the hood, the implementation of the client will
then skip these arguments during serialization and just append them after the serialized message
to the array that will be posted. As shown above, this also works for the return type by just
specifying return
in the post attribute. For web types that need to be transferred, we simply
wrap them in transfer
as follows:
#[web_rpc::service]
pub trait GameEngine {
#[post(transfer(canvas))]
fn send_canvas(
canvas: js_sys::OffscreenCanvas,
);
}
§Bi-directional RPC
In the original example, we created a server on the first port of the message channel and a client on the second port. However, it is possible to define both a client and a server on each side, enabling bi-directional RPC. This is particularly useful if we want to send and receive messages from a worker without sending it a seperate channel for the bi-directional communication. Our original example can be extended as follows:
/* create channel */
let channel = web_sys::MessageChannel::new().unwrap();
let (interface1, interface2) = futures_util::future::join(
web_rpc::Interface::new(channel.port1()),
web_rpc::Interface::new(channel.port2()),
).await;
/* create server1 and client1 */
let (client1, server1) = web_rpc::Builder::new(interface1)
.with_service::<CalculatorService<_>>(CalculatorServiceImpl)
.with_client::<CalculatorClient>()
.build();
/* create server2 and client2 */
let (client2, server2) = web_rpc::Builder::new(interface2)
.with_service::<CalculatorService<_>>(CalculatorServiceImpl)
.with_client::<CalculatorClient>()
.build();
Re-exports§
pub use interface::Interface;
Modules§
Structs§
- Builder
- This struct allows one to configure the RPC interface prior to creating it.
To get an instance of this struct, call
Builder<C, S>::new
with anInterface
. - Server
Server
is the server that is returned from theBuilder::build
method given you configured the RPC interface with a service. Note thatServer
implements future and needs to be polled in order to execute and respond to inbound RPC requests.
Attribute Macros§
- service
- This attribute macro should applied to traits that need to be turned into RPCs. The
macro will consume the trait and output three items in its place. For example,
a trait
Calculator
will be replaced with two structsCalculatorClient
andCalculatorService
and a new trait by the same name with the methods which have had the a&self
receiver added to them.