Skip to main content

service

Attribute Macro service 

Source
#[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 MethodNotFound error with the method name.
  • Unknown interfaces (in GetInterfaceDescription): Return InterfaceNotFound error.

§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 implement CustomType (typically via #[derive(CustomType)]). The types are included in the IDL for any interface that uses them.
  • vendor = <expr> - The vendor name for GetInfo response. Defaults to empty string.
  • product = <expr> - The product name for GetInfo response. Defaults to empty string.
  • version = <expr> - The version string for GetInfo response. Defaults to empty string. E.g. version = env!("CARGO_PKG_VERSION").
  • url = <expr> - The URL for GetInfo response. 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: bool as the first parameter after self. This receives the value of the call’s more flag, allowing the method to behave differently when the client only wants a single reply.
  • Return impl Stream<Item = Reply<T>> (or impl Stream<Item = (Reply<T>, Vec<OwnedFd>)> when combined with #[zlink(return_fds)]). A concrete stream type is also accepted; in that case the macro infers Reply<T> from the type’s first generic parameter.
  • Set Reply::set_continues(Some(true)) on every intermediate item and Some(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:

  1. If the method has #[zlink(interface = "...")], that interface is used.
  2. 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.