Mercurius — A Zero-Trust Network‑Native Window System

A retrofuturist engineering bench with amber source code,
measuring instruments, technical plans, and carefully arranged
electronic components, representing disciplined construction in C.

Plain C. Deliberate discipline.

The Mercurius coding standards turn the Iron rules into clear, portable code that owns its resources, explains its intent, and fails cleanly.

Mercurius C Coding Standards

The material expression of Smalltalk-style software design in C

Mercurius is implemented in plain C, a simple language. C’s minimalism is precisely where its strength lies. Because it imposes almost nothing, it lets you build almost anything — and that same permissiveness is exactly what its critics point to. Nothing in the language stops you from building the wrong thing: there’s no built-in ownership tracking, no automatic lifetime management, no guaranteed order of teardown, no protection against reference cycles, and no safeguard against complexity accumulating unnoticed.

With a more opinionated language, you sometimes end up reshaping your design around what the language itself will permit, rather than what the problem actually calls for. C makes no such demand — but that is not licence to ignore the properties those demands exist to protect. The goal is not merely code that compiles; it is code that holds itself to the same standard, achieved through discipline rather than compulsion.

Where the language stays silent, discipline has to speak instead. The Iron Rules supply that discipline; this document is about how to express it in plain C.

Mercurius begins from a Smalltalk view of software: a running system is a community of independent Objects which collaborate by passing Messages. C does not itself enforce Classes, contracts, ownership, lifetime, or Message boundaries, so the source must make those properties explicit.

This document defines the material elevation of Mercurius: how the design is expressed in C source files, declarations, control flow, error paths, comments, and interfaces. Read Smalltalk_in_C.md first for the structural discipline and the Iron rules. Read architecture.md for the concrete Mercurius assembly. The RFC defines behaviour visible between implementations.

These are project rules, not a generic embedded-C profile. A rule belongs here because it makes Mercurius more correct, portable, intelligible, or faithful to its design. Dynamic allocation, operating-system services, threads, and third-party libraries are legitimate tools when their ownership and failure semantics remain explicit.

Normative words have their usual meanings:

Each rule should make its engineering reason clear. The reason may be correctness, ownership, portability, reviewability, or faithful expression of the Object model; consistency alone is sufficient only for genuinely mechanical layout choices. Knowing the reason lets a maintainer apply the rule to an unfamiliar case and challenge it if its assumptions cease to hold.

The standard is written precisely enough to support mechanical and AI-assisted review. Such tools report evidence; they do not replace engineering judgement.

New code MUST comply. Changed code MUST comply within the area being changed and MUST NOT introduce another instance of a known deviation. Older code may predate a rule; that makes it evidence for review and possible refactoring, not a reason to weaken the standard or to expand an unrelated change. Legacy interfaces do not define the preferred practice merely because their refactoring belongs to a later Waypoint.


1. Governing principles

1.1 Primary qualities

Mercurius C should be unsurprising to an experienced C programmer once the object and Message model is understood.

Code MUST be:

1.2 Design philosophy

These are decision rules, not aspirations added after implementation. They explain why Mercurius favours explicit contracts and deliberately boring local control flow: a moment saved while writing code is a poor trade for ambiguity repaid on every later reading.

The implementation MUST NOT acquire a global current Session, Window, Controller, Compositor, or similar domain singleton. Process-wide facilities such as signal state or library initialisation must remain infrastructure, not ambient domain authority.

A convenient direct call MUST NOT be used to bypass a Message boundary, ownership boundary, or authority boundary. Conversely, code MUST NOT contort an intrinsic local operation into a Message merely to avoid an honest method. The software-design paper defines that distinction.


2. Classes and translation units

2.1 One Class, one implementation boundary

A non-trivial Class SHOULD have:

The concrete struct for an opaque Class MUST be defined in its implementation file, not exposed to callers. A caller operates through the Class interface and MUST NOT reach into another Object’s representation.

Closely related private data types and helpers MAY share the implementation file when they exist only to implement that Class. Splitting code into files is not a substitute for assigning responsibility.

2.2 Object identity

Every Mercurius Object begins with MwsObject as its first member:

struct MwsdPresenter
{
    MwsObject base;
    /* Class state follows. */
};

Every Class declares a unique four-character Class tag and a typed NULL sentinel:

#define MWSD_CLASS_PRESENTER mws_fourcc("PNTR")
#define MWSD_PRESENTER_NONE ((MwsdPresenter *)NULL)

The Class tag provides runtime identity in a language which has no native Class system. It is not a substitute for an ownership or authority check.

When users of a Class reasonably need to know or change one of its facts, the Class provides a getter and/or setter rather than exposing its representation. Such methods are intentionally rare. Users normally ask an Object to perform its responsibility; they do not retrieve and manipulate it as a lump of data.

For example, asking a Window for its identity through mwsd_window_get_id is reasonable because collaborating Objects need that ID to construct Messages which refer to that Window. A Session or Compositor may accept such a Message, but its window_id must still identify which Window the Message concerns. mwsd_window_set_geometry changes geometry through the Window interface rather than exposing the field. A getter returns the requested fact or its documented sentinel. A setter validates the requested value, preserves the Object’s invariants, and follows the ordinary bool and errno method contract. Getters and setters expose facts required for collaboration, not automatic properties for every private field.

2.3 Public and private methods

Public methods:

Private methods and file-local data MUST be declared static. A helper MUST NOT be placed in a public header merely because another module finds it convenient.

Private methods live in the same implementation file as the Class representation, constructor, and destructor. This translation-unit boundary allows the Class implementation to access its own fields directly while preventing users of the Class from depending on them. Users obtain facts through public methods; they do not acquire representation knowledge merely because they own an Object.

A Class should expose as few domain-specific methods as it reasonably can without contortion. Lifecycle and behavioural methods form the recognisable core; additional methods must express operations which genuinely belong to the Class.

2.4 Standard method forms

Every non-trivial Class provides *_create and *_destroy, plus whichever of these its responsibility requires:

A Class without a state machine does not acquire *_process merely for uniformity. A Class without long-running behaviour does not acquire *_run.

The first parameter of every method other than a constructor MUST be the Object and MUST be named self.

These common forms make an unfamiliar Class discoverable. A maintainer can find lifecycle, Message handling, state-machine, and thread-loop entry points without first learning a bespoke vocabulary for every Object. Omitting forms which do not apply is equally important: uniform names must describe real responsibilities rather than create ceremonial methods.

2.5 Inheritance and substitutable roles

Mercurius is not opposed to inheritance. It uses inheritance where callers need genuinely substitutable implementations of one abstract role. What it does not do is treat a simulated Class hierarchy as the purpose of Object-oriented design in C. The important discipline remains cooperating Objects with identity, responsibility, lifetime, and Message boundaries.

Transport is the deliberate example. MwsTransport is the abstract base Class. Its operations table dispatches to a selected implementation such as LOCAL or SCTP, while callers continue to hold and pass MwsTransport *. In a Transport operation, self therefore identifies the base Transport; backend state is reached separately through its private implementation. It is wrong to describe self as an SCTP Transport merely because the selected implementation happens to be SCTP. The abstraction is intentionally open to further concrete Transport types, including implementations for Windows and macOS portals and transport technologies which do not yet exist.

Authenticator illustrates composition instead. An Authenticator owns and coordinates authentication mechanisms; it is not a base Class from which PAM, SSHKEY, and TRUST Authenticators inherit. Projector likewise remains one Projector responsibility while its implementation selects the appropriate Portal display facilities. Variation alone does not require a derived Class.

This distinction places inheritance at boundaries which need substitution without mistaking inheritance for the Object model itself. Shared code, similar names, or several implementation choices may motivate a common base, but the deciding question is whether they are substitutable instances of the same responsibility.


3. Method contracts and Class validation

3.1 Validate the receiving Object

C provides no RTTI, Object runtime, borrow checker, or other runtime help which can establish that a pointer identifies a live Object of the expected Class. The Iron rules therefore depend on explicit discipline at every method boundary: mws_object_is is that discipline. Every method which receives self MUST validate it before any other work. The Object-validation block is the first executable code in the function:

if (!mws_object_is(self, MWSD_CLASS_PRESENTER))
{
    errno = EINVAL;
    return false;
}

Methods which receive other Objects MUST validate their Classes before use. Data pointers, identifiers, lengths, enums, and required state must likewise be validated before they can affect the Object.

mws_object_is is deliberately static inline so that Class validation is cheap enough to perform at every method boundary. It MUST NOT be omitted as an optimisation, deferred until after local setup, or replaced with a NULL check. Without this check, C provides nothing else which enforces the Object model or detects that a Message has been delivered to an Object of the wrong Class. No local-working-state declarations precede the validation block. The receiver check is the gate which decides whether the Message has reached an Object capable of receiving it; only after that gate succeeds does the method’s implementation begin. Declaring or constructing method-local state before the gate therefore puts implementation machinery before the receiving Object has been established.

Only destructors accept self == NULL. Every other method treats a missing or wrong-Class self as EINVAL. A destructor first distinguishes its documented NULL case, which means that there is nothing to destroy and its postcondition already holds. It then validates every non-NULL receiver in the ordinary way:

if (self == NULL)
{
    return true;
}

if (!mws_object_is(self, MWSD_CLASS_PRESENTER))
{
    errno = EINVAL;
    return false;
}

The NULL test is part of the destructor contract, not method-local setup. No local-working-state declaration precedes either destructor gate.

3.2 Constructors

A constructor:

The caller owns an Object returned by *_create. A constructor MUST NOT return a partially usable Object and require the caller to guess which methods are safe.

3.3 Behavioural methods

A non-trivial operation whose result is success or failure returns bool:

Success describes the method’s own responsibility, not necessarily a positive domain outcome. An Authenticator can successfully consider a credential and deny access. Such an outcome is returned through an out-parameter, Object state, or reply Message; it is not confused with failure to perform the check.

For *_handle(self, message), the method’s responsibility is to accept and understand the Message. The receiving Object may consume the Message itself or pass it to the appropriate cooperating Object. A true return means that this responsibility succeeded; it does not mean that the operation represented by the Message has completed, or even that completing it is the receiver’s responsibility.

For example, when a Session accepts MWS_CLOSE_WINDOW, validates its addressing, and passes it towards the addressed Window, the Session’s handler returns true. The Window need not yet be closed. Once it has closed, the Window sends MWS_WINDOW_CLOSED to the original sender. That reply, rather than the return value of the Session’s handler, reports completion of the requested operation.

A handler returns false with errno set when it cannot accept the Message. Examples include an unrecognised opcode, an invalid addressed Window ID, or a resource constraint which prevents the Message from being accepted. This is the ordinary method convention: success is defined by the method’s own postcondition, not by the eventual domain outcome requested in its argument.

Getters are the exception to the bool return convention. A getter returns the requested value directly, or a documented sentinel when it cannot do so. A failure sentinel is returned with errno set. If the sentinel can instead represent an ordinary absence, such as an optional binding which is not present, the interface MUST document that distinction unambiguously.

A successful method does not clear errno. Callers inspect errno only after a documented failure return and before another call can replace it.

3.4 Safe failure

Once a constructor returns an Object, every method is safe to call in any order until destruction. An operation may be invalid in the Object’s current lifecycle state, but it MUST fail cleanly: return false with errno set, rather than crash, invoke undefined behaviour, or damage the Object. A getter reports the same failure through its documented sentinel and errno, as specified in Section 3.3.

A failed method MUST preserve the Object’s invariants unless its documented postcondition explicitly ends the Object. It MUST NOT leave ownership ambiguous, leak a newly acquired resource, or require an undocumented recovery sequence.

Assertions MUST NOT be used to reject untrusted input, resource exhaustion, ordinary lifecycle errors, or any condition which can occur in production. They MAY document an internal condition whose violation is genuinely a programming defect, but the preference in daemon code is still controlled failure and a useful diagnostic.

3.5 Destructors

A destructor:

After *_destroy returns, the former pointer MUST NOT be reused or passed to the destructor again. The owner clears stored child pointers as part of its own teardown.

NULL tolerance exists so an owner can destroy all child slots unconditionally, including children not yet created or already destroyed and cleared during an earlier failure path. It is not a general method convention.

A catastrophic teardown failure still results in orderly unsuccessful shutdown and useful diagnostics, not an intentional core dump.

3.6 A method owns its facts

A method reports only facts it is responsible for knowing. It MUST return control to its caller once its outcome is settled and MUST NOT choose the caller’s recovery policy.

When a called method fails, callers normally preserve and propagate its errno. A caller MUST NOT replace a more precise error with a generic value unless it is deliberately translating between abstraction boundaries.

Set EINVAL for invalid arguments, wrong-Class Objects, and invalid values at the caller boundary. Use a more precise errno-compatible value for valid calls which fail because of lifecycle state, resources, protocol input, policy, or an external subsystem.

A method MAY retry work which remains part of its own postcondition. That is persistence, not an attempt to control its caller.

For example, Transport may be asked to deliver MWS_RENDER_PRESENT to a Portal. If the network is unavailable, the Portal has gone away, or delivery otherwise fails, Transport reports that it could not deliver the Message. It does not decide whether the presentation should be retried, superseded, discarded, or treated as a Session failure. Those consequences belong to the Presenter, which owns the presentation and therefore decides what to do with Transport’s reported fact.

3.7 Choosing errno

errno identifies whose contract failed and why. Choose the most precise errno-compatible value; do not use EINVAL as a generic failure.

errno Meaning in Mercurius
EINVAL The caller supplied an invalid method argument, including NULL, a wrong-Class Object, or a value outside the method contract.
EFAULT A Class found its own Object or private state inconsistent. This reports a broken internal invariant, not bad input from a Portal.
EBADMSG Received bytes cannot be decoded as the Message representation they claim to contain.
EPROTO A decoded Message violates the peer’s protocol obligations. When reported to a Portal, this maps to MWS_ERROR_PROTOCOL and attributes the fault to that Portal.
ENOMEM Allocation failed. This maps to MWS_ERROR_RESOURCE when reported through the protocol.
EACCES, EPERM Authentication, authorisation, or policy refused the operation. These map to MWS_ERROR_POLICY.
ENOENT A validly formed request refers to an Object or identifier which does not exist.
ENOTSUP The requested operation or facility is not supported by this implementation.
EMSGSIZE, EOVERFLOW A representation limit or checked size conversion cannot contain the requested value.

The boundary which understands the source of a value chooses between these categories. In particular, an invalid C method argument is EINVAL; a value received from a Portal which violates the RFC is EPROTO, not EINVAL; and a Class discovering that its own established invariant no longer holds uses EFAULT.

Use mws_opcode_map_errno when translating a failed method into an MWS_ERROR_* reply. That function owns the mapping policy. Callers MUST NOT duplicate the mapping or choose a protocol error opcode independently. Errnos returned by operating-system and external-library boundaries are normally preserved when they already describe the failure accurately.


4. Naming

4.1 big_little_littlest

Function names follow the big_little_littlest form:

Examples:

mws_transport_destroy
mwsd_session_handle
mwsd_broker_attach_session
mwsc_auth_build_ssh_credential

Given the subsystem, Class, and verb, the symbol should be unsurprising. Abbreviations must be established project vocabulary rather than inventions local to one file.

The convention makes symbols searchable and predictable from the design. It also prevents a short local name from concealing which subsystem and Class own the behaviour.

4.2 Types, constants, and sentinels

Do not add _t or _e suffixes mechanically. Use the name that most clearly describes the type.

Typed sentinels make absence or invalidity visible without relying on an unexplained literal. Consistent prefixes distinguish public protocol facts, server and Portal implementation details, and local working state when code is read without IDE type information.


5. Types, values, and arithmetic

5.1 Choose types by meaning

Use fixed-width integer types for:

Give protocol identifiers and other domain quantities semantic typedefs over those representation types. Public and private interfaces MUST use the semantic type when one exists. For example, a stream identifier parameter is an MwsStreamId, not merely a uint16_t.

The typedef makes the parameter’s meaning visible at the call boundary and keeps its representation a property of the type definition. The representation can therefore change, for example from uint16_t to uint32_t, without requiring unrelated interfaces and their callers to be rewritten. Code which is serialising or performing explicitly width-dependent arithmetic may use the underlying fixed-width type where that representation is the fact being expressed.

Use size_t for object sizes, buffer lengths, allocation sizes, and element counts. Use ptrdiff_t for pointer differences. Native integer types are appropriate when their ABI-defined meaning is intended, for example an operating-system file descriptor or API return code.

Do not assume the width of int, long, pointers, enums, or size_t.

Use bool for logical values. Do not invent integer booleans except when adapting an external API.

5.2 Initialisation

Every local variable MUST be declared at its first use and given a defined initial value there:

VkDevice device = mwsd_compositor_get_device(self->compositor);

Zero initialisation is appropriate only when zero is a valid defined state for every member concerned. Otherwise assign explicit sentinels.

Do not collect declarations at the top of a function in anticipation of later work. Reading the function from top to bottom should introduce each name at the point where that part of the operation begins, in the same spirit that a name first appears when it is assigned in Python.

In a method, “first use” is necessarily after the receiver gate described in Section 3.1. The method validates self before declaring or constructing any method-local working state. Variables needed across cleanup paths are declared immediately after receiver and argument validation, and initialised before any path can reach the cleanup code. This preserves safe structured cleanup without placing C machinery before the receiving Object.

Resource cleanup does not create an exception to this rule. When resources are acquired progressively, use staged out_* labels, smaller private methods, or another structured cleanup arrangement so each resource variable can still be introduced at acquisition. Do not declare a screenful of NULL handles merely to support one generic cleanup label.

Every local read by an out_* handler MUST be declared and initialised before the earliest goto which can enter that handler. A jump MUST NOT bypass its initialisation. Although GCC may accept such control flow, the handler would read an indeterminate value and stricter compilers correctly reject it. Give an owned resource its typed sentinel before the first possible jump to the handler, then replace the sentinel when acquisition succeeds:

/* self and out_window have already been validated. */
*out_window = MWSD_WINDOW_NONE;

MwsdWindow *window = MWSD_WINDOW_NONE;
int saved_errno = 0;

if (!prepare_window())
{
    saved_errno = errno;
    goto out_window;
}

window = mwsd_window_create(/* ... */);
if (window == MWSD_WINDOW_NONE)
{
    saved_errno = errno;
    goto out_window;
}

/* Transfer ownership to the caller. */
*out_window = window;
return true;

out_window:
if (!mwsd_window_destroy(window))
{
    MWS_DPRINTF("create_window", "could not release partial Window");
}

errno = saved_errno;
return false;

Prefer staged handlers so each resource sentinel is introduced only when that cleanup stage becomes relevant. This keeps both the lifetime and every permitted jump visible to strict C compilers on all supported systems.

Output parameters MUST be placed in a documented safe state before an operation which may fail after partially producing a result.

5.3 Conversions

Conversions which can change value, signedness, width, alignment, or representation MUST be explicit and justified by a checked invariant.

Before converting:

Do not mix signed and unsigned arithmetic or comparisons until the relevant range has been established explicitly. Signed integer overflow is undefined behaviour and MUST NOT be relied upon. Unsigned wraparound is defined by C but MUST be intentional and documented; it is not a convenient substitute for a checked counter, size, or loop condition. These rules prevent a harmless-looking conversion from changing an ordering test or turning a negative value into a large size.

A cast MUST NOT be used merely to silence a diagnostic. Pointer reinterpretation is permitted only for a representation whose compatibility is guaranteed, or at a documented external API boundary.

The Object runtime is a narrow, deliberate exception. mws_object_is and mws_object_class treat an arbitrary Object pointer as an MwsObject * to read its Class tag. This depends entirely on the invariant that class is the first field of every Object layout. General code MUST use those helpers and MUST NOT imitate their cast to inspect or access other Objects. Moving the Class field or adding anything before it breaks the Object representation.

5.4 Checked sizes and offsets

All arithmetic used for allocation, framing, indexing, or pointer advancement MUST be checked before the operation occurs.

Code validates:

A successful bounds check must dominate the access it protects. Do not validate one representation and then perform arithmetic in another narrower type.

5.5 No arbitrary resource ceilings

Mercurius models resources as 0..many. Code MUST NOT introduce limits such as MAX_SESSIONS, MAX_WINDOWS, or MAX_GPUS merely to simplify storage.

Collections grow dynamically and creation continues until an actual resource, protocol, security, operating-system, or hardware constraint is reached. Failure is then returned honestly and, where appropriate, represented as MWS_ERROR_RESOURCE.

Named maximums are legitimate when the limit is itself part of a protocol, representation, security policy, external API, or physical constraint. The name and documentation must identify that source.


6. Macros and constants

6.1 Macros

Prefer a typed static inline function to a function-like macro. An inline function gives the compiler the same optimisation opportunity while preserving type checking, evaluating each argument once, and behaving like an ordinary C method or helper during debugging.

A function-like macro is permitted only when the C language requires token- or compile-time behaviour which a function cannot express cleanly. It MUST:

Do not use a macro merely to avoid writing a small function. Macros are expanded before C type checking and can duplicate expressions or make control flow invisible at the call site; restricting them preserves the method and failure discipline used by the rest of Mercurius.

6.2 Constants

Replace significant literal values with named constants. Use the narrowest appropriate scope and choose among an enum constant, a typed static const object, or #define according to what the C context requires.

A name states what a value means and gives one place to change it. This is particularly important for protocol values, timeouts, sizes, sentinels, and external-API limits, where two equal numeric literals need not represent the same fact.

A named constant does not legitimise an arbitrary ceiling. Maximums are permitted only for the reasons in Section 5.5, and their name or documentation must identify whether the source is the RFC, a wire representation, a security policy, an operating-system API, or a physical constraint.


7. Ownership and resources

The Iron rules in the software-design paper govern every resource, not only heap memory.

7.1 Owned and borrowed state

Every stored pointer or handle must have an unambiguous lifetime relationship. Object definitions and public interfaces document whether a relationship is:

A borrower MUST NOT retain a reference beyond the lifetime promised by the owner.

7.2 Acquisition

Every allocation and resource acquisition MUST be checked. This includes memory, file descriptors, sockets, threads, locks, Vulkan objects, OpenSSL objects, PAM state, and operating-system registrations.

Use calloc when a genuinely zero-initialised representation is useful. Use malloc when every byte will be assigned explicitly. A realloc result MUST first be assigned to a temporary pointer so failure cannot lose the original allocation.

Allocation is not hidden merely because it occurs inside a method. The interface must make the resulting ownership and destruction obligation clear. A constructor is expected to allocate.

7.3 Cleanup

Release resources in the reverse order of acquisition unless an external API requires another order. Cleanup paths must cover full construction, partial construction, and every failure point.

A staged family of ordered out_* labels is the normal C expression of a fake exception when resources are acquired progressively. One cleanup label is appropriate when the resources naturally share one lifetime or are already owned by one aggregate.

goto MUST be local to one function and used only to reach structured cleanup or a single final outcome. It MUST NOT jump into a scope or create general control flow.

Simple functions SHOULD return directly after validation failure or before any resource has been acquired. Do not force every function through one exit label when that obscures the code.

Cleanup code must preserve the error which caused the failure. A cleanup operation which itself fails must be recorded without silently losing the most useful diagnostic.

7.4 Sensitive material

Passwords, private-key material, authentication responses, derived secrets, and equivalent buffers MUST:

The Authenticator’s consumption and destruction of MWS_AUTH_RESPONSE is the canonical example.


8. Messages and byte representations

8.1 Message ownership

An MwsMessage identifies a discrete communication. Unless a method explicitly documents transfer:

The sender identifies the sending Object for replies and internal routing. It is a borrowed Object reference, not an ownership transfer.

8.2 Header understood, payload opaque

Generic Message infrastructure validates and understands the common header. An intermediate Object may obtain routing information through the Message API when that operation belongs to its responsibility.

The Window ID is the deliberate exception within the otherwise opaque payload. A Message concerning a Window MUST carry window_id in its first four payload bytes. It is absent from the common header because it is irrelevant to Messages which do not concern a Window. Such a Message need not be addressed to the Window Object itself: other Objects may collaborate about that Window. Ask a Window for its own window_id; ask a Message for the window_id of the Window it concerns. Intermediate Objects use mws_message_extract_window_id; they do not infer or inspect any other part of the payload. The common header and established Object path otherwise provide all information needed to pass the Message onward.

Only the semantic receiver interprets the payload. An intermediate Object MUST NOT mutate, consume, or become coupled to a payload merely because the Message passes through its _handle method.

Every complete Message is passed as a Message. A raw buffer MUST NOT be used as an anonymous continuation unless the protocol and Transport abstraction explicitly define that representation.

8.3 Wire data

Wire data is hostile until validated.

A Message payload is a byte representation, not a C object representation. Its sender and semantic receiver may be implemented in different languages and MUST agree on the exact format defined by the RFC. A payload therefore never contains a native C structure, pointer, enum representation, padding, or other C implementation detail.

The distinction between constructing and receiving a payload is deliberate. The peer may be implemented in Ruby, Rust, or any other language; its Message is defined by protocol bytes, not by the C object model used in the reference implementation. A C sender constructing a payload made entirely of 32-bit fields may construct a real uint32_t[] in network byte order and expose its representation through a uint8_t * while sending it. A receiver has only language-neutral bytes supplied by Mallory. It MUST NOT cast those bytes to uint32_t *, MwsHeader *, or another typed wire representation, even when the expected payload consists entirely of fields of that type. It first validates the complete byte extent, then uses memcpy to copy each field—or a fixed group of fields—into an appropriately aligned typed object before interpreting it. This discipline preserves language independence, enforces bounds independently of the claimed representation, and avoids C alignment and effective-type assumptions.

For the same reason, a sender MUST NOT declare a uint8_t[] and cast it to a multi-byte type in order to construct fields. It either declares the genuine typed array and exposes it as bytes, or serialises mixed-width fields into a byte array explicitly.

Code MUST:

Validation belongs at the narrowest Object which understands the relevant representation. A router validates framing and routing facts; the semantic receiver validates its payload.

Protocol changes update the RFC, protocol definitions, codecs, tests, and every semantic sender and receiver affected by the change.


9. Control flow

9.1 Braces and indentation

Indent with four spaces and never tabs.

Every if, else, for, while, and do body uses braces, even when it contains one statement. Opening braces appear on their own line in Allman style.

if (ready)
{
    mwsd_session_run(self);
}
else
{
    errno = EAGAIN;
    return false;
}

An intentionally empty block contains a comment explaining why it is empty.

Mandatory braces prevent an edit from silently changing which statement is controlled and make nested control flow visually unambiguous. The empty-block comment distinguishes deliberate absence from accidentally deleted code. Indentation and brace placement are uniform so reviewers spend attention on behaviour rather than reconstructing local layout conventions.

9.2 Conditions

Continuation operators appear at the beginning of continuation lines:

if (!mws_object_is(self, MWSD_CLASS_SESSION)
 || !mws_message_has_magic(message))
{
    errno = EINVAL;
    return false;
}

Use parentheses liberally where they make grouping evident. Do not depend on a reader reconstructing an elaborate precedence expression.

Pointer and integer truth tests are permitted when their meaning is idiomatic and unambiguous. Comparisons to NULL, 0, or a typed sentinel are preferred when they communicate lifecycle or ownership state.

Leading continuation operators make every additional condition visibly belong to the expression and make a missing operator conspicuous in review. Explicit comparisons are preferred where they name a state; terseness is not valuable when it hides whether zero means false, absent, invalid, or merely empty.

9.3 Switches

Every switch has a default unless every possible value is deliberately enumerated and the compiler is required to diagnose additions.

A case which declares variables or contains more than a trivial statement uses its own braces. Every case ends visibly with break, return, goto, or an explicit fall-through comment.

An unassigned opcode which cannot be decoded as a valid Message is EBADMSG. A valid opcode which violates the current protocol state is EPROTO. A defined operation which this implementation does not support is ENOTSUP.

Braced cases give declarations an unambiguous scope. Visible termination keeps fall-through deliberate, while a default protects code which receives a corrupt, unsupported, or newly introduced value rather than allowing it to continue in an unintended state.

9.4 Loops

The form of a loop states what is known about its execution:

An MwsList walk may use either for or while; choose whichever makes the iteration and termination clearest in context.

The condition in the loop header should normally describe when the loop continues. Use break for loop termination sparingly; prefer expressing the exit condition in the header instead of hiding it in the body. Do not use goto as loop control. When iterating while holding a lock, avoid returning from inside the loop; keep lock release in structured control flow.

Avoid continue unless it significantly improves clarity. Overuse of break and continue makes control flow harder to follow.

Prefer to record a result or change explicit state and let the header condition terminate the loop:

MwsdWindow *window = MWSD_WINDOW_NONE;

MwsListNode *node = mws_list_first(self->windows);
while (node != NULL && window == MWSD_WINDOW_NONE)
{
    MwsdCompositorWindowEntry *entry =
        container_of(node, MwsdCompositorWindowEntry, node);

    if (entry->presenter == presenter)
    {
        window = entry->window;
    }

    node = mws_list_next(node);
}

for (;;) is prohibited without exception. A genuinely non-terminating loop uses while (true) and a comment naming its purpose. A long-running loop which can shut down puts that shutdown state in its while condition rather than concealing it in a body-level exit.

The loop form is therefore part of the explanation: it tells the reader whether execution is mandatory, conditional, or iterative before the body is read. Body-level exits force the reader to search branches to discover termination; several such exits make resource and lock release easy to bypass. for (;;) misuses the iteration form while saying nothing about either progress or lifetime; while (true) states an intentional non-terminating condition directly.

An event loop waits on an event, descriptor, condition, or bounded timer. It MUST NOT busy-wait.

9.5 goto as an exception handler

goto is permitted for error handling and cleanup within a single function. This is established C practice and is the normal way to express a structured exception handler when several resources have been acquired progressively.

An exception-handling goto:

Every value read by a handler is initialised before the earliest jump which can reach it, as required by Section 5.2. Labels name the cleanup stage, normally by the resource which remains to be released, rather than using an unexplained generic destination.

goto MUST NOT implement loops, jump backwards, jump into a block, bypass an initialisation, or connect unrelated normal-control-flow branches. A failure before any resource is acquired normally returns directly; forcing it through an empty cleanup path makes the function harder to read.

setjmp and longjmp MUST NOT be used as a substitute for this discipline. A non-local jump bypasses the intervening Object and resource cleanup paths, making ownership and invariants depend on invisible dynamic control flow.

C has neither exceptions nor automatic deterministic cleanup. Without a structured goto, a function with several acquisitions must duplicate cleanup at every failure point or grow a ladder of nested conditionals. Staged handlers keep the successful path readable, make partial construction explicit, and place each release operation in one auditable location. The restriction is therefore not “never use goto”; it is “use goto only as the function’s local exception mechanism.”

9.6 Preprocessor conditionals

Conditional compilation is used to isolate genuine platform, feature, or diagnostic differences. It MUST NOT create several unrelated implementations inside one unreadable function.

Every closing conditional-compilation directive is labelled with a comment matching its opening condition:

#if defined(MWSD_DEBUG)
/* ... */
#endif /* MWSD_DEBUG */

Prefer a stable interface with platform-specific implementation files over widely scattered #if branches.

Conditional compilation can otherwise produce several programs interleaved in one function, none of which is easy to read or test. Labelled closing directives and narrow platform boundaries make the active program evident on every supported system.


10. Concurrency and blocking

Thread ownership is explicit. An Object which creates a thread owns its lifetime, defines its *_run method, supplies its shutdown condition, and joins or otherwise completes it before destruction.

libmws MUST remain usable by a single-threaded Application and MUST NOT start a hidden background thread. Elsewhere, a thread exists only as explicit behaviour of the Object which owns it. Hidden threads create lifetimes, callbacks, shutdown ordering, and synchronisation requirements which callers cannot see or control.

Shared mutable state MUST have an identified synchronisation owner. The code documents:

The ownership graph helps locate state but is not a lock-ordering proof. Borrowing an Object does not make concurrent access safe.

Wait for the condition, not an assumed amount of time. Sleeps MUST NOT be used as correctness synchronisation. Timers are appropriate for protocol timeouts, grace periods, retries, and other behaviour whose semantics are genuinely temporal.

Blocking calls belong in Objects whose *_run contract permits blocking. A bounded method or Message handler MUST NOT unexpectedly block an unrelated stream, hold a shared lock across an unbounded wait, or make shutdown impossible.

Signals and signal handlers use only async-signal-safe operations. Complex shutdown work is deferred to ordinary Object context.


11. Interfaces and dependencies

Never infer a fact by assuming the internal representation of an Object whose Class the current code does not implement. Calling that Object’s constructor and owning its lifetime grants no knowledge of its representation. Ask the Object through the interface responsible for the fact. To learn an Object’s Class, use the Object interface in mws_object.h, such as mws_object_class. To learn the Window ID of the Window concerned by a Message, use mws_message_extract_window_id; do not find an offset and read the Message storage directly.

Ownership grants responsibility for lifetime, not permission to couple one Class to another Class’s representation. Keeping facts behind their interfaces allows the representation to change without silently breaking its collaborators.

11.1 Headers

Every public header has a traditional include guard. Public C headers which may be consumed by C++ wrap declarations in extern "C" guards.

Do not use #pragma once. Traditional guards are defined by the C preprocessor, work across the supported compiler and filesystem combinations, and make the header’s identity explicit rather than dependent on a compiler’s path canonicalisation.

A header includes what is required to use its declarations and does not rely on include order. Prefer forward declarations for borrowed opaque Objects. Do not include a private implementation header from another Class.

Private headers are implementation details and MUST NOT be installed or presented as public interfaces merely because more than one implementation file needs them.

Self-sufficient headers fail at the point where their own contract is incomplete instead of changing according to accidental include order. Opaque forward declarations preserve Class boundaries and reduce recompilation and dependency cycles.

11.2 Includes

In implementation files, group includes as:

  1. standard C headers;
  2. operating-system and third-party headers;
  3. shared Mercurius headers;
  4. subsystem-local headers.

Within a group, keep the order stable and comprehensible. Remove unused includes. Feature-test macros appear before every system header which depends on them.

Stable grouping distinguishes language, platform, shared-project, and local dependencies at a glance. Removing accidental transitive dependencies ensures that the file states what it actually requires and continues to compile when an unrelated header changes.

11.3 Abstract roles and platform code

A stable abstract role such as Transport presents one interface to its users. Platform-specific implementations satisfy that interface without leaking socket, portal, display-server, or operating-system details into callers.

External API types MAY appear at the boundary of an Object whose responsibility is to encapsulate that API. They SHOULD NOT propagate through unrelated Classes.

Unsupported facilities fail cleanly. Platform detection and resource discovery ask the environment what exists; they do not encode assumptions about the developer’s current machines.


12. Source layout

12.1 Function declarations

The return type and function name begin on the same line. Long parameter lists wrap after a comma and align under the first parameter:

static bool mwsd_session_add_window(MwsdSession     *self,
                                    MwsdWindow      *window,
                                    MwsdApplication *application )
{
    /* ... */
}

Do not put the function name alone on the line following its return type.

Keeping return type and name together makes definitions easy to scan and find with ordinary text tools. Parameter alignment exposes the shape of an interface without making the symbol itself disappear into decorative layout.

12.2 Whitespace

Aim for lines no longer than 128 characters. A longer line is acceptable when breaking it would make the code materially harder to understand.

Keep every argument to a method call on the same line when the complete call fits within 128 characters and remains clear. Otherwise, keep the first argument with the method name and put each subsequent argument on its own line, aligned under the first. A shorter call may also use this form when the arguments themselves are the significant part of the expression and benefit from visual distinction. Do not wrap an arbitrary subset of the subsequent arguments: the layout should present either the call as a whole or each argument after the first individually.

These mechanical rules keep diffs quiet and make source visually stable across editors and terminals. One declaration per line also keeps each variable’s initialisation and lifetime visible; whitespace is serving reviewability, not enforcing aesthetics for their own sake. Two blank lines between top-level functions make function boundaries remain visible in an editor minimap, where individual source lines are too small to read, and provide reliable landmarks for moving through a long implementation file.

Within a method, blank lines separate sequences of meaningful operations. A conversion and copy which writes one wire field is one logical step; the next field begins another. Put a blank line before an if except when it tests the immediately preceding statement. Do the same for while, unless the preceding statement forms part of the condition it tests. This keeps related operations together while making new decisions clearly visible.

The following illustrates the wire-field and immediate-validation rules:

uint32_t value32;
uint16_t value16;

value32 = mws_htobe32(event->window_id);
memcpy(payload + 0U, &value32, sizeof value32);

value16 = mws_htobe16((uint16_t)event->event_type);
memcpy(payload + 20U, &value16, sizeof value16);

memcpy(&value16, message->payload + 22U, sizeof value16);
if (mws_be16toh(value16) != MWS_INPUT_POINTER_DATA_SIZE)
{
    errno = EPROTO;
    return false;
}

*event = decoded;

return true;

12.3 File organisation

Use visible section comments in long implementation files to separate the Class definition, private helpers, lifecycle, Message handling, and other major responsibilities. Section banners are navigation, not an excuse to combine unrelated Classes.

Functions should be small enough that their responsibility and cleanup path can be understood as a unit. Length alone is not a defect: coherent orchestration may legitimately compose several bounded private methods and own their common cleanup.

12.4 Comments

Comments explain intent, responsibility, invariants, ownership, and reasons which the code cannot state for itself. They do not narrate obvious syntax.

If you need half a page of comments to describe what the code actually does, then you need to rewrite the code to make it clear to the reader, not just make it compile.

Use /* ... */ for normal project comments. Existing third-party style or tool-required comments may be retained at their boundary.

Historical discussion, review conversation, obsolete behaviour, and instructions to a particular contributor belong in version history or project planning, not permanent source comments.


13. Documentation

13.1 File headers

Public headers and source files begin with the Mercurius file header containing:

Only @brief is brief. A Class description has no length target. It assumes a competent C programmer who has opened this file first and has read none of the Mercurius documentation or neighbouring code. Define Mercurius-specific terms needed to understand the Class. Explain its trust boundary, owner, owned state, borrowed references, ownership transfers, Message senders and receivers, and any destructive or asynchronous lifetime rules. When a lifecycle such as authenticate/create, detach/resume, or map/present makes those relationships clearer, describe that lifecycle concretely.

The file header is not an API inventory and must not merely restate method names. It teaches the design decisions which cannot be recovered safely by reading one function in isolation. Repetition of architecture documentation is acceptable here because source files must remain auditable when read alone.

A useful Class should:

  1. Orient the reader with the Class’s purpose and architectural position.
  2. Explain its Message flow, trust boundary, ownership and typical lifecycle concretely enough to follow the implementation.
  3. Restate the invariants which must remain true when the Class is changed.

This repetition is intentional. The final summary gives an auditor a checklist to test against the code and gives a maintainer the rules an edit must preserve.

Apply this test separately to the public .h and the implementing .c: either file may be the reader’s first entry point into Mercurius. Each MUST provide a complete introduction to the Class without requiring the other file, this standard, or the architecture documents to have been read first. The .h emphasises the public contract. The .c repeats that context and may add the private representation, locking strategy, cleanup order and other invariants needed to audit the implementation. “See the header” is not a Class description.

In particular, do not assume that a C reader has read Smalltalk_in_C.md or will recognise the Smalltalk design from its implementation conventions. State plainly that the file implements a Class, what one object of that Class represents, who sends it Messages, which method is its receiver, and where it sends Messages next. Explain that self is the receiving object and that a call such as mwsd_window_handle(window, message) implements “send this Message to this Window”; it is not merely an unrelated utility function taking a convenient state structure.

Mercurius has no Smalltalk runtime hidden underneath the C. Class tags, constructors, destructors, named methods, owned pointers, borrowed pointers and explicit ownership transfers implement those object semantics by convention. The file header must explain the parts of that convention on which the Class’s correctness depends. Labour the point where necessary: a maintainer who treats an object as a bucket of C state can introduce a use-after-free, bypass a Message boundary, or accidentally transfer authority even when each edited line looks locally reasonable.

The header describes the file as it exists. It does not recount its revision history.

Multiple authors are listed one per line under one Authors: field, with continuation lines aligned. This keeps attribution readable without making the file header a revision log.

13.2 Doxygen for methods

Every method has a concise Doxygen block immediately before its definition in the .c file. Public methods are also documented in their header. The normal block should fit in an IDE popup—approximately 10–12 lines when the contract permits.

Doxygen describes what the method does, not how its implementation does it. Refactoring the implementation should not ordinarily require rewriting the public contract.

Document:

In Mercurius a bool method return normally reports operational success, not a yes/no answer from the problem domain. true means the method established its postcondition; false means it did not and set errno. If a successful operation can answer “no”—credentials rejected, object absent, condition not met—that answer uses a separate output parameter, enum, pointer, or documented state change. Doxygen MUST distinguish those two result channels where both exist.

For the usual Boolean contract, prefer:

/**
 * Update a Presenter after its Window geometry changes.
 *
 * @param self      Presenter to update.
 * @param geometry  New authoritative Window geometry.
 *
 * @retval true on success or false with errno set.
 */

Do not list every possible errno value unless the distinction is part of the public contract and callers must act differently because of it.

13.3 Header and implementation documentation

A public method is documented in both its header and implementation. The first sentence is identical so the two descriptions cannot quietly assign different responsibilities.

The header is the caller’s contract. The implementation block is the contract available to the maintainer while reading or changing the method. It MUST be complete enough that the maintainer does not have to open the header to discover what the method is intended to do, and it MUST NOT say only “see header.” It may add concise facts relevant only to maintaining that implementation, but does not duplicate the algorithm.

Keeping the contract immediately beside the definition makes disagreement between intention and implementation visible during ordinary reading and review. Requiring a maintainer to infer intention from the code is circular: the implementation may be wrong, and documentation derived from it will merely repeat the same error. Documentation kept only in a separate header makes drift much less likely to be noticed.

When behaviour changes, both descriptions are reviewed together.

13.4 Invariants and assumptions

An invariant or assumption required for correctness MUST be documented beside the code which establishes or depends upon it. Examples include a sorted List, a unique identifier, a required lock, a representation alignment, or a field which must be the first member of an Object.

An invariant recorded only in a distant design document is easily broken by a local edit. Recording it at the dependency makes the reason for an otherwise non-obvious check or layout visible during review. The architectural document may explain the larger rule; the source still identifies where that rule is load-bearing.

13.5 Terminology

Use the terminology of the correct elevation:

Do not describe a Message handler as receiving a generic event. Do not call an Object merely an “object-style struct.” Do not use “client” when the intended Mercurius role is Portal, Application, or Projector.


14. Diagnostics and observability

Operational diagnostics must be useful without requiring a debugger and safe to enable on a running system.

Standard debug tracing records the successful creation and destruction of every Object. This lifetime trail is intentionally comprehensive: C has neither an OO runtime nor a borrow checker to expose missing destruction, stale ownership, or an unexpected Object lifetime. The resulting noise is the cost of making those facts observable in the reference implementation.

Diagnostics SHOULD identify:

A method which returns failure normally logs the reason at the point where it sets errno. Callers which merely propagate that failure SHOULD NOT repeat the same diagnostic. A caller MAY log a distinct action it takes because of the failure, or add context which changes what the diagnostic means operationally.

A failed mws_object_is receiver check is the exception. It sets EINVAL and returns failure without logging: a wrong-Class receiver is an immediately apparent programming error, not an operational runtime failure. Other argument validation and failure paths follow the ordinary rule when a diagnostic adds useful information.

Expected negative outcomes and benign no-ops are not error logs. Debug tracing may record them when useful.

Never log credentials, authentication payloads, private keys, passwords, or unredacted sensitive buffers. Pointer values are diagnostic identity only and MUST NOT be treated as stable external identifiers.

Verbose mode should explain what the system is doing. It is an operational interface, not an unstructured dump.


15. Portability and external APIs

Mercurius is continually built and exercised across materially different systems. Code must assume variation in operating systems, C libraries, SCTP implementations, Vulkan drivers, authentication facilities, and display environments.

Linux and BSD are both first-class targets. Mercurius is built and run on several Linux distributions and on BSD systems, currently FreeBSD. A change is not portable merely because it works on one Linux distribution, or even on Linux generally; it must preserve the common behaviour on BSD as well.

Code conforms to the ISO C language version selected by the build. A compiler extension is used only when the platform boundary genuinely requires it, is guarded for the compilers which support it, and has its necessity documented. An extension accepted silently by GCC is not evidence that Clang or another supported compiler gives it the same meaning.

Platform-dependent code:

Check every external API according to its documented success convention; libraries do not all use bool, errno, or the same ownership rules. Translate the result into the Mercurius method contract at the boundary.

Do not assume:


16. Verification

Code is expected to compile without warnings on supported GCC and Clang configurations. Warnings are investigated rather than silenced by casts, pragmas, or disabling a diagnostic globally.

Relevant changes are exercised in proportion to risk with:

Run clang-tidy, scan-build, or an equivalent static analyser when it is applicable to the changed code. Findings are investigated and either corrected or explicitly justified; they are not dismissed merely because the compiler accepted the program. Static analysis explores control-flow, lifetime, and undefined-behaviour paths which ordinary tests may not reach.

Tests use public interfaces where they are proving public behaviour. Test data sent through a Message interface is a complete valid MwsMessage, not a raw payload which production code could never receive.

A passing test on one machine does not prove portability. A theoretical rule which the actual supported implementations cannot satisfy is not a useful coding standard; either the code or the rule must be corrected.


17. Review checklist

Before accepting new or changed C, ask:

  1. Which Class owns this behaviour?
  2. Does the public interface preserve the Message and authority boundaries?
  3. Is every Object validated before use?
  4. Is every pointer or handle owned, borrowed, transferred, or call-scoped?
  5. Does a returned Object satisfy all invariants?
  6. Can every failure path release what it acquired and leave surviving Objects valid?
  7. Does every method report its own facts through the standard contract?
  8. Is wire and external input completely validated before use?
  9. Does the code work for zero, one, or many instances without an arbitrary ceiling or global current Object?
  10. Are concurrency ownership, locking, blocking, and shutdown explicit?
  11. Is platform-specific knowledge confined to the responsible Object?
  12. Do names, layout, comments, and Doxygen make the same responsibility evident to a future maintainer?
  13. Does the change compile cleanly and have evidence appropriate to its risk?

If the answer to any question is unclear, the implementation is not yet clear enough.


The purpose of this standard is not to make C resemble another language syntactically. It is to make the intended system—cooperating Objects, explicit resources, contractual methods, and precisely bounded authority—remain visible in every translation unit.

Will future me understand why this is correct, or merely observe that it appears to work?