Skip to content

Plugin system overview

Fancy Mumble extends the classic Mumble server with a Rust plugin host. Features like the persistence-backed file store, the collaborative live-doc editor, screen-share signalling glue, and the push-notification bridge are all built as cdylib plugins loaded at startup from a directory on disk.

The host is intentionally loosely coupled: the server does not know about specific plugins. A plugin registers itself by being present in the plugin directory, advertises its capabilities to clients at connect time, and receives addressed messages through a single generic envelope.

Fancy Mumble clientMumble C++ server(murmur)mumble-plugin-host(Rust cdylib)Plugin A(cdylib)Plugin B(cdylib)Plugin C(cdylib)Wire 200PluginMessageWire 201PluginRegistry(after ServerSync)C ABIabi_stable traitabi_stable traitabi_stable trait

The server loads libmumble_plugin_host.so once at boot. That cdylib scans every configured plugin directory, dlopens each entry, and exposes a single C ABI back to the C++ server. Every Mumble plugin is itself a cdylib that depends on the mumble-plugin-api crate.

Plugins do not get their own protobuf message types. Instead, the client and server speak two generic envelopes:

Wire IDNameDirectionPurpose
200PluginMessageBidirectionalAddressed plugin payload.
201PluginRegistryServer -> ClientAnnounces loaded plugins after ServerSync.

A PluginMessage carries:

  • plugin_name (string) - the stable identifier of the target plugin.
  • payload_type (string) - a plugin-defined inner discriminator ("OpenRequest", "Invite", "Vote", …).
  • payload (bytes) - opaque bytes; each plugin picks its own encoding (JSON, protobuf, MessagePack, …).
  • target_sessions (repeated u32) - explicit recipient sessions.
  • channel_id (optional u32) - channel-scoped fan-out hint.
  • sender_session / sender_name - stamped by the server on inbound.

The server routes inbound envelopes to exactly one plugin (the one whose name() matches plugin_name). On outbound, the server delivers to every session in target_sessions, or - if that list is empty - to every member of channel_id. Clients that report a Fancy version older than 0.4.0 are skipped.

Right after the server delivers ServerSync to a connecting client, it sends a PluginRegistry listing every loaded plugin:

{
"plugins": [
{
"plugin_name": "fancy-live-doc",
"version": "0.1.0",
"plugin_slot": 0,
"info_json": "{...}"
},
{
"plugin_name": "fancy-file-server",
"version": "0.1.0",
"plugin_slot": 1,
"info_json": "{...}"
}
]
}

The info_json blob is whatever the plugin returned from MumblePlugin::info_json(). For Fancy-built plugins this is a typed PluginInfo struct serialised as JSON, hard-capped at 64 KiB. Clients use it to populate the developer Server Info panel and to decide whether a feature is available on this server.

Plugins are loaded across an abi_stable-based FFI boundary (abi_stable on crates.io). That means:

  • Plugins can be built with a different rustc version than the host and still load cleanly.
  • The host refuses to load any cdylib whose abi_version does not match PLUGIN_ABI_VERSION (currently 2). When the trait surface changes, that constant is bumped and every plugin must be rebuilt.
  • Every trait method receives FFI-safe types (RString, RSlice, RVec, ROption, RArc) instead of their std counterparts.

Each plugin owns its own private tokio runtime. A plugin panicking inside an async task only takes down its own runtime; the host catches the panic at the FFI boundary, logs an error, and the rest of the server continues running. Other plugins are unaffected.

Using plugins

Where the plugin directory lives, how to enable plugins, configuration key naming, and the built-in plugins shipped with every Fancy Mumble image. See Using plugins.

Develop your own plugin

A complete walk-through of building a cdylib plugin from scratch in Rust, registering it with the host, and round-tripping a message with the client. See Developing a plugin.