std1.0.0-beta-7 SafeC docs

Source thread.h

View in the repository

#pragma once
// SafeC Standard Library — Thread
// Cross-platform: POSIX pthreads (Linux/macOS) or Win32 (Windows).
// Backend is selected automatically from '_WIN32', which safec auto-defines
// for a Windows host/target (see main.cpp's platform-detection block) —
// same convention as std/stderr_compat.h.
//
// Thread handles: unsigned long long
//   POSIX → pthread_t value (8 bytes on 64-bit)
//   Win32 → HANDLE (kernel object pointer, 8 bytes on x64)
//
// Mutex / condvar / rwlock handles: unsigned long long
//   POSIX → pointer to heap-allocated pthread object
//   Win32 → pointer to heap-allocated Windows primitive

#include <std/mem.h>
#include <std/panic.h>
#include <std/ownership.h>

// ── Thread ───────────────────────────────────────────────────────────────────
#include <std/erased.h>

namespace std {

// OS-boundary entry point (POSIX/Win32 take an untyped start routine; see
// std/erased.h); call thread_create below instead.
int  thread_create_raw_(&unsigned long long tid, fn ?&Erased(?&Erased) func, ?&Erased arg);

// Starts func(arg) on a new thread and stores its handle in *tid.
// 0 on success. func's return value is discarded (thread_join only waits).
//   static ?&Job work(?&Job j) { ... return null; }
//   unsigned long long tid = 0ULL;
//   std::thread_create(&tid, work, (?&Job)&job);
generic<T>
int thread_create(&unsigned long long tid, fn ?&T(?&T) func, ?&T arg) {
    unsafe { return thread_create_raw_(tid, (fn ?&Erased(?&Erased))func, (?&Erased)arg); }
}
int  thread_join(unsigned long long tid);
int  thread_detach(unsigned long long tid);
void thread_yield();
void thread_sleep_ms(unsigned long ms);
unsigned long long thread_self();

// ── Mutex ─────────────────────────────────────────────────────────────────────
int mutex_init(&unsigned long long m);
int mutex_destroy(&unsigned long long m);
int mutex_lock(&unsigned long long m);
int mutex_trylock(&unsigned long long m);  // 0=locked, non-zero=busy
// Don't re-lock a mutex the calling thread already holds: a Windows
// CRITICAL_SECTION allows it (recursive), a default POSIX mutex deadlocks
// (mutex_lock) or reports busy (mutex_trylock).
int mutex_unlock(&unsigned long long m);

// ── Condition variable ────────────────────────────────────────────────────────
// cond_wait atomically releases mutex and sleeps; re-acquires on wake.
int cond_init(&unsigned long long cv);
int cond_destroy(&unsigned long long cv);
int cond_wait(&unsigned long long cv, &unsigned long long m);
// Waits at most 'ms' milliseconds: 0 if woken, THREAD_TIMEDOUT if the time
// ran out, -1 on another error (the same codes on every platform).
#define THREAD_TIMEDOUT 1
int cond_timedwait_ms(&unsigned long long cv, &unsigned long long m, unsigned long ms);
int cond_signal(&unsigned long long cv);
int cond_broadcast(&unsigned long long cv);

// ── Read-write lock ───────────────────────────────────────────────────────────
// Multiple simultaneous readers; exclusive writers.
int rwlock_init(&unsigned long long rw);
int rwlock_destroy(&unsigned long long rw);
int rwlock_rdlock(&unsigned long long rw);
int rwlock_wrlock(&unsigned long long rw);
int rwlock_tryrdlock(&unsigned long long rw);
int rwlock_trywrlock(&unsigned long long rw);
int rwlock_rdunlock(&unsigned long long rw);
int rwlock_wrunlock(&unsigned long long rw);


// ══ Typed, checked layer ═════════════════════════════════════════════════════
// The functions above are the OS primitives: they move untyped handles and
// nothing checks what the threads share. These build on them so the
// compiler can (see the language reference's "Thread safety" page):
//
//   * thread_spawn(f, arg) moves 'arg' (a Send value) into a new thread;
//     the returned SpawnedThread is a Drop value: join() it for f's result (drop()
//     joins and drops it). Nothing is shared, so nothing can race.
//   * Mutex<T> owns a T that is reachable only through the MutexGuard
//     lock() returns; the guard unlocks in drop(), and a reference from
//     guard.get() can't be used after the guard is dropped. A Mutex is Sync,
//     so threads may share one (a static Mutex, or a local lent to
//     spawn_scoped threads).
//   * RwLock<T>: many readers (read() guards give 'const &T') or one writer.

// ── thread_spawn / Thread ────────────────────────────────────────────────────

generic<T, R>
struct ThreadStart_ {
    fn R(T) f;
    T       arg;    // moved in by thread_spawn, moved out by the thread
    R       out;    // the thread's result, moved out by join()
};

generic<T, R>
?&ThreadStart_<T, R> thread_start_main_(?&ThreadStart_<T, R> sp) {
    match (sp) {
        case some(s): { unsafe { T a = s.arg; s.out = s.f(a); } }
        case null: { }
    }
    return null;
}

// A running thread started by thread_spawn, and its result. A Drop value:
// join() it (or drop() it, which joins and drops the result).
generic<T, R>
struct SpawnedThread {
    unsigned long long        tid_;
    ?&heap ThreadStart_<T, R> start_;   // null if the thread couldn't start
    impl !Send;                         // joined by the thread that started it

    int  started() const;               // 0 if the thread couldn't be created
    R    join() consume;                // waits; f's result (panics if it never started)
    void drop();                        // join() and drop the result
};

// Runs f(arg) on a new thread. 'arg' moves into it (using it afterwards is
// an error); T and R must be Send.
generic<T: Send, R: Send>
struct SpawnedThread<T, R> thread_spawn(fn R(T) f, T arg) {
    struct SpawnedThread<T, R> t;
    t.tid_ = 0ULL;
    t.start_ = null;
    ?&heap ThreadStart_<T, R> sp = null;
    unsafe { sp = (?&heap ThreadStart_<T, R>)alloc_raw_(sizeof(struct ThreadStart_<T, R>)); }
    match (sp) {
        case null: { drop(arg); return t; }
        case some(s): {
            unsafe { s.f = f; s.arg = arg; }   // arg moves into the start block
            fn ?&ThreadStart_<T, R>(?&ThreadStart_<T, R>) entry = thread_start_main_;
            if (thread_create(&t.tid_, entry, (?&ThreadStart_<T, R>)s) != 0) {
                unsafe { drop(s.arg); dealloc(s); }
                return t;
            }
            t.start_ = s;
        }
    }
    return t;
}

generic<T, R>
int SpawnedThread::started() const { return self.start_.is_null() ? 0 : 1; }

generic<T, R>
R SpawnedThread::join() {
    match (self.start_) {
        case some(s): {
            thread_join(self.tid_);
            self.start_ = null;
            unsafe {
                R r = s.out;   // moves out of the start block, which goes away
                dealloc(s);
                return r;
            }
        }
        case null: { }
    }
    panic_at("SpawnedThread::join: the thread never started", "thread.h", 0);
    unsafe { return *(R*)0; }   // unreachable
}

generic<T, R>
void SpawnedThread::drop() {
    if (self.start_.is_null()) return;
    R r = self.join();
    drop(r);
}

// ── Mutex<T> / MutexGuard<T> ─────────────────────────────────────────────────

// A T shared between threads, reachable only while locked. Moving a Mutex
// after locking it is not allowed (guards point at it).
generic<T: Send>
struct Mutex {
    unsigned long long raw_;     // the OS lock
    T*                 value_;   // heap box; only a guard reaches it (raw: unsafe to touch)
    unsafe impl Sync;            // every access to *value_ is under raw_

    static struct Mutex<T> new(T value);
    // Blocks until this thread holds the lock. The guard is the only way to
    // the value; dropping it unlocks. Don't lock twice on one thread.
    struct MutexGuard<T> lock() const;
    // The value back; the Mutex is gone (no guard may be alive).
    T    into_inner() consume;
    void drop();                 // drops the value, destroys the lock
};

// Proof of holding a Mutex's lock: get() is the protected value, valid
// until the guard is dropped (which unlocks).
generic<T>
struct MutexGuard {
    const &Mutex<T> m_;
    &T              value_;
    impl !Send;                  // must unlock on the locking thread

    &T   get();                  // the value: don't keep it past drop()
    void set(T value);           // replace it (dropping the old one)
    void drop();                 // unlocks
};

generic<T>
struct Mutex<T> Mutex::new(T value) {
    struct Mutex<T> m;
    m.raw_ = 0ULL;
    mutex_init(&m.raw_);
    unsafe {
        m.value_ = (T*)alloc_raw_(sizeof(T));
        if (m.value_ == (T*)0) panic_at("Mutex::new: out of memory", "thread.h", 0);
        *m.value_ = value;   // moves in
    }
    return m;
}

generic<T>
struct MutexGuard<T> Mutex::lock() const {
    unsafe { mutex_lock((&unsigned long long)&self.raw_); }
    struct MutexGuard<T> g;
    g.m_ = self;
    unsafe { g.value_ = (&T)self.value_; }
    return g;
}

generic<T>
T Mutex::into_inner() {
    unsafe {
        T v = *self.value_;   // moves out of the box
        dealloc(self.value_);
        mutex_destroy(&self.raw_);
        return v;
    }
}

generic<T>
void Mutex::drop() {
    unsafe {
        drop(*self.value_);
        dealloc(self.value_);
        self.value_ = (T*)0;
    }
    mutex_destroy(&self.raw_);
}

generic<T>
&T MutexGuard::get() { return self.value_; }

generic<T>
void MutexGuard::set(T value) { drop(std::replace(self.value_, value)); }

generic<T>
void MutexGuard::drop() { unsafe { mutex_unlock((&unsigned long long)&self.m_.raw_); } }

// ── RwLock<T> ────────────────────────────────────────────────────────────────

// Like Mutex<T>, with many readers at once (const access) or one writer.
// Readers share the value, so T must be Sync as well as Send.
generic<T: Send + Sync>
struct RwLock {
    unsigned long long raw_;
    T*                 value_;
    unsafe impl Sync;

    static struct RwLock<T> new(T value);
    struct RwReadGuard<T>  read() const;    // shared: get() is const
    struct RwWriteGuard<T> write() const;   // exclusive
    void drop();
};

generic<T>
struct RwReadGuard {
    const &RwLock<T> l_;
    const &T         value_;
    impl !Send;
    const &T get() const;
    void     drop();                // read-unlocks
};

generic<T>
struct RwWriteGuard {
    const &RwLock<T> l_;
    &T               value_;
    impl !Send;
    &T   get();
    void drop();                    // write-unlocks
};

generic<T>
struct RwLock<T> RwLock::new(T value) {
    struct RwLock<T> l;
    l.raw_ = 0ULL;
    rwlock_init(&l.raw_);
    unsafe {
        l.value_ = (T*)alloc_raw_(sizeof(T));
        if (l.value_ == (T*)0) panic_at("RwLock::new: out of memory", "thread.h", 0);
        *l.value_ = value;
    }
    return l;
}

generic<T>
struct RwReadGuard<T> RwLock::read() const {
    unsafe { rwlock_rdlock((&unsigned long long)&self.raw_); }
    struct RwReadGuard<T> g;
    g.l_ = self;
    unsafe { g.value_ = (const &T)self.value_; }
    return g;
}

generic<T>
struct RwWriteGuard<T> RwLock::write() const {
    unsafe { rwlock_wrlock((&unsigned long long)&self.raw_); }
    struct RwWriteGuard<T> g;
    g.l_ = self;
    unsafe { g.value_ = (&T)self.value_; }
    return g;
}

generic<T>
void RwLock::drop() {
    unsafe {
        drop(*self.value_);
        dealloc(self.value_);
        self.value_ = (T*)0;
    }
    rwlock_destroy(&self.raw_);
}

generic<T>
const &T RwReadGuard::get() const { return self.value_; }

generic<T>
void RwReadGuard::drop() { unsafe { rwlock_rdunlock((&unsigned long long)&self.l_.raw_); } }

generic<T>
&T RwWriteGuard::get() { return self.value_; }

generic<T>
void RwWriteGuard::drop() { unsafe { rwlock_wrunlock((&unsigned long long)&self.l_.raw_); } }

} // namespace std