#[service]Expand description
Transforms an impl block into a Service trait implementation.
Requires the service feature to be enabled. The service feature automatically enables
the introspection feature, allowing the macro to generate interface descriptions and handle
the standard org.varlink.service interface automatically.
This attribute macro takes a regular impl block and generates the necessary code to implement
the Service trait, enabling the type to handle Varlink method calls.
§Automatic Introspection Support
The generated service automatically handles the org.varlink.service interface:
GetInfo: Returns service metadata (vendor, product, version, URL) and a list of all implemented interfaces.GetInterfaceDescription: Returns the IDL description for any implemented interface, generated at compile time from the method signatures and types.- Unknown methods: Return
MethodNotFounderror with the method name. - Unknown interfaces (in
GetInterfaceDescription): ReturnInterfaceNotFounderror.
§Supported Attributes
§On the impl block:
crate = "path"- Specifies the crate path to use for zlink types. Defaults to::zlink.interface = "..."- Sets the default interface name for all methods. Useful for services that implement a single interface. Methods can still override this with method-level#[zlink(interface = "...")].types = [Type1, Type2, ...]- Custom types to include in interface descriptions. These types must implementCustomType(typically via#[derive(CustomType)]). The types are included in the IDL for any interface that uses them.vendor = <expr>- The vendor name forGetInforesponse. Defaults to empty string.product = <expr>- The product name forGetInforesponse. Defaults to empty string.version = <expr>- The version string forGetInforesponse. Defaults to empty string. E.g.version = env!("CARGO_PKG_VERSION").url = <expr>- The URL forGetInforesponse. Defaults to empty string.
§On methods:
#[zlink(interface = "...")]- Set the interface name for this and subsequent methods. If an interface is specified at the impl block level, this overrides it for the current method.#[zlink(rename = "MethodName")]- Custom Varlink method name.
§On parameters:
#[zlink(rename = "paramName")]- Custom serialized parameter name.#[zlink(connection)]- Mark this parameter to receive a mutable reference to the connection. This is useful for accessing peer credentials or other connection-specific functionality. Requires an explicit generic socket type parameter (e.g.,impl<Sock> MyService).
§Generated Code
The macro generates an impl<Sock: Socket> Service<Sock> for YourType with the handle method,
along with internal helper types for serialization/deserialization.
§Error Handling
Methods can return Result<T, E> with any error type E that implements Serialize and
Debug. Different methods can use different error types - the macro automatically generates
internal wrapper types to handle all unique error types.
When a method returns Err(e), the macro generates code that wraps it in the appropriate
combo enum variant and returns MethodReply::Error(...).
When a method returns Ok(v), it returns MethodReply::Single(Some(v)).
Methods can also return plain values (not wrapped in Result) - these are always treated as successful responses.
§Custom Socket Bounds
By default, the generated Service impl uses a generic socket parameter with just the Socket
bound. If you need additional bounds (e.g., for peer credential checking), you can provide your
own generics on the impl block:
use zlink::{service, connection::socket::FetchPeerCredentials, introspect};
struct MyService;
#[derive(Debug, Clone, PartialEq, zlink::ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.service")]
enum MyError {
ServiceError,
}
#[service]
impl<Sock> MyService
where
Sock::ReadHalf: FetchPeerCredentials,
{
#[zlink(interface = "org.example.service")]
async fn get_status(&self) -> Result<(), MyError> {
Ok(())
}
}The first type parameter is used as the socket type for the generated Service impl. The
Socket bound is automatically added, so you only need to specify additional bounds.
§Connection Parameter
Methods can receive a mutable reference to the connection using #[zlink(connection)]:
use zlink::{service, Connection, connection::socket::FetchPeerCredentials, introspect};
struct MyService;
#[derive(Debug, Clone, PartialEq, zlink::ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.service")]
enum MyError {
ServiceError,
}
#[service]
impl<Sock> MyService
where
Sock::ReadHalf: FetchPeerCredentials,
{
#[zlink(interface = "org.example.service")]
async fn check_credentials(
&self,
#[zlink(connection)] conn: &mut Connection<Sock>,
) -> Result<(), MyError> {
let _creds = conn.peer_credentials().await;
Ok(())
}
}Methods with connection parameters are only callable through the Service trait (not directly
on the type), since they require the socket type to be known.
§Streaming Methods
Methods that send multiple replies (the Varlink more flag) are marked with #[zlink(more)].
Such a method must:
- Take
more: boolas the first parameter afterself. This receives the value of the call’smoreflag, allowing the method to behave differently when the client only wants a single reply. - Return
impl Stream<Item = Reply<T>>(orimpl Stream<Item = (Reply<T>, Vec<OwnedFd>)>when combined with#[zlink(return_fds)]). A concrete stream type is also accepted; in that case the macro infersReply<T>from the type’s first generic parameter. - Set
Reply::set_continues(Some(true))on every intermediate item andSome(false)on the final one so that the client knows when the stream ends.
§Returning Method Errors From Streams
Streaming methods can also opt in to emitting Varlink error replies as stream items. To do so,
return a stream whose item is Result<Reply<T>, E> (or (Result<Reply<T>, E>, Vec<OwnedFd>)
for #[zlink(return_fds)]). When the stream yields Err(e), the server sends an error reply
to the client. The error type E must implement serde::Serialize and Debug, just like for
non-streaming methods, and (because the stream outlives &self) cannot borrow from the
service. Different streaming methods may use different error types; the macro combines them
into a single internal enum exposed as Service::ReplyStreamError.
Note that, on the wire, an error reply terminates the stream — a client that receives an error reply should not expect any further items. The macro does not enforce this on the server side, so callers can still drain the rest of the stream if the server emits more items after an error.
§Example
use futures_util::Stream;
use serde::{Deserialize, Serialize};
use zlink::{introspect::Type, service, Reply};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
struct Tick {
value: u32,
}
struct Counter;
#[service(interface = "org.example.counter")]
impl Counter {
// Streams `Tick` values from 1 to `to`. If the caller did not set the `more` flag, only a
// single tick is emitted.
#[zlink(more)]
async fn count(
&self,
more: bool,
to: u32,
) -> impl Stream<Item = Reply<Tick>> + Unpin {
let to = if more { to.max(1) } else { 1 };
futures_util::stream::iter((1..=to).map(move |value| {
Reply::new(Some(Tick { value })).set_continues(Some(value < to))
}))
}
}§Streaming With Errors
use futures_util::Stream;
use serde::{Deserialize, Serialize};
use zlink::{introspect, service, Reply};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, introspect::Type)]
struct Tick { value: u32 }
#[derive(Debug, Clone, PartialEq, zlink::ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.counter")]
enum CountError {
AtZero,
}
struct Counter;
#[service(interface = "org.example.counter")]
impl Counter {
#[zlink(more)]
async fn count(
&self,
_more: bool,
to: u32,
) -> impl Stream<Item = Result<Reply<Tick>, CountError>> + Unpin {
if to == 0 {
return futures_util::stream::iter(vec![Err(CountError::AtZero)]);
}
let last = to;
let items: Vec<Result<Reply<Tick>, CountError>> = (1..=to)
.map(move |value| {
Ok(Reply::new(Some(Tick { value })).set_continues(Some(value < last)))
})
.collect();
futures_util::stream::iter(items)
}
}§Example
use zlink::{
introspect::{self, Type},
service,
unix::{bind, connect},
Server,
};
use serde::{Deserialize, Serialize};
// Response type for balance operations.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type)]
struct Balance {
amount: i64,
}
// Error type - must derive zlink::ReplyError for proper serialization.
#[derive(Debug, Clone, PartialEq, zlink::ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.bank")]
enum BankError {
InsufficientFunds { available: i64, requested: i64 },
InvalidAmount { amount: i64 },
AccountLocked,
}
struct BankAccount {
balance: i64,
locked: bool,
}
impl BankAccount {
fn new(initial_balance: i64) -> Self {
Self { balance: initial_balance, locked: false }
}
}
// Service implementation with error handling.
#[service]
impl BankAccount {
// Method that returns a plain value (not Result) - always succeeds.
#[zlink(interface = "org.example.bank")]
async fn get_balance(&self) -> Balance {
Balance { amount: self.balance }
}
// Method that can fail - returns Result<Balance, BankError>.
async fn deposit(&mut self, amount: i64) -> Result<Balance, BankError> {
if self.locked {
return Err(BankError::AccountLocked);
}
if amount <= 0 {
return Err(BankError::InvalidAmount { amount });
}
self.balance += amount;
Ok(Balance { amount: self.balance })
}
async fn withdraw(&mut self, amount: i64) -> Result<Balance, BankError> {
if self.locked {
return Err(BankError::AccountLocked);
}
if amount <= 0 {
return Err(BankError::InvalidAmount { amount });
}
if amount > self.balance {
return Err(BankError::InsufficientFunds {
available: self.balance,
requested: amount,
});
}
self.balance -= amount;
Ok(Balance { amount: self.balance })
}
// Method returning Result<(), E> - void success, can fail.
async fn lock_account(&mut self) -> Result<(), BankError> {
if self.locked {
return Err(BankError::AccountLocked);
}
self.locked = true;
Ok(())
}
}
// Client-side proxy definition.
#[zlink::proxy("org.example.bank")]
trait BankProxy {
async fn get_balance(&mut self) -> zlink::Result<Result<Balance, BankError>>;
async fn deposit(&mut self, amount: i64) -> zlink::Result<Result<Balance, BankError>>;
async fn withdraw(&mut self, amount: i64) -> zlink::Result<Result<Balance, BankError>>;
async fn lock_account(&mut self) -> zlink::Result<Result<(), BankError>>;
}
// Server setup.
let socket_path = "/tmp/zlink-service-example.sock";
let _ = std::fs::remove_file(socket_path);
let listener = bind(socket_path)?;
let service = BankAccount::new(1000);
let server = Server::new(listener, service);
// Run server and client concurrently.
tokio::select! {
res = server.run() => res?,
res = async {
let mut conn = connect(socket_path).await?;
// Check initial balance.
let balance = conn.get_balance().await?.unwrap();
assert_eq!(balance.amount, 1000);
// Successful deposit.
let balance = conn.deposit(500).await?.unwrap();
assert_eq!(balance.amount, 1500);
// Successful withdrawal.
let balance = conn.withdraw(200).await?.unwrap();
assert_eq!(balance.amount, 1300);
// Error: withdraw more than available.
let err = conn.withdraw(5000).await?.unwrap_err();
assert_eq!(err, BankError::InsufficientFunds { available: 1300, requested: 5000 });
// Error: invalid amount.
let err = conn.deposit(-100).await?.unwrap_err();
assert_eq!(err, BankError::InvalidAmount { amount: -100 });
// Lock account and verify subsequent operations fail.
conn.lock_account().await?.unwrap();
let err = conn.withdraw(100).await?.unwrap_err();
assert_eq!(err, BankError::AccountLocked);
Ok::<(), Box<dyn std::error::Error>>(())
} => res?,
}§Introspection Example
The service automatically provides introspection via the org.varlink.service interface:
use zlink::{
introspect::{self, CustomType, Type},
service,
varlink_service::Proxy,
};
use serde::{Deserialize, Serialize};
// Custom type - must derive CustomType to be included in IDL.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
struct Balance {
amount: i64,
}
#[derive(Debug, Clone, PartialEq, zlink::ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.bank")]
enum BankError {
InsufficientFunds { available: i64 },
}
struct BankService;
// Include custom types in the service for IDL generation.
#[service(
types = [Balance],
vendor = "Example Corp",
product = "Bank Service",
version = env!("CARGO_PKG_VERSION"),
url = "https://example.com/bank"
)]
impl BankService {
#[zlink(interface = "org.example.bank")]
async fn get_balance(&self) -> Result<Balance, BankError> {
Ok(Balance { amount: 1000 })
}
}
// GetInfo returns metadata and list of interfaces.
let info = conn.get_info().await?.unwrap();
assert_eq!(info.vendor, "Example Corp");
let interfaces: Vec<&str> = info.interfaces.iter().map(|s| s.as_ref()).collect();
assert_eq!(interfaces.as_slice(), ["org.example.bank", "org.varlink.service"]);
// GetInterfaceDescription returns the IDL, which can be parsed to verify methods and types.
let desc = conn.get_interface_description("org.example.bank").await?.unwrap();
let interface = desc.parse()?;
assert_eq!(interface.name(), "org.example.bank");
// Verify methods are present.
let method_names: Vec<_> = interface.methods().map(|m| m.name()).collect();
assert_eq!(method_names.as_slice(), ["GetBalance"]);
// Verify custom types are included.
let type_names: Vec<_> = interface.custom_types().map(|t| t.name()).collect();
assert_eq!(type_names.as_slice(), ["Balance"]);
// Verify errors are present.
let error_names: Vec<_> = interface.errors().map(|e| e.name()).collect();
assert_eq!(error_names.as_slice(), ["InsufficientFunds"]);§Method Name Conversion
By default, method names are converted from snake_case to PascalCase for the Varlink call.
For example, get_balance becomes GetBalance. Use #[zlink(rename = "...")] to override
this.
§Full Method Path
The full Varlink method path is constructed as {interface}.{MethodName}. For example,
if the interface is org.example.bank and the method is GetBalance, the full path
will be org.example.bank.GetBalance.
§Interface Propagation
The interface for methods is determined in this order:
- If the method has
#[zlink(interface = "...")], that interface is used. - Otherwise, the interface is inherited from the previous method or from the macro-level
interface = "..."attribute.
For services implementing a single interface, specifying interface = "..." at the macro level
is the simplest approach - all methods automatically use that interface without needing
individual attributes.