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:
- Applications run on a workstation or central server.
- Users move freely between different terminals and locations.
- Client devices cannot be trusted to store secrets or enforce policy.
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 conventions used to talk about objects;
- the Iron Rules that govern ownership;
- the main objects (Transport, Controller, Broker, Session, Compositor, Client);
- how those objects interact to realise Mercurius’ security and session semantics.
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:
Capitalised names refer to conceptual object types in the model:
Transport, Controller, Broker, Session, Compositor, Vulkan, Client, and so on.C implementation names include subsystem prefixes and are written verbatim where needed, for example:
MwsTransport,MwsdController,MwsdBroker,MwsdSession,MwsdCompositor,MwsdVulkan,MwscSession.When in doubt:
- “A Session asks the Broker…” refers to the conceptual objects.
- “
mwsd_session_createis called bymwsd_broker_create_session…” refers to the concrete C APIs.
- “A Session asks the Broker…” refers to the conceptual objects.
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 owns the GPU, the Compositor, and all Session state.
- Applications run on the Workstation.
- Rendering is performed on the Workstation’s GPU.
- Policy about focus, input, and window management lives on the Workstation.
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:
- They are untrusted, mobile, and potentially compromised.
- They provide a screen, input devices, and enough compute to run a Mercurius client and decode surfaces.
- They do not store secrets or authoritative Session state.
- Losing a client should mean losing a view, not losing the workstation.
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:
- The protocol is structured, message‑oriented, and multi‑stream from the outset.
- Sessions are designed to be detachable and resumable.
- The security model assumes untrusted networks and untrusted clients.
- Local and remote interaction share the same Session semantics.
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:
- a transport profile based on SCTP multistreaming for network communication;
- a rendering model based on Vulkan for GPU work.
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
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.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.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.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:
A predictable object graph. Every object has one owner, and the ownership graph is acyclic and visible in the architecture. There are no mysteries about who frees what, and no surprises during teardown.
Deterministic lifetimes. Long‑lived components cannot accidentally depend on short‑lived ones, and destruction happens in a well‑defined order. Nothing dies early, nothing lingers.
Safe error paths. Failure cannot leak memory, lose ownership, or leave objects half‑alive. The rules forbid the patterns that make error handling in C dangerous.
Reasonable concurrency. With explicit ownership and no cycles, lock ordering and object access become mechanical rather than guesswork.
Architectural clarity. The Transport → Controller → Broker → Session pipeline only works if lifetimes are strict, ownership is explicit, and no transient object can destabilise long‑lived state.
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:
- a strict naming scheme;
- a uniform “Smalltalk‑in‑C” method shape;
- a consistent error‑handling contract.
4.1 big_little_littlest
Every function name follows a big_little_littlest pattern:
big — subsystem prefix:
mwsc_— Client (mwscCLI and support library)
mwsd_— server daemon
mws_— shared protocol and Transport layer
little — object type or component:
mwsc_control_,mwsc_session_,mwsd_session_,mwsd_broker_,mws_transport_.littlest — a verb describing the action:
mwsc_control_run_handshake,mwsd_session_add_window,mwsd_broker_attach_session,mws_transport_destroy.
Once the object and the verb are known, the symbol name is usually obvious:
- “Server‑side Session add window” →
mwsd_session_add_window.
- “Client Control run handshake” →
mwsc_control_run_handshake.
- “Destroy a Transport” →
mws_transport_destroy.
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:
- The first parameter is always the object, named
self.
- The trailing verb describes the action:
*_create,*_destroy,*_process,*_handle,*_attach,*_detach, and so on.
Conceptually, these are messages:
- “Session, process this message.”
- “Broker, create a Session for this association.”
- “Transport, close yourself.”
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
- Clarity — every operation is anchored to a specific object.
- Safety — no hidden state, no accidental cross‑talk.
- Scalability — dozens of objects can coexist cleanly.
- Testability — each object can be exercised in isolation.
- Maintainability — the API surface is uniform and predictable.
- Extensibility — new methods fit naturally into the model.
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:
Constructors (
*_create(...))
Return a pointer to a newly allocated object on success, orNULLon failure witherrnoset.Destructors and behavioural methods
Returnbool:trueon success;
falseon failure, witherrnoset to an errno‑compatible value describing the error.
Even if a specific *_destroy cannot fail in practice, it is still defined to return bool for consistency.
The uniform contract means:
- “Did this work?” → check the return value.
- “Why did it fail?” → check
errno.
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:
*_create(...)
Allocate and fully initialise an object. The caller becomes its owner.*_destroy(self)
Destroy the object and release all resources it owns. Safe to call withself == NULL.*_handle(self, event)
Handle a discrete event, usually a single decoded message or notification. This method should not block for long.*_process(self, ...)or*_run(self)
Drive long‑running behaviour such as protocol state machines or event loops. _run typically runs in its own thread.
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.
Public methods form part of the object’s interface. They are declared in the header, have external linkage, and follow the naming and method-shape rules above.
Private methods are implementation details of a single translation unit. In C they MUST be declared
staticso they have internal linkage and cannot be called from outside the implementing.cfile.
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:
- one Vulkan object that creates the Vulkan instance and discovers GPUs;
- one Compositor per GPU, responsible for windows and presentation on that device;
- one Controller per Transport, responsible for sending and receiving message over that channel;
- the Client stack, which attaches to Sessions and presents them to users.
The relationships are intentionally regular:
- A Controller is to a Transport as the Broker is to a Compositor:
- Controller: owns protocol semantics for one Transport.
- Broker: owns Session semantics across one or more Compositors.
- Controller: owns protocol semantics for one Transport.
- A Transport hides “how bytes arrive”.
- A Compositor hides “how pixels appear”.
Each core object has a single, well‑defined responsibility:
- Transport moves bytes.
- Controller understands the protocol.
- Broker owns Sessions and mediates attachments and detaches.
- Session represents the user’s presence.
- Vulkan enumerates GPUs and provides an instance.
- Compositor owns a GPU and presents Sessions’ windows on it.
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:
- a local Transport (for example, a UNIX domain socket);
- an SCTP Transport on platforms with kernel SCTP support;
- a user‑space SCTP Transport (using usrsctp) on platforms such as Windows.
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:
MwsTransportdefines what it means to send and receive Mercurius messages.
- Concrete backends implement that interface using whatever mechanisms their environment provides.
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:
mainnever switches on “which Transport”. It never has anif (have_sctp)orif (on_windows); it simply iterates.
- Adding a new backend (for example, usrsctp) is a matter of teaching the iterator to construct it when appropriate; the rest of the server remains unchanged.
- Transports are persistent infrastructure: created at startup, destroyed at orderly shutdown.
6.2 Responsibilities and boundaries
A Transport:
- does:
- accept or establish associations;
- frame and deliver messages;
- surface transport‑level errors and association loss.
- accept or establish associations;
- does not:
- authenticate users;
- decide which Session a message belongs to;
- interpret opcodes or enforce application policy.
- authenticate users;
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:
- Owns the control stream (for example, SCTP stream 0).
- Runs the handshake:
- capability negotiation;
- authentication mechanism selection;
- user authentication;
- Session creation or attachment.
- capability negotiation;
- Implements control‑plane semantics:
- creating new Sessions;
- attaching to existing Sessions;
- detaching Sessions;
- advertising resume options;
- sending and interpreting protocol errors.
- creating new Sessions;
- Routes non‑control messages to Sessions:
- identifies the Session from the association identity and Session IDs;
- chooses the correct Session;
- passes each decoded message to that Session as a discrete event.
- identifies the Session from the association identity and Session IDs;
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:
- borrows:
- a Transport;
- the Broker.
- a Transport;
- owns:
- per‑association control state;
- handshake and authentication contexts;
- any internal tables needed for routing messages.
- per‑association control state;
Controllers are ephemeral:
- There is one Controller per Transport.
- A Controller enters a long‑running method (
*_run/*_process), registers itself with the Transport, and consumes messages until instructed to shut down.
- When the Transport goes away or a fatal error occurs, the Controller exits and is destroyed.
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:
- which Sessions exist;
- which Controller, if any, is attached to each Session;
- how to create, detach, resume, and destroy Sessions.
8.1 Responsibilities
The Broker:
- Creates new Sessions at the Controller’s request after a successful handshake and authentication.
- Assigns each Session a server‑side identifier.
- Records which Controller (and therefore which Client and Transport) is currently attached to each Session.
- Implements detach:
- unbinds the Session from any Controller;
- keeps the Session running on the server.
- unbinds the Session from any Controller;
- Implements resume:
- validates that the new Controller is authorised to resume the Session;
- re‑binds the Session to that Controller.
- validates that the new Controller is authorised to resume the Session;
- Destroys Sessions when:
- policy declares them abandoned; or
- the user explicitly closes them.
- policy declares them abandoned; or
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:
- It is created once at startup and destroyed only during orderly shutdown.
- It owns all Sessions:
- it is the only object that creates them;
- it is the only object that destroys them.
- it is the only object that creates them;
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”:
- the set of windows and their properties;
- workspaces and layout;
- which window has focus;
- seat state and input routing;
- GPU resources and bindings.
9.1 Responsibilities
A Session:
- Owns all server‑side windows associated with its user.
- Owns seat and input routing state for its user.
- Interprets application‑plane messages forwarded by the Controller:
- create/destroy windows;
- move/resize;
- update window properties;
- perform seat and input operations;
- drive other application‑visible actions.
- create/destroy windows;
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:
- The Broker is the sole owner of Sessions:
- It calls
*_session_createwhen a new Session is needed.
- It calls
*_session_destroywhen policy decides the Session is finished.
- It calls
- Sessions are long‑lived relative to Controllers and Transports:
- They can survive Client disconnects according to policy (grace periods, explicit detaches).
- They can survive Controller exits and Transport failures.
- They are destroyed only when the Broker instructs them to.
- They can survive Client disconnects according to policy (grace periods, explicit detaches).
- Controllers:
- ask the Broker to create or attach to a Session;
- borrow a Session pointer while attached;
- ask the Broker to detach;
- never destroy Sessions directly.
- ask the Broker to create or attach to a Session;
Detach/resume is implemented as rebinding:
- The Session never moves. It remains in the Broker’s ownership, with its Compositor bindings and state intact.
- Controllers come and go; a resume simply means “attach a new Controller to this existing Session”.
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:
- A Session may be bound to one Compositor (and thus one GPU) at one point in time.
- Later, the Session may be returned to the Broker and attached to a different Compositor, for example on a different GPU.
- This allows different Sessions, even from the same Client, to live on different GPUs, and allows Sessions to migrate between GPUs according to policy or load.
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:
- creating the Vulkan instance and enabling required extensions;
- enumerating physical devices (GPUs);
- handing those devices to callers so they can create Compositors.
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:
- SCTP is required as the underlying network transport.
- Transports are discovered via an iterator and wrapped in Controllers.
- Vulkan is required as the GPU API.
- GPUs are discovered via Vulkan and wrapped in Compositors.
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:
- owns the logical device, queues, swapchains, and GPU‑local resources for that GPU;
- manages windows, stacking, focus, and outputs for Sessions that render on that GPU;
- decides per surface whether to represent it as structured rendering commands or as a video surface.
A key feature of this model is that Sessions are not permanently tied to a particular GPU:
- Different Sessions, even from the same Client, may be bound to different Compositors, and thus to different GPUs.
- A Session may be detached from one Compositor and later attached to another, for example to migrate load or to take advantage of different hardware.
The Broker stands to Compositors as Controllers stand to Transports:
- A Controller “has a Transport” and implements protocol over SCTP streams.
- The Broker “has Compositors” and decides which GPUs Sessions render on.
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:
Session binding to associations
A Session is always reached via a Controller attached to a specific Transport association. The association identity is the authoritative Session identity, and the Controller simply has no way to deliver messages for “some other Session” over that association.Window scoping
Windows are always owned by exactly one Session. Window identifiers are scoped to that Session, and only that Session’s methods can operate on its windows. A Controller cannot hand a “foreign” window pointer to the wrong Session because the type system and ownership rules never produce such a pointer in the first place.Input scoping
Seats and input routing live inside the Session. Input events arrive tagged with the seat and Session context implied by the association and Session attachment, and the Compositor routes them only within that Session’s window tree.No cross‑Session observation
Clients never see global tables of Sessions or windows. They interact with exactly one Session through one Controller. There is no API for “give me all windows on the system”, so there is nothing to forget to filter.Client outside the trusted computing base
The Client owns only its local objects (MwscSession,MwscWindow,MwscControl). Server‑side state is never stored on the Client, and server objects never trust the Client for authorisation decisions; they always consult the Broker and the association identity.
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:
- drives the initial handshake;
- negotiates authentication mechanisms;
- performs user authentication;
- activates a
MwscSessiononce the server has created or attached a Session.
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:
MwscSessiontracks which windows exist, their properties, and their layout from the client’s point of view.
MwscWindowrepresents individual windows as they appear on the client display.
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:
- Treat C as a way to build objects with explicit ownership, not as a thin veneer over structs and global variables.
- Enforce ownership and lifetime via a small, project‑wide set of rules (the Iron Rules), applied consistently.
- Shape the API surface so that:
- Every non‑trivial type has *_create, *_destroy, *_handle, *_process/*_run.
- Every function name shows subsystem, object, and verb.
- Every method takes self first and acts only on its own state and arguments.
- Objects never reach into each other’s internals; all interaction is via methods.
- Private helpers are file‑local: in C, they are declared
staticand never appear in headers.
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:
- Authority is structural, not procedural.
- If you don’t have a reference to an object, you cannot act on it.
- If you do have a reference, it is valid, alive, and owned.
- Security emerges from the object graph.
- No ambient authority, no global registry, no “find the session for this connection”.
- Capability boundaries fall out of structure, not checks.
- Correctness becomes architectural.
- Calling a method on an object means you are dealing with that object — never an accidental neighbour.
- Resource allocation becomes honest.
- The “0..many” model works because ownership is explicit and acyclic.
- No leaks, no cycles, no runaway growth.
- The pattern works anywhere C works.
- Filesystems, network stacks, game engines, embedded systems, GUI toolkits — any domain with objects, identity, and lifetimes can adopt this style.
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.