aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/jobqueue.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-17 16:36:10 +0200
committerGitHub Enterprise <[email protected]>2026-04-17 16:36:10 +0200
commitd5e92ce97a55c39158329257dd80dc6a24f393ad (patch)
tree7cf2db98b9a101ede63311962e7dba3ae72d7a9d /src/zencore/jobqueue.cpp
parentreplace pretty progress with prettyscroll implementation (#970) (diff)
downloadarchived-zen-d5e92ce97a55c39158329257dd80dc6a24f393ad.tar.xz
archived-zen-d5e92ce97a55c39158329257dd80dc6a24f393ad.zip
zenbase hardening (#971)
A series of correctness and API hygiene fixes to the intrusive refcount primitives in `zenbase`, culminating in the removal of `RefPtr<T>` in favour of a single unified `Ref<T>` smart pointer. The changes are motivated by two pieces of latent UB sitting under every `Ref<T>` / `TRefCounted<T>` in the codebase, plus a handful of API footguns on the smart-pointer side (silent raw-pointer decay, missing converting moves, unconstrained conversions from unrelated types). ## Correctness fixes - **Strict-aliasing UB in atomic helpers** — `AtomicIncrement`/`Decrement`/`Add` took a `volatile uint32_t&` and reinterpret-cast it to `std::atomic<T>*`. The object was never constructed as a `std::atomic`, so the access was type-punning UB. Fixed by changing `m_RefCount` to `std::atomic<uint32_t>` directly in `RefCounted`, `TRefCounted<T>` and `IoBufferCore`. The helpers (and `zenbase/atomic.h`) are later removed entirely — the three callers now invoke `fetch_add`/`fetch_sub` directly. - **const_cast of non-mutable member** — `AddRef()` / `Release()` are `const` but mutated `m_RefCount` via `const_cast`. Since `m_RefCount` wasn't `mutable`, writing through the cast was UB for any `const`-qualified holder (e.g. a `static const` refcounted singleton). Fixed by marking `m_RefCount` `mutable` and dropping the `const_cast` in `AddRef`/`Release`. - **Public non-virtual `TRefCounted` destructor** — allowed `delete basePtr;` to slice past the CRTP `DeleteThis()` contract. Moved to `protected`. ## Memory-ordering cleanup - `AddRef` weakened from seq_cst to **relaxed** (a thread can only take a new reference via one it already holds; nothing needs to synchronize). - `Release` weakened from seq_cst to **acq_rel** (sufficient to order prior writes before the destructor, and make the decrement visible to observers). - Diagnostic `RefCount()` / `GetRefCount()` reads made **relaxed** and spelled out as explicit `.load()` — the returned value is stale the moment it's observed, so stronger ordering gives no guarantee. - No-op on x86 (`lock xadd` either way), but removes a full barrier on every `Ref<T>` copy on ARM64 (Apple silicon / Windows-on-ARM). ## `RefPtr` / `Ref` unification Before this branch, `RefPtr<T>` and `Ref<T>` were subtly different in ways that made the safer of the two (`Ref`) harder to use and the looser one (`RefPtr`) dangerous: - `RefPtr::operator T*()` was implicit — `delete refPtr;` compiled silently (double-delete), and the raw pointer could outlive the temporary `RefPtr` it was extracted from. Made `explicit`, then removed entirely once call sites were migrated to `.Get()`. - `RefPtr(T*)` was implicit while `RefPtr(RefPtr<Derived>&&)` was `explicit` — exactly the opposite of the safety intent. Reversed. - `RefPtr`'s converting move was unconstrained (any `RefPtr<U>` with an implicitly-convertible `U*` satisfied it, including `void*` and multiple-inheritance base offsets). Added a `DerivedFrom<U, T>` constraint matching `Ref<T>`. - `Ref<T>` was missing a converting move ctor / move-assignment from `Ref<Derived>` — upcasts of rvalues were going through `AddRef`+`Release` instead of a pointer steal. Added. - `Release()` and the non-move smart-pointer ops were not `noexcept`, despite being so in practice. Marked `noexcept` throughout. After all of the above, the two types were functionally identical. The final commit deletes `RefPtr` and rewrites the ~10 consumer files to use `Ref`.
Diffstat (limited to 'src/zencore/jobqueue.cpp')
-rw-r--r--src/zencore/jobqueue.cpp24
1 files changed, 12 insertions, 12 deletions
diff --git a/src/zencore/jobqueue.cpp b/src/zencore/jobqueue.cpp
index 3e58fb97d..a5a82717d 100644
--- a/src/zencore/jobqueue.cpp
+++ b/src/zencore/jobqueue.cpp
@@ -93,7 +93,7 @@ public:
{
NewJobId = IdGenerator.fetch_add(1);
}
- RefPtr<Job> NewJob(new Job());
+ Ref<Job> NewJob(new Job());
NewJob->Queue = this;
NewJob->Name = Name;
NewJob->Callback = std::move(JobFunc);
@@ -124,7 +124,7 @@ public:
QueueLock.WithExclusiveLock([&]() {
if (auto It = std::find_if(QueuedJobs.begin(),
QueuedJobs.end(),
- [NewJobId](const RefPtr<Job>& Job) { return Job->Id.Id == NewJobId; });
+ [NewJobId](const Ref<Job>& Job) { return Job->Id.Id == NewJobId; });
It != QueuedJobs.end())
{
QueuedJobs.erase(It);
@@ -156,7 +156,7 @@ public:
Result = true;
return;
}
- if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const RefPtr<Job>& Job) { return Job->Id.Id == Id.Id; });
+ if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const Ref<Job>& Job) { return Job->Id.Id == Id.Id; });
It != QueuedJobs.end())
{
ZEN_DEBUG("Cancelling queued background job {}:'{}'", (*It)->Id.Id, (*It)->Name);
@@ -301,7 +301,7 @@ public:
AbortedJobs.erase(It);
return;
}
- if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const RefPtr<Job>& Job) { return Job->Id.Id == Id.Id; });
+ if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const Ref<Job>& Job) { return Job->Id.Id == Id.Id; });
It != QueuedJobs.end())
{
Result = Convert(JobStatus::Queued, *(*It));
@@ -340,20 +340,20 @@ public:
std::atomic_uint64_t IdGenerator = 1;
- std::atomic_bool InitializedFlag = false;
- RwLock QueueLock;
- std::deque<RefPtr<Job>> QueuedJobs;
- std::unordered_map<uint64_t, RefPtr<Job>> RunningJobs;
- std::unordered_map<uint64_t, RefPtr<Job>> CompletedJobs;
- std::unordered_map<uint64_t, RefPtr<Job>> AbortedJobs;
+ std::atomic_bool InitializedFlag = false;
+ RwLock QueueLock;
+ std::deque<Ref<Job>> QueuedJobs;
+ std::unordered_map<uint64_t, Ref<Job>> RunningJobs;
+ std::unordered_map<uint64_t, Ref<Job>> CompletedJobs;
+ std::unordered_map<uint64_t, Ref<Job>> AbortedJobs;
WorkerThreadPool WorkerPool;
Latch WorkerCounter;
void Worker()
{
- int CurrentThreadId = GetCurrentThreadId();
- RefPtr<Job> CurrentJob;
+ int CurrentThreadId = GetCurrentThreadId();
+ Ref<Job> CurrentJob;
QueueLock.WithExclusiveLock([&]() {
if (!QueuedJobs.empty())
{