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
-
Create a file whose name ends in
_g_fuse.rssomewhere belowsrc/, and add it as a module of its parent. By convention the file is called<name>_dfs_g_fuse.rsand sits in the directory of the type it describes. The file must start with a/* ... */comment that contains the declarations. -
Run the generator:
~$ ./codegen/codegen.shIt requires a nightly
rustfmt. For every_g_fuse.rsfile it- appends, or updates, an
include!block between the markers// FUSE GENERATED STARTand// 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.rsfiles are removed. - appends, or updates, an
-
build.rsreads the descriptions and writes the Rust code intoOUT_DIR. Theinclude!block pulls it into the file as a module namedgenerated. -
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_globalsinstead 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 directoriesdfs_object,dfs_node,dfs_toplevel_node,dfs_global, anddfs_backendcontain 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>. link- 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 typeView<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 byget_<name>. key = <n>- Only for
viewandcustom. The inode is created with the fixed keyn. 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<'_>) link-
fn readlink_<name>(&self, depth: u64, buf: &mut String) view-
type View<Name>: FuseView<Self>Without
key, additionallyfn keyof_<name>(&self, key: u64) -> u64. view (other)-
type Base<Name>: GetLiveness,type View<Name>: FuseView<Self::Base<Name>>, andfn get_<name>(self: &Rc<Self>, key: u64) -> Rc<Self::Base<Name>>Without
key, additionallyfn 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
regandlink:fn has_<name>(&self) -> bool. - For
viewwithoutother:fn has_<name>(&self, key: u64) -> bool. - For
view (other)andcustom:get_<name>returns anOption.
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, andviewwith a fixed key and withoutoptorother, 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.
Links
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_linklinks to a Wayland object of a client.format_client_link,format_output_link, andformat_workspace_linklink to a client, an output, or a workspace.write_root_linkwrites 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.
VimplementsFuseRegView. FuseLink<V>- A symbolic link.
VimplementsFuseLinkView. IterDir<V>- One entry per element produced by
iter, looked up byget.VimplementsIterDirView. IterDirKeyed<V>- Like
IterDir, but each element has its own key.VimplementsIterDirKeyedView. IterDirDyn<V>- Like
IterDir, but each element is an arbitraryFuseInodeWithKey.VimplementsIterDirDynView. CopyHashMapDir<V>,CopyHashMapDir2<V>- The values of a
CopyHashMap.VimplementsCopyHashMapDirVieworCopyHashMapDir2View. HashMapDir<V>- The values of a
HashMap.VimplementsHashMapDirView. BinarySearchMapDir<V>- The values of a binary search map.
VimplementsBinarySearchMapDirView. DevTDir<V>- Devices named
major:minor.VimplementsDevTDirView. DfsObjectLinkDir<V>,DfsCopyHashMapObjectLinkDir<V>- Links to Wayland objects of one client.
VimplementsDfsObjectLinkDirVieworDfsCopyHashMapObjectLinkDirView. ClientObjectDir<V>- Links to Wayland objects of several clients, named
client:object.VimplementsClientObjectDirView.
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.