Proven C Book←↑→

109 The boundaries — running things overlapped, and when there is no OS

What to know first

chapter 95, Operations that do not split · operations that do not split
chapter 98, A program’s memory layout · when there is no OS

Looking back

Chapter 13 said that the clock’s limits are why processors came to have several cores, and that accidents such as false sharing arise. Then how many different meanings does the phrase “doing several things at once” have?

A. At least two. Concurrency is several tasks progressing by turns while waiting for each other, and it holds even with one core — for example, a server doing other work while waiting for input and output. Parallelism is several cores really calculating at the same moment. The former is a problem of structure, the latter a problem of performance. This chapter’s coroutines treat the former and the job system the latter.

The need for this chapter, and its context

This chapter draws Part XIII’s boundary. Overlapped execution and OS-less environments are bound into one chapter because both ask where the assumptions of the six chapters from chapter 103 to chapter 108 break. Chapter 95 (atomics) and chapter 98 (memory layout) get their last use here. It is also where the range this library supports is set down honestly.

By the end of this chapter

A look at this library’s edges. We see the two ways of running several things overlapped (stackless coroutines and the job system), why allocators and pointer provenance become a problem in a program running on several threads, and what shape this library takes in a place with no operating system at all. The last section is when it is better not to use it.

The questions this chapter answers

  1. Then what is this part to be read for?
  2. Then what remains of what was learned in this part if proven is not used?

109.1 Stackless coroutines — overlapping without threads#

If a function can do some work, stop, and later start again from that place, much becomes easier, because the code can be written in order instead of a state machine written by hand.

examples-en/proven-edges/coro.c

#include <proven.h>
#include <stdio.h>

/* A stackless coroutine: how far it got is held in one slot of a struct, and
   the macros return to that spot with a switch. No thread, no separate stack,
   no allocation. */
typedef struct {
    proven_coro_t co;      /* the slot that remembers how far it got */
    int           sent;
    int           limit;
} producer_t;

/* returning 0 means "one value handed out, paused"; 1 means "all done" */
static int produce(producer_t *p, int *out)
{
    PROVEN_CORO_BEGIN(&p->co);
    while (p->sent < p->limit) {
        *out = p->sent * 10;
        p->sent += 1;
        PROVEN_CORO_YIELD(&p->co);   /* it leaves here and comes back to this spot on the next call */
    }
    PROVEN_CORO_END(&p->co);
}

int main(void)
{
    producer_t p = { .sent = 0, .limit = 4 };
    PROVEN_CORO_INIT(&p.co);

    int value = -1;
    while (!PROVEN_CORO_IS_DONE(&p.co)) {
        if (produce(&p, &value) == 0)
            printf("produced %d\n", value);
    }
    printf("finished after %d values; coroutine state is %zu bytes\n",
           p.sent, sizeof(proven_coro_t));
    return 0;
}

Output

produced 0
produced 10
produced 20
produced 30
finished after 4 values; coroutine state is 4 bytes

The heart of it is the last line of the output — the state this coroutine remembers is 4 bytes. There is no separate stack, no thread and no allocation. It packages an old knack in C in which macros make re-entry points with switch and case (a cousin of Duff’s device — that syntax seen in chapter 33 is used again here).

The price is clear too. Local variables do not survive — stop and come back and they are gone, so state that must be maintained has to be kept in a struct. It is why the example kept sent as a struct member. And, being implemented with switch, PROVEN_CORO_YIELD cannot be put inside another switch.

Even so the reason this model is loved in embedded work is plain. If one task is 4 bytes, hundreds can be started at little cost, and since no stack is taken separately the memory usage can be calculated at compile time.

A common misconception. “A coroutine is a light version of a thread”

What they do is entirely different. Threads are switched by the operating system cutting in as it pleases (pre-emption), while a coroutine stops only where it itself wrote YIELD (co-operation). So race conditions do not arise between coroutines — because two coroutines never run at the same moment. There is a price in exchange. If one coroutine calculates long without yielding, all the rest starve. And coroutines cannot use several cores — which is why the next section’s job system exists separately.

109.2 The job system — using several cores#

proven_job_sys_t is a small system equipped with a queue of work and worker threads. What sets it apart is that the queue’s size is fixed — if it overflows, submission fails, and that failure comes as a value. The judgement is that an infinitely growing queue is merely a device for putting the problem off until memory is exhausted.

There are five calls, and their order is itself the contract.

proven_job_sys_t *sys;
proven_err_t e = proven_job_system_init(alloc, 4, 256, &sys);  /* ① 4 workers, queue 256 */
bool ok = proven_job_submit(sys, routine, arg);                /* ② submit (false if full or closed) */
bool did = proven_job_execute_one(sys);                        /* ③ this thread handles one too */
proven_job_system_close(sys);                                  /* ④ take no more */
proven_job_system_destroy(sys);                                /* ⑤ join the workers and clean up */

That it is an opaque type stands out — the inside of proven_job_sys_t is not in the header and it is handled only by pointer (the opaque type of chapter 69 in the flesh). Platform resources such as threads and locks are inside, and their layout is not to be exposed to user code.

The contract is also why ④ and ⑤ are separate. Closing is “no more submissions are taken”, and destroying is “wait for the workers to finish and then clean up”. Between them, the header requires the producer threads to be joined.

proven_job_execute_one is a little special. It lets the submitting side handle one item of work itself — so when submission fails because the queue is full, instead of merely waiting, the submitter can handle one item and try again (a simple form of work stealing).

The false of proven_job_submit, though, means two things. It is false when the queue is full and also when the system is closed. The first calls for trying again and the second for stopping. To tell them apart, use proven_job_submit_ex — PROVEN_ERR_AGAIN comes when the queue is full, PROVEN_ERR_INVALID_STATE when the system is closed.

To wait until all the submitted work has finished, use a job group (proven_job_group_t).

proven_job_group_t group;
proven_job_group_init(&group);
proven_err_t e = proven_job_group_submit(sys, &group, routine, arg);  /* the group counts it */
proven_job_group_wait(sys, &group);                                  /* returns when all are done */

While it waits, proven_job_group_wait takes work out of the queue and runs it itself. So the wait makes progress even when every worker is busy. After it returns, what the jobs wrote may be read. The group must stay alive until every job submitted to it has finished — leaving the block that holds the group before the wait is the lifetime problem seen in chapter 45.

Counter-example. Overlapping destroying with submitting

/* thread A */                    /* thread B */
proven_job_submit(sys, job);      proven_job_system_destroy(sys);

A pattern the header explicitly forbids. The correct order is close, join all the producers, then destroy. Such ordering contracts are not solved by hiding a lock inside the data structure — rather, a hidden lock breeds more code that “mistakenly believes it works”. It is also why this library puts no locks in its containers and pins down that shared mutation is synchronised by the caller.

109.3 Threads, allocators, and provenance#

In a program running on several threads the allocator demands special care. If two threads use one arena together, chapter 104′s simple action of “cutting from the front” becomes a race at once. The prescription is usually one per thread — and then no synchronisation is needed at all.

Pointer provenance, which was only named in chapter 15, becomes practical here too. Since even at the same address it matters which allocation a pointer came from, giving a block obtained from one allocator back to another is a contract violation (as in chapter 104′s counterexample) even if the address happens to match. The library’s name came from here.

109.4 When there is no OS — freestanding#

Running with no OS is the constraint that most shaped this library. It is the demand to run in the very environment chapter 76 gave as the reason the standard library is thin — a place with no operating system, no heap and no files.

What changes in a freestanding build is this.

Why the disciplines of the earlier chapters — “take the allocator as an argument”, “a view is borrowed”, “no hidden globals” — were insisted on so persistently becomes clear here. Those disciplines were not a matter of taste but the minimum condition for running in this environment.

109.4.1 The actual build procedure#

Left in words alone it stays vague, so here is the order.

  1. Take platform/ out of the compilation list. Leave only src/proven/*.c. Files, time, OS randomness, streams, mmap and the job system go out with it.
  2. Define PROVEN_FREESTANDING (-DPROVEN_FREESTANDING=1). proven_heap_allocator() then returns an unusable value (all zeros), and if it is used by mistake proven_alloc_is_valid says false.
  3. Reserve the backing memory statically. Put one array where the linker script knows it and lay an arena over it (chapter 104).
  4. Register a panic handler. There being no console, one of an LED, a watchdog or a reboot (chapter 102′s example).
  5. Take out what is not needed. If real-number formatting is not used, -DPROVEN_FMT_NO_FLOAT strips the large-integer arithmetic code out whole.
/* the skeleton of a freestanding program */
static proven_byte_t g_pool[8 * 1024];      /* the memory budget is settled here */

int main(void)
{
    proven_set_panic_handler(board_panic);
    proven_arena_t arena = proven_arena_create(
        (proven_mem_mut_t){ .ptr = g_pool, .size = sizeof g_pool });
    proven_allocator_t alloc = proven_arena_as_allocator(&arena);

    for (;;) {
        proven_arena_reset(&arena);          /* reset on every pass */
        handle_one_event(alloc);
    }
}

These twenty lines contain all of this part’s discipline — the allocator is an argument, lifetime is per arena, main does not return (chapter 59), and the memory budget is written as a number in the source.

In practice. The same code in two worlds — and its price

Keeping a single codebase that runs both on embedded targets and on a host really is of great value. If a protocol parser can be tested on a PC and put into the firmware as it stands, the time spent floundering on a board with no debugging environment shrinks greatly. It is why many embedded teams keep a separate “test build that runs on the host”, and what makes that structure possible is exactly the design of confining platform dependence to one layer. The price is that the API becomes a little more formal — code that works anywhere is specialised to nowhere.

109.5 Where this library stands — what it is, and what it is not#

Before closing the part, the library’s place has to be made clear. The design seen so far — taking an allocator as a parameter, returning failure as a value, views that carry a length, refusing rather than truncating — was not invented by proven. Zig’s allocator parameter, Rust’s Result and slices, recent C++‘s span and expected, and the in-house C conventions of many companies have all moved in the same direction. There is considerable common ground about it.

proven, however, is not that common ground itself. It is one attempt to implement that direction in C23 — neither a standard nor an industry component. The distinction matters for a simple reason: the direction has been verified in many places, but this implementation has not yet. What to take from this part is the direction rather than the code, and if you do take the code, take it knowing what follows.

109.5.1 What has been verified so far#

What this book has put in print is exactly this much.

confirmednot confirmed
every example in this book that uses proven builds and runs under GCC 14 and Clang 22.1behaviour under other compilers (MSVC, older GCC)
those examples all pass on x86-64 Linuxcontinuous verification on other systems and architectures
the contracts of the API the examples use match the documentationcoverage of the whole API
the platform layer has two branches, POSIX and Win32continuous automated verification of the Win32 branch

Table 109.1 — proven — what this book has verified and what it has not

That is, what this book vouches for is that the code printed here runs in this environment — not that the whole library is verified in every environment. The book claims no more than that.

109.5.2 Stability — what may still change#

proven is not at 1.0. The version this book uses is a snapshot of v0.113.0 (the base library proven_base only — the same release’s cryptography library proven_crypto, compression library proven_codec and network layer proven_net are not included). Versions have the form MAJOR.MINOR.PATCH, and while the first number is 0 that means:

109.5.3 What is not there yet#

To set it down honestly: the following are absent or thin in this version.

Q. Then what is this part to be read for?

A. Two things. One is how to read a design — the eye that asks, of whatever library you meet, “how does it report failure, who supplies the memory, where is the length, what happens at the boundary?” That eye remains whether or not you use proven.

The other is grounds for a choice. If, looking at Table 109.1, you judge that it is too early for your project, that too is the result of reading this part properly. It means the same as the preface saying that reading the first part of this book and deciding you do not need proven is a fine outcome too.

109.6 When it is better not to use it#

We close honestly. There are cases where this library is not the answer.

Q. Then what remains of what was learned in this part if proven is not used?

A. Almost all of it. Strings that carry their length, writes that refuse rather than truncate, errors that come back as values, allocation visible in the signature, the distinction of owning and borrowing, size calculation done with checked arithmetic, comparators that keep a total order, hashes that assume adversarial input — these are not a library but design principles, and they can be applied by hand to any C code. Open chapter 100′s table again and the right-hand column consists entirely of such items. What this part really wanted to sell is not the code but that column.

Recap

A summary of the whole of Part XIII — problems and answers.

chapter 100′s problemthe answerchapter
strings that do not know the sizeviews (ptr+size), refusing rather than truncatingchapters 103 and 105
unconfirmed failureerrors as values, [[nodiscard]]chapter 102
format mismatch{} and PROVEN_ARG (_Generic)chapter 106
unclear ownershipthe allocator parameter, owning versus borrowingchapter 104
unchecked callbacksdocumented contracts, introsort, keyed hasheschapter 107
the hidden type of bytesproven_byte_tchapter 103
environments with no OSseparating the platform layer, static arenaschapter 109

Table 109.2 — The answers to the five bugs and the other problems, and their chapters

With this, the promise this book made in chapter 1 has been kept — showing C’s problems first, and showing one answer to them through to the end. The next chapter closes this part — writing the same program in plain C and in proven, side by side. Beyond it lies the story outside these pages: chapter 111 looks round the terrain of practice (build tools, version control, real projects), chapter 114 follows, and the last chapter gathers up the road this book has travelled.