#[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
§Combining with Standard Varlink Service
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 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 (
Ok
case of the innerResult
) must be a type that implementsserde::Deserialize
and deserializes itself from a JSON object. Typically you’d just use a struct that derivesserde::Deserialize
. - The reply error type (
Err
case of the innerResult
) must be a typeserde::Deserialize
that 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.
§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());