Mercurius — Draft RFC

Mercurius System Architecture and Object Model

A Smalltalk‑style design expressed in C

Mercurius is a secure, workstation‑centric, network‑native window system. It is designed for a world in which:

Rather than treating the laptop in front of the user as “the machine”, Mercurius treats the workstation as a presence that can be inhabited from anywhere. The workstation owns the GPU, the Compositor, and the Session state. Client devices are portals into that environment: authenticated endpoints for pixels and input.

Mercurius is implemented in plain C, but architecturally it behaves like a carefully‑designed Smalltalk: everything is an object; objects send messages; ownership is explicit; lifetimes are clear; and important behaviour is attached to well‑named objects rather than scattered through global state or conditionals.

This chapter describes that architectural model:

The protocol details and use cases are covered elsewhere. Here, the focus is on structure and style.


1. Conventions

To keep the description close to the conceptual model, the following conventions are used:

The object model is defined in terms of the capitalised conceptual names; the implementation is a faithful realisation of that model in C.


2. Philosophy: presence, not pixels

Mercurius starts from a simple belief:

A workstation is a presence, not a device.
You should be able to inhabit it from anywhere.

That belief drives the architecture.

2.1 The workstation as centre of gravity

The workstation is a long‑lived machine with serious CPU, serious storage, and a home directory that actually means something. Around it lives a constellation of small Unix systems doing one job well: NTP, mail, DNS, storage, and other services.

In this view:

The Workstation is not “a box under a desk”; it is the centre of gravity for everything a user does.

2.2 Clients as untrusted terminals

Clients are treated as terminals:

Mercurius treats local console access as “just another terminal”: a special case in terms of performance and hardware, but not in terms of protocol semantics.

2.3 Remote as a first‑class mode

Remote presence is not a bolt‑on:

X11 showed that network‑transparent GUIs were possible; Wayland showed how to do local compositing properly; remote‑desktop systems showed how to stream pixels. Mercurius instead defines a network‑native window system with explicit semantics for windows, focus, and input, and a security model appropriate for long‑lived workstations.

Mercurius is built on two explicit technical pillars:

These are architectural requirements, not implementation accidents. The protocol and object model depend on properties that SCTP and Vulkan provide natively: structured, multi‑stream message delivery in the transport, and an explicit, cross‑platform GPU abstraction in the renderer.


3. The Iron Rules: ownership as law

Mercurius is written in C, but the mental model is “Smalltalk with Rust‑like ownership discipline”. C has no notion of ownership or lifetime, so Mercurius adopts four Iron Rules which every object must obey.

3.1 The four rules

  1. Whoever creates an object owns it.
    If a function creates an object (typically via *_create), the caller becomes its owner and is responsible for eventually destroying it.

  2. Ownership never silently changes.
    If ownership moves from one object to another, that transfer is explicit in the API and in the documentation. No helper function frees a pointer behind a caller’s back.

  3. Borrowers never free what they borrow.
    Objects often “borrow” references to other objects. Borrowers may use these references but must not destroy the borrowed objects. Only the owner may call *_destroy.

  4. Long‑lived objects own short‑lived ones, never the reverse.
    The ownership graph must point from long‑lived to short‑lived objects, not the other way around. The Broker owns Sessions; Sessions do not own the Broker. Compositors outlive the Sessions that render through them.

These rules make ownership visible from the architecture diagram. Given two boxes, only the longer‑lived one is allowed to own the shorter‑lived one. No cycles, no “who frees this?” puzzles, and no ability for a transient object (such as a per‑connection Controller) to accidentally control the lifetime of long‑lived state (such as Sessions).

3.2 Why “Iron Rules”?

Mercurius uses a Smalltalk‑style object model expressed in plain C.

When the project was first described, someone remarked that the code felt “very Rust‑like”. That was true — but the style itself long predates Rust. The intrusive lockable list used throughout Mercurius, for example, was originally written in 2013, years before Rust became widely known. Although this style predates Rust, it shares many of Rust’s ownership guarantees: clear lifetimes, explicit ownership, and no hidden transfers.

So what comes before Rust?
Iron.

The name is a small joke, but the rules themselves are serious:
they are rigid, load‑bearing, and uncompromising.
If they are followed, the system stays safe and predictable.
If they are violated, things break.

The Iron Rules are the mechanism by which Mercurius enforces a Smalltalk‑style object model in a language that has no classes, no destructors, no ownership types, and no lifetime checker. It looks this way because I like Smalltalk — and Rust’s creator probably does too.

3.3 What this gives you that plain C does not

C is a simple language, and that simplicity is its power. It lets you do anything — which is exactly why its detractors criticise it. C makes it trivially easy to do the wrong thing, and it offers nothing to help you do the right thing. There is no ownership model, no lifetime semantics, no destructor ordering, no prohibition on cycles, and no guardrails against accidental complexity.

But that same simplicity also makes it possible to do the Right Thing — if you bring consistency and discipline instead of expedience. The Iron Rules are that consistency and discipline.

They give Mercurius properties that plain C does not:

In short: the Iron Rules give Mercurius the safety and predictability of a higher‑level language while retaining the performance, portability, and directness of C. They turn “plain old C” into a language in which a Smalltalk‑style object system can be implemented without fear — and in which the behaviour of the system under load is something you can reason about, not hope for.


4. Naming and method shape

A disciplined architecture is easier to maintain if the code surface reflects that discipline. Mercurius leans heavily on:

4.1 big_little_littlest

Every function name follows a big_little_littlest pattern:

Once the object and the verb are known, the symbol name is usually obvious:

This makes the code easy to search: a grep for mwsd_session_ shows the surface of the Session object; a grep for _attach_session shows all attach points.

4.2 Methods: self‑first, verbs last

Non‑trivial functions are treated as methods on objects:

Conceptually, these are messages:

The code reads in terms of objects and verbs rather than in terms of raw functions and global state.

Why this rule exists

This convention is not aesthetic, historical, or just because Smalltalk methods take ‘self’. It exists because the system manages 0..many live objects of each type.

There may be dozens of Handshake objects, for example, in flight simultaneously — each representing a different client, each with its own state machine, each progressing independently.

By requiring every method to take self: - the identity of the object is always explicit - no function can accidentally operate on the wrong instance - no global or implicit “current object” is ever needed - the call graph remains clear and predictable - concurrency and interleaving become safe and manageable - the code scales naturally to many simultaneous objects

C does not enforce object boundaries for you. This rule ensures the boundaries exist.

Benefits

This pattern uses C’s simplicity to build a disciplined, object‑oriented architecture without runtime overhead or language‑level machinery. It keeps the system explicit, modular, and robust under load.

4.3 Error handling: bool + errno

All non‑trivial operations follow the same basic contract:

Even if a specific *_destroy cannot fail in practice, it is still defined to return bool for consistency.

The uniform contract means:

There is no proliferation of ad‑hoc error channels.

4.4 Lifecycle and behaviour

Every non‑trivial object type offers (or is expected to offer) at least:

Once this pattern is understood, unfamiliar functions become easier to place: if the name starts with mwsd_session_ and ends with _process, it acts on a Session, takes self first, and runs some part of the Session’s behaviour for a while.

4.5 Public vs. private methods

Mercurius objects are implemented in C, so “public” and “private” are expressed using linkage rather than language keywords.

This keeps each object’s public surface small and explicit, and ensures that helper functions cannot accidentally become part of the external API.


5. The top‑level structure

At a high level, the server side of Mercurius consists of four core objects:

Transport  →  Controller  →  Broker  →  Session
(persistent)   (stable)   (stable)    (long‑lived)

Around this pipeline sit:

The relationships are intentionally regular:

Each core object has a single, well‑defined responsibility:

There are no embedded “one of this, two of that” assumptions in the architecture; everything is discoverable and created on demand.


6. Transport: how bytes arrive

A Transport represents a listening endpoint and the associations that arrive on it.

Concrete examples include:

The protocol specification requires SCTP semantics for network communication. Other mechanisms may be used under the hood (for example, a local optimisation), but a conforming implementation must provide SCTP‑like properties to the protocol: message boundaries, multi‑streaming, and independent ordering.

Transport is an abstract role in the object model:

6.1 Discovery by iteration

The server does not hard‑code which Transports exist. Instead, it asks the Transport layer:

“Given this configuration, which Transports should I use?”

An iterator yields zero, one, or several MwsTransport instances, each wrapped in a small entry object and stored in a list. For each discovered Transport, the server creates exactly one Controller.

Important properties:

6.2 Responsibilities and boundaries

A Transport:

Only the Controller “has a Transport” in the object‑oriented sense. All other objects see messages in terms of Sessions and windows, not in terms of sockets or streams.


7. Controller: the protocol brain

A Controller is the server‑side control‑plane brain for a single Transport.

It understands Mercurius messages, runs the control plane, and uses the Broker and Sessions to implement protocol semantics.

7.1 Responsibilities

For its Transport, the Controller:

All protocol questions—“what does this opcode mean?”, “is this message valid here?”, “which Session should see this?”—are answered by the Controller.

7.2 Ownership and lifetime

A Controller:

Controllers are ephemeral:

By design, the Controller never owns Sessions or the Broker. It is a guest in the long‑lived part of the object graph, which prevents per‑Transport failures from directly destroying or corrupting Sessions.


8. Broker: the session authority

The Broker is the stable centre of the server. It knows:

8.1 Responsibilities

The Broker:

Every attach/detach/resume flow passes through the Broker. No Controller or Client can sidestep it and “steal” a Session.

8.2 Ownership and lifetime

The Broker obeys the Iron Rules:

Controllers receive only borrowed Session pointers for as long as they are attached. When a Controller exits, the Broker retains ownership of any Sessions it created or managed.

This single‑owner model makes Session mobility and isolation tractable: there is always exactly one place in the system that knows which Sessions exist and who is allowed to attach to them.


9. Session: the user’s presence

A Session is the authoritative server‑side representation of a user’s graphical environment. It corresponds to what a user informally calls “my desktop”:

9.1 Responsibilities

A Session:

The Controller does not directly manipulate the Session’s internal tables or window structures. It decodes messages and calls methods on the Session. The Session is the only place that knows what those messages mean for its own state.

9.2 Ownership, lifetime, and detachment

In the intended model:

Detach/resume is implemented as rebinding:

This is the object‑model counterpart of detachable operation and Session mobility: a Session is independent of how, or from where, it is currently being viewed.

Sessions must also be detachable with respect to Compositors and GPUs:

In the object model, this is just another rebinding: the Session never owns the Compositor; it borrows one, uses it, and can later borrow a different one.


10. Compositor, Vulkan, and discovery

Rendering in Mercurius is server‑side and defined in terms of Vulkan. The object model reflects the fact that Vulkan is a requirement, not an option.

10.1 Vulkan: required GPU abstraction

The Vulkan object (MwsdVulkan in code) is responsible for:

A typical enumeration interface is:

bool mwsd_vulkan_enumerate_devices(const MwsdVulkan *self,
                                   VkPhysicalDevice **out_devices,
                                   uint32_t          *out_count);

The daemon does not guess or configure “how many GPUs” exist. It asks Vulkan, then creates one Compositor per device it discovers. If creating a Compositor fails, the relevant *_create returns NULL, and the system reports an appropriate resource error to the Client. There are no hard‑coded limits such as MAX_COMPOSITORS.

This mirrors the Transport side:

In both cases, discovery and *_create determine how many objects exist, not compile‑time constants.

10.2 Compositor: one per Vulkan device

A Compositor is created for each physical device returned by Vulkan. It:

A key feature of this model is that Sessions are not permanently tied to a particular GPU:

The Broker stands to Compositors as Controllers stand to Transports:

Neither side knows how many objects exist; both simply operate over whatever has been discovered and created successfully. Objects are created by discovery and *_create, not by static limits, and creation failure is signalled in the usual way (NULL return, errno, and an eventual MWS_ERROR_RESOURCE to the Client).

Any design that relies on constants such as MAX_SESSIONS, MAX_COMPOSITORS, or “this machine has two GPUs” is deliberately avoided. The object model scales up and down by construction: if the system can support more objects, *_create succeeds; if it cannot, creation fails cleanly.


11. Security emerging from the object model

The Mercurius specification describes a strict security model: clients are untrusted; Sessions are bound to associations; window identifiers are scoped to Sessions; input events are scoped to seats and Sessions; and clients cannot enumerate or reference resources belonging to other Sessions.

The object model is shaped so that many of these guarantees fall out naturally, rather than being enforced by a tangle of if (this_is_allowed) checks.

Some key examples:

Message validation, authentication, and encryption still matter, and they are handled in the protocol and transport layers. But the object graph itself carries a lot of the load: it is simply impossible, within the architecture, to obtain a pointer to “someone else’s window” or to “some other user’s Session” and do something meaningful with it.

This is a deliberate design choice. The goal is not just to implement a secure protocol in C, but to make it hard to break the protocol’s security model without first breaking the object model.


12. Client‑side mirrors (without authority)

On the Client side, the architecture mirrors the server enough to be recognisable, but with an important constraint: no client‑side object is authoritative.

12.1 Client Control

MwscControl is the client‑side counterpart of the Controller. It:

MwscControl owns its own control‑plane state, but never owns the server‑side Session. It reflects the server’s state; it does not define it.

12.2 Client Session and windows

MwscSession and MwscWindow are client‑side representations of what the server describes:

Destroying a client‑side Session or window does not destroy the server‑side Session or windows; that is always the Broker’s and Session’s responsibility. Losing a Client device does not compromise server‑resident state; the server retains the Session according to policy, ready to be resumed from another terminal.

The client is explicitly not part of the trusted computing base. It is a display and input endpoint; nothing more.


13. Smalltalk in C: a reusable pattern

Mercurius uses this object model to realise a specific goal: a network‑native, zero‑trust window system for long‑lived workstations. The underlying design pattern, however, is more general:

The result is a system that: - can be drawn as a clean object graph and reasoned about like a Smalltalk image; - enjoys ownership guarantees enforced by discipline rather than by the compiler; - naturally enforces many of the protocol’s security guarantees through structure, not through ad‑hoc checks.

Mercurius is one instance of this approach: a network window system whose architecture is intentionally Smalltalk‑like, expressed in plain C, and held together by a handful of Iron Rules. The pattern itself is broader: any C system that needs a robust, object‑oriented architecture with clear lifetimes and strong security boundaries can adopt the same style.

13.1 Why this model generalises beyond Mercurius

C is a simple language, and that simplicity is its greatest strength. It gives you the power to do anything — which is exactly why it is so easy to do the wrong thing. Most C systems accumulate hidden ownership, ambiguous lifetimes, scattered permission checks, and fragile teardown paths that only work by accident.

The Iron Rules turn that on its head. They show that the same simplicity that makes C dangerous also makes it possible to build systems that are predictable, secure, and structurally honest — provided you bring consistency and discipline instead of expedience.

This is why the pattern generalises:

13.2 This pattern is not tied to C

This design pattern is not an argument for using C instead of Rust, Go, Zig, Swift, or anything else. Mercurius is written in C simply because it is the language I have used for forty years; it is the one in which I can express ideas fluently.

The pattern itself is language‑agnostic. As stated from the start, it is taken straight from Smalltalk (Picasso would be proud).

The Iron Rules make ownership, lifetimes, and capability boundaries explicit in the structure of the code. That means the semantics are portable: any language that can express objects with identity and methods can implement the same architecture.

A Mercurius server written in Rust would be entirely welcome. The structure is so explicit that an AI could generate a Rust implementation from the C version and get it right first time. The ownership graph is visible, the lifetimes are deterministic, and the capability boundaries are structural rather than implicit.

C happens to be the medium in which this particular instance of the pattern is written. The pattern itself belongs to no language.