Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 64 additions & 127 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,20 @@ The zlink project consists of several subcrates:

### 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:
Here's a complete example showing both service and client implementations using zlink's attribute
macros:

```rust
use serde::{Deserialize, Serialize};
use tokio::{select, sync::oneshot, fs::remove_file};
use zlink::{
proxy,
service::{self, MethodReply, Service},
connection::{Connection, Socket},
unix, Call, ReplyError, Server,
};
use zlink::{introspect, proxy, service, unix, ReplyError, Server};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a channel to signal when server is ready
// Create a channel to signal when server is ready.
let (ready_tx, ready_rx) = oneshot::channel();

// Run server and client concurrently
// Run server and client concurrently.
select! {
res = run_server(ready_tx) => res?,
res = run_client(ready_rx) => res?,
Expand All @@ -76,42 +67,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}

async fn run_client(ready_rx: oneshot::Receiver<()>) -> Result<(), Box<dyn std::error::Error>> {
// Wait for server to be ready
// Wait for server to be ready.
ready_rx.await.map_err(|_| "Server failed to start")?;

// Connect to the calculator service
// Connect to the calculator service.
let mut conn = unix::connect(SOCKET_PATH).await?;

// Use the proxy-generated methods
// Use the proxy-generated methods.
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);

// Handle errors properly
// Handle errors properly.
let Err(CalculatorError::DivisionByZero { message }) = conn.divide(10.0, 0.0).await? else {
panic!("Expected DivisionByZero error");
};
assert_eq!(message, "Cannot divide by zero");

// Test invalid input error with large dividend
let Err(CalculatorError::InvalidInput {
field,
reason,
}) = conn.divide(2000000.0, 2.0).await? else {
// Test invalid input error with large dividend.
let Err(CalculatorError::InvalidInput { field, reason }) =
conn.divide(2000000.0, 2.0).await?
else {
panic!("Expected InvalidInput error");
};
println!("Field: {}, Reason: {}", field, reason);
println!("Field: {field}, Reason: {reason}");

let stats = conn.get_stats().await?.unwrap();
assert_eq!(stats.count, 2);
println!("Stats: {:?}", stats);
println!("Stats: {stats:?}");

Ok(())
}

// The client proxy - this implements the trait for `Connection<S>`
// The client proxy.
#[proxy("org.example.Calculator")]
trait CalculatorProxy {
async fn add(
Expand All @@ -134,7 +124,7 @@ trait CalculatorProxy {
) -> zlink::Result<Result<Statistics<'_>, CalculatorError<'_>>>;
}

// Types shared between client and server
// Types shared between client and server.
#[derive(Debug, Serialize, Deserialize)]
struct CalculationResult {
result: f64,
Expand All @@ -147,11 +137,11 @@ struct Statistics<'a> {
operations: Vec<&'a str>,
}

#[derive(Debug, ReplyError)]
#[derive(Debug, PartialEq, ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.Calculator")]
enum CalculatorError<'a> {
DivisionByZero {
message: &'a str
message: &'a str,
},
InvalidInput {
field: &'a str,
Expand All @@ -162,123 +152,69 @@ enum CalculatorError<'a> {
async fn run_server(ready_tx: oneshot::Sender<()>) -> Result<(), Box<dyn std::error::Error>> {
let _ = remove_file(SOCKET_PATH).await;

// Setup the server
// Setup and run the server.
let listener = unix::bind(SOCKET_PATH)?;
let service = Calculator::new();
let server = Server::new(listener, service);
let server = Server::new(listener, Calculator::new());

// Signal that server is ready
// Signal that server is ready.
let _ = ready_tx.send(());

server.run().await.map_err(|e| e.into())
}

// The calculator service
// The calculator service.
struct Calculator {
operations: Vec<String>,
}

impl Calculator {
fn new() -> Self {
Self {
operations: Vec::new(),
}
Self { operations: Vec::new() }
}
}

// Implement the Service trait
impl<Sock> Service<Sock> for Calculator
where
Sock: Socket,
{
type MethodCall<'de> = CalculatorMethod;
type ReplyParams<'ser> = CalculatorReply<'ser>
where
Self: 'ser;
type ReplyStreamParams = ();
type ReplyStream = futures_util::stream::Empty<zlink::service::ReplyStreamItem<()>>;
type ReplyError<'ser> = CalculatorError<'ser>
where
Self: 'ser;

async fn handle<'service>(
&'service mut self,
call: &'service Call<Self::MethodCall<'_>>,
conn: &mut Connection<Sock>,
fds: Vec<std::os::fd::OwnedFd>,
) -> service::HandleResult<
Self::ReplyParams<'service>,
Self::ReplyStream,
Self::ReplyError<'service>,
> {
let _ = (conn, fds);
let reply = 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,
})))
}
};
(reply, Vec::new())
#[service(interface = "org.example.Calculator")]
impl Calculator {
async fn add(&mut self, a: f64, b: f64) -> CalculationResult {
self.operations.push(format!("add({a}, {b})"));
CalculationResult { result: a + b }
}
}

// Method calls the service handles
#[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,
}
async fn multiply(&mut self, x: f64, y: f64) -> CalculationResult {
self.operations.push(format!("multiply({x}, {y})"));
CalculationResult { result: x * y }
}

// Reply types
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum CalculatorReply<'a> {
Result(CalculationResult),
#[serde(borrow)]
Stats(Statistics<'a>),
async fn divide(
&mut self,
dividend: f64,
divisor: f64,
) -> Result<CalculationResult, CalculatorError<'_>> {
if divisor == 0.0 {
Err(CalculatorError::DivisionByZero {
message: "Cannot divide by zero",
})
} else if dividend < -1000000.0 || dividend > 1000000.0 {
Err(CalculatorError::InvalidInput {
field: "dividend",
reason: "must be within range",
})
} else {
self.operations
.push(format!("divide({dividend}, {divisor})"));
Ok(CalculationResult {
result: dividend / divisor,
})
}
}

async fn get_stats(&self) -> Statistics<'_> {
let ops: Vec<&str> = self.operations.iter().map(|s| s.as_str()).collect();
Statistics {
count: self.operations.len() as u64,
operations: ops,
}
}
}

const SOCKET_PATH: &str = "/tmp/calculator_example.varlink";
Expand Down Expand Up @@ -454,6 +390,7 @@ cargo run \
- `tokio` (default): Enable tokio runtime integration.
- `smol`: Enable smol runtime integration.
- `server` (default): Enable server-related functionality (Server, Listener, Service).
- `service` (default): Enable the `#[service]` macro. Implies `server` and `introspection`.
- `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
Expand Down
2 changes: 1 addition & 1 deletion zlink-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ default = ["std", "server", "proxy", "tracing"]
std = ["dep:rustix", "dep:libc", "zlink-macros/std"]
server = []
proxy = ["zlink-macros/proxy"]
service = ["server", "zlink-macros/service"]
service = ["server", "zlink-macros/service", "introspection"]
# IDL and introspection support
idl = []
idl-parse = ["idl", "dep:winnow", "zlink-macros/idl-parse"]
Expand Down
42 changes: 42 additions & 0 deletions zlink-core/src/connection/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,46 @@
//! Contains connection related API.
//!
//! The [`Connection`] type provides a low-level API for sending and receiving Varlink messages.
//! For most use cases, you'll want to use the higher-level [`proxy`] and [`service`] attribute
//! macros instead, which generate type-safe client and server code respectively.
//!
//! # Client Usage with `proxy` Macro
//!
//! The [`proxy`] macro generates methods on `Connection<S>` for calling remote service methods:
//!
//! ```
//! #[zlink_core::proxy(
//! interface = "org.example.Calculator",
//! // Not needed in the real code because you'll use `proxy` through `zlink` crate.
//! crate = "zlink_core",
//! )]
//! trait CalculatorProxy {
//! async fn add(&mut self, a: f64, b: f64) -> zlink_core::Result<Result<f64, CalcError>>;
//! }
//!
//! #[derive(Debug, zlink_core::ReplyError)]
//! #[zlink(
//! interface = "org.example.Calculator",
//! // Not needed in the real code because you'll use `ReplyError` through `zlink` crate.
//! crate = "zlink_core",
//! )]
//! enum CalcError {}
//! ```
//!
//! # Server Usage with `service` Macro
//!
//! The [`service`] macro generates the [`Service`] trait implementation. See the [`service`] macro
//! documentation for details and examples.
//!
//! # Low-Level API
//!
//! For advanced use cases that require more control, the [`Connection`] type provides direct access
//! to message sending and receiving via methods like [`Connection::send_call`],
//! [`Connection::receive_reply`], and [`Connection::chain_call`] for pipelining.
//!
//! [`proxy`]: macro@crate::proxy
//! [`service`]: macro@crate::service
//! [`Service`]: crate::service::Service

#[cfg(feature = "std")]
mod credentials;
Expand Down
5 changes: 5 additions & 0 deletions zlink-core/src/introspect/type/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn complex_type() {
}
}

#[cfg(feature = "std")]
#[test]
fn map_types() {
use std::collections::{BTreeMap, HashMap};
Expand All @@ -58,6 +59,7 @@ fn map_types() {
}
}

#[cfg(feature = "std")]
#[test]
fn set_types() {
use std::collections::{BTreeSet, HashSet};
Expand Down Expand Up @@ -122,6 +124,7 @@ fn core_time_types() {
assert_eq!(*<core::time::Duration>::TYPE, idl::Type::Float);
}

#[cfg(feature = "std")]
#[test]
fn std_time_types() {
use std::time::{Instant, SystemTime};
Expand All @@ -130,6 +133,7 @@ fn std_time_types() {
assert_eq!(*<SystemTime>::TYPE, idl::Type::Float);
}

#[cfg(feature = "std")]
#[test]
fn path_types() {
use std::path::{Path, PathBuf};
Expand All @@ -138,6 +142,7 @@ fn path_types() {
assert_eq!(*<Path>::TYPE, idl::Type::String);
}

#[cfg(feature = "std")]
#[test]
fn osstring_types() {
use std::ffi::{OsStr, OsString};
Expand Down
8 changes: 8 additions & 0 deletions zlink-core/src/server/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ pub type ReplyStreamItem<Params> = (Reply<Params>, Vec<std::os::fd::OwnedFd>);
pub type ReplyStreamItem<Params> = Reply<Params>;

/// Service trait for handling method calls.
///
/// Instead of implementing this trait manually, prefer using the [`service`] attribute macro which
/// generates the implementation for you. The macro provides a more ergonomic API and handles the
/// boilerplate of method dispatching, error handling, and streaming replies.
///
/// See the [`service`] macro documentation for details and examples.
///
/// [`service`]: macro@crate::service
pub trait Service<Sock>
where
Sock: Socket,
Expand Down
Loading