cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
gc.c
Go to the documentation of this file.
1#include <cwist/core/mem/gc.h>
3#include <ttak/mem/epoch.h>
4#include <pthread.h>
5#include <stdint.h>
6#include <stdlib.h>
7#include <string.h>
9#if defined(CWIST_PLATFORM_HOSTED)
10#include <sys/mman.h>
11#endif
12#include <unistd.h>
13
19/* --- cwist_reg_ptr registry: CWIST-side bookkeeping for pointers handed to
20 * a GC context via cwist_reg_ptr()/cwist_reg_ptr_sized().
21 *
22 * libttak's epoch GC releases whatever is still in its mem_tree at destroy
23 * time with ttak_mem_free(), which validates the ttak block header and
24 * aborts on any pointer that does not carry one (lib/libttak/src/mem/mem.c's
25 * V_HEADER). cwist_alloc() blocks have no such header when the owner guard
26 * is off (the default), so the wrapper must detach every CWIST-registered
27 * block from the tree before destruction and release it with cwist_free()
28 * itself. This table records exactly which pointers went through the
29 * wrapper (and which context owns them) so cwist_gc_shutdown(), an explicit
30 * cwist_free(), and cross-context re-registration can do that detachment.
31 *
32 * Pointers registered directly with ttak via cwist_gc_raw() are
33 * intentionally absent here: they keep libttak's native teardown semantics
34 * (freed by ttak_mem_free() at destroy), e.g. example/rps-showcase's
35 * ttak_mem_alloc_safe() payloads. Never mix the two registration paths for
36 * the same pointer.
37 *
38 * The table, like the other registries in this file, uses the raw
39 * allocator: it is the GC's own bookkeeping and must never sit on a
40 * full-GC pending list. */
41
42typedef struct {
43 void *ptr; /* NULL marks an empty slot */
44 cwist_gc_t *owner; /* context responsible for releasing the block */
45} cwist_reg_entry_t;
46
47static pthread_mutex_t g_reg_lock = PTHREAD_MUTEX_INITIALIZER;
48static cwist_reg_entry_t *g_reg_slots = NULL; /* open addressing, power-of-two cap */
49static size_t g_reg_cap = 0;
50static size_t g_reg_count = 0;
51
52static size_t cwist_reg_slot(const void *ptr, size_t mask) {
53 uint64_t h = (uint64_t)((uintptr_t)ptr >> 4) * UINT64_C(0x9E3779B97F4A7C15);
54 return (size_t)(h >> 32) & mask;
55}
56
58static cwist_reg_entry_t *cwist_reg_find_locked(const void *ptr) {
59 if (g_reg_count == 0) return NULL;
60 size_t mask = g_reg_cap - 1;
61 for (size_t i = cwist_reg_slot(ptr, mask);; i = (i + 1) & mask) {
62 if (!g_reg_slots[i].ptr) return NULL;
63 if (g_reg_slots[i].ptr == ptr) return &g_reg_slots[i];
64 }
65}
66
68static void cwist_reg_remove_locked(cwist_reg_entry_t *e) {
69 size_t mask = g_reg_cap - 1;
70 size_t hole = (size_t)(e - g_reg_slots);
71 for (size_t j = (hole + 1) & mask; g_reg_slots[j].ptr; j = (j + 1) & mask) {
72 size_t home = cwist_reg_slot(g_reg_slots[j].ptr, mask);
73 if (((j - home) & mask) >= ((j - hole) & mask)) {
74 g_reg_slots[hole] = g_reg_slots[j];
75 hole = j;
76 }
77 }
78 g_reg_slots[hole] = (cwist_reg_entry_t){0};
79 g_reg_count--;
80}
81
83static bool cwist_reg_reserve_locked(void) {
84 if ((g_reg_count + 1) * 2 <= g_reg_cap) return true;
85 size_t new_cap = g_reg_cap ? g_reg_cap * 2 : 64;
86 cwist_reg_entry_t *slots = (cwist_reg_entry_t *)calloc(new_cap, sizeof(*slots));
87 if (!slots) return false;
88 size_t mask = new_cap - 1;
89 for (size_t i = 0; i < g_reg_cap; i++) {
90 if (!g_reg_slots[i].ptr) continue;
91 size_t j = cwist_reg_slot(g_reg_slots[i].ptr, mask);
92 while (slots[j].ptr) j = (j + 1) & mask;
93 slots[j] = g_reg_slots[i];
94 }
95 free(g_reg_slots);
96 g_reg_slots = slots;
97 g_reg_cap = new_cap;
98 return true;
99}
100
113bool cwist_gc_reg_untrack(void *ptr) {
114 if (!ptr) return false;
115 pthread_mutex_lock(&g_reg_lock);
116 cwist_reg_entry_t *e = cwist_reg_find_locked(ptr);
117 if (!e) {
118 pthread_mutex_unlock(&g_reg_lock);
119 return false;
120 }
121 cwist_gc_t *owner = e->owner;
122 cwist_reg_remove_locked(e);
123 pthread_mutex_unlock(&g_reg_lock);
124
125 if (owner && owner->initialized) {
126 ttak_mem_node_t *node = ttak_mem_tree_find_node(&owner->impl.tree, ptr);
127 if (node) ttak_mem_tree_remove(&owner->impl.tree, node);
128 }
129 return true;
130}
131
144static void cwist_reg_drain(cwist_gc_t *gc) {
145 for (;;) {
146 pthread_mutex_lock(&g_reg_lock);
147 cwist_reg_entry_t *e = NULL;
148 for (size_t i = 0; i < g_reg_cap; i++) {
149 if (g_reg_slots[i].ptr && g_reg_slots[i].owner == gc) {
150 e = &g_reg_slots[i];
151 break;
152 }
153 }
154 if (!e) {
155 pthread_mutex_unlock(&g_reg_lock);
156 return;
157 }
158 void *ptr = e->ptr;
159 cwist_reg_remove_locked(e);
160 pthread_mutex_unlock(&g_reg_lock);
161
162 ttak_mem_node_t *node = ttak_mem_tree_find_node(&gc->impl.tree, ptr);
163 if (node) ttak_mem_tree_remove(&gc->impl.tree, node);
164 cwist_free(ptr);
165 }
166}
167
173void cwist_gc(cwist_gc_t *gc, bool manual_rotation) {
174 if (!gc) return;
175 if (!gc->initialized) {
176 ttak_epoch_gc_init(&gc->impl);
177 gc->initialized = true;
178 }
179 ttak_epoch_gc_manual_rotate(&gc->impl, manual_rotation);
180}
181
193 if (!gc || !gc->initialized) return;
194 cwist_reg_drain(gc);
195 ttak_epoch_gc_destroy(&gc->impl);
196 gc->initialized = false;
197}
198
204 if (!gc || !gc->initialized) return;
205 ttak_epoch_gc_rotate(&gc->impl);
206}
207
212void cwist_gc_auto_rotate(cwist_gc_t *gc, bool enabled) {
213 if (!gc || !gc->initialized) return;
214 bool gc_status = atomic_load(&gc->auto_rotated);
215 while (!atomic_compare_exchange_weak(&gc->auto_rotated, &gc_status, enabled));
216}
217
223 return atomic_load(&gc->auto_rotated);
224}
225
236void cwist_reg_ptr(cwist_gc_t *gc, void *ptr) {
237 cwist_reg_ptr_sized(gc, ptr, 0);
238}
239
261void cwist_reg_ptr_sized(cwist_gc_t *gc, void *ptr, size_t size) {
262 if (!gc || !gc->initialized || !ptr) return;
263
264 pthread_mutex_lock(&g_reg_lock);
265 cwist_reg_entry_t *e = cwist_reg_find_locked(ptr);
266 if (e) {
267 if (e->owner == gc) {
268 /* Already tracked by this context: skip a duplicate ttak tree
269 * node, which destroy would hand to ttak_mem_free() twice. */
270 pthread_mutex_unlock(&g_reg_lock);
271 return;
272 }
273 /* Registration moves to a new context: drop the pointer from the
274 * previous owner first, so that context's shutdown neither frees
275 * the block nor trips over it. */
276 cwist_gc_t *prev = e->owner;
277 cwist_reg_remove_locked(e);
278 pthread_mutex_unlock(&g_reg_lock);
279 if (prev && prev->initialized) {
280 ttak_mem_node_t *node = ttak_mem_tree_find_node(&prev->impl.tree, ptr);
281 if (node) ttak_mem_tree_remove(&prev->impl.tree, node);
282 }
283 pthread_mutex_lock(&g_reg_lock);
284 if (cwist_reg_find_locked(ptr)) {
285 /* Another thread registered it in the meantime; that
286 * registration stands. */
287 pthread_mutex_unlock(&g_reg_lock);
288 return;
289 }
290 }
291 bool tracked = cwist_reg_reserve_locked();
292 if (tracked) {
293 size_t mask = g_reg_cap - 1;
294 size_t i = cwist_reg_slot(ptr, mask);
295 while (g_reg_slots[i].ptr) i = (i + 1) & mask;
296 g_reg_slots[i] = (cwist_reg_entry_t){.ptr = ptr, .owner = gc};
297 g_reg_count++;
298 }
299 pthread_mutex_unlock(&g_reg_lock);
300
301 /* Best-effort, matching cwist_gc_scope_track(): when the registry
302 * itself cannot record the pointer, leave it untracked rather than
303 * register it with ttak, whose destroy-time free would abort on a
304 * headerless cwist_alloc block. */
305 if (!tracked) return;
306 ttak_epoch_gc_register(&gc->impl, ptr, size);
307}
308
321ttak_epoch_gc_t *cwist_gc_raw(cwist_gc_t *gc) {
322 if (!gc || !gc->initialized) return NULL;
323 return &gc->impl;
324}
325
331void *cwist_ebr_alloc(size_t size) {
332 /* Nothing to protect yet: a block nobody else can see does not need
333 * epoch coverage until it is published to another thread. */
334 return cwist_alloc(size);
335}
336
341static void cwist_ebr_free_cb(void *ptr) {
342 cwist_free(ptr);
343}
344
350void cwist_ebr_free(void *ptr) {
351 if (!ptr) return;
352 ttak_epoch_retire(ptr, cwist_ebr_free_cb);
353}
354
359 ttak_epoch_reclaim();
360}
361
367 if (!guard) return;
368 atomic_store_explicit(guard, false, memory_order_relaxed);
369}
370
377 if (!guard) return false;
378 bool expected = false;
379 return atomic_compare_exchange_strong_explicit(guard, &expected, true, memory_order_acq_rel,
380 memory_order_relaxed);
381}
382
383/* --- Full-GC mode: process-wide toggle + per-thread pending-sweep list --- */
384
404typedef struct {
405 _Atomic bool enabled;
406 _Atomic bool locked;
407} cwist_full_gc_guard_t;
408
414static cwist_full_gc_guard_t *g_full_gc_guard = NULL;
415
422__attribute__((constructor)) static void cwist_full_gc_guard_init(void) {
423#if !defined(CWIST_PLATFORM_HOSTED)
424 static cwist_full_gc_guard_t s_wasm_guard;
425 atomic_init(&s_wasm_guard.enabled, false);
426 atomic_init(&s_wasm_guard.locked, false);
427 g_full_gc_guard = &s_wasm_guard;
428#else
429 long page_size = sysconf(_SC_PAGESIZE);
430 if (page_size <= 0) page_size = 4096;
431 void *page =
432 mmap(NULL, (size_t)page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
433 if (page == MAP_FAILED) return;
434 cwist_full_gc_guard_t *guard = (cwist_full_gc_guard_t *)page;
435 atomic_init(&guard->enabled, false);
436 atomic_init(&guard->locked, false);
437 g_full_gc_guard = guard;
438#endif
439}
440
442static cwist_gc_t g_full_gc;
443static pthread_once_t g_full_gc_once = PTHREAD_ONCE_INIT;
444
446static void cwist_full_gc_lazy_init(void) {
447 cwist_gc(&g_full_gc, true);
448}
449
454static cwist_gc_t *cwist_full_gc_instance(void) {
455 pthread_once(&g_full_gc_once, cwist_full_gc_lazy_init);
456 return &g_full_gc;
457}
458
471static pthread_mutex_t g_full_gc_claim_mu = PTHREAD_MUTEX_INITIALIZER;
472
483void cwist_full_gc(bool enable) {
484 if (!g_full_gc_guard) return; /* guard page unavailable; fail safe, not silently unhardened */
485
486 /* Fast path: once locked, this plain load is the only thing every
487 * later caller ever does -- safe indefinitely, even after the page
488 * below becomes read-only. */
489 if (atomic_load_explicit(&g_full_gc_guard->locked, memory_order_acquire)) return;
490
491 pthread_mutex_lock(&g_full_gc_claim_mu);
492 if (atomic_load_explicit(&g_full_gc_guard->locked, memory_order_acquire)) {
493 /* Someone else claimed it while we were waiting for the mutex. */
494 pthread_mutex_unlock(&g_full_gc_claim_mu);
495 return;
496 }
497
498 /* We hold the mutex and locked is still false: nobody has mprotect()'d
499 * the page yet, so it is still writable and we are its sole writer. */
500 atomic_store_explicit(&g_full_gc_guard->enabled, enable, memory_order_relaxed);
501 atomic_store_explicit(&g_full_gc_guard->locked, true, memory_order_release);
502#if defined(CWIST_PLATFORM_HOSTED)
503 long page_size = sysconf(_SC_PAGESIZE);
504 mprotect(g_full_gc_guard, (size_t)(page_size > 0 ? page_size : 4096), PROT_READ);
505#endif
506 pthread_mutex_unlock(&g_full_gc_claim_mu);
507
508 cwist_gc_auto_rotate(cwist_full_gc_instance(), enable);
509}
510
516 if (!g_full_gc_guard) return false;
517 return atomic_load_explicit(&g_full_gc_guard->enabled, memory_order_relaxed);
518}
519
525 if (!g_full_gc_guard) return false;
526 return atomic_load_explicit(&g_full_gc_guard->locked, memory_order_acquire);
527}
528
534 return g_full_gc_guard;
535}
536
554typedef struct {
555 void **slots;
556 size_t cap; /* 0 or a power of two */
557 size_t count;
558} cwist_gc_pending_t;
559
561#define CWIST_GC_PENDING_MIN_CAP 16
564#define CWIST_GC_PENDING_KEEP_CAP 1024
565
570static inline size_t cwist_gc_pending_slot(const void *ptr, size_t mask) {
571 uint64_t h = (uint64_t)((uintptr_t)ptr >> 4) * UINT64_C(0x9E3779B97F4A7C15);
572 return (size_t)(h >> 32) & mask;
573}
574
576static bool cwist_gc_pending_grow(cwist_gc_pending_t *pending) {
577 size_t new_cap = pending->cap ? pending->cap * 2 : CWIST_GC_PENDING_MIN_CAP;
578 void **slots = (void **)calloc(new_cap, sizeof(void *));
579 if (!slots) return false;
580 size_t mask = new_cap - 1;
581 for (size_t i = 0; i < pending->cap; i++) {
582 void *key = pending->slots[i];
583 if (!key) continue;
584 size_t j = cwist_gc_pending_slot(key, mask);
585 while (slots[j]) j = (j + 1) & mask;
586 slots[j] = key;
587 }
588 free(pending->slots);
589 pending->slots = slots;
590 pending->cap = new_cap;
591 return true;
592}
593
604static void cwist_gc_pending_flush(cwist_gc_pending_t *pending) {
605 if (!pending) return;
606 void **slots = pending->slots;
607 size_t cap = pending->cap;
608 size_t count = pending->count;
609 if (count == 0 && cap <= CWIST_GC_PENDING_KEEP_CAP) return;
610
611 pending->slots = NULL;
612 pending->cap = 0;
613 pending->count = 0;
614 size_t retired = 0;
615 for (size_t i = 0; i < cap && count > 0; i++) {
616 if (!slots[i]) continue;
617 cwist_ebr_free(slots[i]);
618 count--;
619 retired++;
620 }
622 memset(slots, 0, cap * sizeof(void *));
623 pending->slots = slots;
624 pending->cap = cap;
625 } else {
626 free(slots);
627 }
628 /* Drive reclaim here: the full-GC instance rotates manually, so nothing
629 * else frees what was just retired unless the app happens to run an
630 * io_queue (#347). Every retired block was tracked by this thread and,
631 * by the cwist_gc_scope_flush() contract, is no longer in use, so
632 * freeing it once the grace period has passed is safe.
633 * Skipped when nothing was retired, so an io_queue job that freed
634 * everything itself pays nothing. */
635 if (retired > 0) {
636 /* libttak keeps a retired block for TTAK_EPOCH_GRACE epochs, and
637 * each reclaim advances at most one: with no reader inside an epoch
638 * this many calls free what was just retired. */
639 for (unsigned i = 0; i < TTAK_EPOCH_GRACE; i++) ttak_epoch_reclaim();
640 }
641}
642
643#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L)
644#define CWIST_GC_PENDING_TLS 1
645static _Thread_local cwist_gc_pending_t *t_gc_pending = NULL;
646#elif defined(__GNUC__) || defined(__clang__)
647#define CWIST_GC_PENDING_TLS 1
648static __thread cwist_gc_pending_t *t_gc_pending = NULL;
649#endif
650
655static void cwist_gc_pending_destroy(void *arg) {
656 cwist_gc_pending_t *pending = (cwist_gc_pending_t *)arg;
657 if (!pending) return;
658#ifdef CWIST_GC_PENDING_TLS
659 /* Destructors run on the exiting thread. Drop the cached pointer first,
660 * so an allocation made by a later destructor creates a fresh set (and
661 * re-registers it) instead of touching this one after it is freed. */
663#endif
664 cwist_gc_pending_flush(pending);
665 free(pending->slots);
666 free(pending);
667}
668
669static pthread_key_t g_pending_key;
670static pthread_once_t g_pending_key_once = PTHREAD_ONCE_INIT;
671
672static void cwist_gc_pending_key_init(void) {
673 pthread_key_create(&g_pending_key, cwist_gc_pending_destroy);
674}
675
683static cwist_gc_pending_t *cwist_gc_pending_get(void) {
684#ifdef CWIST_GC_PENDING_TLS
685 if (t_gc_pending) return t_gc_pending;
686#endif
687 pthread_once(&g_pending_key_once, cwist_gc_pending_key_init);
688 cwist_gc_pending_t *pending = (cwist_gc_pending_t *)pthread_getspecific(g_pending_key);
689 if (!pending) {
690 pending = (cwist_gc_pending_t *)calloc(1, sizeof(*pending));
691 if (pending) pthread_setspecific(g_pending_key, pending);
692 }
693#ifdef CWIST_GC_PENDING_TLS
694 t_gc_pending = pending;
695#endif
696 return pending;
697}
698
707void cwist_gc_scope_track(void *ptr) {
708 if (!ptr) return;
709 cwist_gc_pending_t *pending = cwist_gc_pending_get();
710 if (!pending) return;
711 /* Best-effort, as before: if the table cannot grow, leave ptr untracked
712 * rather than fail the allocation. */
713 if ((pending->count + 1) * 2 > pending->cap && !cwist_gc_pending_grow(pending)) return;
714 size_t mask = pending->cap - 1;
715 size_t i = cwist_gc_pending_slot(ptr, mask);
716 while (pending->slots[i]) {
717 if (pending->slots[i] == ptr) return; /* already tracked: keep one entry */
718 i = (i + 1) & mask;
719 }
720 pending->slots[i] = ptr;
721 pending->count++;
722}
723
734bool cwist_gc_scope_untrack(void *ptr) {
735 if (!ptr) return false;
736 cwist_gc_pending_t *pending = cwist_gc_pending_get();
737 if (!pending || pending->count == 0) return false;
738 size_t mask = pending->cap - 1;
739 size_t i = cwist_gc_pending_slot(ptr, mask);
740 while (pending->slots[i] != ptr) {
741 if (!pending->slots[i]) return false;
742 i = (i + 1) & mask;
743 }
744
745 /* Backward-shift deletion: pull each following entry of the probe run
746 * back into the hole unless the hole lies before that entry's home
747 * slot, so every remaining key stays reachable without tombstones. */
748 size_t hole = i;
749 for (size_t j = (i + 1) & mask; pending->slots[j]; j = (j + 1) & mask) {
750 size_t home = cwist_gc_pending_slot(pending->slots[j], mask);
751 if (((j - home) & mask) >= ((j - hole) & mask)) {
752 pending->slots[hole] = pending->slots[j];
753 hole = j;
754 }
755 }
756 pending->slots[hole] = NULL;
757 pending->count--;
758 return true;
759}
760
766bool cwist_gc_scope_disown(void *ptr) {
767 return cwist_gc_scope_untrack(ptr);
768}
769
777 cwist_gc_pending_flush(cwist_gc_pending_get());
778}
779
785 cwist_gc_pending_t *pending = cwist_gc_pending_get();
786 return pending ? pending->count : 0;
787}
788
789/* --- Connection registry: thread/process-exit sweep for non-memory
790 * resources (sockets, TLS sessions, protocol state). See gc.h's
791 * cwist_conn_registry_* doc comments for the contract.
792 *
793 * One process-wide table maps each tracked handle to its close_fn and its
794 * owner, the thread whose exit sweep closes it (#345). The owner starts as
795 * the tracking thread and moves with cwist_conn_registry_adopt() when
796 * another thread takes over serving the connection, so a thread that exits
797 * no longer closes connections other threads are still using. Untrack is a
798 * hash delete instead of a walk over every thread's list, and close_fn
799 * always runs with no registry lock held, so a close_fn may track or
800 * untrack other handles.
801 *
802 * The table, batches and owner tokens use the raw allocator: they are the
803 * GC's own bookkeeping and must never sit on a full-GC pending list. */
804
806typedef struct cwist_conn_owner {
807 char unused;
809
810typedef struct {
811 void *handle; /* NULL marks an empty slot */
812 cwist_conn_close_fn close_fn;
813 cwist_conn_owner_t *owner;
814} cwist_conn_entry_t;
815
816static pthread_key_t g_conn_key;
817static pthread_once_t g_conn_key_once = PTHREAD_ONCE_INIT;
818static pthread_mutex_t g_conn_registry_lock = PTHREAD_MUTEX_INITIALIZER;
819static cwist_conn_entry_t *g_conn_slots = NULL; /* open addressing, power-of-two cap */
820static size_t g_conn_cap = 0;
821static size_t g_conn_count = 0;
822static _Atomic bool g_conn_hooks_registered = false;
823
824static size_t cwist_conn_slot(const void *handle, size_t mask) {
825 /* 64-bit mix even where pointers are 32-bit (wasm32, Cortex-M): a
826 * 33-bit shift of a 32-bit uintptr_t is undefined. */
827 uint64_t x = (uint64_t)(uintptr_t)handle;
828 x ^= x >> 33;
829 x *= 0xff51afd7ed558ccdULL;
830 x ^= x >> 33;
831 return (size_t)x & mask;
832}
833
835static size_t cwist_conn_find_locked(const void *handle) {
836 if (g_conn_count == 0) return SIZE_MAX;
837 size_t mask = g_conn_cap - 1;
838 for (size_t i = cwist_conn_slot(handle, mask);; i = (i + 1) & mask) {
839 if (g_conn_slots[i].handle == handle) return i;
840 if (!g_conn_slots[i].handle) return SIZE_MAX;
841 }
842}
843
845static void cwist_conn_remove_at_locked(size_t i) {
846 size_t mask = g_conn_cap - 1;
847 size_t hole = i;
848 for (size_t j = (i + 1) & mask; g_conn_slots[j].handle; j = (j + 1) & mask) {
849 size_t home = cwist_conn_slot(g_conn_slots[j].handle, mask);
850 if (((j - home) & mask) >= ((j - hole) & mask)) {
851 g_conn_slots[hole] = g_conn_slots[j];
852 hole = j;
853 }
854 }
855 g_conn_slots[hole] = (cwist_conn_entry_t){0};
856 g_conn_count--;
857}
858
860static bool cwist_conn_reserve_locked(void) {
861 if ((g_conn_count + 1) * 2 <= g_conn_cap) return true;
862 size_t new_cap = g_conn_cap ? g_conn_cap * 2 : 64;
863 cwist_conn_entry_t *slots = (cwist_conn_entry_t *)calloc(new_cap, sizeof(*slots));
864 if (!slots) return false;
865 size_t mask = new_cap - 1;
866 for (size_t i = 0; i < g_conn_cap; i++) {
867 if (!g_conn_slots[i].handle) continue;
868 size_t j = cwist_conn_slot(g_conn_slots[i].handle, mask);
869 while (slots[j].handle) j = (j + 1) & mask;
870 slots[j] = g_conn_slots[i];
871 }
872 free(g_conn_slots);
873 g_conn_slots = slots;
874 g_conn_cap = new_cap;
875 return true;
876}
877
886static void cwist_conn_close_owned(cwist_conn_owner_t *owner) {
887 for (;;) {
888 pthread_mutex_lock(&g_conn_registry_lock);
889 size_t want = 0;
890 for (size_t i = 0; i < g_conn_cap; i++) {
891 if (g_conn_slots[i].handle && (!owner || g_conn_slots[i].owner == owner)) want++;
892 }
893 if (want == 0) {
894 pthread_mutex_unlock(&g_conn_registry_lock);
895 return;
896 }
897 cwist_conn_entry_t one;
898 cwist_conn_entry_t *heap = (cwist_conn_entry_t *)calloc(want, sizeof(*heap));
899 cwist_conn_entry_t *batch = heap ? heap : &one;
900 size_t room = heap ? want : 1;
901 size_t n = 0;
902 for (size_t i = 0; i < g_conn_cap && n < room;) {
903 if (g_conn_slots[i].handle && (!owner || g_conn_slots[i].owner == owner)) {
904 batch[n++] = g_conn_slots[i];
905 cwist_conn_remove_at_locked(i); /* may shift a later entry into i */
906 } else {
907 i++;
908 }
909 }
910 pthread_mutex_unlock(&g_conn_registry_lock);
911 for (size_t k = 0; k < n; k++) batch[k].close_fn(batch[k].handle);
912 free(heap);
913 }
914}
915
916static void cwist_conn_registry_atexit(void) {
917 cwist_conn_close_owned(NULL);
918}
919
920/* fork(): the child inherits the parent's table, but the connections are
921 * the parent's. A child that later runs exit() would otherwise shut down
922 * TLS sessions on sockets the parent is still serving. The lock is held
923 * across fork so the child never inherits it mid-update. */
924static void cwist_conn_atfork_prepare(void) {
925 pthread_mutex_lock(&g_conn_registry_lock);
926}
927
928static void cwist_conn_atfork_parent(void) {
929 pthread_mutex_unlock(&g_conn_registry_lock);
930}
931
932static void cwist_conn_atfork_child(void) {
933 if (g_conn_slots) memset(g_conn_slots, 0, g_conn_cap * sizeof(*g_conn_slots));
934 g_conn_count = 0;
935 pthread_mutex_unlock(&g_conn_registry_lock);
936}
937
939static void cwist_conn_owner_destroy(void *arg) {
941 if (!owner) return;
942 cwist_conn_close_owned(owner);
943 free(owner);
944}
945
946static void cwist_conn_key_init(void) {
947 pthread_key_create(&g_conn_key, cwist_conn_owner_destroy);
948}
949
951static cwist_conn_owner_t *cwist_conn_owner_get(bool create) {
952 pthread_once(&g_conn_key_once, cwist_conn_key_init);
954 if (owner || !create) return owner;
955
956 owner = (cwist_conn_owner_t *)calloc(1, sizeof(*owner));
957 if (!owner) return NULL;
958 if (pthread_setspecific(g_conn_key, owner) != 0) {
959 free(owner);
960 return NULL;
961 }
962
963 /* Register the process-exit sweep and the fork handler exactly once,
964 * the first time any thread tracks a connection (not eagerly at load
965 * time: most processes never enable full-GC). */
966 if (!atomic_exchange_explicit(&g_conn_hooks_registered, true, memory_order_acq_rel)) {
967 atexit(cwist_conn_registry_atexit);
968 pthread_atfork(cwist_conn_atfork_prepare, cwist_conn_atfork_parent, cwist_conn_atfork_child);
969 }
970 return owner;
971}
972
984void cwist_conn_registry_track(void *handle, cwist_conn_close_fn close_fn) {
985 if (!handle || !close_fn) return;
986 if (!cwist_full_gc_enabled()) return;
987 cwist_conn_owner_t *owner = cwist_conn_owner_get(true);
988 if (!owner) return;
989
990 pthread_mutex_lock(&g_conn_registry_lock);
991 size_t i = cwist_conn_find_locked(handle);
992 if (i != SIZE_MAX) {
993 g_conn_slots[i].close_fn = close_fn;
994 g_conn_slots[i].owner = owner;
995 } else if (cwist_conn_reserve_locked()) {
996 size_t mask = g_conn_cap - 1;
997 size_t j = cwist_conn_slot(handle, mask);
998 while (g_conn_slots[j].handle) j = (j + 1) & mask;
999 g_conn_slots[j] = (cwist_conn_entry_t){.handle = handle, .close_fn = close_fn, .owner = owner};
1000 g_conn_count++;
1001 }
1002 pthread_mutex_unlock(&g_conn_registry_lock);
1003}
1004
1011 if (!handle) return false;
1012 pthread_mutex_lock(&g_conn_registry_lock);
1013 size_t i = cwist_conn_find_locked(handle);
1014 if (i != SIZE_MAX) cwist_conn_remove_at_locked(i);
1015 pthread_mutex_unlock(&g_conn_registry_lock);
1016 return i != SIZE_MAX;
1017}
1018
1025bool cwist_conn_registry_adopt(void *handle) {
1026 if (!handle || !cwist_full_gc_enabled()) return false;
1027 cwist_conn_owner_t *owner = cwist_conn_owner_get(true);
1028 if (!owner) return false;
1029 pthread_mutex_lock(&g_conn_registry_lock);
1030 size_t i = cwist_conn_find_locked(handle);
1031 if (i != SIZE_MAX) g_conn_slots[i].owner = owner;
1032 pthread_mutex_unlock(&g_conn_registry_lock);
1033 return i != SIZE_MAX;
1034}
1035
1042 cwist_conn_owner_t *owner = cwist_conn_owner_get(false);
1043 if (owner) cwist_conn_close_owned(owner);
1044}
1045
1053 cwist_conn_close_owned(NULL);
1054}
1055
1061 cwist_conn_owner_t *owner = cwist_conn_owner_get(false);
1062 if (!owner) return 0;
1063 size_t n = 0;
1064 pthread_mutex_lock(&g_conn_registry_lock);
1065 for (size_t i = 0; i < g_conn_cap; i++) {
1066 if (g_conn_slots[i].handle && g_conn_slots[i].owner == owner) n++;
1067 }
1068 pthread_mutex_unlock(&g_conn_registry_lock);
1069 return n;
1070}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
__attribute__((constructor))
Map the guard page before main() runs, so cwist_full_gc_enabled() – called on every cwist_alloc()/cwi...
Definition gc.c:422
void cwist_full_gc(bool enable)
Claim full-GC mode permanently, exactly once, then harden the toggle.
Definition gc.c:483
ttak_epoch_gc_t * cwist_gc_raw(cwist_gc_t *gc)
Expose the underlying libttak epoch GC structure for advanced integrations.
Definition gc.c:321
#define CWIST_GC_PENDING_KEEP_CAP
Definition gc.c:564
void cwist_gc_auto_rotate(cwist_gc_t *gc, bool enabled)
Enable auto-rotation of epoch GC.
Definition gc.c:212
void cwist_conn_registry_sweep_all(void)
Close every tracked handle, whichever thread owns it.
Definition gc.c:1052
bool cwist_conn_registry_untrack(void *handle)
Remove a handle from the registry, whichever thread owns it.
Definition gc.c:1010
void cwist_ebr_free(void *ptr)
Defer release of a block until every thread that might still be reading it (inside ttak_epoch_enter()...
Definition gc.c:350
#define CWIST_GC_PENDING_MIN_CAP
Definition gc.c:561
bool cwist_gc_scope_disown(void *ptr)
Alias of cwist_gc_scope_untrack(): drop tracking without retiring.
Definition gc.c:766
void cwist_reg_ptr(cwist_gc_t *gc, void *ptr)
Register a pointer with zero-size accounting metadata.
Definition gc.c:236
bool cwist_full_gc_enabled(void)
Query whether full-GC mode was enabled by the (single) cwist_full_gc() claim.
Definition gc.c:515
struct cwist_conn_owner cwist_conn_owner_t
bool cwist_gc_reg_untrack(void *ptr)
Detach ptr from whichever GC context tracks it, without freeing.
Definition gc.c:113
void cwist_release_guard_init(cwist_release_guard_t *guard)
Reset a release guard to its unclaimed state.
Definition gc.c:366
bool cwist_conn_registry_adopt(void *handle)
Make the calling thread the owner of a tracked handle.
Definition gc.c:1025
bool cwist_gc_auto_rotated(cwist_gc_t *gc)
Return if epoch GC is auto-rotated. @ param gc GC context to get status.
Definition gc.c:222
void cwist_gc_shutdown(cwist_gc_t *gc)
Destroy the wrapped libttak GC state when it has been initialized.
Definition gc.c:192
bool cwist_release_guard_acquire(cwist_release_guard_t *guard)
Claim a release guard.
Definition gc.c:376
void cwist_gc_rotate(cwist_gc_t *gc)
Advance the current epoch and reclaim retired nodes when eligible.
Definition gc.c:203
void cwist_gc_scope_flush(void)
Retire every pending block this thread is still tracking.
Definition gc.c:776
size_t cwist_conn_registry_pending_count(void)
Count the tracked handles the calling thread owns.
Definition gc.c:1060
void cwist_gc_pipeline_tick(void)
Advance the EBR epoch and run any callbacks that are now safe.
Definition gc.c:358
void cwist_conn_registry_flush(void)
Close every handle the calling thread owns, right now.
Definition gc.c:1041
bool cwist_gc_scope_untrack(void *ptr)
Stop tracking a block without retiring it.
Definition gc.c:734
bool cwist_full_gc_locked(void)
Query whether the full-GC toggle has already been claimed and locked.
Definition gc.c:524
void cwist_reg_ptr_sized(cwist_gc_t *gc, void *ptr, size_t size)
Register a pointer and its approximate size with the epoch GC.
Definition gc.c:261
void cwist_conn_registry_track(void *handle, cwist_conn_close_fn close_fn)
Track a non-memory resource for the full-GC exit sweep.
Definition gc.c:984
void * cwist_ebr_alloc(size_t size)
Allocate a block that will later be released through cwist_ebr_free().
Definition gc.c:331
size_t cwist_gc_scope_pending_count(void)
Count this thread's not-yet-retired tracked blocks.
Definition gc.c:784
void cwist_gc_scope_track(void *ptr)
Track a live cwist_alloc() block so full-GC can retire it later.
Definition gc.c:707
void * cwist_full_gc_guard_page(void)
Expose the hardened full-GC guard page for introspection/tests.
Definition gc.c:533
Epoch-based garbage collection wrapper.
void(* cwist_conn_close_fn)(void *handle)
Full-GC's connection registry: thread/process-exit sweep for resources that aren't plain cwist_alloc(...
Definition gc.h:301
atomic_bool cwist_release_guard_t
One-shot guard ensuring a shared teardown path runs exactly once.
Definition gc.h:276
size_t batch
Definition otel.c:54
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
Target capability macros shared by all CWIST sources.
High-level wrapper over libttak's epoch GC.
Definition gc.h:20
_Atomic bool auto_rotated
Check if GC should scan and reclaim objects.
Definition gc.h:25
bool initialized
Check if CWIST GC has initialized.
Definition gc.h:23
ttak_epoch_gc_t impl
Definition gc.h:21