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:
- MUST and MUST NOT state requirements.
- SHOULD and SHOULD NOT state defaults which require a documented reason to depart from.
- MAY states a permitted choice.
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:
- Explicit. Identity, ownership, lengths, state, and failure are visible.
- Local. A function acts within the responsibility of its Class or module.
- Defensive. Invalid input and invalid state produce controlled failure.
- Portable. Assumptions about the compiler, ABI, operating system, byte order, and hardware are isolated and stated.
- Resource-honest. Allocation is dynamic, every resource has an owner, and exhaustion is reported only when it genuinely occurs.
- Readable. Intent is preferred over brevity, cleverness, or fashionable idiom.
1.2 Design philosophy
- Clarity over cleverness. If the intent is not evident, rewrite it.
- Explicit over implicit. Conversions, ownership, lifetimes, state, and failure must be visible.
- Safety over convenience. Prefer the construct whose correctness remains apparent after maintenance and refactoring.
- Small cooperating pieces. A Class has one bounded responsibility and collaborates through narrow methods and Messages.
- The long-term view. Write code which remains understandable and sound to a maintainer years after the immediate problem has been solved.
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:
- a public header declaring its opaque type and public methods;
- one primary
.cfile defining its representation and private methods; - a subsystem prefix which identifies where it belongs.
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:
- MUST be declared in the Class header;
- MUST use external linkage;
- MUST expose only types needed by the caller;
- SHOULD accept forward-declared opaque Classes rather than include unrelated implementation headers.
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:
*_handle(self, message)receives, consumes, or routes one Message.*_process(self, ...)advances an internal state machine or performs one bounded unit of work.*_run(self)owns long-running behaviour such as a thread loop or Message pump.
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:
- returns a fully initialised Object whose invariants hold;
- returns
NULLwitherrnoset when it cannot do so; - assigns the Class tag before exposing the Object;
- records every owned and borrowed relationship explicitly;
- uses defined sentinels for resources not yet acquired;
- destroys all partially acquired resources before returning failure.
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:
truemeans that the method achieved its postcondition;falsemeans that it did not, witherrnoset to an errno-compatible reason.
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:
- is safe to call with
self == NULL; - releases every resource the Object owns;
- never releases a borrowed resource;
- continues teardown after an individual release failure;
- returns
trueonly when its complete postcondition was achieved; - returns
falsewitherrnoset when complete release failed; - ends the Object irrespective of its return value.
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:
- big identifies the subsystem:
mws_,mwsd_, ormwsc_; - little identifies the Class or component;
- littlest is a verb describing the operation.
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
- Public Class types use
Mws,Mwsd, orMwscCamelCase names. - Class tags use
MWS_CLASS_*,MWSD_CLASS_*, orMWSC_CLASS_*. - Typed absence sentinels normally use
*_NONE. WhenNONEwould obscure that every ordinary value, including zero, is valid, use an explicit*_INVALIDsentinel instead. SCTP stream zero is valid, soMwsStreamIdusesMWS_STREAM_ID_INVALID. - Protocol constants and opcodes use
MWS_*. - Local variables use lower-case
snake_case. - Boolean names SHOULD read as conditions rather than commands.
- Names include units where ambiguity is possible, such as
timeout_msorsize_bytes.
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:
- protocol-visible values;
- serialised representations;
- identifiers whose width is part of their contract;
- arithmetic which requires a stated width.
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:
- prove that the source value is in range;
- prove that size arithmetic cannot overflow;
- preserve signed values intentionally;
- respect alignment and effective-type requirements.
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:
- addition and multiplication used to form a byte count;
- payload length against the complete buffer;
- offset plus field length before reading or writing;
- conversion from
size_tto a narrower API or wire type; - stride and geometry relationships before accessing pixels.
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:
- be small and have one evident purpose;
- parenthesise every parameter use and the complete result where applicable;
- never evaluate an argument more than once;
- avoid hidden allocation, ownership transfer,
return,goto, or other control flow; - document any type, scope, or side-effect constraint which is not obvious at the invocation.
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:
- owned — destroyed by this Object;
- borrowed — used but never destroyed by this Object;
- transferred — ownership moves explicitly as part of the call;
- observed — valid only for the duration of the call or callback.
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:
- have the shortest practical lifetime;
- never be written to ordinary diagnostics;
- be erased with
explicit_bzeroor an equivalent non-optimisable primitive before release; - be owned by the Object entitled to interpret them.
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 caller owns the Message storage;
- the caller owns any heap-backed payload;
- a synchronous
*_handlemethod borrows both for the duration of the call; - a receiver which must retain data makes its own copy;
- Transport and codec helpers do not silently free or reallocate the payload.
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:
- validate magic, opcode, reserved fields, and declared lengths;
- validate the complete enclosing buffer before accessing a field;
- convert multi-byte fields with the project endian helpers;
- encode and decode fields explicitly;
- treat received payloads and framing buffers as bytes until copied into validated, aligned typed objects;
- avoid transmitting or reading a native C structure as a wire format;
- reject malformed or unsupported input with a precise error;
- avoid acting on partially validated semantic content.
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:
- Use
do { ... } while (condition);when the body must execute at least once. - Use
while (condition) { ... }when the body might not execute at all. - Use
foronly for iteration: when the number or bounds of the repetitions are known, or when walking an iterator such asMwsList.
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:
- jumps forward to an
out_*label; - releases resources in reverse order of acquisition;
- enters the cleanup stage appropriate to the resources acquired so far;
- preserves the
errnowhich caused the failure; - ends in one clearly visible unsuccessful outcome.
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:
- which lock protects the state;
- whether a method expects the lock held or acquires it;
- the lock ordering when more than one lock can be held;
- whether callbacks or cross-Object methods are called while locked.
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:
- standard C headers;
- operating-system and third-party headers;
- shared Mercurius headers;
- 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
- Use four spaces per indentation level.
- Use one statement per line.
- Use one declaration per line.
- Use blank lines to separate logical steps.
- Put a blank line before an
ifstatement except when it tests the immediately preceding statement. - Apply the same rule to
while: put a blank line before it unless the preceding statement forms part of the condition being tested. - Separate consecutive validation or decision blocks with a blank line.
- Treat each wire-field encode or decode operation as a distinct logical step.
- Separate a declaration block from subsequent statements unless an initialised declaration and its immediate consumer form one operation.
- Separate assignment of a method’s result from its final return.
- Separate top-level function definitions with two blank lines.
- Remove trailing whitespace.
- End every text file with one newline.
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:
@file;- a concise
@brief; - for a Class, a complete, standalone description of what the Class is, why it exists, where it fits in the architecture, and how it follows the Iron rules;
- the author list;
- copyright and licence notice.
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:
- Orient the reader with the Class’s purpose and architectural position.
- Explain its Message flow, trust boundary, ownership and typical lifecycle concretely enough to follow the implementation.
- 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:
- the method’s responsibility;
- each parameter whose meaning is not self-evident;
- ownership or borrowing visible at the interface;
- a significant precondition or side effect;
- the return contract.
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:
- Object, Class, and Message have the meanings defined by the software-design paper;
- protocol requirements use the RFC’s terms;
- implementation comments use concrete C symbol names where precision requires them.
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:
- the method or Object reporting the fact;
- relevant non-secret Object identity;
- Message opcode or resource identifier;
- the operation which failed;
- the system or library error where useful.
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:
- lives behind the Object responsible for that facility;
- uses compile-time checks only for genuine compile-time differences;
- uses runtime discovery for resources available at runtime;
- reports absence as a controlled error or an empty 0..many collection;
- does not weaken the common interface to match the least capable platform.
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:
- a particular byte order;
- the size or signedness of native types;
- that file descriptors are small or reusable in a particular order;
- that one GPU, Portal, user, association, or network interface exists;
- that a successful call changed
errno; - that an API which worked on Linux has identical teardown semantics elsewhere.
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:
- focused unit or regression tests;
- debug and release builds;
- sanitizers and dynamic analysis where supported;
- more than one supported operating system when platform behaviour is involved;
- live multi-Object and multi-association tests when concurrency or routing is involved.
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:
- Which Class owns this behaviour?
- Does the public interface preserve the Message and authority boundaries?
- Is every Object validated before use?
- Is every pointer or handle owned, borrowed, transferred, or call-scoped?
- Does a returned Object satisfy all invariants?
- Can every failure path release what it acquired and leave surviving Objects valid?
- Does every method report its own facts through the standard contract?
- Is wire and external input completely validated before use?
- Does the code work for zero, one, or many instances without an arbitrary ceiling or global current Object?
- Are concurrency ownership, locking, blocking, and shutdown explicit?
- Is platform-specific knowledge confined to the responsible Object?
- Do names, layout, comments, and Doxygen make the same responsibility evident to a future maintainer?
- 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?