- Zig 87.1%
- Nix 10.2%
- Shell 2.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The seeds are split into calls, each of which must be answered, and refusals, each of which must be refused: values the specification does not allow, properties that cannot be written or not with that type, names and signatures that are not there. The test checks every seed's answer rather than that three in four succeed. New calls reach a seek past the end, a stale or negative SetPosition, GetPlaylists reversed and out of range, a negative volume, GetAll of everything, and calls naming no interface. Two more bytes of input choose the state after the call, rather than the input's length, and seeds change the track, jump the position and swap the track list and playlists, which is what update makes signals of. The built-in fuzzer is now given the seeds as its corpus, framed with the length Smith reads first; it was starting from nothing. Every seed is replayed with each of its allocations failing in turn, on an allocator that never grows in place, so that the count of allocations is the same each time. That found a double free in zig-dbus-service's queue, fixed there in 0b127fb, which this now uses. zig build coverage writes a kcov report of what the seeds reach of src, now 99.7% of it, and the devshell has kcov. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFAKTW62t5J1qkcKDuZ8Gy |
||
| .forgejo/workflows | ||
| dbus | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-mpris
MPRIS for Zig: a media player's side of the Media Player Remote Interfacing
Specification, the D-Bus interfaces through which desktop shells,
media keys, playerctl and the like see and control players.
A program describes what it is doing as a State whenever it is asked, and
says what it can do with the commands of a Server.Player. The server owns
org.mpris.MediaPlayer2.<name> — or .instance<pid> after it, when another
copy already does — and serves all four MPRIS interfaces at
/org/mpris/MediaPlayer2:
org.mpris.MediaPlayer2— identity, desktop entry, raise and quit;.Player— playback status, metadata, position, loop, shuffle, volume, and the transport commands;.TrackList— the tracks around the current one, their metadata, and going to one;.Playlists— the player's playlists and activating one.
It is built on zig-dbus-service, which answers the calls, and mdbus, which carries them. It was written for Pipit.
The four interfaces are written out in dbus/ as introspection XML — names,
types and annotations, taken from the specification without its prose — and
zig-dbus-service's dbus-codegen turns them into typed bindings at build
time. The server is built on those, and mpris.interfaces exports them, so
each interface's generated Client can be used to call some other player.
Using it
const mpris = @import("mpris");
var connection = try mdbus.Connection.connectSession(io, gpa, environ);
const server = try mpris.Server.create(gpa, io, &connection, .{
.name = "myplayer",
.identity = "My Player",
.desktop_entry = "myplayer",
}, .{
.context = &app,
.snapshot = snapshot, // fill in a State
.play_pause = playPause, // and whichever commands the player has
.next = next,
.set_position = setPosition,
});
defer server.destroy();
try server.start();
// server.run() on a thread of its own, and server.update() whenever
// anything the snapshot describes changes.
update works out what to say. It takes a new snapshot and compares it
with the last, and emits what the specification asks for and nothing more:
PropertiesChanged for the properties whose values differ; Seeked when the
position jumped within the same track, the one position change the
specification signals; and TrackListReplaced when the list of tracks is a
different list. A program never tracks what it last told the bus.
Position is read live. The snapshot says where the track was when it
was taken, and while playing the server moves it on by the time since — so
a program need not update every second for playerctl position to be right.
Commands arrive on the server's thread, and should hand their work off
rather than do it there. Seek, which is relative, arrives as an absolute
set_position; a seek past the end of the track is next, as the
specification says. A command left null is one the player does not have,
and CanQuit and CanRaise say so.
tools/example.zig is a complete pretend player, and the documentation is
generated from the doc comments: https://jeff.jcollie.page/zig-mpris/.
Building and testing
main builds with Zig 0.17. Zig 0.16 is served by the zig-0.16 branch,
which starts at the v0.1.0 tag and takes only critical fixes. The Nix
flake's devshell provides the right Zig, D-Bus and the REUSE tooling.
git clone https://git.jcollie.dev/jeff/zig-mpris.git
cd zig-mpris
nix develop -c zig build test --summary all
nix develop -c zig build && nix develop -c dbus-run-session -- bash tests/bus.sh
tests/bus.sh runs the example player on a private session bus and drives
it with playerctl and gdbus: status, metadata, play, a position that
moves by itself, seeking, next and previous, shuffle, loop, volume, the
track list, playlists, and the signals a monitor hears — including that
changing track sends no Seeked. Both it and the unit tests run in CI.
The same script also runs in a NixOS virtual machine. There the player is on
a logged-in user's own session bus, which is dbus-broker as on a NixOS
desktop rather than the dbus-daemon that dbus-run-session starts. It runs
once for each NixOS release the flake names:
nix build -L .#checks.x86_64-linux.broker-stable # NixOS 26.05
nix build -L .#checks.x86_64-linux.broker-unstable # NixOS unstable
Fuzzing
Anything on the session bus can call a media player, so the server is
fuzzed with calls nobody wrote: any path, interface, member and signature,
with arguments of any values that fit the signature, dispatched to a real
Server whose player is in a state the input also chooses, and which
changes to another state the input chooses once the call is answered.
Whatever arrives, the server has to answer with something that can be sent,
free what it allocated, and pass on to the player only what the
specification allows: a position inside the track, and a volume that is a
finite number.
The seeds, made by tools/fuzz_corpus.zig, are calls to every method and
property of the four interfaces, which must be answered, and calls that
must be refused — a value the specification does not allow, a property
that cannot be written, a name or signature that is not there. zig build test checks that each is answered as it should be, and then runs each
again with every one of its allocations failing in turn, which is the only
way the code that cleans up after a failed allocation is ever run.
zig build coverage writes a kcov report to zig-out/coverage of how much
of src the seeds reach.
There are two ways to fuzz it. Zig's own fuzzer steers by coverage, and
reports a failing input as input saved to '.zig-cache/f/crash':
$ zig build fuzz --fuzz # until interrupted, with a web interface
$ zig build fuzz --fuzz=1M # a bounded run, then a report
fuzz-run is a loop of this project's own in tools/fuzz.zig, which
mutates the seeds without coverage feedback, can be run for a fixed time or
from a fixed seed, and writes a failing input to fuzz-findings/, where
--input runs it again:
$ zig build fuzz-run # a minute
$ zig build fuzz-run -- --seconds 300
$ zig build fuzz-run -Dfuzz-optimize=debug -- --input fuzz-findings/x.bin
Where this lives
The repository has three homes, and they hold the same history.
| Where | How to get it |
|---|---|
| Forgejo | git clone https://git.jcollie.dev/jeff/zig-mpris.git |
| Tangled | git clone https://tangled.org/jcollie.dev/zig-mpris |
| Radicle | rad clone rad:z3ZWFny68kSWDo3VGj2V6rKZtmM2s |
A Radicle repository is findable only by its ID, so that one is written out in
full: rad:z3ZWFny68kSWDo3VGj2V6rKZtmM2s.
Licensing
MIT, following the REUSE specification; reuse lint checks it.
References cited
Kept in the zig-mpris Zotero collection.
- freedesktop.org. MPRIS D-Bus Interface Specification, version 2.2. https://specifications.freedesktop.org/mpris/latest/.
- Pennington, Havoc, Anders Carlsson, Alexander Larsson, Sven Herzberg, Simon McVittie, and David Zeuthen. D-Bus Specification. freedesktop.org. https://dbus.freedesktop.org/doc/dbus-specification.html.