Windowing — Linux DBus
Overview
WIP — works on Wayland-on-Mutter and X11-on-GNOME 3+ when the compositor exposes the GTK menu globals. KDE / non-GNOME compositors fall back to the in-window CSD menu bar. The dbus/ and gnome_menu/ subtrees implement the GTK org.gtk.Menus / org.gtk.Actions DBus protocol so Azul windows can publish their menu bar to GNOME Shell's panel („global menu“). Azul loads libdbus-1.so.3 at runtime — there is no compile-time dependency on libdbus-dev, so cross-compiling from macOS still produces a Linux binary that picks up DBus when the target system has it installed.
Two layers
shell2/linux/
├── dbus/ ← Generic libdbus-1 dlopen layer
│ ├── mod.rs (re-exports DBusLib, DBusConnection, …)
│ └── dlopen.rs (DynamicLibrary loader for libdbus-1)
└── gnome_menu/ ← Application of dbus/ to GNOME's menu protocol
├── mod.rs (should_use_gnome_menus() entry point)
├── shared_dbus.rs (OnceLock<Arc<DBusLib>>)
├── manager.rs (GnomeMenuManager — owns the connection)
├── menu_conversion.rs (azul Menu → DbusMenuGroup)
├── menu_protocol.rs (DbusMenuItem / DbusMenuGroup types)
├── actions_protocol.rs (PendingMenuCallback queue)
├── protocol_impl.rs (object-path message handlers)
└── x11_properties.rs (sets _GTK_* atoms on the X11 window)
DBusLib — the dlopen layer
DBusLib:
pub struct DBusLib {
_lib: Library,
pub dbus_bus_get: unsafe extern "C" fn(c_int, *mut DBusError) -> *mut DBusConnection,
pub dbus_connection_unref: unsafe extern "C" fn(*mut DBusConnection),
pub dbus_connection_read_write_dispatch:
unsafe extern "C" fn(*mut DBusConnection, c_int) -> c_int,
pub dbus_connection_flush: unsafe extern "C" fn(*mut DBusConnection),
pub dbus_bus_request_name:
unsafe extern "C" fn(*mut DBusConnection, *const c_char, c_uint, *mut DBusError) -> c_int,
pub dbus_connection_register_object_path: unsafe extern "C" fn(
*mut DBusConnection, *const c_char, *const DBusObjectPathVTable, *mut c_void,
) -> c_int,
pub dbus_message_new_method_return: unsafe extern "C" fn(*mut DBusMessage) -> *mut DBusMessage,
pub dbus_message_iter_init: unsafe extern "C" fn(*mut DBusMessage, *mut DBusMessageIter) -> c_int,
pub dbus_message_iter_get_arg_type: unsafe extern "C" fn(*mut DBusMessageIter) -> c_int,
// … ~30 more function pointers, one per libdbus-1 entry point we use
}
DBusLib::new calls Library::load("libdbus-1.so.3") then load_first_available::<Library>(&["libdbus-1.so.3", "libdbus-1.so"]) under the hood; each function pointer is populated by load_symbol!.
Constants exposed from the dbus module:
DBUS_BUS_SESSION. Connect to the user's session bus rather than the system bus.DBUS_NAME_FLAG_DO_NOT_QUEUE. Refuse to queue if the requested name is already taken.DBUS_HANDLER_RESULT_HANDLED/_NOT_YET_HANDLED/_NEED_MEMORY. Return values from object-path message handlers.DBUS_TYPE_STRING/_UINT32/_ARRAY/_VARIANT. Argument-type tags used during message marshalling.
Many other type constants (DBUS_TYPE_BOOLEAN, DBUS_TYPE_INT32, DBUS_TYPE_DOUBLE, etc.) are defined but not currently re-exported; the autoreview report flags them as [MEDIUM] Dead Code — they're kept for future protocol expansion.
DBusError is the C-ABI error struct. The dummy* and pad* field names mirror the upstream /usr/include/dbus-1.0/dbus/dbus-types.h — these are not stub fields, they are the layout libdbus expects.
should_use_gnome_menus
The gate:
pub fn should_use_gnome_menus() -> bool {
if env::var("AZ_DISABLE_GNOME_MENUS").unwrap_or_default() == "1" { return false; }
let desktop = env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
if !desktop.to_lowercase().contains("gnome") { return false; }
if env::var("DBUS_SESSION_BUS_ADDRESS").is_err() { return false; }
true
}
Both X11 and Wayland windows call this at startup. When false, no DBus connection is opened and the windowing backend renders its menu bar in the title-bar area as part of the client-side decorations.
AZ_GNOME_MENU_DEBUG=1 enables verbose logging via debug_log — useful when GNOME Shell silently ignores a published menu.
GnomeMenuManager
pub struct GnomeMenuManager {
app_name: String,
bus_name: String, // "org.gtk.{app_name}"
object_path: String, // "/org/gtk/{app}"
dbus_lib: Arc<DBusLib>,
connection: *mut DBusConnection,
menu_groups: Arc<Mutex<HashMap<u32, DbusMenuGroup>>>,
actions: Arc<Mutex<HashMap<String, DbusAction>>>,
}
GnomeMenuManager::new(app_name, dbus_lib):
- Sanitises
app_name('.' / ' ' / '-' → '_') so it's a valid DBus name component. - Computes
bus_name = format!("org.gtk.{}", sanitized)andobject_path = format!("/org/gtk/{}", sanitized.replace('_','/')). dbus_bus_get(DBUS_BUS_SESSION, &mut error)to open the session bus connection.dbus_bus_request_name(connection, bus_name, DBUS_NAME_FLAG_DO_NOT_QUEUE, &mut error)to claim the name.register_menus_interface(...)andregister_actions_interface(...)to install the message handlers on the object path.
The dbus_lib is loaded once via shared_dbus::get_shared_dbus_lib() — a OnceLock<Option<Arc<DBusLib>>> that ensures a single dlopen per process even with many windows.
The *mut DBusConnection is not wrapped in Arc — Drop for GnomeMenuManager calls dbus_connection_unref when the window is closed.
org.gtk.Menus interface
pub struct DbusMenuItem {
pub label: String,
pub action: Option<String>, // "app.{action_name}"
pub target: Option<String>, // optional argument
pub submenu: Option<(u32, u32)>, // (group_id, menu_id)
pub section: Option<(u32, u32)>, // for separators
pub enabled: bool,
}
pub struct DbusMenuGroup {
pub group_id: u32,
pub menu_id: u32,
pub items: Vec<DbusMenuItem>,
}
MenuConversion::convert_menu walks the recursive azul_core::menu::Menu tree and flattens it: each level of submenus becomes its own DbusMenuGroup with a fresh group_id. The submenu: Some((id, 0)) field on a parent item references the child group by id — that is how GTK reconstructs the hierarchy on the panel side.
extract_actions is the second pass over the same tree: every menu item with a callback becomes a DbusAction named app.{label} (lower-cased + sanitised). The callback itself is kept on DbusAction.menu_callback: Option<CoreMenuCallback> so the event loop can dispatch it later.
The autoreview report flags that DbusMenuItem.enabled may not be serialised into the DBus variant dict — verify against protocol_impl.rs::menus_message_handler before relying on it.
org.gtk.Actions interface
actions_protocol.rs documents the four DBus methods Azul implements on its org.gtk.Actions object:
List. Signature() → as. Returns the action-name array.Describe. Signature(s) → (bsav). Returns(enabled, param_type, state)for one action.DescribeAll. Signature() → a{s(bsav)}. Returns all actions with descriptions.Activate. Signature(s, av, a{sv}). Invokes a callback.
Activation runs on the libdbus dispatch thread, not the window's event-loop thread. Calling the Azul callback there is unsafe — it would race the layout pass.
The fix:
pub struct PendingMenuCallback {
pub action_name: String,
pub menu_callback: CoreMenuCallback, // RefAny + fn-ptr
}
static PENDING_MENU_CALLBACKS: LazyLock<Mutex<Vec<PendingMenuCallback>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
pub fn queue_menu_callback(callback: PendingMenuCallback) { /* push */ }
pub fn drain_pending_menu_callbacks() -> Vec<PendingMenuCallback> { /* take */ }
The DBus handler queues; the X11/Wayland event loop drains and dispatches the callbacks at a safe point (between process_window_events and regenerate_layout).
Object-path registration
register_menus_interface and register_actions_interface install message handlers via dbus_connection_register_object_path:
let vtable = DBusObjectPathVTable {
unregister_function: Some(menus_unregister_handler),
message_function: Some(menus_message_handler),
..
};
let state = Box::new(HandlerState { dbus_lib, menu_groups, actions });
dbus_connection_register_object_path(
connection,
object_path_cstr.as_ptr(),
&vtable as *const _,
Box::into_raw(state) as *mut c_void,
);
The state box is leaked — it must outlive every async DBus message; libdbus passes it back as user_data to the message function. The unregister_function is the cleanup hook, fired by libdbus when the connection drops.
menus_message_handler decodes the incoming DBusMessage, switches on dbus_message_get_member ("Start", "End", "List", etc.), and marshals the response with dbus_message_iter_open_container / dbus_message_iter_append_basic.
X11 window properties
GNOME Shell needs to know which DBus name and object path serves a given window. On X11, this is published through window properties:
- Atom
_GTK_APPLICATION_IDcarries the app name string. - Atom
_GTK_UNIQUE_BUS_NAMEcarriesorg.gtk.{app_name}. - Atom
_GTK_APPLICATION_OBJECT_PATHcarries/org/gtk/{app_name_with_slashes}. - Atom
_GTK_APP_MENU_OBJECT_PATHcarries{object_path}/menus/AppMenu. - Atom
_GTK_MENUBAR_OBJECT_PATHcarries{object_path}/menus/MenuBar.
All five atoms are interned via XInternAtom, and values are written with XChangeProperty(format=8, type=UTF8_STRING). GNOME Shell polls these properties when the window maps; once set, the menu bar appears in the panel.
The Wayland equivalent uses the same protocol but a different discovery path (Mutter introspects the org.gtk.* services via org.freedesktop.DBus.ListNames instead of window properties) — the manager publishes identically; the X11-properties step is simply skipped on Wayland.
End-to-end flow
Azul Menu (azul_core::menu::Menu)
│
│ MenuConversion::convert_menu
▼
Vec<DbusMenuGroup> + Vec<DbusAction>
│ │
│ menu_groups Arc │ actions Arc
▼ ▼
GnomeMenuManager
│ register_menus_interface (org.gtk.Menus)
│ register_actions_interface (org.gtk.Actions)
│ X11Properties::set_properties (X11 only)
▼
GNOME Shell ─── panel renders menus
│
│ user clicks → DBus org.gtk.Actions.Activate("app.foo", ...)
▼
actions_protocol::queue_menu_callback (DBus thread)
│
▼
event loop ── drain_pending_menu_callbacks ── invoke CoreMenuCallback
with full CallbackInfo
Testing without DBus
The handful of unit tests in this subtree all #[cfg_attr(miri, ignore)] because Miri can't dlopen, and they gracefully degrade when libdbus-1.so is not installed (shared_dbus::test_dbus_library_loading reports either „loaded successfully“ or „not available“).
For integration testing on a CI runner without GNOME, set AZ_DISABLE_GNOME_MENUS=1 to force the in-window menu bar path and avoid every code path on this page.
Where to add new menu features
- Map a new
MenuItemvariant (e.g., toggleable check). Handle it in the recursive walk inmenu_conversion.rs. - Add a new DBus method on
org.gtk.Actions. Update the interface comment and the message handler inactions_protocol.rsandprotocol_impl.rs::actions_message_handler. - Support a new desktop-environment-specific atom. Add the atom write in
x11_properties.rs::set_properties. - Support a non-GNOME menu protocol (KDE Plasma, ...). Add a new sibling module under
linux/and reusedbus/dlopen.rs.
Coming Up Next
- Linux Wayland — Linux Wayland shell - wl_surface, xdg-shell, libinput
- Linux X11 — Linux X11 shell - Xlib, GLX, XInput2
- Accessibility — Per-platform a11y back-ends - UIA, AT-SPI, NSAccessibility
- Windowing Overview — Per-window aggregate, headless variant, and the platform shell layer