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 {
    // Non-streaming methods can use borrowed types.
    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<'_>>>;
    #[zlink(rename = "ListMachines")]
    async fn list_machines(&mut self) -> zlink::Result<Result<Vec<Machine<'_>>, MyError<'_>>>;
    // Streaming methods must use owned types (DeserializeOwned) because the internal buffer may
    // be reused between stream iterations.
    #[zlink(rename = "GetStatus", more)]
    async fn stream_status(
        &mut self,
    ) -> zlink::Result<
        impl Stream<Item = zlink::Result<Result<OwnedStatus, OwnedMyError>>>,
    >;
}

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

// Borrowed types for non-streaming methods.
#[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 },
}

// Owned types for streaming methods (required by the `more` attribute).
#[derive(Debug, Serialize, Deserialize)]
struct OwnedStatus {
    active: bool,
    message: String,
}

#[prefix_all("org.example.MyService.")]
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "error", content = "parameters")]
enum OwnedMyError {
    NotFound,
    InvalidRequest,
    CodedError { code: u32, message: String },
}

// 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.

Important: Chain methods are only generated for proxy methods that use owned types (DeserializeOwned) in their return type. Methods with borrowed types (non-static lifetimes) don’t get chain variants since the internal buffer may be reused between stream iterations. Input arguments can still use borrowed types.

§Example: Chaining Method Calls

// Owned reply types for chain API.
// Define proxies with owned return types - chain methods are generated.
#[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>>;
}

let chain = conn
    .chain_create_user("Alice")?
    .create_post(1, "My first post!")?
    .get_posts_by_user(1)?
    .get_user(1)?;

// Send all calls in a single batch.
let replies = chain.send::<BlogReply, BlogError>().await?;
pin_mut!(replies);

// Process replies in order.
let mut reply_count = 0;
while let Some((reply, _fds)) = replies.try_next().await? {
    reply_count += 1;
    if let Ok(response) = reply {
        match response.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

§Combining Multiple Services

You can chain calls across multiple custom services. Define a combined reply type that can deserialize responses from all interfaces:

// Multiple proxies with owned return types.
#[proxy("com.example.StatusService")]
trait StatusProxy {
    async fn get_status(&mut self) -> zlink::Result<Result<Status, ServiceError>>;
}

#[proxy("com.example.HealthService")]
trait HealthProxy {
    async fn get_health(&mut self) -> zlink::Result<Result<HealthInfo, ServiceError>>;
}

// Combined reply type for cross-interface chaining.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum CombinedReply {
    Status(Status),
    Health(HealthInfo),
}

// Chain calls across both services.
let chain = conn
    .chain_get_status()?
    .get_health()?;

let replies = chain.send::<CombinedReply, ServiceError>().await?;
pin_mut!(replies);

let mut count = 0;
while let Some((reply, _fds)) = replies.try_next().await? {
    count += 1;
    if let Ok(response) = reply {
        match response.parameters() {
            Some(CombinedReply::Status(s)) => println!("Status: {}", s.message),
            Some(CombinedReply::Health(h)) => println!("Uptime: {}", h.uptime),
            None => {}
        }
    }
}
assert_eq!(count, 2);

§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.

§One-way Methods

For fire-and-forget methods that don’t expect a reply, use the #[zlink(oneway)] attribute. One-way methods send the call and return immediately without waiting for a response. The method must return zlink::Result<()> (just the outer Result for connection errors, no inner Result since there’s no reply to process).

One-way methods cannot be combined with #[zlink(more)] or #[zlink(return_fds)].

This attribute is particularly useful in combination with chaining method calls. When you chain oneway methods with regular methods, the oneway calls are sent but don’t contribute to the reply stream. For example, if you chain 4 calls where 2 are regular and 2 are oneway, you’ll only receive 2 replies. This allows you to efficiently batch side-effect operations (like resets or notifications) alongside queries in a single round-trip.

#[proxy("org.example.Notifications")]
trait NotificationsProxy {
    /// Fire-and-forget notification - returns immediately without waiting for a reply.
    #[zlink(oneway)]
    async fn notify(&mut self, message: &str) -> zlink::Result<()>;

    /// Another one-way method with multiple parameters.
    #[zlink(oneway)]
    async fn log_event(&mut self, level: &str, message: &str, timestamp: u64)
        -> zlink::Result<()>;
}

§File Descriptor Passing

Requires the std feature to be enabled.

Methods can send and receive file descriptors using the following attributes:

§Sending File Descriptors

Use #[zlink(fds)] on a parameter of type Vec<OwnedFd> to send file descriptors with the method call. Only one FD parameter is allowed per method.

§Receiving File Descriptors

Use #[zlink(return_fds)] on a method to indicate it returns file descriptors. The method’s return type must be Result<(Result<ReplyType, ErrorType>, Vec<OwnedFd>)> - a tuple containing both the method result and the received file descriptors. The FDs are available regardless of whether the method succeeded or failed.

§Example: File Descriptor Passing

File descriptors are passed out-of-band from the encoded JSON parameters. The typical pattern is to include integer indexes in your JSON parameters that reference positions in the FD vector. This is similar to how D-Bus handles FD passing.

use zlink::proxy;
use serde::{Deserialize, Serialize};
use std::os::fd::OwnedFd;

#[proxy("org.example.FileService")]
trait FileServiceProxy {
    // Send file descriptors to the service
    // The stdin/stdout parameters are indexes into the FDs vector
    async fn spawn_process(
        &mut self,
        command: String,
        stdin_fd: u32,
        stdout_fd: u32,
        #[zlink(fds)] fds: Vec<OwnedFd>,
    ) -> zlink::Result<Result<ProcessInfo, FileError>>;

    // Receive file descriptors from the service
    // Returns metadata with FD indexes and the actual FDs
    #[zlink(return_fds)]
    async fn open_files(
        &mut self,
        paths: Vec<String>,
    ) -> zlink::Result<(Result<Vec<FileInfo>, FileError>, Vec<OwnedFd>)>;
}

#[derive(Debug, Serialize, Deserialize)]
struct ProcessInfo {
    pid: u32,
}

// Response contains FD indexes referencing the FD vector
#[derive(Debug, Serialize, Deserialize)]
struct FileInfo {
    path: String,
    fd: u32,  // Index into the FD vector (0, 1, 2, etc.)
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "error")]
enum FileError {
    NotFound { path: String },
    PermissionDenied { path: String },
}

// Example usage:
// Sending FDs: Pass indexes as regular parameters
let fds = vec![stdin_pipe.into(), stdout_pipe.into()];
// Parameters reference FD indexes: stdin_fd=0, stdout_fd=1
let result = send_conn.spawn_process("/bin/cat".to_string(), 0, 1, fds).await?;
let process_info = result?;
assert_eq!(process_info.pid, 1234);

// Receiving FDs: Response contains indexes that reference the FD vector
let (result, received_fds) = recv_conn
    .open_files(vec!["/etc/config.txt".to_string(), "/var/data.bin".to_string()])
    .await?;
let file_list = result?;
assert_eq!(file_list.len(), 2);
assert_eq!(received_fds.len(), 2);
// Use the fd field to match file info with actual FDs
for file_info in &file_list {
    let fd = &received_fds[file_info.fd as usize];
    println!("File {} has FD at index {}", file_info.path, file_info.fd);
}

§Parameter Renaming

Use #[zlink(rename = "name")] on parameters to customize their serialized names in the Varlink protocol. This is useful when the Rust parameter name doesn’t match the expected Varlink parameter name.

#[proxy("org.example.Users")]
trait UsersProxy {
    async fn create_user(
        &mut self,
        #[zlink(rename = "user_name")] name: String,
        #[zlink(rename = "user_email")] email: String,
    ) -> zlink::Result<Result<UserId, UserError>>;
}

§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());