Mercurius

Smalltalk-Style Software Design in C

Cooperating Objects, Message Passing, and the Iron Rules

Mercurius is implemented in plain C, but its design begins from a Smalltalk view of software: a running system is a community of independent objects which collaborate by passing Messages. Each object has identity, limited responsibility, explicit authority, and a well-defined lifetime. Behaviour emerges from their collaboration rather than being concentrated in procedures which know the whole system.

The purpose is not to reproduce class syntax in C. A hierarchy of structures with functions attached would retain the surface appearance of object-oriented programming while gaining little from it. This model is concerned instead with the organisation of behaviour: which object owns a fact, which object may act, which object receives a Message, and which object owns the lifetime of another. Structures, functions, and pointers are simply the material in which that model is expressed.

This paper makes explicit the structural discipline used to build Mercurius:

This is the structural elevation: it describes how any object in this style must behave. It was written down so that someone encountering the code could understand, extend, and improve it without reducing the design to procedural C. Its principles have value beyond that immediate purpose. Mercurius supplies the examples and evidence, but its concrete runtime assembly belongs in architecture.md. Protocol details and wire formats belong in the RFC, while the mechanical expression of the model in C belongs in the coding standards.


1. Conventions

To keep the description close to the conceptual model, the following conventions are used. Capitalisation identifies terms with a precise meaning in this model; it is not emphasis.

The model is defined in terms of these capitalised concepts; the implementation is a faithful realisation of that model in C.


2. The Iron Rules: ownership as law

Mercurius is written in C, but the mental model combines Smalltalk-style message passing with an explicit, acyclic ownership and lifetime discipline. C has no notion of ownership or lifetime, so Mercurius adopts four Iron rules which every object must obey.

2.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).

2.2 Why “Iron Rules”?

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

The discipline reflects its author’s longstanding C and object-oriented practice. Smalltalk is its primary influence: the system is conceived as independent objects collaborating through Messages. Design by Contract is a more specific influence on method obligations and preserved invariants. The intrusive lockable List used throughout Mercurius, for example, dates from 2013. Rust enters the story only as a later point of comparison: when the project was described, someone remarked that its explicit ownership and lifetime reasoning felt “very Rust-like”.

The comparison is useful because Rust has made the vocabulary of ownership and borrowing widely familiar and can enforce many related properties in its type system. The Iron Rules express the corresponding concerns as architectural invariants which a C implementation must uphold through design, review, and testing. In Design by Contract terms, they are system-wide invariants: constructors, destructors, and every method which establishes or uses an ownership relationship must preserve them.

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. When the implementation upholds them, the system is substantially easier to reason about and keep predictable. C does not enforce them; a violation remains a defect even when the compiler accepts it.

The Iron Rules provide the ownership foundation for a Smalltalk-style object model in a language with no classes, destructors, ownership types, or lifetime checker.

2.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 a discipline that plain C does not provide:

In short: the Iron Rules let Mercurius pursue the safety and predictability of a higher‑level object system while retaining the performance, portability, and directness of C. They do not turn C into a memory-safe language. They make the ownership and lifetime obligations explicit enough that behaviour under load can be reasoned about, reviewed, and tested rather than left to hope.


3. Objects, methods, and Messages

The model becomes useful only when it governs how objects collaborate. Naming conventions and method shapes make the discipline visible in C, but they follow from the object community; they do not create it.

3.1 Messages: how objects collaborate

The first question when one object needs something from another is not:

Which function should it call?

It is:

Which Message should it send?

A Mercurius Message represents a discrete communication from one object to another. The sender constructs the Message and hands it to the next object in the ownership graph. That object performs only the routing or handling for which it is responsible, then hands the Message onward. The sender need not know where the receiver lives, which Session or association carries the Message, which Transport is in use, or whether the Message crosses a process or network boundary at all.

_handle(self, message) is the standard entry point for this collaboration. When an object receives a Message, it does one of two things:

Forwarding is therefore not an incidental helper operation. It is part of the meaning of _handle. An object may inspect the universally understood header to decide where the Message belongs, but an intermediate object does not interpret or mutate the semantic payload merely because the Message passed through its handler.

Why Smalltalk is the touchstone

The Iron Rules do not magically turn a C program into a Smalltalk program, nor is “Smalltalk” shorthand here for classes, inheritance, or a language feature which C lacks. Mercurius uses very little inheritance. Its Smalltalk character lies in the spirit of the running system: independent objects with identity and limited responsibilities cooperate by passing Messages to one another.

No individual method appears to do very much, because no individual method is the system. Each object receives a Message, performs the small part for which it has authority, and either consumes the Message or hands it onward. The meaningful behaviour emerges at run time from the collaboration of the whole object community rather than from one procedure which knows and directs every step.

Mercurius makes those collaborations explicit as first-class MwsMessage objects which may pass through several _handle methods before reaching their semantic recipients. This is not a literal reimplementation of the Smalltalk runtime. Smalltalk is the design touchstone which keeps attention on Messages, responsibility, and emergent behaviour rather than on the surface machinery of object-oriented syntax.

Object orientation is not one tradition

Many programmers first meet object orientation through C++, Java, Python, or Rust. That experience can make OO appear to mean attaching methods to data, constructing a class hierarchy, selecting an implementation through virtual dispatch or a trait, and using encapsulation to hide the fields. Those are useful language mechanisms, but none of them by itself determines how a running system divides responsibility or how its parts collaborate. A program can use classes, inheritance, traits, and impeccable ownership while remaining a procedural design whose real behaviour lives in a few omniscient routines.

Smalltalk represents another and older emphasis: the running image is a society of objects, and computation proceeds as those objects send messages to one another. Ruby deliberately inherited much of that outlook; its objects respond to messages according to behaviour rather than their position in an elaborate static hierarchy. Python can be designed in the same spirit, even though it is often introduced through classes and method calls. Rust provides powerful ownership, borrowing, enums, and traits, but those facilities answer different questions from “Which object should receive this Message?” and “Where should this authority live?”

Mercurius takes its cue from the Smalltalk and Ruby side of that history while adding the explicit lifetime discipline required by C. Reproducing a C++-like class hierarchy manually would gain little and lose compiler support. The purpose of the Iron Rules is not to imitate the syntax of another OO language; it is to preserve independent identity, bounded responsibility, explicit ownership, and Message-driven collaboration in a language which imposes none of them.

The following Mercurius examples illustrate the general rule. A Presenter sends rendered content to a Projector by constructing MWS_RENDER_PRESENT and giving it to its parent Compositor:

/* Give the message to our parent compositor */
return mwsd_compositor_send(self->compositor, &message);

The Presenter does not find a Projector and does not know about Portals, Sessions, associations, streams, or Transports. It has completed its responsibility when it has constructed the right Message and handed it to its parent. The intervening objects route the Message according to their own responsibilities until the intended Projector receives it.

Header understood, payload opaque

The Message header is universally understood and identifies the Message type and extent. Routing also uses context established by the object path: a Transport supplies association context, a Controller selects a Session, and a Session may ask the Message object for a Window identifier. Asking the Message object for a routing field does not give an intermediate object the right to interpret the semantic payload.

The payload belongs to the semantic exchange between the objects that produce and consume that Message type. It is opaque to every other object. Routers do not mutate it, consume it, or become coupled to its representation. This is what permits each pair of collaborating Classes to evolve its payload in lockstep without teaching the entire object graph about it.

MWS_AUTH_RESPONSE is the canonical example. The response is meaningful to the Authenticator, not to the objects which carry it there. The Authenticator consumes the Message and destroys its sensitive contents with explicit_bzero; no intermediate object needs to understand them.

Messages are not wire packets

Some Messages cross the network and some deliberately do not. This does not change the object model. MWS_RENDER_UPLOAD, for example, is an internal Application-to-Window Message on the server side. Application does not call a Presenter function simply because both objects ultimately execute on the same machine. The Message follows the real object path, allowing Window, Compositor, and Presenter to retain their separate responsibilities.

That path is also a security boundary. The Channel belongs to an Application, the Application belongs to exactly one Session, and the Session resolves the Message’s Window identifier only within its own Window collection. No current transport association is required for that internal authority path. A direct Application-to-Presenter call would bypass this structural routing and make it much easier for one Application to reach a Window belonging to another Session. Message passing therefore preserves the isolation which prevents Eve or Mallory from capturing Alice’s or Bob’s Windows, as well as avoiding implementation coupling between Application and Presenter.

Keep the method surface small

Every non-trivial Class exposes the standard lifecycle and behavioural interface described below: _create, _destroy, and whichever of _handle, _process, and _run its role requires. A Class may also have domain-specific methods where ownership, local inspection, or an intrinsic operation makes a direct call the honest interface.

Those methods should be as few as the Class can reasonably have without contortion. A convenience method that reaches across an object boundary is not harmless: it adds coupling and encourages the system to become a collection of procedures acting on structs. Before adding one, ask whether the operation is really a Message that should travel through the cooperating object graph.

Orchestration is legitimate when coordinating a sequence is itself part of the object’s responsibility. The test is not method length and it is not whether a method calls several helpers. The test is whether the orchestrator owns the operation and composes bounded steps without taking decisions, state, or authority which belong to another object.

mwsc_auth_build_ssh_credential() is a good example. Building one SSH credential belongs wholly to the Auth object. The method creates temporary mechanism state, initialises the private key, builds the signing envelope, extracts the public key, signs the envelope, assembles the credential, and unconditionally destroys the sensitive intermediate state. Each step remains a separate bounded private method, while the orchestration expresses the one coherent operation and owns its cleanup path.

What the model resists is not orchestration but omniscience: a method which coordinates distant objects by reaching through their boundaries, interprets Messages which belong to them, or accumulates enough foreign knowledge to become the real system in procedural disguise. Larger system behaviour should still emerge from cooperating objects observed together at run time.

This is the practical meaning of the mantra:

Design in Smalltalk. Implement in C.

3.2 Methods: self‑first, verbs last

Non‑trivial functions are treated as methods on objects:

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

A method call is not, however, merely another spelling of a Mercurius Message. Methods provide an object’s lifecycle and behavioural interface; Messages are how independent objects collaborate. Keeping that distinction clear is essential. Otherwise a collection of structs with functions attached can look object-oriented while remaining a procedural call graph underneath.

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 portal, 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.

3.3 Naming: 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.

3.4 Error handling: bool + errno

Mercurius takes inspiration from Design by Contract, most closely associated with Bertrand Meyer and Eiffel. Every public method has required inputs, an explicit responsibility, and a postcondition by which success can be judged. C cannot express those contracts as Eiffel does, so Mercurius makes them part of the method interface and implementation discipline. A method which cannot establish its postcondition reports failure while preserving the object’s invariants.

All non‑trivial operations follow the same basic contract:

NULL-tolerance is not a general property of methods. A behavioural method has no object on which to operate when self == NULL, and treating that as success would conceal a caller error. The exception exists only for destruction and serves the ownership and teardown rules described below.

A destructor failure is exceptional and serious, but it must remain observable. A destructor which encounters one release failure continues destroying every other resource it owns rather than returning early and leaving a half-destroyed object graph. It then returns false with errno set so the caller can preserve a useful diagnostic and escalate the failure. In practice an owner being unable to destroy an object it owns is likely catastrophic. Catastrophic does not mean uncontrolled: the caller continues the orderly destruction of its own object graph and drives a clean unsuccessful shutdown rather than deliberately crashing or producing a core dump. The return value reports whether the destructor fully achieved its postcondition; it does not preserve the object for another attempt.

The uniform contract means:

There is no proliferation of ad‑hoc error channels. This ensures “something went wrong” is clearly separated from “computer says no”. The latter can be conveyed by some other means such as an out-parameter or a reply message (messages carry a sender field for this purpose). A method returns ‘true’ to say that it ran successfully, not necessarily what the outcome was: for example an Authenticator might say “yes I successfully considered your request, and no you can’t come in”.

A constructed object is always safe to use

If _create returns an object, every public method of that object is safe to call until _destroy is called. Safe does not mean that every operation must succeed. It means that the method must validate its arguments and current object state, then either perform its responsibility or fail cleanly according to its declared contract. It must never crash, invoke undefined behaviour, or depend on the caller knowing an undocumented fragment of its construction history.

Lifecycle dependencies remain legitimate. A Transport may need to be opened before it can carry a Message; a state machine may accept an operation only in one phase; a resource may not yet have been bound. Calling the corresponding method too early or in the wrong state is nevertheless non-fatal. The method returns false with errno set, or reports the negative semantic outcome by the Class’s normal Message contract, while leaving the object in a valid state.

This guarantee begins at the constructor boundary. A constructor must either return a fully initialised object whose invariants hold, including explicit sentinel values for resources not yet acquired, or clean up internally and return NULL. A partially initialised object is never handed to its caller. After _destroy returns, the object no longer exists and its former pointer must not be used.

3.5 A method owns its facts and returns control

Design by Contract defines what a method promises. Responsibility determines which facts it is entitled to report and which decisions remain with its caller.

Objects use methods for lifecycle, intrinsic operations, and for accepting or handing onward Messages. Each method has a well-defined responsibility and a limited view of the system. It therefore reports only the facts that arise from performing its own work.

A method owns its facts; it does not own its caller’s decisions.

When a method returns, it reports what it knows:

These facts describe the outcome of the method itself. They do not prescribe what should happen next.

The caller always retains control. Having received the facts, the caller decides how to respond according to its own responsibilities and the wider context in which it is operating. It may retry the operation, choose an alternative course of action, report the outcome elsewhere, or decide that no further action is required.

A method may perform whatever work is necessary to fulfil its own responsibilities, including retrying an action that failed. Persisting in pursuit of the result it was asked for is not a decision about the caller; it is simply the method doing its job. The rule engages only once the outcome is settled — whether the method succeeded, or has genuinely determined that it cannot. At that point it reports the facts it knows and returns control. It does not choose the caller’s recovery strategy or make decisions that belong to another object.

This separation of responsibilities keeps object boundaries clear. Each object is responsible for its own behaviour and reports only the facts within its knowledge. Higher-level behaviour emerges naturally from collaboration between objects rather than from lower-level methods making assumptions about their callers’ intentions.

A method’s postcondition defines success; the caller must not be required to prove incidental facts in order to discharge its responsibility. A call that finds the required state already holding has succeeded, because the responsibility is met. A _destroy asked to release what is already released, or an unlink asked to remove a path already absent, has done its job: the thing the caller needed gone is gone. Returning failure for a no‑op reports the wrong fact.

The two destructor rules are opposite sides of the same invariant:

A destructor must completely destroy its object and release every resource the object owns; therefore an owner must be able to call the destructor for every child unconditionally.

This is why destructors, and only destructors, accept NULL. An owner must not have to reconstruct the history of a partially created or partially torn-down object before it can destroy it. Some children may not yet have been created; others may already have been destroyed on an earlier path and their pointers cleared. The owner’s destructor can call _destroy on every child slot in ownership order without a ladder of tests for which earlier operations did or did not occur.

A destructor receiving NULL has nothing to release, so its postcondition already holds. This is a small, deliberate exception to the rule that a method acts on an object identified by self, justified by the stronger rule that an owner’s destructor must reliably release the complete object graph beneath it.

3.6 Lifecycle and behaviour

Every non‑trivial object type offers _create and _destroy, together with whichever of _handle, _process, and _run its responsibility requires. These methods define how an object is created, collaborates, performs its own work, and is eventually destroyed under the Iron Rules.

Once this pattern is understood, unfamiliar functions become easy to classify: if a function is named mwsd_handshake_process, it advances the Handshake’s state machine; if it is named mwsd_channel_run, it is the Channel’s long‑running thread loop; if it is named mwsd_projector_run, it is the Projector’s GUI/render loop.

3.7 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.


4. Applying the model to Mercurius

The rules above are a general engineering discipline. Mercurius supplies a substantial worked example of that discipline in use.

4.1 Presence, not pixels

Mercurius begins from a simple belief:

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

The Workstation is the long-lived centre of gravity. It owns authoritative Session state and the resources which make that state useful. A Portal is an authenticated access point which supplies display and input; it is not the authority for the user’s work. Local access is another Portal rather than a separate model of the system.

This philosophy creates an unusually clear test for object boundaries. Sessions must outlive connections. A transient network object must not own a user’s long-lived state. A Portal must not acquire authority merely because it presents a Window. Routing input and rendering output must preserve the identity of the association, Session, and Window to which they belong.

Those requirements do not prescribe one large procedure. They call for objects whose ownership, authority, and Messages express the required relationships directly.

4.2 A community of cooperating objects

Mercurius contains objects named Transport, Controller, Broker, Session, Application, Channel, Compositor, Presenter, and Projector. Their complete runtime arrangement is documented in architecture.md; what matters here is how they illustrate this model.

A Transport owns transport mechanics, not Session policy. A Controller understands the control-plane Messages for which it is responsible and routes other Messages onwards. A Broker owns Sessions and decides which Controller may attach to them. A Session remains authoritative for its own Applications and Windows. A Compositor, Presenter, and Projector collaborate to make a Window visible without collapsing into one rendering procedure.

No object needs to understand the whole path. Each knows its neighbours, its own state, and the Messages it is entitled to interpret. The system’s behaviour appears only when those objects run together.

This is the important distinction between an object community and a collection of structures with functions attached. The names and method shapes make objects visible in C; the limited knowledge and Message-driven collaboration make them objects in the architectural sense.

4.3 Ownership makes persistence possible

The Iron Rules turn the philosophy of long-lived presence into a lifetime structure. The Broker owns Sessions; a Controller may borrow an attached Session but cannot destroy it. A Controller or Transport may disappear while the Broker and Session remain. Detachment is therefore a change in a relationship, not a migration of ownership or a reconstruction of the user’s state.

The same rule applies throughout the system: the longer-lived authority owns the shorter-lived state, while transient collaborators borrow only what they need. Teardown follows the ownership graph, and loss of one edge does not silently confer ownership on another object.

This is more than memory management. Ownership determines who may decide, which failures are local, and which state survives the loss of a collaborator.

4.4 Security emerges from legitimate routes

Mercurius Messages have universally understood headers and payloads which belong only to their semantic sender and receiver. The Message type and the object path together provide the context needed to route and authorise the Message. Intermediate objects may obtain the routing information for which they are responsible; they do not thereby acquire the right to interpret or mutate the semantic payload.

That rule preserves security even for internal Messages. An MWS_RENDER_UPLOAD enters through a Channel owned by an Application belonging to exactly one Session. The Session resolves the Window identifier only within its own Window collection, so sharing a process does not give Eve or Mallory a route to Alice’s or Bob’s Window.

A detached Session has no current Portal association, but retains its identity, ownership graph, and authority. Suspending or resuming its Applications is Session lifecycle policy; it does not supply the security boundary. If a Message is subsequently sent to a Portal, the Session uses its current Controller and association binding.

The object graph does not replace authentication, validation, or cryptography. It ensures that successful authentication does not lead into a program full of global tables and convenient cross-object functions which must each recreate the same authority checks. Legitimate references and Messages are supplied along legitimate relationships; crossing a security boundary consequently requires breaking a visible object-model invariant.

4.5 Zero to many

Mercurius treats absence and multiplicity as ordinary. There may be zero, one, or many Transports, Controllers, Sessions, Applications, Channels, GPUs, Compositors, Presenters, Projectors, Windows, and Portals. The implementation discovers or creates objects as needed and reports a resource failure only when it genuinely cannot satisfy a request.

This is another consequence of object identity and explicit ownership. No component depends on a global current Session, current Window, or only GPU. Each instance carries its own state and participates in the same relationships as every other instance. Code written for one object therefore remains structurally correct when a second appears.


5. What the Mercurius example demonstrates

Mercurius is not included here as a catalogue of Classes. It is evidence that the discipline can organise a concurrent systems program with long-lived and short-lived state, local and network delivery, multiple authentication mechanisms, several resource domains, and security-sensitive routing.

The example supports several general claims:

These properties do not arise from the C syntax. They arise because the running program has been designed as cooperating objects and because the Iron Rules are treated as load-bearing constraints.

For the complete Mercurius object graph, startup order, concrete ownership relationships, and individual component responsibilities, see architecture.md.


6. Applicability beyond Mercurius

Mercurius is one application of this object model, not the definition of its limits. The model is useful wherever a system is easier to understand as a community of independent objects than as a sequence of procedures operating on shared state.

The essential pattern is small:

None of these ideas depends on windows, graphics, SCTP, Vulkan, or any other Mercurius-specific concern. They describe how to divide responsibility and preserve boundaries in a long-lived software system.

6.1 Why Message passing matters

Ordinary procedural decomposition answers the question, “Which function performs this step?” Message-oriented design first asks, “Which object has the authority and responsibility to receive this request?” That difference remains important even when both implementations ultimately execute a C function.

An explicit Message makes the collaboration itself visible. It records what happened, who initiated it, and what kind of object should consume it. The sender need not discover the receiver’s implementation or manipulate its state. Intermediaries can route the Message without learning its semantic payload. A receiver can change its implementation without requiring every sender and router to change with it.

This produces loose coupling of knowledge, not an absence of cooperation. Objects may collaborate closely and their Message formats may deliberately evolve together. The boundary is that unrelated objects do not become coupled to that exchange merely because they carry it.

Message passing is also independent of physical topology. A Message may stay inside one translation unit, cross a thread boundary, travel between processes, or be encoded on a network. Moving the receiver should change the delivery mechanism, not force the sender to learn a different model of the system.

6.2 What the Iron Rules contribute

Message passing alone does not define lifetime. In a garbage-collected Smalltalk image, the runtime carries much of that burden. In C, ownership must be made explicit by design.

The Iron Rules give each object one place in an acyclic ownership graph. An owner knows what it must destroy; a borrower knows what it must not destroy; and a short-lived collaborator cannot silently take control of a longer-lived object. The standard _create and _destroy interface makes that graph executable during both normal shutdown and partial failure.

This has consequences beyond memory management:

The rules do not magically prevent every leak, race, denial of service, or authorisation defect. C still permits mistakes, and validation is still required at every untrusted boundary. Their value is that ownership, authority, and collaboration are made explicit enough to inspect, test, and reason about.

6.3 Where the model is useful

The approach is most useful when software has several of these properties:

Examples include network services, storage systems, device managers, GUI toolkits, game engines, embedded controllers, protocol implementations, and distributed applications. In each case the nouns differ, but the questions remain the same: Who owns this object? Who may send it a Message? Which object is entitled to act? Who destroys it? What happens when there are zero, one, or many?

The model has a cost. It requires discipline, more deliberate naming, and a willingness to resist convenient calls which bypass the object graph. A small calculation with no identity, lifetime, or collaboration does not become better merely by being wrapped in a Class. The purpose is not to turn every function into an object; it is to keep systems made of real objects from degenerating into shared state and procedural reach-through.

6.4 A review test

When adding behaviour to a system built this way, ask:

  1. Which object has the responsibility and authority for this behaviour?
  2. Is this an intrinsic local operation, or should it be expressed as a Message?
  3. Who is the semantic sender, and which Class is entitled to consume the payload?
  4. Are intermediate objects routing the Message, or have they begun to interpret an exchange that does not belong to them?
  5. Does a new domain-specific method clarify a real boundary, or bypass one for convenience?
  6. If a method orchestrates several steps, does that complete operation belong to the object, with every delegated step and cleanup path remaining inside its authority?
  7. Is every reference owned or explicitly borrowed, and is any transfer of ownership unmistakable?
  8. Can every public method reject invalid arguments or an invalid lifecycle state without crashing or damaging the object?
  9. Can the owner destroy the object and every resource it owns after full, partial, or failed construction?
  10. Does the design still make sense with zero, one, or many instances and no implicit “current” object?

If those questions have clear answers, the design is likely preserving the model. If the answers require global state, knowledge of a distant object’s internals, or a special case for the second instance, the design has probably started to become a bucket of C.

6.5 Intellectual lineage and language independence

Mercurius is implemented in C because C is the language in which its author can express systems ideas fluently and because every computer of interest has a C compiler. The architectural model belongs to no language.

This discipline makes no claim to have invented object collaboration, contractual interfaces, or explicit ownership. Its primary influence is Smalltalk’s message-oriented object model. It also draws from Design by Contract, described by Bertrand Meyer in Object-Oriented Software Construction and embodied in Eiffel; established C systems practice; and the ownership vocabulary later made familiar by Rust. Picasso would be proud.

What is offered here is the synthesis: an explicit account of how its author believes software should be designed when given the time and space to do so. Smalltalk supplies the central idea of software as cooperating objects sending Messages, with behaviour emerging from their collaboration at run time. Design by Contract informs the explicit obligations and invariants of every method. C requires ownership and dispatch to be expressed through convention and ordinary data structures, while Rust supplies a useful modern reference point for several of the ownership and lifetime goals. Mercurius provides a substantial contemporary example.

Other languages can enforce different parts of the same model directly:

The syntax and enforcement mechanisms change; the architectural questions do not. A successful implementation in another language would preserve the same independent responsibilities, Message boundaries, ownership relationships, and deliberately small behavioural interfaces rather than reproduce the surface appearance of the C code.

This is therefore not the claim that “object-oriented” means the implementation should have been written in C++ or Rust. Those languages may be suitable implementation media, but choosing one does not create this architecture. A C++ hierarchy can still become a procedural system hidden behind methods, and a Rust program can have impeccable memory ownership while assigning behaviour and authority poorly. Conversely, plain C can express the model faithfully when its object and Message boundaries are treated as load-bearing design.

That is the broader lesson of Smalltalk-style software design in C:

Design in Smalltalk. Implement in the language appropriate to the system.