playbit / docs

Threads

Threads provide both a security boundary—isolation from effects—and parallelism.

All programs start with a single thread; the "main thread" which is given initial capabilities by the system, like a GUI window, access to a console for logging etc. The main thread may then spawn any number of additional threads, which can be limited to just some capabilities (handles and rights)

Simple example of spawning a second thread:

#include <playbit/playbit.h>

void bg_thread(u64 arg) {
    log("Hello from bg_thread");
}

void main() {
    PBThread thread = PBOptOk(PBThreadStart(bg_thread, 0, NULL));
    log("thread #%d started", thread.handle);
}

#include <playbit/thread.h>

PBThread

typedef struct {
    PBSysHandle handle;
} PBThread;

PBThreadOpt

typedef PBOpt(PBThread) PBThreadOpt;

PBThreadConfig

typedef struct PBThreadConfig {
    PBThread parentThread; // defaults to PBThread_SELF if zero
    u64      flags;

    /*
    rights for the thread itself.
    Usually you want PBSysRight_SAME_RIGHTS. 0 means "no rights."
    */
    PBSysRights rights;

    void* nullable stack;
    u32            stackSize;

    u32                         transferHandlesCount;
    const PBSysHandle* nullable transferHandles;
} PBThreadConfig;

PBThreadConfig describes the configuration of a new thread

PBThreadFn

typedef void (*PBThreadFn)(u64);

PBThreadFn is the thread entry point function

PBProcessStart

void PBProcessStart(void* mainFn,
                    u64   flags);

PBProcessStart starts the main function. flags are bits of PBSysThread_

PBThreadStart

PBThreadOpt PBThreadStart(PBThreadFn            arg,
                          u64                   arg,
                          const PBThreadConfig* config);

PBThreadStart starts a new thread

PBThreadExit

PBSysErr PBThreadExit(PBThread thread,
                      int      status);

PBExit terminates the thread. If this is called on the main thread, the process is terminated as an effect.

PBExit

PBSysErr PBExit(int status);

PBExit terminates the calling thread. If this is called on the main thread, the process is terminated as an effect.

PBExitProcess

PBSysErr PBExitProcess(int status);

PBExitProcess terminates the current process. Fails with PBSysErr_ACCESS_DENIED if the calling thread's PBSysHandle_SELF_THREAD does not have PBSysRight_MANAGE_PROCESS.

PBCallCtors

#define PBCallCtors()
    PBCallCtors invokes wasm constructor list for builds without libc startup.

PB_C_START_ATTR

#define PB_C_START_ATTR attribute
PB_C_START_ATTR defines attributes for the _start function, the program's entry point.
This is only needed when no libc (crt, really) is used.

PB_START_WITHOUT_GUI

#define PB_START_WITHOUT_GUI

PB_START_WITHOUT_GUI defines a _start function which sets PBSysThread_NOGUI

PBSetupInitialHandles

void PBSetupInitialHandles();

PBSetupInitialHandles sets up internal knowledge of what handles a thread is given at start


#include <playbit/mutex.h>

PBMutex

typedef struct PBMutex {
    _Atomic(u32) state;
} PBMutex;

PBMutex is a reader/writer mutual exclusion lock.

The lock can be held by up to 2^31-1 readers or by a single writer. The high bit is used as a "writer pending/active" flag, so the zero-initialized value for a PBMutex is a valid, unlocked mutex.

PBMutex is not re-entrant. In particular, a thread must not try to take a read lock recursively while a writer is waiting, since the pending writer bit blocks new readers until the writer runs.

Memory model: The Nth call to PBMutexUnlock "happens before" the Mth call to PBMutexLock for any N < M. For any call to PBMutexRLock, there exists an N such that the Nth call to PBMutexUnlock "happens before" that call to PBMutexRLock, and the corresponding call to PBMutexRUnlock "happens before" the N+1th call to PBMutexUnlock.

In other words: There's no need to use atomic instructions for shared data guarded by a PBMutex.

PBMutexInit

void PBMutexInit(PBMutex* mu);

PBMutexInit initializes mu to an unlocked state with zero readers and no writer.

PBMutexRLock

void PBMutexRLock(PBMutex* mu);

PBMutexRLock acquires a shared read lock.

PBMutexRUnlock

void PBMutexRUnlock(PBMutex* mu);

PBMutexRUnlock releases a shared read lock.

PBMutexLock

void PBMutexLock(PBMutex* mu);

PBMutexLock acquires an exclusive write lock.

PBMutexUnlock

void PBMutexUnlock(PBMutex* mu);

PBMutexUnlock releases an exclusive write lock.

PBMutexIsLocked

bool PBMutexIsLocked(const PBMutex* mu);

PBMutexIsLocked returns true if the mutex is write locked at the time of the call

PBMutexIsRLocked

bool PBMutexIsRLocked(const PBMutex* mu);

PBMutexIsLocked returns true if the mutex is read locked at the time of the call

PBMutexLockInScope

macro void PBMutexLockInScope(PBMutex* mu)

PBMutexLockInScope locks a mutex for the duration of the current scope. I.e. void example(PBMutex* mu1, PBMutex* mu2) { PBMutexLockInScope(mu1); // mu1 locked until function returns { PBMutexLockInScope(mu2); // mu2 locked until this scope ends } // mu2 unlocked } // mu1 unlocked

PBMutexRLockInScope

macro void PBMutexRLockInScope(PBMutex* mu)

#include <playbit/semaphore.h>

PBSemaphore

typedef struct PBSemaphore {
    _Atomic(u32) state;
} PBSemaphore;

PBSemaphore is a counting semaphore.

The zero-initialized value for a PBSemaphore is valid and has count 0.

PBSemaphoreWait blocks until the count is nonzero, then consumes one permit. PBSemaphoreSignal adds count permits and releases up to count waiters.

Example:

static PBSemaphore workReady = { 0 };

void worker(void) {
    PBSemaphoreWait(&workReady);
    process_work();
}

void queue_work(void) {
    add_work();
    PBSemaphoreSignal(&workReady, 1);
}

PBSemaphoreInit

void PBSemaphoreInit(PBSemaphore* sem,
                     u32          initCount);

PBSemaphoreInit initializes sem with initCount permits.

Note: While U32_MAX is allowed for initCount, any later signal would overflow (and panic.)

PBSemaphoreWait

void PBSemaphoreWait(PBSemaphore* sem);

PBSemaphoreWait blocks until one permit is available, then consumes it.

The successful wait has acquire semantics and synchronizes with a prior PBSemaphoreSignal that made the consumed permit available.

PBSemaphoreSignal

void PBSemaphoreSignal(PBSemaphore* sem,
                       u32          count);

PBSemaphoreSignal adds count permits and releases up to count waiters.

The signal has release semantics. Passing count 0 is a no-op. This function fails fast if adding count would overflow the 32-bit permit count.

PBSemaphoreValue

u32 PBSemaphoreValue(const PBSemaphore* sem);

PBSemaphoreValue returns a best-effort count snapshot for diagnostics and tests.

The returned value is not a synchronization primitive and may be stale immediately.