Skip to content

C++ modernisation followups: FFI thread safety, RAII, jthread migration#420

Merged
benswift merged 4 commits intomasterfrom
cpp-modernisation-followup
Apr 19, 2026
Merged

C++ modernisation followups: FFI thread safety, RAII, jthread migration#420
benswift merged 4 commits intomasterfrom
cpp-modernisation-followup

Conversation

@benswift
Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #419 covering three backlog items: TASK-040, partial TASK-039 and partial TASK-044. Based on cpp-modernisation so it only shows the follow-up work.

TASK-040 — FFI thread safety (done)

  • s_ffiCount is now std::atomic<int> with fetch_add for slot assignment.
  • s_ffiTable entries are std::atomic<foreign_func> with release/acquire pairing between mk_foreign_func and ffi_trampoline<N> — no mutex on the hot path.
  • LLVM_SCHEME_FF_MAP is guarded with std::shared_mutex (shared on get, unique on set). The getter now uses find() rather than operator[], which also fixes a latent bug of inserting empty strings for unknown keys.

TASK-039 — RAII (partial)

  • AudioDevice::m_threads[MAX_RT_AUDIO_THREADS] raw EXTThread* array → std::array<std::unique_ptr<EXTThread>, N>. Destructor detaches each running thread so unique_ptr destruction doesn't trip std::terminate (these threads intentionally run while(true) for the life of the process). The unused getMTThreads() accessor is removed.
  • thread_local std::minstd_rand* → value-typed thread_local std::minstd_rand, seeded lazily with time(nullptr) on first access. No more heap allocation, no more per-call null check.

Remaining (kept on the backlog): SchemeProcess/REPL registries → Meyers singletons, and SchemeTask::m_ptrstd::variant.

TASK-044 — EXTThread → std::thread/jthread (partial)

  • The primary delayed-connect thread and the linenoise REPL thread in Extempore.cpp were leaked new EXTThread(...). Now std::thread(...).detach(), matching the fire-and-forget intent.

Kept on EXTThread for documented reasons (RT priority, setSubsume, or xtlang FFI ABI): AudioDevice m_threads, SchemeProcess::m_threadTask/m_threadServer, and the xtlang thread_fork API.

Test plan

  • cmake --build build --target extempore — clean
  • ctest --label-regex \"libs-core|cpp-unit|compiler-unit\" — 19/19 pass
  • ctest --label-regex \"libs-external\" — 1/1 pass
  • cmake --build build --target aot_external_audio — builds
  • ./extempore --batch \"(begin (println 'hello) (quit 0))\" — runs
  • CI: Linux x64 / macOS arm64 / Windows x64 matrix

@benswift
Copy link
Copy Markdown
Collaborator Author

TSan run — 2026-04-19

Built with EXTEMPORE_SANITIZE=tsan and ran the full libs-core | cpp-unit | compiler-unit suite (11 tests, ~24min wall time). All tests pass.

Four unique races were reported, all pre-existing and untouched by this PR:

Location Cause
SchemeS7.cpp:309 (scheme_load_file) ×11 reads UNIV::TIME on main thread while the scheduler writes it
SchemeS7.cpp:79 (begin_hook) ×2 same UNIV::TIME race
SchemeProcess.cpp:293 (taskImpl) ×2 m_libsLoaded bool read without synchronisation
stl_deque.h:273 ×1 SchemeTask queue iterator race (taskQueue access, matches TASK-042's this is a race :( comment)

These fall under existing backlog items: the UNIV::TIME and deque races belong to TASK-042 (audio thread dispatcher race conditions), and the m_libsLoaded bool should be added there.

Nothing reported on code this PR changed — the atomic s_ffiCount / s_ffiTable and shared_mutex-guarded LLVM_SCHEME_FF_MAP produced zero TSan warnings under load. Audio m_threads and the thread-local PRNG were also clean.

@benswift benswift changed the base branch from cpp-modernisation to master April 19, 2026 10:57
Both tables were carrying "not thread safe" comments and could race when
scheme instances registered foreign functions concurrently or when a
reader on one thread observed a table element before the writer's store
was visible.

- s_ffiTable / s_ffiCount (SchemeS7.cpp): s_ffiCount becomes
  std::atomic<int> with fetch_add so slot assignment is unique; each
  table entry becomes std::atomic<foreign_func>, paired release/acquire
  so the trampoline's load sees a fully-constructed entry without
  needing a mutex on the hot path.
- LLVM_SCHEME_FF_MAP (EXTLLVM.cpp): guard with std::shared_mutex.  The
  getter now uses find() instead of operator[], fixing a latent bug that
  would insert an empty string for unknown ff pointers, and returns ""
  when the key is absent.

std::map keeps stored strings stable across inserts, so the c_str() a
reader copies out under its shared lock stays valid while the caller
uses it (we never erase entries).
Two small ownership fixes from the #419 follow-up backlog:

- AudioDevice::m_threads[MAX_RT_AUDIO_THREADS] was a C array of raw
  owning EXTThread* that were `new`ed in init and never `delete`d.
  Now a std::array<std::unique_ptr<EXTThread>, N>.  The dtor detaches
  each running thread so unique_ptr destruction doesn't call
  std::terminate on the underlying std::thread (the RT threads
  intentionally run `while(true)` and are cleaned up at process exit).
  The unused getMTThreads() accessor is removed; no callers existed.

- The imp_randd PRNG was a heap-allocated thread_local pointer that
  leaked on thread exit and required a null check per call.  Now a
  value-typed thread_local seeded with time(nullptr) on first access
  per thread, keeping the existing per-thread seeding semantics.
The primary delayed-connect thread and the linenoise REPL thread were
spawned via `new extemp::EXTThread(...)` + start(), leaking the
EXTThread object (never joined or detached, destructor never runs).

Both are fire-and-forget — they run until the process exits and need
none of EXTThread's RT-scheduling or subsume-current-thread features.
Replace with std::thread constructed in place and detached, so the
std::thread object's destructor doesn't trip std::terminate and the
lambda captures make the arg wiring explicit.
- TASK-040 (FFI thread safety): mark Done; moved to completed/.
- TASK-039: tick off m_threads and PRNG; leave singletons and
  SchemeTask::m_ptr as remaining scope.
- TASK-044: tick off the two REPL threads in Extempore.cpp; list the
  call sites that must stay on EXTThread (RT priority / setSubsume /
  xtlang ABI) and the deferred candidates (TaskScheduler, OSC).
@benswift benswift force-pushed the cpp-modernisation-followup branch from 867f11e to 0c3a4c0 Compare April 19, 2026 10:58
@benswift benswift merged commit f1cf1b0 into master Apr 19, 2026
4 checks passed
@benswift benswift deleted the cpp-modernisation-followup branch April 19, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant