A Rust implementation of the Varlink IPC protocol. zlink provides a safe,
async API for building Varlink services and clients with support for both standard and embedded
(no-std) environments.
Overview
Varlink is a simple, JSON-based IPC protocol that enables communication between system services and
applications. zlink makes it easy to implement Varlink services in Rust with:
- Async-first design: Built on async/await for efficient concurrent operations.
- Type safety: Leverage Rust's type system with derive macros and code generation.
- No-std support: Run on embedded systems.
- Multiple transports: Unix domain sockets and (upcoming) USB support.
- Code generation: Generate Rust code from Varlink IDL files.
Project Structure
The zlink project consists of several subcrates:
zlink: The main unified API crate that re-exports functionality based on enabled features.
This is the only crate you will want to use directly in your application and services.
zlink-core: Core no-std foundation providing essential Varlink types and traits.
zlink-macros: Contains the attribute and derive macros.
zlink-tokio: Tokio-based transport implementations and runtime integration.
zlink-codegen: Code generation tool for creating Rust bindings from Varlink IDL files.
Examples
Example: Calculator Service and Client
Note: For service implementation, zlink currently only provides a low-level API. A high-level
service API with attribute macros (similar to the proxy macro for clients) is planned for the
near future.
Here's a complete example showing both service implementation and client usage through the proxy
macro:
use serde::{Deserialize, Serialize};
use tokio::{select, sync::oneshot, fs::remove_file};
use zlink::{
proxy,
service::{MethodReply, Service},
connection::{Connection, Socket},
unix, Call, ReplyError, Server,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (ready_tx, ready_rx) = oneshot::channel();
select! {
res = run_server(ready_tx) => res?,
res = run_client(ready_rx) => res?,
}
Ok(())
}
async fn run_client(ready_rx: oneshot::Receiver<()>) -> Result<(), Box<dyn std::error::Error>> {
ready_rx.await.map_err(|_| "Server failed to start")?;
let mut conn = unix::connect(SOCKET_PATH).await?;
let result = conn.add(5.0, 3.0).await?.unwrap();
assert_eq!(result.result, 8.0);
let result = conn.multiply(4.0, 7.0).await?.unwrap();
assert_eq!(result.result, 28.0);
let Err(CalculatorError::DivisionByZero { message }) = conn.divide(10.0, 0.0).await? else {
panic!("Expected DivisionByZero error");
};
assert_eq!(message, "Cannot divide by zero");
let Err(CalculatorError::InvalidInput {
field,
reason,
}) = conn.divide(2000000.0, 2.0).await? else {
panic!("Expected InvalidInput error");
};
println!("Field: {}, Reason: {}", field, reason);
let stats = conn.get_stats().await?.unwrap();
assert_eq!(stats.count, 2);
println!("Stats: {:?}", stats);
Ok(())
}
#[proxy("org.example.Calculator")]
trait CalculatorProxy {
async fn add(
&mut self,
a: f64,
b: f64,
) -> zlink::Result<Result<CalculationResult, CalculatorError<'_>>>;
async fn multiply(
&mut self,
x: f64,
y: f64,
) -> zlink::Result<Result<CalculationResult, CalculatorError<'_>>>;
async fn divide(
&mut self,
dividend: f64,
divisor: f64,
) -> zlink::Result<Result<CalculationResult, CalculatorError<'_>>>;
async fn get_stats(
&mut self,
) -> zlink::Result<Result<Statistics<'_>, CalculatorError<'_>>>;
}
#[derive(Debug, Serialize, Deserialize)]
struct CalculationResult {
result: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Statistics<'a> {
count: u64,
#[serde(borrow)]
operations: Vec<&'a str>,
}
#[derive(Debug, ReplyError)]
#[zlink(interface = "org.example.Calculator")]
enum CalculatorError<'a> {
DivisionByZero {
message: &'a str
},
InvalidInput {
field: &'a str,
reason: &'a str,
},
}
async fn run_server(ready_tx: oneshot::Sender<()>) -> Result<(), Box<dyn std::error::Error>> {
let _ = remove_file(SOCKET_PATH).await;
let listener = unix::bind(SOCKET_PATH)?;
let service = Calculator::new();
let server = Server::new(listener, service);
let _ = ready_tx.send(());
server.run().await.map_err(|e| e.into())
}
struct Calculator {
operations: Vec<String>,
}
impl Calculator {
fn new() -> Self {
Self {
operations: Vec::new(),
}
}
}
impl Service for Calculator {
type MethodCall<'de> = CalculatorMethod;
type ReplyParams<'ser> = CalculatorReply<'ser>;
type ReplyStreamParams = ();
type ReplyStream = futures_util::stream::Empty<zlink::Reply<()>>;
type ReplyError<'ser> = CalculatorError<'ser>;
async fn handle<'ser, 'de: 'ser, Sock: Socket>(
&'ser mut self,
call: Call<Self::MethodCall<'de>>,
conn: &mut Connection<Sock>,
) -> MethodReply<Self::ReplyParams<'ser>, Self::ReplyStream, Self::ReplyError<'ser>> {
match call.method() {
CalculatorMethod::Add { a, b } => {
self.operations.push(format!("add({}, {})", a, b));
MethodReply::Single(Some(CalculatorReply::Result(CalculationResult { result: a + b })))
}
CalculatorMethod::Multiply { x, y } => {
self.operations.push(format!("multiply({}, {})", x, y));
MethodReply::Single(Some(CalculatorReply::Result(CalculationResult { result: x * y })))
}
CalculatorMethod::Divide { dividend, divisor } => {
if *divisor == 0.0 {
MethodReply::Error(CalculatorError::DivisionByZero {
message: "Cannot divide by zero",
})
} else if dividend < &-1000000.0 || dividend > &1000000.0 {
MethodReply::Error(CalculatorError::InvalidInput {
field: "dividend",
reason: "must be within range",
})
} else {
self.operations.push(format!("divide({}, {})", dividend, divisor));
MethodReply::Single(Some(CalculatorReply::Result(CalculationResult {
result: dividend / divisor,
})))
}
}
CalculatorMethod::GetStats => {
let ops: Vec<&str> = self.operations.iter().map(|s| s.as_str()).collect();
MethodReply::Single(Some(CalculatorReply::Stats(Statistics {
count: self.operations.len() as u64,
operations: ops,
})))
}
}
}
}
#[derive(Debug, Deserialize)]
#[serde(tag = "method", content = "parameters")]
enum CalculatorMethod {
#[serde(rename = "org.example.Calculator.Add")]
Add { a: f64, b: f64 },
#[serde(rename = "org.example.Calculator.Multiply")]
Multiply { x: f64, y: f64 },
#[serde(rename = "org.example.Calculator.Divide")]
Divide { dividend: f64, divisor: f64 },
#[serde(rename = "org.example.Calculator.GetStats")]
GetStats,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum CalculatorReply<'a> {
Result(CalculationResult),
#[serde(borrow)]
Stats(Statistics<'a>),
}
const SOCKET_PATH: &str = "/tmp/calculator_example.varlink";
Note: Typically you would want to spawn the server in a separate task but that's not what we
did in the example above. Please refer to Server::run docs for the reason.
Code Generation from IDL
zlink-codegen can generate Rust code from Varlink interface description files:
cargo install zlink-codegen
cat <<EOF > calculator.varlink
# Calculator service interface
interface org.example.Calculator
type CalculationResult (
result: float
)
type DivisionByZeroError (
message: string
)
method Add(a: float, b: float) -> (result: float)
method Multiply(x: float, y: float) -> (result: float)
method Divide(dividend: float, divisor: float) -> (result: float)
error DivisionByZero(message: string)
EOF
zlink-codegen calculator.varlink > src/calculator_gen.rs
The generated code includes type definitions and proxy traits ready to use in your application.
Pipelining
zlink supports method call pipelining for improved throughput and reduced latency. The proxy macro
adds variants for each method named chain_<method_name> and a trait named <TraitName>Chain that
allow you to batch multiple requests and send them out at once without waiting for individual
responses:
use futures_util::{StreamExt, pin_mut};
use serde::{Deserialize, Serialize};
use zlink::{proxy, unix, ReplyError};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut conn = unix::connect("/tmp/batch_processor.varlink").await?;
let replies = conn
.chain_process::<ProcessReply, ProcessError>(1, "first")?
.process(2, "second")?
.process(3, "third")?
.batch_process(vec![
ProcessRequest { id: 4, data: "batch1" },
ProcessRequest { id: 5, data: "batch2" },
])?
.send()
.await?;
pin_mut!(replies);
let mut results = Vec::new();
while let Some(reply) = replies.next().await {
let (reply, _fds) = reply?;
if let Ok(response) = reply {
match response.into_parameters() {
Some(ProcessReply::Result(result)) => {
results.push(result);
}
Some(ProcessReply::BatchResult(batch)) => {
results.extend(batch.results);
}
None => {}
}
}
}
for result in results {
println!("Processed item {}: {}", result.id, result.processed);
}
Ok(())
}
#[proxy("org.example.BatchProcessor")]
trait BatchProcessorProxy {
async fn process(
&mut self,
id: u32,
data: &str,
) -> zlink::Result<Result<ProcessReply<'_>, ProcessError>>;
async fn batch_process(
&mut self,
requests: Vec<ProcessRequest<'_>>,
) -> zlink::Result<Result<ProcessReply<'_>, ProcessError>>;
}
#[derive(Debug, Serialize)]
struct ProcessRequest<'a> {
id: u32,
#[serde(borrow)]
data: &'a str,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ProcessReply<'a> {
#[serde(borrow)]
Result(ProcessResult<'a>),
#[serde(borrow)]
BatchResult(BatchResult<'a>),
}
#[derive(Debug, Deserialize)]
struct ProcessResult<'a> {
id: u32,
#[serde(borrow)]
processed: &'a str,
}
#[derive(Debug, Deserialize)]
struct BatchResult<'a> {
#[serde(borrow)]
results: Vec<ProcessResult<'a>>,
}
#[derive(Debug, ReplyError)]
#[zlink(interface = "org.example.BatchProcessor")]
enum ProcessError {
InvalidRequest,
}
Examples
The repository includes a few examples:
Run examples with:
cargo run --example resolved -- example.com systemd.io
cargo run \
--example varlink-inspect \
--features idl-parse,introspection -- \
/run/systemd/resolve/io.systemd.Resolve
Features
Main Features
tokio (default): Enable tokio runtime integration and use of standard library.
server (default): Enable server-related functionality (Server, Listener, Service).
proxy (default): Enable the #[proxy] macro for type-safe client code.
tracing (default): Enable tracing-based logging.
defmt: Enable defmt-based logging. If both tracing and defmt is enabled, tracing is
used.
IDL and Introspection
idl: Support for IDL type representations.
introspection: Enable runtime introspection of service interfaces.
idl-parse: Parse Varlink IDL files at runtime (requires std).
Getting Help and/or Contributing
If you need help in using these crates, are looking for ways to contribute, or just want to hang out
with the cool kids, please come chat with us in the
#zlink:matrix.org Matrix room. If something doesn't seem
right, please file an issue.
We welcome contributions! Please see our Contributing Guide for details.
License
This project is licensed under the MIT License.