Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Exposing Internals in the Debugfs

This chapter is for Jay developers. It describes how to make compositor state visible in the filesystem that jay debugfs mounts (see jay debugfs).

The filesystem is not written by hand. Each directory is declared in a small language at the top of a Rust file. A code generator turns the declaration into a Rust trait, and the compositor state is exposed by implementing that trait.

Workflow

  1. Create a file whose name ends in _g_fuse.rs somewhere below src/, and add it as a module of its parent. By convention the file is called <name>_dfs_g_fuse.rs and sits in the directory of the type it describes. The file must start with a /* ... */ comment that contains the declarations.

  2. Run the generator:

    ~$ ./codegen/codegen.sh
    

    It requires a nightly rustfmt. For every _g_fuse.rs file it

    • appends, or updates, an include! block between the markers // FUSE GENERATED START and // FUSE GENERATED STOP,
    • writes a description of the directories to build/fuse/generated/m_<hash>.rs, where <hash> is derived from the path of the file,
    • updates the list of all such descriptions in build/fuse/generated.rs.

    Commit all of these files. Descriptions of deleted _g_fuse.rs files are removed.

  3. build.rs reads the descriptions and writes the Rust code into OUT_DIR. The include! block pulls it into the file as a module named generated.

  4. Implement the generated traits in the same file.

Run the generator again after every change to the declarations.

A minimal file looks like this:

#![allow(unused)]
fn main() {
/*
dir wl_buffer {
    @inherit dfs_object,
    width: reg,
    color: reg (opt),
}
 */
use crate::ifs::wl_buffer::WlBuffer;
use crate::ifs::wl_buffer::wl_buffer_dfs_g_fuse::generated::wl_buffer;
use crate::utils::str_fmt::StrCtx;
use crate::utils::str_fmt::StrFmt;

impl wl_buffer::Dir for WlBuffer {
    fn read_width(&self, buf: &mut String, ctx: &StrCtx<'_>) {
        self.width.str_fmt(buf, ctx);
    }

    fn has_color(&self) -> bool {
        self.color.is_some()
    }

    fn read_color(&self, buf: &mut String, ctx: &StrCtx<'_>) {
        if let Some(v) = &self.color {
            v.str_fmt(buf, ctx);
        }
    }
}

// FUSE GENERATED START
include!(concat!(
    env!("OUT_DIR"),
    "/fuse/m_8b91a6dac50768623cab19ffe8ee101561f7aa63e020634152381b318f7e2326.rs",
));
// FUSE GENERATED STOP
}

Declarations

A file contains any number of directories:

dir <name> (<attributes>) {
    <entry>,
    <entry>,
    @inherit <other dir>,
}

The attribute list is optional. Directory attributes:

abstract
The directory is never shown on its own. Only its trait is generated. It is used as the target of @inherit.
global
The directory is generated into crate::utils::fuse::fuse_globals instead of the module of the file, so that other files can inherit from it. Global directory names must be unique across the crate. The global abstract directories dfs_object, dfs_node, dfs_toplevel_node, dfs_global, and dfs_backend contain the files shared by all Wayland objects, tree nodes, toplevels, globals, and backends.

Each entry has the form <name>: <type> (<attributes>). The name is the file name and must be unique in the directory, including inherited entries. The types are:

reg
A regular file. Its contents are produced by read_<name>.
A symbolic link. Its target is produced by readlink_<name>.
view
A subdirectory, or any other inode, whose behavior is defined by a type that implements FuseView. The type is chosen in the implementation through the associated type View<Name>.
custom
An arbitrary inode returned by get_<name>. This is used to embed a directory that is implemented elsewhere, for example the directory of a tree node.

Entry attributes:

opt
The entry does not always exist.
other
Only for view. The view is implemented by a different object than the directory itself. That object is returned by get_<name>.
key = <n>
Only for view and custom. The inode is created with the fixed key n. See Keys.
no_timeout
Overrides the entry timeout. See Entry Timeouts.

@inherit <dir> adds all entries of another directory, which is either defined in the same file or global.

The Generated Trait

For each directory <dir> the generator creates a module generated::<dir>, which contains a trait Dir and, unless the directory is abstract, a type View that implements FuseView<T> for every T: Dir. Dir requires GetLiveness (see Liveness). If the directory inherits from other directories, their traits are supertraits of Dir and their entries do not appear in Dir itself.

The methods depend on the type and attributes of each entry:

reg

fn read_<name>(&self, buf: &mut String, ctx: &StrCtx<'_>)

fn readlink_<name>(&self, depth: u64, buf: &mut String)

view

type View<Name>: FuseView<Self>

Without key, additionally fn keyof_<name>(&self, key: u64) -> u64.

view (other)

type Base<Name>: GetLiveness, type View<Name>: FuseView<Self::Base<Name>>, and fn get_<name>(self: &Rc<Self>, key: u64) -> Rc<Self::Base<Name>>

Without key, additionally fn keyof_<name>(&self, key: u64) -> u64.

custom

fn get_<name>(self: &Rc<Self>, key: u64) -> FuseInodeWithKey

custom (key = n)

fn get_<name>(self: &Rc<Self>, key: u64) -> Rc<dyn FuseInode>

<Name> is the entry name in camel case.

opt adds the following:

  • For reg and link: fn has_<name>(&self) -> bool.
  • For view without other: fn has_<name>(&self, key: u64) -> bool.
  • For view (other) and custom: get_<name> returns an Option.

The entry exists if has_<name> returns true or get_<name> returns Some. read_<name> and readlink_<name> can still be called after has_<name> has returned false, because the state can change between the two calls. They must then write nothing and must not panic.

A view without other is implemented by the same object as the directory. This is how a directory is split into subdirectories. For example, the pending subdirectory of a surface is declared as pending: view (key = 0) and is implemented by WlSurface itself:

#![allow(unused)]
fn main() {
impl wl_surface::Dir for WlSurface {
    type ViewPending = pending::View;
    // ...
}

impl pending::Dir for WlSurface {
    // ...
}
}

Keys

Every inode consists of an object and a 64-bit key. The key lets one object back several inodes. For example, the directory that describes a Wayland object without dedicated debugfs support is implemented by the Client and uses the object ID as the key.

Most directories ignore the key and are created with key 0. Entries with key = <n> use that key. For other view entries, keyof_<name> maps the key of the directory to the key of the child. The key argument of the generated methods is the key of the directory.

Entry Timeouts

The kernel caches the result of looking up a name. The generator sets the cache duration from the entry:

  • Entries that always exist, that is reg, link, and view with a fixed key and without opt or other, are cached without a time limit.
  • All other entries are cached for 100 milliseconds.

no_timeout makes an entry cached without a time limit, even if it is optional. Use it when the existence of the entry is decided when the object is created and never changes. wl_buffer uses it for dmabuf_device, which exists only for buffers created from a dmabuf.

The contents of files are not cached.

Liveness

The filesystem identifies an inode by the address of its object, the Rust type of its view, its key, and its parent and name. The inode cache holds a Weak reference to the object, which keeps the address reserved, so it cannot be reused while the inode is cached.

Dead inodes are found by a separate thread, which cannot access the Weak. Instead, every object that backs an inode has a Liveness, which is updated atomically when the object is dropped. The thread uses it to find inodes whose Weak can no longer be upgraded. The compositor thread then removes those inodes from the cache and drops the Weak.

Add a field and derive GetLiveness:

#![allow(unused)]
fn main() {
#[derive(GetLiveness)]
pub struct WlBuffer {
    // ...
    liveness: Liveness,
}
}

Initialize it with Default::default(). If the field has a different name, annotate it with #[liveness].

Connecting a Directory

A directory is visible only if an existing directory refers to it. To turn an Rc<T> into an inode, wrap it in a view:

#![allow(unused)]
fn main() {
use crate::utils::fuse::fuse_inode::FuseInodeExt;
use crate::utils::type_view::TypeViewExt1;

self.tv_wrap_rc::<wl_buffer::View>().without_key()
}

This returns a FuseInodeWithKey, which is what custom entries and several of the collection views below expect.

Wayland objects appear in clients/<client>/objects/<id>. By default the Object derive shows a directory with the ID, interface, and version. To replace it, add #[debugfs] to the struct and implement ObjectDebugfs:

#![allow(unused)]
fn main() {
#[derive(Object, GetLiveness)]
#[debugfs]
pub struct WlBuffer {
    // ...
}

impl ObjectDebugfs for WlBuffer {
    fn object_debugfs(self: Rc<Self>, _client: &Rc<Client>) -> FuseInodeWithKey {
        self.debugfs()
    }
}
}

where debugfs is a function in the _g_fuse.rs file that performs the wrapping shown above. The directory should inherit dfs_object, which is implemented for all objects.

Tree nodes are connected through Node::node_debugfs. Directories of other types need a view (other) or custom entry in a directory that is already connected, for example in the root directory in src/state/state_dfs_g_fuse.rs.

Link targets are relative paths. depth is the depth of the link below the root of the filesystem, so a target that starts with depth - 1 times ../ is relative to the root. src/dfs/dfs_helpers.rs contains functions that write such targets:

  • format_object_link links to a Wayland object of a client.
  • format_client_link, format_output_link, and format_workspace_link link to a client, an output, or a workspace.
  • write_root_link writes only the prefix.
#![allow(unused)]
fn main() {
fn readlink_viewport(&self, depth: u64, buf: &mut String) {
    if let Some(obj) = self.viewporter.get() {
        format_object_link(buf, depth, self.client.id, obj.id());
    }
}
}

Prefer a link over a file that contains a flag or an ID when the target has a directory of its own. An optional link carries the same information as a has_* file.

Formatting Values

read_* methods write through the StrFmt trait. It supports two formats: human-readable text, which is used when the filesystem is mounted, and JSON, which is used by jay --json debugfs snapshot. The format is available as ctx.fmt. In the human-readable format a newline is appended to every file automatically.

StrFmt is implemented for integers, floats, bool, str, Option, slices, arrays, pairs, Rc, Box, and many compositor types. An Option that is None is written as nil or null.

Enums implement StaticText to give each variant a name, and read_* writes self.value.get().text(). Names are lower case with underscores.

Structs derive StrFmt. Each field is written as name: value in the human-readable format and as a JSON object otherwise. Two attributes change the output:

  • #[str_fmt(skip)] on a field omits it.
  • #[str_fmt(transparent)] on the struct writes the only field that is not skipped as if it were the whole struct. This is used for newtypes.
#![allow(unused)]
fn main() {
#[derive(StrFmt)]
#[str_fmt(transparent)]
pub struct ColorMatrix<To = Local, From = Local>(
    pub [[F64; 4]; 3],
    #[str_fmt(skip)] PhantomData<(To, From)>,
);
}

To write values that do not exist as a struct, such as a width and a height stored separately, declare a local struct that derives StrFmt and write an instance of it. Do not implement StrFmt by hand for structs.

Collection Views

src/utils/fuse/fuse_views.rs and src/dfs/dfs_helpers.rs contain FuseView implementations that are used as the View<Name> type of view entries. Each of them is configured by a marker type V that implements a trait:

FuseReg<V>
A regular file. V implements FuseRegView.
FuseLink<V>
A symbolic link. V implements FuseLinkView.
IterDir<V>
One entry per element produced by iter, looked up by get. V implements IterDirView.
IterDirKeyed<V>
Like IterDir, but each element has its own key. V implements IterDirKeyedView.
IterDirDyn<V>
Like IterDir, but each element is an arbitrary FuseInodeWithKey. V implements IterDirDynView.
CopyHashMapDir<V>, CopyHashMapDir2<V>
The values of a CopyHashMap. V implements CopyHashMapDirView or CopyHashMapDir2View.
HashMapDir<V>
The values of a HashMap. V implements HashMapDirView.
BinarySearchMapDir<V>
The values of a binary search map. V implements BinarySearchMapDirView.
DevTDir<V>
Devices named major:minor. V implements DevTDirView.
DfsObjectLinkDir<V>, DfsCopyHashMapObjectLinkDir<V>
Links to Wayland objects of one client. V implements DfsObjectLinkDirView or DfsCopyHashMapObjectLinkDirView.
ClientObjectDir<V>
Links to Wayland objects of several clients, named client:object. V implements ClientObjectDirView.

The elements of these collections must implement GetLiveness.

A generated View can also be used as the element view of a collection. For example, IterDirKeyed<ColorDescriptions> in src/state/state_dfs_g_fuse.rs lists all color descriptions as regular files named after their IDs.

Stability

The layout of the filesystem is not a stable interface. Entries can be renamed or removed freely.