flowchart LR
P["Producer agent
(shm = true)"]
R[("Shared-memory ring
slots 0..N-1")]
B["Broker"]
C1["Consumer on
the same host"]
C2["Agent that did
not opt in"]
P -- "writes blob" --> R
P -- "descriptor" --> B
B -- "descriptor" --> C1
B -- "descriptor" --> C2
R -. "reads in place" .-> C1
R -. "one copy" .-> C2
Local transport and shared memory
Agents on the same machine as the broker can skip the network stack entirely: they talk to the broker over ipc sockets, and large binary blobs can stay in shared memory while only a small descriptor travels on the bus. This guide explains both mechanisms, when to use them, and how plugins can read and write blobs without copying them.
Context
MADS was born for distributed systems, so every agent talks to the broker over TCP. On a single machine, though, much of that work is wasted: an agent on the broker’s own host still goes through the loopback TCP stack, and a large binary payload, such as a camera frame (see Binary payloads), is copied into the ZeroMQ message, through the broker, and again into the receiving agent’s memory.
MADS offers two independent remedies for agents that share a host:
- the local transport: the broker also listens on
ipc://endpoints (Unix-domain sockets), and local agents use them instead of loopback TCP. This is automatic, needs no configuration, and benefits every message; - the shared-memory data plane: a producer keeps its blobs in a ring of shared-memory slots, and only a small descriptor goes through the broker. Consumers on the same host read the bytes in place. This is opt-in, per producer, and pays off for large blobs.
Local transport
When the broker starts, it binds its frontend, backend and settings sockets twice: on their usual tcp:// addresses, and on ipc:// socket files in a per-user runtime directory ($XDG_RUNTIME_DIR/mads or /tmp/mads-<uid> on Unix, %LOCALAPPDATA%\mads\run on Windows), for example /tmp/mads-1000/broker-9090-front.ipc.
At startup each agent asks the broker which ipc endpoints it offers, and with the default transport = "auto" it switches to them when the broker runs on the same host and the socket files are visible. Otherwise it stays on tcp. You can force either transport with the transport key (in [agents] or in the agent’s own section) or the --transport option:
[agents]
transport = "auto" # or "tcp", or "ipc" (fail if ipc is not possible)The agent’s startup information shows which transport it ended up using, and mads doctor predicts it for any agent you would start on that host. The broker side is covered in detail in the broker manual, and the agent side in Choosing the transport.
ip_whitelist does not apply to ipc peers, which have no IP address. The owner-only runtime directory is their access control. See Security in MADS.
Plugin zero-copy API
Plugin protocol P9 (mads_plugin v2.6-P9) adds two plain structs and a few virtual methods. All of them have defaults that copy, so a P8 plugin only needs to be rebuilt: mads plugin --update moves the pin and prints the opt-in notes.
struct BlobView { const unsigned char *data; size_t size; }; // read-only
struct BlobBuffer { unsigned char *data; size_t capacity; size_t size; }; // a ring slotReading blobs in place
Filters and sinks override load_data_view(). The default implementation copies the view into a vector and calls load_data(), which is why old plugins keep working.
return_type load_data_view(json const &meta, std::string topic,
BlobView blob) override {
// Valid only during this call (for a filter: through the next process()).
cv::Mat frame(meta["height"].get<int>(), meta["width"].get<int>(), CV_8UC3,
const_cast<unsigned char *>(blob.data));
_mean = cv::mean(frame)[0]; // use it now, or copy what you need to keep
return return_type::success;
}Never keep blob.data after the call, and never write through it. After the call the loader re-checks the slot; if the producer overwrote it meanwhile, the result is discarded as a torn read.
Writing blobs into a slot
Sources (and filters) opt in by returning true from supports_shared_output() and implementing get_output_shared() (or process_shared() for filters). The loader hands over a ring slot as a BlobBuffer: write at most buf.capacity bytes into buf.data and set buf.size.
bool supports_shared_output() const override { return true; }
return_type get_output_shared(json &out, BlobBuffer &buf) override {
cv::Mat frame;
if (!_cap.read(frame) || frame.empty())
return return_type::retry;
size_t n = frame.total() * frame.elemSize();
if (n > buf.capacity) {
_pending = frame; // keep it for get_output()
buf.size = n; // tell the host how much room was needed
return return_type::retry;
}
std::memcpy(buf.data, frame.data, n);
buf.size = n;
out["width"] = frame.cols;
out["height"] = frame.rows;
out["format"] = "bgr8";
return return_type::success;
}The oversize contract matters: if the blob does not fit, write nothing, set buf.size to the size you need (so that it exceeds buf.capacity) and return return_type::retry without losing the data. The loader then calls the classic get_output() (or process()) in the same loop iteration and sends that blob inline.
Likewise, buf.size == 0 (no blob this time) must mean that nothing was written: the loader then hands the slot back untouched, and the blob previously published in it stays readable for slow consumers. Never scribble into buf.data unless you set buf.size to what you wrote.
A plugin that does not opt in still benefits when its agent has shm = true: the loader copies the get_output() blob into a slot, which costs one copy but saves all the ZeroMQ and broker copies downstream.
Housekeeping
On Linux and macOS a shared-memory segment outlives a producer that crashes. To reclaim them, every producer registers its segments under <runtime dir>/shm/ together with its pid, and a new producer unlinks the segments of dead owners when it starts. mads doctor lists the registry and counts the orphans, and mads doctor --fix removes them on demand.
On Windows the segments are named file mappings (Local\mads_...), which the kernel frees with their last handle: there is nothing to clean up.
Windows notes
- ipc uses AF_UNIX sockets, available since Windows 10. libzmq enables them only when it is built against the Windows 10 SDK (which provides
afunix.h). With a libzmq built without them, the broker warns and serves tcp only, and agents fall back to tcp silently;mads doctorreports which case applies. - Shared memory works on any supported Windows version, but the same-host rule applies: it is only used when the producer’s broker is local.
Summary
| Local transport | Shared-memory data plane | |
|---|---|---|
| Scope | Every message between local agents and the broker | Blobs from producers with shm = true |
| Configuration | None (transport = "auto" default) |
shm, shm_slots, shm_slot_size on the producer |
| Remote agents | Keep using tcp | Get an error: keep producers with remote consumers on shm = false |
| Loss | None | Lossy ring: slow consumers drop overwritten slots |
| Plugins | Unchanged | Optional P9 view API for zero-copy |