Skip to main content

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