proxy

Attribute Macro proxy 

Source
#[proxy]
Expand description

Creates a client-side proxy for calling Varlink methods on a connection.

Requires the proxy feature to be enabled.

This attribute macro generates an implementation of the provided trait for Connection<S>, automatically handling the serialization of method calls and deserialization of responses. Each proxy trait targets a single Varlink interface.

The macro also generates a chain extension trait that allows you to chain multiple method calls together for efficient batching across multiple interfaces.

§Supported Attributes

The following attributes can be used to customize the behavior of this macro:

  • interface (required) - The Varlink interface name (e.g., "org.varlink.service").
  • crate - Specifies the crate path to use for zlink types. Defaults to ::zlink.
  • chain_name - Custom name for the generated chain extension trait. Defaults to {TraitName}Chain.

§Example

use zlink::proxy;
use serde::{Deserialize, Serialize};
use serde_prefix_all::prefix_all;
use futures_util::stream::Stream;

#[proxy("org.example.MyService")]
trait MyServiceProxy {
    async fn get_status(&mut self) -> zlink::Result<Result<Status<'_>, MyError<'_>>>;
    async fn set_value(
        &mut self,
        key: &str,
        value: i32,
    ) -> zlink::Result<Result<(), MyError<'_>>>;
    // This will call the `io.systemd.Machine.List` method when `list_machines()` is invoked.
    #[zlink(rename = "ListMachines")]
    async fn list_machines(&mut self) -> zlink::Result<Result<Vec<Machine<'_>>, MyError<'_>>>;
    // Streaming version of get_status - calls the same method but returns a stream
    #[zlink(rename = "GetStatus", more)]
    async fn stream_status(
        &mut self,
    ) -> zlink::Result<
        impl Stream<Item = zlink::Result<Result<Status<'_>, MyError<'_>>>>,
    >;
}

// The macro generates:
// impl<S: Socket> MyServiceProxy for Connection<S> { ... }

#[derive(Debug, Serialize, Deserialize)]
struct Status<'m> {
    active: bool,
    message: &'m str,
}

#[derive(Debug, Serialize, Deserialize)]
struct Machine<'m> { name: &'m str }

#[prefix_all("org.example.MyService.")]
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "error", content = "parameters")]
enum MyError<'a> {
    NotFound,
    InvalidRequest,
    // Parameters must be named.
    CodedError { code: u32, message: &'a str },
}

// Example usage:
let result = conn.get_status().await?.unwrap();
assert_eq!(result.active, true);
assert_eq!(result.message, "System running");

§Chaining Method Calls

The proxy macro generates chain extension traits that allow you to batch multiple method calls together. This is useful for reducing round trips and efficiently calling methods across multiple interfaces. Each method gets a chain_ prefixed variant that starts a chain.

§Example: Chaining Method Calls

// Define proxies for two different services
#[proxy("org.example.blog.Users")]
trait UsersProxy {
    async fn get_user(&mut self, id: u64) -> zlink::Result<Result<BlogReply<'_>, BlogError>>;
    async fn create_user(&mut self, name: &str)
        -> zlink::Result<Result<BlogReply<'_>, BlogError>>;
}

#[proxy("org.example.blog.Posts")]
trait PostsProxy {
    async fn get_posts_by_user(&mut self, user_id: u64)
        -> zlink::Result<Result<BlogReply<'_>, BlogError>>;
    async fn create_post(&mut self, user_id: u64, content: &str)
        -> zlink::Result<Result<BlogReply<'_>, BlogError>>;
}

// Chain calls across both interfaces in a single batch
let chain = conn
    .chain_create_user::<BlogReply<'_>, BlogError>("Alice")? // Start with Users interface
    .create_post(1, "My first post!")?                       // Chain Posts interface
    .get_posts_by_user(1)?                                   // Get all posts
    .get_user(1)?;                                           // Get user details

// Send all calls in a single batch
let replies = chain.send().await?;
pin_mut!(replies);

// Process replies in order
let mut reply_count = 0;
while let Some(reply) = replies.try_next().await? {
    let reply = reply?;
    reply_count += 1;
    match reply.parameters() {
        Some(BlogReply::User(user)) => assert_eq!(user.name, "Alice"),
        Some(BlogReply::Post(post)) => assert_eq!(post.content, "My first post!"),
        Some(BlogReply::Posts(posts)) => assert_eq!(posts.len(), 1),
        None => {} // set_value returns empty response
    }
}
assert_eq!(reply_count, 4); // We made 4 calls

When the idl-parse feature is enabled, you can also chain calls between your custom interfaces and the standard Varlink service interface for introspection:

#[proxy("com.example.MyService")]
trait MyServiceProxy {
    async fn get_status(&mut self) -> zlink::Result<Result<Status, MyError>>;
}

// Combined types for cross-interface chaining
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum CombinedReply<'a> {
    #[serde(borrow)]
    VarlinkService(varlink_service::Reply<'a>),
    MyService(Status),
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum CombinedError {
    VarlinkService(varlink_service::Error),
    MyService(MyError),
}

// Example usage:
use varlink_service::Proxy;
use zlink::varlink_service::Chain;

// Get service info and custom status in one batch
let chain = conn
    .chain_get_info::<CombinedReply<'_>, CombinedError>()? // Varlink service interface
    .get_status()?                                         // MyService interface
    .get_interface_description("com.example.MyService")?;  // Back to Varlink service

let replies = chain.send().await?;

§Chain Extension Traits

For each proxy trait, the macro generates a corresponding chain extension trait. For example, FtlProxy gets FtlProxyChain. This trait is automatically implemented for Chain types, allowing seamless method chaining across interfaces.

§Method Requirements

Proxy methods must:

  • Take &mut self as the first parameter
  • Can be either async fn or return impl Future
  • Return zlink::Result<Result<ReplyType, ErrorType>> (outer Result for connection errors, inner for method errors)
  • The arguments can be any type that implement serde::Serialize
  • The reply type (Ok case of the inner Result) must be a type that implements serde::Deserialize and deserializes itself from a JSON object. Typically you’d just use a struct that derives serde::Deserialize.
  • The reply error type (Err case of the inner Result) must be a type serde::Deserialize that deserializes itself from a JSON object with two fields:
    • error: a string containing the fully qualified error name
    • parameters: an optional object containing all the fields of the error

§Method Names

By default, method names are converted from snake_case to PascalCase for the Varlink call. To specify a different Varlink method name, use the #[zlink(rename = "...")] attribute. See list_machines in the example above.

§Streaming Methods

For methods that support streaming (the ‘more’ flag), use the #[zlink(more)] attribute. Streaming methods must return Result<impl Stream<Item = Result<Result<ReplyType, ErrorType>>>>. The proxy will automatically set the ‘more’ flag on the call and return a stream of replies.

§Generic Parameters

The proxy macro supports generic type parameters on individual methods. Note that generic parameters on the trait itself are not currently supported.

#[proxy("org.example.Storage")]
trait StorageProxy {
    // Method-level generics with trait bounds
    async fn store<'a, T: Serialize + std::fmt::Debug>(
        &mut self,
        key: &'a str,
        value: T,
    ) -> zlink::Result<Result<(), StorageError>>;

    // Generic methods with where clauses
    async fn process<T>(&mut self, data: T)
        -> zlink::Result<Result<ProcessReply<'_>, StorageError>>
    where
        T: Serialize + std::fmt::Debug;

    // Methods can use generic type parameters in both input and output
    async fn store_and_return<'a, T>(&mut self, key: &'a str, value: T)
        -> zlink::Result<Result<StoredValue<T>, StorageError>>
    where
        T: Serialize + for<'de> Deserialize<'de> + std::fmt::Debug;
}

// Example usage:
// Store a value with generic type
let result = conn.store("my-key", 42i32).await?;
assert!(result.is_ok());