Projects / yerpc
The idea
You write your API methods in Rust once, and yerpc generates a type-safe
TypeScript client and an
OpenRPC spec for free. The
#[rpc] proc macro does the heavy lifting:
#[rpc(all_positional, ts_outdir = "ts/generated")]
impl Api {
/// Send a message and get back the uppercased version.
async fn shout(&self, msg: String) -> String {
msg.to_uppercase()
}
/// Add two numbers together.
async fn add(&self, a: f32, b: f32) -> f32 {
a + b
}
} At compile time this generates a TypeScript client with matching function signatures:
// AUTO-GENERATED by yerpc-derive
export class RawClient {
constructor(private _transport: Transport) {}
public shout(msg: string): Promise<string> {
return this._transport.request("shout", [msg]);
}
public add(a: number, b: number): Promise<number> {
return this._transport.request("add", [a, b]);
}
}
Types plus async functions that internally invoke JSON-RPC and return
a promise. Try it - type client. to see autocomplete:
const result = await client.shout("hello");
// result: "HELLO"
const sum = await client.add(1, 2);
// sum: 3
// Try typing: client. The server and client stay in sync automatically: change the Rust API, and the TypeScript types update on the next build. The generated OpenRPC spec can also drive client generation in other languages (Python, Go, Swift, Java), though that work is still in progress.
Transport-independent
yerpc itself doesn't care how messages travel between server and client. It takes JSON in and produces JSON out. You plug in whatever transport fits your use case: WebSocket, stdio, an in-process channel, or something custom. For Delta Chat, this meant significantly more flexibility than the old C FFI, which had to be linked into the same process.
Origin
The idea of replacing Delta Chat's C FFI with a JSON-RPC API had been discussed at various team meetings. I took the initiative and built a prototype to solve several pain points:
- Adding a new core API method required changes in many places across Desktop.
- Every call to core blocked the UI (no async).
- Error reporting through the C FFI was limited.
There was also a prototype project for Delta Chat on KaiOS, where core runs as a separate native binary and needed an API over a local WebSocket.
I couldn't find a suitable minimal JSON-RPC library at the time (this
was around 2020, and the Rust ecosystem was smaller than it is today),
so I built a prototype. Frando
extracted and cleaned up the JSON-RPC protocol and type generation parts into
yerpc as a standalone library. We then worked together on stabilizing the
Delta Chat JSON-RPC API and integrating it into the official core project.
Delta Chat Desktop now uses yerpc exclusively to communicate with core, over
around 170 different methods (@deltachat/jsonrpc-client). Having the types generated means you can change and iterate on the
API quickly without fear of breaking things - the type checker catches
mismatches immediately.
I wrote about the full story of the C FFI to JSON-RPC transition in a blog post on delta.chat.