tokio_dbus_codegen/lib.rs
1//! [<img alt="github" src="https://img.shields.io/badge/github-udoprog/tokio--dbus-8da0cb?style=for-the-badge&logo=github" height="20">](https://github.com/udoprog/tokio-dbus)
2//! [<img alt="crates.io" src="https://img.shields.io/crates/v/tokio-dbus-codegen.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/tokio-dbus-codegen)
3//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-tokio--dbus--codegen-66c2a5?style=for-the-badge&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20">](https://docs.rs/tokio-dbus-codegen)
4//!
5//! Generate asynchronous D-Bus clients and servers from interface files at
6//! build time.
7//!
8//! The generated code speaks in owned Rust types — [`String`], [`Vec`],
9//! [`HashMap`] and tuples — and is driven by a
10//! [`tokio_dbus_runtime::Connection`], which both crates a consumer needs to
11//! depend on.
12//!
13//! [`HashMap`]: std::collections::HashMap
14//! [`tokio_dbus_runtime::Connection`]: https://docs.rs/tokio-dbus-runtime
15//!
16//! # Using it from a build script
17//!
18//! Add the generator as a build dependency and the runtime as a regular one:
19//!
20//! ```toml
21//! [dependencies]
22//! tokio-dbus-runtime = "0.1.0"
23//!
24//! [build-dependencies]
25//! tokio-dbus-codegen = "0.1.0"
26//! ```
27//!
28//! Then write a `build.rs` which names the interface files to read and what to
29//! generate for each interface in them:
30//!
31//! ```no_run
32//! fn main() -> Result<(), Box<dyn std::error::Error>> {
33//! tokio_dbus_codegen::Builder::new()
34//! .file("interfaces/org.freedesktop.Notifications.xml")
35//! .client("org.freedesktop.Notifications")
36//! .generate("notifications.rs")?;
37//!
38//! Ok(())
39//! }
40//! ```
41//!
42//! Finally include the generated file:
43//!
44//! ```rust,ignore
45//! include!(concat!(env!("OUT_DIR"), "/notifications.rs"));
46//! ```
47//!
48//! # What is generated
49//!
50//! For an interface `com.example.Example`, a module `example` is generated
51//! holding:
52//!
53//! * `INTERFACE` and `MATCH_RULE` constants.
54//! * A `Signal` enum with a variant per signal, which can be decoded from an
55//! incoming message and emitted from an outgoing one. Present whenever the
56//! interface has signals.
57//! * With [`client()`], an `Example` struct with an `async` method per D-Bus
58//! method, a getter and setter per property, and `all_properties()`.
59//! * With [`server()`], an `ExampleServer` trait with an `async` method per
60//! D-Bus method and per property accessor, and a `dispatch()` function which
61//! routes an incoming call to it. `dispatch()` also answers
62//! `org.freedesktop.DBus.Properties` for the interface.
63//!
64//! [`client()`]: Builder::client
65//! [`server()`]: Builder::server
66//!
67//! # Type mapping
68//!
69//! | D-Bus | Rust |
70//! |-------------|---------------------------------|
71//! | `y` | [`u8`] |
72//! | `b` | [`bool`] |
73//! | `n` / `q` | [`i16`] / [`u16`] |
74//! | `i` / `u` | [`i32`] / [`u32`] |
75//! | `x` / `t` | [`i64`] / [`u64`] |
76//! | `d` | [`f64`] |
77//! | `s` | [`String`] |
78//! | `o` | `ObjectPathBuf` |
79//! | `g` | `SignatureBuf` |
80//! | `v` | `Value` |
81//! | `aT` | `Vec<T>` |
82//! | `a{KV}` | `HashMap<K, V>` |
83//! | `(T1 T2)` | `(T1, T2)` |
84//!
85//! Client methods take the borrowed form of each argument where borrowing is
86//! free, so an `s` is passed as a [`&str`] and an `as` as a `&[String]`.
87//!
88//! Passing a file descriptor (`h`) is not supported.
89
90#![deny(missing_docs)]
91
92#[cfg(test)]
93mod tests;
94
95#[doc(inline)]
96pub use self::error::{Error, Result};
97mod error;
98
99#[doc(inline)]
100pub use self::naming::{argument_name, module_name, pascal_case, snake_case};
101mod naming;
102
103#[doc(inline)]
104pub use self::types::{owned_type, parameter_type};
105mod types;
106
107mod generate;
108
109use std::collections::HashMap;
110use std::env;
111use std::fs;
112use std::path::{Path, PathBuf};
113
114use genco::prelude::*;
115
116use self::error::ErrorKind;
117
118/// What to generate for an interface.
119#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
120pub struct Mode {
121 /// Generate an asynchronous client.
122 pub client: bool,
123 /// Generate a server trait and a dispatcher.
124 pub server: bool,
125}
126
127/// Reads interface files and generates bindings for the interfaces in them.
128///
129/// # Examples
130///
131/// ```no_run
132/// tokio_dbus_codegen::Builder::new()
133/// .file("interfaces/org.kde.StatusNotifierItem.xml")
134/// .file("interfaces/com.canonical.dbusmenu.xml")
135/// .server("org.kde.StatusNotifierItem")
136/// .server("com.canonical.dbusmenu")
137/// .client("org.kde.StatusNotifierWatcher")
138/// .generate("systray.rs")?;
139/// # Ok::<_, tokio_dbus_codegen::Error>(())
140/// ```
141#[derive(Debug, Default)]
142pub struct Builder {
143 files: Vec<PathBuf>,
144 modes: Vec<(String, Mode)>,
145}
146
147impl Builder {
148 /// Construct an empty builder.
149 pub fn new() -> Self {
150 Self::default()
151 }
152
153 /// Read an interface file.
154 ///
155 /// Every interface in the file becomes available to [`client()`],
156 /// [`server()`] and [`both()`], including the ones in child nodes.
157 ///
158 /// [`client()`]: Self::client
159 /// [`server()`]: Self::server
160 /// [`both()`]: Self::both
161 pub fn file(mut self, path: impl AsRef<Path>) -> Self {
162 self.files.push(path.as_ref().to_owned());
163 self
164 }
165
166 /// Generate an asynchronous client for the named interface.
167 pub fn client(self, interface: impl AsRef<str>) -> Self {
168 self.mode(interface, |mode| mode.client = true)
169 }
170
171 /// Generate a server trait and dispatcher for the named interface.
172 pub fn server(self, interface: impl AsRef<str>) -> Self {
173 self.mode(interface, |mode| mode.server = true)
174 }
175
176 /// Generate both a client and a server for the named interface.
177 pub fn both(self, interface: impl AsRef<str>) -> Self {
178 self.mode(interface, |mode| {
179 mode.client = true;
180 mode.server = true;
181 })
182 }
183
184 fn mode(mut self, interface: impl AsRef<str>, apply: impl FnOnce(&mut Mode)) -> Self {
185 let interface = interface.as_ref();
186
187 if let Some((_, mode)) = self.modes.iter_mut().find(|(name, _)| name == interface) {
188 apply(mode);
189 return self;
190 }
191
192 let mut mode = Mode::default();
193 apply(&mut mode);
194 self.modes.push((interface.to_owned(), mode));
195 self
196 }
197
198 /// Generate into `$OUT_DIR/<name>`, which is where a build script should
199 /// put it.
200 ///
201 /// This also emits a `cargo::rerun-if-changed` line for every interface file
202 /// which was read.
203 pub fn generate(self, name: impl AsRef<Path>) -> Result<PathBuf> {
204 let Some(out_dir) = env::var_os("OUT_DIR") else {
205 return Err(Error::new(ErrorKind::MissingOutDir));
206 };
207
208 for file in &self.files {
209 println!("cargo::rerun-if-changed={}", file.display());
210 }
211
212 let path = PathBuf::from(out_dir).join(name.as_ref());
213 self.write_to(&path)?;
214 Ok(path)
215 }
216
217 /// Generate into an explicit path.
218 pub fn write_to(self, path: impl AsRef<Path>) -> Result<()> {
219 let path = path.as_ref();
220 let output = self.to_string()?;
221
222 if let Some(parent) = path.parent() {
223 fs::create_dir_all(parent).map_err(|error| Error::io(parent, error))?;
224 }
225
226 fs::write(path, output).map_err(|error| Error::io(path, error))?;
227 Ok(())
228 }
229
230 /// Generate the bindings and return them as a string.
231 ///
232 /// This is what [`generate()`] and [`write_to()`] use, and is useful for
233 /// inspecting the output in a test.
234 ///
235 /// [`generate()`]: Self::generate
236 /// [`write_to()`]: Self::write_to
237 #[allow(clippy::inherent_to_string)]
238 pub fn to_string(&self) -> Result<String> {
239 let mut sources = Vec::new();
240
241 for file in &self.files {
242 let source = fs::read_to_string(file).map_err(|error| Error::io(file, error))?;
243 sources.push((file.clone(), source));
244 }
245
246 let mut interfaces = HashMap::new();
247
248 for (path, source) in &sources {
249 let node = tokio_dbus_xml::parse_interface(source)
250 .map_err(|error| Error::from(error).context(path.display()))?;
251
252 for interface in node.all_interfaces() {
253 interfaces.insert(interface.name.to_owned(), interface.clone());
254 }
255 }
256
257 let mut tokens = rust::Tokens::new();
258
259 tokens.append(quote! {
260 $("// This file is generated by tokio-dbus-codegen. Do not edit it by hand.")
261 });
262
263 tokens.line();
264
265 for (name, mode) in &self.modes {
266 let Some(interface) = interfaces.get(name.as_str()) else {
267 return Err(Error::new(ErrorKind::MissingInterface(
268 name.as_str().into(),
269 )));
270 };
271
272 let generated = self::generate::interface(interface, *mode)
273 .map_err(|error| error.context(format!("interface {name}")))?;
274
275 tokens.append(generated);
276 tokens.line();
277 }
278
279 Ok(tokens.to_file_string()?)
280 }
281}