Mercurius — Draft RFC

Mercurius — C Coding Standards

This document defines how C is written in the Mercurius codebase.

It is:

All new C code in Mercurius MUST comply with this document.

The intent is that this document is machine‑checkable: an AI or static tool should be able to scan the codebase and flag violations.


1. Goals and Philosophy

1.1 Primary goals

Mercurius C code MUST be:

1.2 Design philosophy


2. Language Subset

Mercurius uses a conservative subset of ISO C (C11 or later where available).

2.1 Forbidden features

The following are NOT PERMITTED:

2.2 Strongly discouraged features

These MAY be used only with clear justification in comments:


3. Types and Integer Safety

3.1 Fixed‑width integers

All protocol‑visible and size‑sensitive integers MUST use fixed‑width types from <stdint.h>:

These types provide explicit width, predictable layout, and stable wire formats.

Use size_t for memory sizes, buffer lengths, and element counts.
Use ptrdiff_t for pointer differences.

Native integer types (int, long, long long) MAY be used only when width does not affect correctness and the value never crosses a module boundary.

Do NOT assume the size of any native integer type.


3.2 Signed vs. unsigned

Signedness must be explicit and intentional.


3.3 Booleans

Use bool from <stdbool.h> for logical values.


3.4 Type casts

Casts MUST be avoided unless they are strictly necessary. Unnecessary casts hide bugs, silence useful compiler diagnostics, and make invariants harder to reason about.

When a cast is required:

Examples of acceptable casts:

/* Converting a validated 16‑bit length field to size_t. */
size_t len = (size_t)hdr->length;  /* length ≤ MWS_MAX_PAYLOAD_BYTES */

/* Casting to uint32_t after checking the value fits. */
if (x <= UINT32_MAX)
{
    out = (uint32_t)x;
}

Examples of unacceptable casts:

/* Hides a signed/unsigned bug. */
if ((int)u < 0) …

/* Blindly truncates. */
uint16_t n = (uint16_t)big_number;

/* Pointer reinterpretation without justification. */
struct foo *f = (struct foo *)ptr;

Casts MUST NOT be used to silence warnings. If the compiler complains, fix the code or document the invariant that makes the cast safe.

If you lie to the compiler, it will get its revenge.


3.5 Variable initialisation

All variables MUST be initialised at the point of declaration.


4. Naming and Structure

4.1 Files and modules

4.2 Header guards

Every header MUST use a traditional include guard:

#ifndef MWS_FOO_H
#define MWS_FOO_H

/* declarations */

#endif // MWS_FOO_H

Do not use #pragma once.

4.3 Naming conventions

These are conventions, but consistency is expected: Every function name follows a big_little_littlest pattern:

No enforced _t or _e suffixes. Use names that read naturally.


5. Control Flow

5.1 Braces and layout

Braces are mandatory, even for single statements. This avoids ambiguity, prevents accidental logic changes during edits, and keeps the code visually consistent.

if (condition)
{
    do_something();
}
else
{
    do_something_else();
}

No single‑line if/else without braces.

The infamous goto fail bug in Apple’s TLS stack was caused by a duplicated, un‑braced if statement. Always use braces.

5.1.1 Placement

Opening braces always go on their own line, aligned with the controlling statement:

while (true)
{
    do_work();
}

Closing braces align with the construct that opened them.

5.1.2 Empty blocks

Empty blocks must contain a placeholder comment to make the emptiness intentional:

while (something)
{
    /* this space intentionally left blank */
}

Never use {} on a single line.

Why this rule exists

5.1.3 Multi‑line conditions

If a condition spans multiple lines, operators go at the start of continuation lines:

if (very_long_expression
    && another_condition
    && something_else)
{
    handle_case();
}

This makes grouping unambiguous and prevents misreading.

5.1.4 Explicit parentheses in conditions

Parentheses may be used liberally to make logical grouping explicit, even when not required by C operator precedence:

if ((a && b)
    || (force && (mode == MODE_OVERRIDE)))
{
    do_work();
}

5.1.5 else if chains

if (a)
{

}
else if (b)
{

}
else
{

}

5.1.6 Loops

All loops require braces, even for a single statement:

for (int i = 0; i < n; i++)
{
    process(i);
}

5.1.7 Indentation

Indentation is 4 spaces, never tabs.
Braces define the indentation level; nothing else does.


5.2 switch statements

switch (state)
{
    case MWS_STATE_INIT:
    {
        init_stuff();
        /* fall through */
    }
    case MWS_STATE_RUNNING:
    {
        run_stuff();
        break;
    }
    default:
    {
        handle_unexpected_state(state);
        break;
    }
}

5.3 Loops

while (true)
{
    /* main event loop */
}
/* Avoid termination buried inside an apparently infinite loop. */
for (;;)
{
    do_something();
    if (condition)          /* <= do not do this */
        break;              /* <=                */
    do_something_else();
    do_other_thing();
}

5.4 goto

goto is permitted only for error handling and cleanup within a single function:

int mws_thing_do(struct mws_thing *t)
{
    int rc = 0;
    resource_a *a = NULL;
    resource_b *b = NULL;

    a = acquire_a();
    if (a == NULL)
    {
        rc = -1;
        goto out;
    }

    b = acquire_b();
    if (b == NULL)
    {
        rc = -1;
        goto out_a;
    }

    /* normal work */

out_b:
    release_b(b);
out_a:
    release_a(a);
out:
    return rc;
}

No jumping into blocks, no spaghetti control flow.


5.5 Preprocessor conditionals

All conditional compilation blocks must label their closing #endif with a comment matching the opening directive:

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

Nested conditionals must also be labelled:

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

This prevents mis‑nested or unintended conditional blocks.


6. Memory and Resource Management

Mercurius is not an embedded system; dynamic memory is allowed, but it MUST be disciplined.

6.1 Ownership

Every dynamically allocated object MUST have a clear owner.

Example:

/**
 * Takes ownership of `msg`. Caller MUST NOT use `msg` after this call.
 */
void mws_queue_message(struct mws_queue *q, struct mws_message *msg);

6.2 Allocation and failure

6.3 No hidden allocations

Functions MUST NOT allocate memory behind the caller’s back unless:

6.4 Lifetime and cleanup

6.5 Global state

6.6 Object-style structs and payloads

Many Mercurius subsystems use C structs in an object-like way (e.g. handshake contexts, AUTH lists, surface capabilities, session info). These MUST follow a clear init/use/destroy pattern.

Example:

struct mws_surface_caps caps = {0};

if (!mws_surface_caps_from_message(&caps, msg)) {
    mws_surface_caps_destroy(&caps);
    return false;
}

/* use caps */

mws_surface_caps_destroy(&caps);

6.6.1 Message payload ownership

MwsMessage is a container; it does not own the lifetime of its payload by itself. Heap-backed payloads follow these rules:


7. Macros and Constants

7.1 Macros

Prefer static inline functions over function‑like macros.

Function‑like macros are only allowed when:

Macros MUST NOT evaluate arguments multiple times.

7.2 Constants

Use const variables or #define for constants:

#define MWS_MAX_CLIENTS 64

static const uint32_t MWS_DEFAULT_PORT = 4242u;

Magic numbers in code MUST be replaced with named constants.


8. Error Handling

8.1 Return codes

8.2 Logging

8.3 Defensive checks

8.4 Error Categories and Control Flow

Mercurius distinguishes between three categories of error conditions.
These categories determine how functions return, whether logging occurs,
and whether the fake‑exception cleanup path is used.

8.4.1 Benign no‑ops

Some operations are naturally idempotent. Examples include:

These are not errors. Functions SHOULD:

This is normal behaviour and not considered a failure.

8.4.2 Caller‑side errors (invalid parameters)

Invalid parameters are the caller’s responsibility, not the subsystem’s.
Examples include:

These MUST:

The onus is on the caller to correct the misuse.

8.4.3 Subsystem/runtime errors

These represent actual failures within the subsystem. Examples include:

These MUST:

Subsystem/runtime errors are the only errors that use the fake‑exception pattern.


9. Protocol Handling

Mercurius is a network‑native system. Protocol correctness is non‑negotiable.

9.1 Never trust the wire

9.2 Lengths and buffers

9.3 SCTP and transport

9.4 Protocol evolution

Any change to the protocol MUST be reflected in:


10. Concurrency and I/O

10.1 Threads

10.2 Event loops

10.3 No busy waiting


11. Formatting and Style

11.1 Indentation and whitespace

11.2 Line length

11.3 Includes

11.4 File endings


12. Documentation

12.1 File headers

All public header and source files MUST begin with the standard Mercurius file header block. This block provides a clear description of the file’s purpose and authorship.

Example:

/***********************************************************************************
 * @file    mws_proto.h
 * @brief   Mercurius Wire Stream (MWS) protocol definitions.
 *
 *          Defines the wire-level message structures, enumerations, and constants
 *          used by both client and compositor.
 *
 * Authors:  Christopher Ross <chris@tebibyte.org>
 *           Another Contributor <name@example.com>
 *
 * COPYRIGHT (C) 2026 Christopher Ross. All rights reserved.
 * See the LICENSE file for terms of use.
 ***********************************************************************************/

The Authors: field MUST follow the multi‑author list format:

* Authors:  First Author <email>
*           Second Author <email>
*           ...

12.2 Function documentation

All non‑trivial, non‑static functions MUST have a brief Doxygen comment describing:

Example:

/**
 * Initialise the Mercurius server context.
 *
 * @param ctx  Pointer to an uninitialised context structure.
 * @return 0 on success, non-zero error code on failure.
 *
 * On success, the caller is responsible for calling mwsd_ctx_destroy().
 */
int mwsd_ctx_init(struct mwsd_ctx *ctx);

12.3 Invariants and assumptions

Wherever code relies on invariants (e.g. “this list is sorted”, “this ID is unique”, “this buffer is always N‑aligned”), those invariants MUST be documented in comments near the code that enforces or depends on them.

Documentation MUST make implicit assumptions explicit.

12.4 Header and implementation documentation

Every public function MUST be documented in both its header (.h) and its implementation (.c). These two documentation blocks serve different purposes, but they MUST describe the same behaviour.

12.4.1 Header documentation

The header provides the public API contract:

Header comments MUST be complete enough for a caller to use the function correctly without reading the implementation.

12.4.2 Implementation documentation

The implementation provides the operational description:

The .c file MUST stand alone.
It MUST NOT say “see header” or rely on the header for behavioural description.

12.4.3 Consistency requirement

The header and implementation documentation MUST:

If the header says “twiddle the knob” and the .c file says “push the button”, the documentation is wrong.

12.4.4 Rationale

Developers read headers when using a function.
Developers read .c files when changing a function.
Both deserve complete, accurate documentation.

This rule prevents drift, eliminates ambiguity, and keeps the codebase maintainable over time.

12.5 Audience and intent of documentation

All comments and Doxygen blocks MUST be written for future maintainers, implementers, and reviewers. Documentation MUST describe:

Comments MUST NOT:

Comments exist to explain what the code does and why it does it that way. How it does it is expressed by the code itself.


13. Testing and Tools

13.1 Compilers

Code MUST compile cleanly (no warnings) with:

Warnings are treated as errors in CI and during review.

13.2 Static analysis

The codebase SHOULD be regularly checked with:

Warnings from these tools MUST be taken seriously and either fixed or explicitly justified.

13.3 AI‑assisted linting

This document is written to be machine‑checkable.

It is expected that AI tools will be used to:

AI suggestions are advisory, not authoritative. Human judgement prevails.


14. Code Review Expectations

Reviewers will check:

“Works on my machine” is not sufficient.


15. Final Notes

Mercurius is written in C by choice, not by accident.

We write C in a way that is:

If you find yourself reaching for something clever, stop and ask:

“Will future‑me thank me for this, or swear at me for it?”

If the answer is the latter, rewrite it.


© Chris Ross — chris@tebibyte.org