#[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 selfas the first parameter - Can be either
async fnor returnimpl 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 (
Okcase of the innerResult) must be a type that implementsserde::Deserializeand deserializes itself from a JSON object. Typically you’d just use a struct that derivesserde::Deserialize. - The reply error type (
Errcase of the innerResult) must be a typeserde::Deserializethat deserializes itself from a JSON object with two fields:error: a string containing the fully qualified error nameparameters: 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());