Introspection & code generation
We’ve seen that the service macro implements the standard
org.varlink.service introspection interface for you. This chapter covers the other side — how to
consume introspection from a client — and how to turn IDL into ready-made Rust code.
The relevant cargo features are:
idl— the Rust representation of Varlink IDL (zlink::idlmodule:Interface,Method,Typeand friends);idl-parse— parsing IDL text into those types at runtime (works inno_std, too);introspection— theintrospectmodule: traits and derives that describe Rust types in IDL terms at compile time, plus theorg.varlink.serviceclient support.
Querying a service
The zlink::varlink_service module ships a ready-made proxy for
org.varlink.service. Since proxy traits are implemented directly on Connection, any
connection can be introspected — just bring the trait into scope:
use zlink::varlink_service::Proxy as _;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut conn = zlink::tokio::unix::connect("/run/systemd/resolve/io.systemd.Resolve").await?;
// Who are you?
let info = conn.get_info().await?.map_err(|e| e.to_string())?;
println!("{} {} (by {})", info.product, info.version, info.vendor);
for interface in &info.interfaces {
println!(" implements {interface}");
}
// Tell me about that interface.
let description = conn
.get_interface_description("io.systemd.Resolve")
.await?
.map_err(|e| e.to_string())?;
let interface = description.parse()?; // -> zlink::idl::Interface (needs `idl-parse`)
for method in interface.methods() {
println!("method {}", method.name());
}
Ok(())
}
The parsed Interface gives you programmatic access to everything in the IDL: methods with their
parameters, custom types (interface.custom_types()), errors (interface.errors()), and doc
comments. The repository’s varlink-inspect example is a tiny varlinkctl introspect clone
built on exactly this API:
cargo run --example varlink-inspect --features="introspection idl-parse" -- \
/run/systemd/resolve/io.systemd.Resolve
Describing your types: the introspect derives
For the compile-time direction — describing Rust types in IDL terms so the service macro can
generate interface descriptions — the introspect module provides three derives:
introspect::Typefor structs and enums used as parameters or replies;introspect::CustomTypefor types that should appear as named custom types in the IDL;introspect::ReplyErrorfor error enums (derived alongsidezlink::ReplyError).
You’ve already seen them in earlier chapters; they all support #[zlink(rename)]/rename_all to
control the IDL-level naming.
Code generation
Writing proxy traits and types by hand is fine for small interfaces, but when a service publishes
its IDL, you can generate the Rust side mechanically with zlink-codegen — zlink’s analog of
zbus’s zbus_xmlgen:
# Install the code generator
cargo install zlink-codegen
# Get the IDL of a running service...
varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve \
> io.systemd.Resolve.varlink
# ...and generate Rust code from it
zlink-codegen io.systemd.Resolve.varlink > src/resolve.rs
For each interface, the generated code contains a #[proxy] trait with one method per IDL method,
structs for the custom types (and for multi-value method returns), and a ReplyError enum covering
the interface’s errors. IDL types map the way you’d expect: ?T → Option<T>, []T → Vec<T>,
[string]T → HashMap<String, T>; input parameters use borrowed types (&str, &[T]) for
zero-copy calls.
From a build script
zlink-codegen is also a library, so you can regenerate bindings on every build instead of
committing them:
// build.rs
use std::{env, path::PathBuf};
fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let idl = PathBuf::from(&manifest_dir).join("io.systemd.Resolve.varlink");
println!("cargo:rerun-if-changed={}", idl.display());
let out = PathBuf::from(env::var("OUT_DIR").unwrap()).join("generated.rs");
zlink_codegen::generate_files(&zlink_codegen::CodegenOptions {
files: vec![idl],
output: Some(out),
rustfmt: true,
..Default::default()
})
.expect("Failed to generate code");
}
…and then pull the generated code into your crate with
include!(concat!(env!("OUT_DIR"), "/generated.rs")); — the
test-integration crate in the zlink repository demonstrates this exact setup
end to end.
As with zbus_xmlgen, treat generator output as a starting point when hand-tuning is warranted —
e.g. to use richer Rust types than the IDL can express — and as a fully-automatic solution when it
isn’t.