diff --git a/README.md b/README.md index 0ed3c2e..2c0a6c1 100644 --- a/README.md +++ b/README.md @@ -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> { - // 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?, @@ -76,42 +67,41 @@ async fn main() -> Result<(), Box> { } async fn run_client(ready_rx: oneshot::Receiver<()>) -> Result<(), Box> { - // 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` +// The client proxy. #[proxy("org.example.Calculator")] trait CalculatorProxy { async fn add( @@ -134,7 +124,7 @@ trait CalculatorProxy { ) -> zlink::Result, CalculatorError<'_>>>; } -// Types shared between client and server +// Types shared between client and server. #[derive(Debug, Serialize, Deserialize)] struct CalculationResult { result: f64, @@ -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, @@ -162,123 +152,69 @@ enum CalculatorError<'a> { async fn run_server(ready_tx: oneshot::Sender<()>) -> Result<(), Box> { 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, } impl Calculator { fn new() -> Self { - Self { - operations: Vec::new(), - } + Self { operations: Vec::new() } } } -// Implement the Service trait -impl Service 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>; - type ReplyError<'ser> = CalculatorError<'ser> - where - Self: 'ser; - - async fn handle<'service>( - &'service mut self, - call: &'service Call>, - conn: &mut Connection, - fds: Vec, - ) -> 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> { + 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"; @@ -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 diff --git a/zlink-core/Cargo.toml b/zlink-core/Cargo.toml index 76e3b96..4ac83b4 100644 --- a/zlink-core/Cargo.toml +++ b/zlink-core/Cargo.toml @@ -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"] diff --git a/zlink-core/src/connection/mod.rs b/zlink-core/src/connection/mod.rs index e66f23d..7f241bc 100644 --- a/zlink-core/src/connection/mod.rs +++ b/zlink-core/src/connection/mod.rs @@ -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` 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>; +//! } +//! +//! #[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; diff --git a/zlink-core/src/introspect/type/tests.rs b/zlink-core/src/introspect/type/tests.rs index 5c89b4b..4867c4d 100644 --- a/zlink-core/src/introspect/type/tests.rs +++ b/zlink-core/src/introspect/type/tests.rs @@ -42,6 +42,7 @@ fn complex_type() { } } +#[cfg(feature = "std")] #[test] fn map_types() { use std::collections::{BTreeMap, HashMap}; @@ -58,6 +59,7 @@ fn map_types() { } } +#[cfg(feature = "std")] #[test] fn set_types() { use std::collections::{BTreeSet, HashSet}; @@ -122,6 +124,7 @@ fn core_time_types() { assert_eq!(*::TYPE, idl::Type::Float); } +#[cfg(feature = "std")] #[test] fn std_time_types() { use std::time::{Instant, SystemTime}; @@ -130,6 +133,7 @@ fn std_time_types() { assert_eq!(*::TYPE, idl::Type::Float); } +#[cfg(feature = "std")] #[test] fn path_types() { use std::path::{Path, PathBuf}; @@ -138,6 +142,7 @@ fn path_types() { assert_eq!(*::TYPE, idl::Type::String); } +#[cfg(feature = "std")] #[test] fn osstring_types() { use std::ffi::{OsStr, OsString}; diff --git a/zlink-core/src/server/service.rs b/zlink-core/src/server/service.rs index 57f65fb..3f2a61d 100644 --- a/zlink-core/src/server/service.rs +++ b/zlink-core/src/server/service.rs @@ -21,6 +21,14 @@ pub type ReplyStreamItem = (Reply, Vec); pub type ReplyStreamItem = Reply; /// 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 where Sock: Socket, diff --git a/zlink-macros/src/service/codegen.rs b/zlink-macros/src/service/codegen.rs index 51c07b3..0d2d59e 100644 --- a/zlink-macros/src/service/codegen.rs +++ b/zlink-macros/src/service/codegen.rs @@ -7,6 +7,7 @@ use quote::{format_ident, quote, ToTokens}; use syn::{Error, GenericParam, ItemImpl, Type}; use super::{attrs::ServiceAttrs, method::MethodInfo}; +use crate::utils::convert_type_lifetimes; /// Context for generating the `handle` method body. struct HandleBodyContext<'a> { @@ -280,7 +281,7 @@ fn generate_method_call_enum( .iter() .map(|param| { let name = ¶m.name; - let ty = ¶m.ty; + let converted_ty = convert_type_lifetimes(¶m.ty, "'__de"); let serde_attr = if let Some(ref renamed) = param.serialized_name { quote! { #[serde(rename = #renamed)] } @@ -290,7 +291,7 @@ fn generate_method_call_enum( quote! { #serde_attr - #name: #ty + #name: #converted_ty } }) .collect(); @@ -452,13 +453,14 @@ fn generate_reply_error_enum( }; if &error_type.to_token_stream().to_string() == type_str { let variant_name = format_ident!("__{}Variant{}", enum_name, idx); + let converted = convert_type_lifetimes(error_type, "'__ser"); variants.push(quote! { - #variant_name(#error_type) + #variant_name(#converted) }); from_impls.push(quote! { - impl ::core::convert::From<#error_type> for #enum_name<'_> { - fn from(e: #error_type) -> Self { + impl<'__ser> ::core::convert::From<#converted> for #enum_name<'__ser> { + fn from(e: #converted) -> Self { #enum_name::#variant_name(e) } } @@ -702,8 +704,9 @@ fn generate_reply_params_enum( }; if &return_type.to_token_stream().to_string() == type_str { let variant_name = format_ident!("__{}Variant{}", method_call_name, idx); + let converted = convert_type_lifetimes(return_type, "'__ser"); variants.push(quote! { - #variant_name(#return_type) + #variant_name(#converted) }); break; } diff --git a/zlink-macros/tests/service.rs b/zlink-macros/tests/service.rs index 5623735..c702308 100644 --- a/zlink-macros/tests/service.rs +++ b/zlink-macros/tests/service.rs @@ -5,6 +5,8 @@ #[path = "service/basic.rs"] mod basic; +#[path = "service/borrowed-types.rs"] +mod borrowed_types; #[path = "service/custom_bounds.rs"] mod custom_bounds; #[path = "service/fd_passing.rs"] diff --git a/zlink-macros/tests/service/borrowed-types.rs b/zlink-macros/tests/service/borrowed-types.rs new file mode 100644 index 0000000..9bb2b85 --- /dev/null +++ b/zlink-macros/tests/service/borrowed-types.rs @@ -0,0 +1,121 @@ +//! Tests for service macro with borrowed types (lifetimes in error and return types). + +use serde::{Deserialize, Serialize}; +use zlink::{ + introspect::{self, CustomType}, + unix::{bind, connect}, + Server, +}; + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn service_macro_borrowed_types() -> Result<(), Box> { + let socket_path = "/tmp/zlink-service-borrowed-types-test.sock"; + if let Err(e) = tokio::fs::remove_file(socket_path).await { + if e.kind() != std::io::ErrorKind::NotFound { + return Err(e.into()); + } + } + + let listener = bind(socket_path).unwrap(); + let service = Calculator; + let server = Server::new(listener, service); + tokio::select! { + res = server.run() => res?, + res = run_client(socket_path) => res?, + } + + Ok(()) +} + +async fn run_client(socket_path: &str) -> Result<(), Box> { + let mut conn = connect(socket_path).await?; + + // Test successful operation. + let reply = conn.divide(10.0, 2.0).await?.unwrap(); + assert_eq!(reply.result, 5.0); + + // Test error with borrowed string fields. + 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 another error variant. + let Err(CalculatorError::InvalidInput { field, reason }) = conn.divide(2000000.0, 2.0).await? + else { + panic!("Expected InvalidInput error"); + }; + assert_eq!(field, "dividend"); + assert_eq!(reason, "must be within range"); + + // Test method with borrowed params. + let reply = conn.greet("world").await?.unwrap(); + assert_eq!(reply.message, "Hello, world!"); + + Ok(()) +} + +// Return type with owned data. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +struct CalculationResult { + result: f64, +} + +// Return type with owned data. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +struct Greeting { + message: String, +} + +// Error type with borrowed string fields. +#[derive(Debug, PartialEq, zlink::ReplyError, introspect::ReplyError)] +#[zlink(interface = "org.example.BorrowedCalc")] +enum CalculatorError<'a> { + DivisionByZero { message: &'a str }, + InvalidInput { field: &'a str, reason: &'a str }, +} + +struct Calculator; + +#[zlink::service(types = [CalculationResult, Greeting])] +impl Calculator { + #[zlink(interface = "org.example.BorrowedCalc")] + async fn divide( + &self, + dividend: f64, + divisor: f64, + ) -> Result> { + 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 { + Ok(CalculationResult { + result: dividend / divisor, + }) + } + } + + // Method that takes a borrowed param. + async fn greet(&self, name: &str) -> Greeting { + Greeting { + message: format!("Hello, {name}!"), + } + } +} + +// Proxy with borrowed error type. +#[zlink::proxy("org.example.BorrowedCalc")] +trait CalculatorProxy { + async fn divide( + &mut self, + dividend: f64, + divisor: f64, + ) -> zlink::Result>>; + async fn greet(&mut self, name: &str) -> zlink::Result>>; +}