File Coverage

pubsub.h
Criterion Covered Total %
statement 376 465 80.8
branch 242 450 53.7
condition n/a
subroutine n/a
pod n/a
total 618 915 67.5


line stmt bran cond sub pod time code
1             /*
2             * pubsub.h -- Shared-memory broadcast pub/sub for Linux
3             *
4             * Two variants:
5             * Int -- lock-free MPMC publish, lock-free subscribe (int64 values)
6             * Str -- mutex-protected publish, lock-free subscribe (variable-length
7             * byte strings up to msg_size, one fixed arena region per slot)
8             *
9             * Ring buffer broadcast: publishers write, each subscriber independently
10             * reads with its own cursor. Messages are never consumed -- the ring
11             * overwrites old data when it wraps. Subscribers auto-recover from
12             * overflow by resetting to the oldest available position.
13             *
14             * File-backed mmap(MAP_SHARED) for cross-process sharing,
15             * futex for blocking poll, PID-based stale lock recovery (Str mode).
16             */
17              
18             #ifndef PUBSUB_H
19             #define PUBSUB_H
20              
21             #include
22             #include
23             #include
24             #include
25             #include
26             #include
27             #include
28             #include
29             #include
30             #include
31             #include
32             #include
33             #include
34             #include
35             #include
36             #include
37              
38             /* ================================================================
39             * Constants
40             * ================================================================ */
41              
42             #define PUBSUB_MAGIC 0x50534231U /* "PSB1" */
43             #define PUBSUB_VERSION 1
44             #define PUBSUB_MODE_INT 0
45             #define PUBSUB_MODE_STR 1
46             #define PUBSUB_MODE_INT32 2
47             #define PUBSUB_MODE_INT16 3
48             #define PUBSUB_ERR_BUFLEN 256
49             #define PUBSUB_SPIN_LIMIT 32
50             #define PUBSUB_LOCK_TIMEOUT_SEC 2
51             #define PUBSUB_DEFAULT_MSG_SIZE 256
52             #define PUBSUB_STR_UTF8_FLAG 0x80000000U
53             #define PUBSUB_STR_LEN_MASK 0x7FFFFFFFU
54             #define PUBSUB_POLL_RETRIES 8
55              
56             /* ================================================================
57             * Header (256 bytes = 4 cache lines)
58             * ================================================================ */
59              
60             typedef struct {
61             /* ---- Cache line 0 (0-63): immutable after create ---- */
62             uint32_t magic; /* 0 */
63             uint32_t version; /* 4 */
64             uint32_t mode; /* 8 */
65             uint32_t capacity; /* 12 */
66             uint64_t total_size; /* 16 */
67             uint64_t slots_off; /* 24 */
68             uint64_t data_off; /* 32: str: offset to arena; int: 0 */
69             uint32_t msg_size; /* 40: str: max bytes per message; int: 0 */
70             uint32_t _reserved0; /* 44 */
71             uint64_t arena_cap; /* 48: str: arena byte capacity; int: 0 */
72             uint8_t _pad0[8]; /* 56-63 */
73              
74             /* ---- Cache line 1 (64-127): writer hot ---- */
75             uint64_t write_pos; /* 64 */
76             uint32_t mutex; /* 72: str: futex mutex */
77             uint32_t mutex_waiters; /* 76 */
78             uint32_t arena_wpos; /* 80: str: next write position in arena */
79             uint8_t _pad1[44]; /* 84-127 */
80              
81             /* ---- Cache line 2 (128-191): subscriber notification ---- */
82             uint32_t sub_futex; /* 128 */
83             uint32_t sub_waiters; /* 132 */
84             uint8_t _pad2[56]; /* 136-191 */
85              
86             /* ---- Cache line 3 (192-255): stats ---- */
87             uint64_t stat_publish_ok; /* 192 */
88             uint64_t stat_recoveries; /* 200 */
89             uint8_t _pad3[48]; /* 208-255 */
90             } PubSubHeader;
91              
92             #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
93             _Static_assert(sizeof(PubSubHeader) == 256, "PubSubHeader must be 256 bytes");
94             #endif
95              
96             /* ================================================================
97             * Slot types
98             * ================================================================ */
99              
100             typedef struct {
101             uint64_t sequence;
102             int64_t value;
103             } PubSubIntSlot; /* 16 bytes */
104              
105             /* Compact int slots: 32-bit sequence + value = 8 bytes (2x cache density) */
106             typedef struct {
107             uint32_t sequence;
108             int32_t value;
109             } PubSubInt32Slot; /* 8 bytes */
110              
111             typedef struct {
112             uint32_t sequence;
113             int16_t value;
114             int16_t _pad;
115             } PubSubInt16Slot; /* 8 bytes */
116              
117             typedef struct {
118             uint64_t sequence;
119             uint32_t packed_len; /* bit 31 = UTF-8, bits 0-30 = byte length */
120             uint32_t arena_off; /* offset into data arena */
121             } PubSubStrSlot; /* 16 bytes */
122              
123             /* ================================================================
124             * Process-local handles
125             * ================================================================ */
126              
127             typedef struct {
128             PubSubHeader *hdr;
129             void *slots;
130             char *data; /* NULL for int mode */
131             size_t mmap_size;
132             uint32_t capacity;
133             uint32_t cap_mask;
134             uint32_t msg_size;
135             uint32_t mode; /* validated variant; NOT re-read from peer-writable hdr->mode */
136             uint64_t arena_cap;
137             char *path;
138             int notify_fd;
139             int backing_fd;
140             } PubSubHandle;
141              
142             typedef struct {
143             PubSubHeader *hdr;
144             void *slots;
145             char *data;
146             uint64_t cursor;
147             uint32_t capacity;
148             uint32_t cap_mask;
149             uint32_t msg_size;
150             uint64_t arena_cap;
151             char *copy_buf;
152             uint32_t copy_buf_cap;
153             uint64_t overflow_count;
154             int notify_fd;
155             void *userdata;
156             } PubSubSub;
157              
158             /* ================================================================
159             * Utility
160             * ================================================================ */
161              
162 149           static inline uint32_t pubsub_next_pow2(uint32_t v) {
163 149 50         if (v < 2) return 2;
164 149 50         if (v > 0x80000000U) return 0;
165 149           v--;
166 149           v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16;
167 149           return v + 1;
168             }
169              
170 0           static inline void pubsub_spin_pause(void) {
171             #if defined(__x86_64__) || defined(__i386__)
172 0           __asm__ volatile("pause" ::: "memory");
173             #elif defined(__aarch64__)
174             __asm__ volatile("yield" ::: "memory");
175             #else
176             __asm__ volatile("" ::: "memory");
177             #endif
178 0           }
179              
180 958           static inline int pubsub_ensure_copy_buf(PubSubSub *sub, uint32_t needed) {
181 958 100         if (needed <= sub->copy_buf_cap) return 1;
182 270 100         uint32_t ns = sub->copy_buf_cap ? sub->copy_buf_cap : 64;
183 339 100         while (ns < needed) {
184 69           uint32_t n2 = ns * 2;
185 69 50         if (n2 <= ns) { ns = needed; break; }
186 69           ns = n2;
187             }
188 270           char *nb = (char *)realloc(sub->copy_buf, ns);
189 270 50         if (!nb) return 0;
190 270           sub->copy_buf = nb;
191 270           sub->copy_buf_cap = ns;
192 270           return 1;
193             }
194              
195             /* ================================================================
196             * Futex helpers
197             * ================================================================ */
198              
199             #define PUBSUB_MUTEX_WRITER_BIT 0x80000000U
200             #define PUBSUB_MUTEX_PID_MASK 0x7FFFFFFFU
201             #define PUBSUB_MUTEX_VAL(pid) (PUBSUB_MUTEX_WRITER_BIT | ((uint32_t)(pid) & PUBSUB_MUTEX_PID_MASK))
202              
203 0           static inline int pubsub_pid_alive(uint32_t pid) {
204 0 0         if (pid == 0) return 1;
205 0 0         return !(kill((pid_t)pid, 0) == -1 && errno == ESRCH);
    0          
206             }
207              
208             static const struct timespec pubsub_lock_timeout = { PUBSUB_LOCK_TIMEOUT_SEC, 0 };
209              
210 0           static inline void pubsub_recover_stale_mutex(PubSubHeader *hdr, uint32_t observed) {
211 0 0         if (!__atomic_compare_exchange_n(&hdr->mutex, &observed, 0,
212             0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED))
213 0           return;
214 0           __atomic_add_fetch(&hdr->stat_recoveries, 1, __ATOMIC_RELAXED);
215 0 0         if (__atomic_load_n(&hdr->mutex_waiters, __ATOMIC_RELAXED) > 0)
216 0           syscall(SYS_futex, &hdr->mutex, FUTEX_WAKE, 1, NULL, NULL, 0);
217             }
218              
219 397           static inline void pubsub_mutex_lock(PubSubHeader *hdr) {
220 397           uint32_t mypid = PUBSUB_MUTEX_VAL((uint32_t)getpid());
221 397           for (int spin = 0; ; spin++) {
222 397           uint32_t expected = 0;
223 397 50         if (__atomic_compare_exchange_n(&hdr->mutex, &expected, mypid,
224             1, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
225 397           return;
226 0 0         if (__builtin_expect(spin < PUBSUB_SPIN_LIMIT, 1)) {
227 0           pubsub_spin_pause();
228 0           continue;
229             }
230 0           __atomic_add_fetch(&hdr->mutex_waiters, 1, __ATOMIC_RELAXED);
231 0           uint32_t cur = __atomic_load_n(&hdr->mutex, __ATOMIC_RELAXED);
232 0 0         if (cur != 0) {
233 0           long rc = syscall(SYS_futex, &hdr->mutex, FUTEX_WAIT, cur,
234             &pubsub_lock_timeout, NULL, 0);
235 0 0         if (rc == -1 && errno == ETIMEDOUT) {
    0          
236 0           __atomic_sub_fetch(&hdr->mutex_waiters, 1, __ATOMIC_RELAXED);
237 0           uint32_t val = __atomic_load_n(&hdr->mutex, __ATOMIC_RELAXED);
238 0 0         if (val >= PUBSUB_MUTEX_WRITER_BIT) {
239 0           uint32_t pid = val & PUBSUB_MUTEX_PID_MASK;
240 0 0         if (!pubsub_pid_alive(pid))
241 0           pubsub_recover_stale_mutex(hdr, val);
242             }
243 0           spin = 0;
244 0           continue;
245             }
246             }
247 0           __atomic_sub_fetch(&hdr->mutex_waiters, 1, __ATOMIC_RELAXED);
248 0           spin = 0;
249             }
250             }
251              
252 397           static inline void pubsub_mutex_unlock(PubSubHeader *hdr) {
253 397           __atomic_store_n(&hdr->mutex, 0, __ATOMIC_RELEASE);
254 397 50         if (__atomic_load_n(&hdr->mutex_waiters, __ATOMIC_RELAXED) > 0)
255 0           syscall(SYS_futex, &hdr->mutex, FUTEX_WAKE, 1, NULL, NULL, 0);
256 397           }
257              
258 976           static inline void pubsub_wake_subscribers(PubSubHeader *hdr) {
259             /* SEQ_CST fence pairs with consumer's SEQ_CST waiters increment in
260             * poll_wait. Without it, the RELAXED load below may be reordered
261             * before the prior RELEASE store of the slot sequence on weak-memory
262             * architectures, letting us observe waiters == 0 even when a consumer
263             * has already incremented it after publishing the data. */
264 976           __atomic_thread_fence(__ATOMIC_SEQ_CST);
265 976 100         if (__atomic_load_n(&hdr->sub_waiters, __ATOMIC_RELAXED) > 0) {
266 97           __atomic_add_fetch(&hdr->sub_futex, 1, __ATOMIC_RELEASE);
267 97           syscall(SYS_futex, &hdr->sub_futex, FUTEX_WAKE, INT_MAX, NULL, NULL, 0);
268             }
269 976           }
270              
271 11           static inline int pubsub_remaining_time(const struct timespec *deadline,
272             struct timespec *remaining) {
273             struct timespec now;
274 11           clock_gettime(CLOCK_MONOTONIC, &now);
275 11           remaining->tv_sec = deadline->tv_sec - now.tv_sec;
276 11           remaining->tv_nsec = deadline->tv_nsec - now.tv_nsec;
277 11 100         if (remaining->tv_nsec < 0) {
278 6           remaining->tv_sec--;
279 6           remaining->tv_nsec += 1000000000L;
280             }
281 11           return remaining->tv_sec >= 0;
282             }
283              
284 11           static inline void pubsub_make_deadline(double timeout, struct timespec *deadline) {
285 11           clock_gettime(CLOCK_MONOTONIC, deadline);
286 11           deadline->tv_sec += (time_t)timeout;
287 11           deadline->tv_nsec += (long)((timeout - (double)(time_t)timeout) * 1e9);
288 11 50         if (deadline->tv_nsec >= 1000000000L) {
289 0           deadline->tv_sec++;
290 0           deadline->tv_nsec -= 1000000000L;
291             }
292 11           }
293              
294             /* ================================================================
295             * Header validation
296             * ================================================================ */
297              
298 20           static inline int pubsub_validate_header(PubSubHeader *hdr, uint32_t mode,
299             uint64_t file_size) {
300 20 50         if (hdr->magic != PUBSUB_MAGIC ||
301 20 50         hdr->version != PUBSUB_VERSION ||
302 20 100         hdr->mode != mode ||
303 16 50         hdr->capacity == 0 ||
304 16 50         (hdr->capacity & (hdr->capacity - 1)) != 0 ||
305 16 50         hdr->total_size != file_size ||
306 16 50         hdr->slots_off != sizeof(PubSubHeader))
307 4           return 0;
308             /* Slot array must fit within file. */
309 16           uint64_t slot_size = (mode == PUBSUB_MODE_INT) ? sizeof(PubSubIntSlot)
310 22 100         : (mode == PUBSUB_MODE_INT32) ? sizeof(PubSubInt32Slot)
311 11 100         : (mode == PUBSUB_MODE_INT16) ? sizeof(PubSubInt16Slot)
312 5 100         : sizeof(PubSubStrSlot);
313 16 50         if (hdr->capacity > (hdr->total_size - hdr->slots_off) / slot_size)
314 0           return 0;
315 16 100         if (mode == PUBSUB_MODE_STR) {
316 4 50         if (hdr->data_off == 0 || hdr->msg_size == 0 || hdr->arena_cap == 0)
    50          
    50          
317 0           return 0;
318 4           uint64_t slots_end = hdr->slots_off + (uint64_t)hdr->capacity * slot_size;
319 4 50         if (hdr->data_off < slots_end ||
320 4 50         hdr->data_off + hdr->arena_cap > hdr->total_size)
321 0           return 0;
322             }
323 16           return 1;
324             }
325              
326             /* ================================================================
327             * Create / Open / Close
328             * ================================================================ */
329              
330             #define PUBSUB_ERR(fmt, ...) do { if (errbuf) snprintf(errbuf, PUBSUB_ERR_BUFLEN, fmt, ##__VA_ARGS__); } while(0)
331              
332 149           static PubSubHandle *pubsub_init_handle(void *base, size_t map_size,
333             uint32_t mode, const char *path) {
334 149           PubSubHeader *hdr = (PubSubHeader *)base;
335 149           PubSubHandle *h = (PubSubHandle *)calloc(1, sizeof(PubSubHandle));
336 149 50         if (!h) return NULL;
337              
338 149           h->hdr = hdr;
339 149           h->slots = (char *)base + hdr->slots_off;
340 149 100         h->data = (mode == PUBSUB_MODE_STR) ? (char *)base + hdr->data_off : NULL;
341 149           h->mmap_size = map_size;
342 149           h->capacity = hdr->capacity;
343 149           h->cap_mask = hdr->capacity - 1;
344 149           h->msg_size = hdr->msg_size;
345 149           h->mode = mode;
346 149           h->arena_cap = hdr->arena_cap;
347 149 100         h->path = path ? strdup(path) : NULL;
348 149           h->notify_fd = -1;
349 149           h->backing_fd = -1;
350              
351 149           return h;
352             }
353              
354 133           static void pubsub_init_header(void *base, uint32_t mode, uint32_t cap,
355             uint64_t total_size, uint64_t slots_off,
356             uint64_t data_off, uint32_t msg_size,
357             uint64_t arena_cap) {
358 133           PubSubHeader *hdr = (PubSubHeader *)base;
359 133           memset(hdr, 0, sizeof(PubSubHeader));
360 133           hdr->magic = PUBSUB_MAGIC;
361 133           hdr->version = PUBSUB_VERSION;
362 133           hdr->mode = mode;
363 133           hdr->capacity = cap;
364 133           hdr->total_size = total_size;
365 133           hdr->slots_off = slots_off;
366 133           hdr->data_off = data_off;
367 133           hdr->msg_size = msg_size;
368 133           hdr->arena_cap = arena_cap;
369 133           }
370              
371             /* Returns 1 on success, 0 if the requested capacity * msg_size would
372             * exceed the 32-bit arena addressing limit (out_arena_cap is set to 0). */
373 149           static int pubsub_calc_layout(uint32_t cap, uint32_t mode, uint32_t msg_size,
374             uint64_t *out_slots_off, uint64_t *out_data_off,
375             uint64_t *out_arena_cap, uint64_t *out_total_size) {
376 149           uint64_t slots_off = sizeof(PubSubHeader);
377 149           uint64_t slot_size = (mode == PUBSUB_MODE_INT) ? sizeof(PubSubIntSlot)
378 231 100         : (mode == PUBSUB_MODE_INT32) ? sizeof(PubSubInt32Slot)
379 151 100         : (mode == PUBSUB_MODE_INT16) ? sizeof(PubSubInt16Slot)
380 69 100         : sizeof(PubSubStrSlot);
381 149           uint64_t data_off = 0, arena_cap = 0, total_size;
382              
383 149 100         if (mode == PUBSUB_MODE_STR) {
384 57           uint64_t slots_end = slots_off + (uint64_t)cap * slot_size;
385 57           data_off = (slots_end + 63) & ~(uint64_t)63;
386 57           arena_cap = (uint64_t)cap * ((uint64_t)msg_size + 8);
387             /* Safety invariant: in any window of `capacity` consecutive publishes
388             * (one full slot-ring wrap), the arena must not wrap. Otherwise a
389             * publish to slot K could overwrite slot N's still-current arena
390             * region without bumping slot N's sequence -- the seqlock would
391             * not catch the corruption. arena_cap = capacity*(msg_size+8)
392             * guarantees this; silently capping to UINT32_MAX would break the
393             * invariant for extreme (capacity, msg_size) combinations. */
394 57 50         if (arena_cap > UINT32_MAX) {
395 0           *out_arena_cap = 0;
396 0           return 0;
397             }
398 57           total_size = data_off + arena_cap;
399             } else {
400 92           total_size = slots_off + (uint64_t)cap * slot_size;
401             }
402              
403 149           *out_slots_off = slots_off;
404 149           *out_data_off = data_off;
405 149           *out_arena_cap = arena_cap;
406 149           *out_total_size = total_size;
407 149           return 1;
408             }
409              
410             /* Securely obtain a fd: create exclusively (O_CREAT|O_EXCL|O_NOFOLLOW at mode,
411             * default 0600), or attach an existing file (O_RDWR|O_NOFOLLOW, no O_CREAT). */
412 51           static int pubsub_secure_open(const char *path, mode_t mode, char *errbuf) {
413 51 50         for (int attempt = 0; attempt < 100; attempt++) {
414 51           int fd = open(path, O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC, mode);
415 51 100         if (fd >= 0) { (void)fchmod(fd, mode); return fd; } /* exact mode: umask narrowed the O_EXCL create */
416 16 50         if (errno != EEXIST) { PUBSUB_ERR("create %s: %s", path, strerror(errno)); return -1; }
    0          
417 16           fd = open(path, O_RDWR|O_NOFOLLOW|O_CLOEXEC);
418 16 50         if (fd >= 0) return fd;
419 0 0         if (errno == ENOENT) continue; /* creator unlinked between our two opens; retry */
420 0 0         PUBSUB_ERR("open %s: %s", path, strerror(errno)); /* ELOOP => symlink rejected */
421 0           return -1;
422             }
423 0 0         PUBSUB_ERR("open %s: create/attach kept racing", path);
424 0           return -1;
425             }
426              
427 145           static PubSubHandle *pubsub_create(const char *path, uint32_t capacity,
428             uint32_t mode, uint32_t msg_size,
429             mode_t fmode, char *errbuf) {
430 145 50         if (errbuf) errbuf[0] = '\0';
431              
432 145           uint32_t cap = pubsub_next_pow2(capacity);
433 145 50         if (cap == 0) { PUBSUB_ERR("invalid capacity"); return NULL; }
    0          
434 145 50         if (mode > PUBSUB_MODE_INT16) { PUBSUB_ERR("unknown mode %u", mode); return NULL; }
    0          
435              
436 145 100         if (mode == PUBSUB_MODE_STR && msg_size == 0)
    100          
437 35           msg_size = PUBSUB_DEFAULT_MSG_SIZE;
438              
439             uint64_t slots_off, data_off, arena_cap, total_size;
440 145 50         if (!pubsub_calc_layout(cap, mode, msg_size, &slots_off, &data_off, &arena_cap, &total_size)) {
441 0 0         PUBSUB_ERR("capacity * (msg_size+8) exceeds 4GB arena limit");
442 0           return NULL;
443             }
444              
445 145           int anonymous = (path == NULL);
446             size_t map_size;
447             void *base;
448              
449 145 100         if (anonymous) {
450 94           map_size = (size_t)total_size;
451 94           base = mmap(NULL, map_size, PROT_READ | PROT_WRITE,
452             MAP_SHARED | MAP_ANONYMOUS, -1, 0);
453 94 50         if (base == MAP_FAILED) {
454 0 0         PUBSUB_ERR("mmap(anonymous): %s", strerror(errno));
455 0           return NULL;
456             }
457 94           pubsub_init_header(base, mode, cap, total_size, slots_off, data_off,
458             msg_size, arena_cap);
459             } else {
460 51           int fd = pubsub_secure_open(path, fmode, errbuf);
461 67 50         if (fd < 0) return NULL;
462              
463 51 50         if (flock(fd, LOCK_EX) < 0) {
464 0 0         PUBSUB_ERR("flock(%s): %s", path, strerror(errno));
465 0           close(fd); return NULL;
466             }
467              
468             struct stat st;
469 51 50         if (fstat(fd, &st) < 0) {
470 0 0         PUBSUB_ERR("fstat(%s): %s", path, strerror(errno));
471 0           flock(fd, LOCK_UN); close(fd); return NULL;
472             }
473              
474 51           int is_new = (st.st_size == 0);
475              
476 51 100         if (!is_new && (uint64_t)st.st_size < sizeof(PubSubHeader)) {
    50          
477 0 0         PUBSUB_ERR("%s: file too small (%lld)", path, (long long)st.st_size);
478 0           flock(fd, LOCK_UN); close(fd); return NULL;
479             }
480              
481 51 100         if (is_new) {
482 35 50         if (ftruncate(fd, (off_t)total_size) < 0) {
483 0 0         PUBSUB_ERR("ftruncate(%s): %s", path, strerror(errno));
484 0           flock(fd, LOCK_UN); close(fd); return NULL;
485             }
486             }
487              
488 51 100         map_size = is_new ? (size_t)total_size : (size_t)st.st_size;
489 51           base = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
490 51 50         if (base == MAP_FAILED) {
491 0 0         PUBSUB_ERR("mmap(%s): %s", path, strerror(errno));
492 0           flock(fd, LOCK_UN); close(fd); return NULL;
493             }
494              
495 51 100         if (!is_new) {
496 16 100         if (!pubsub_validate_header((PubSubHeader *)base, mode, (uint64_t)st.st_size)) {
497 4 50         PUBSUB_ERR("%s: invalid or incompatible pubsub file", path);
498 4           munmap(base, map_size); flock(fd, LOCK_UN); close(fd); return NULL;
499             }
500 12           flock(fd, LOCK_UN);
501 12           close(fd);
502 12           return pubsub_init_handle(base, map_size, mode, path);
503             }
504              
505 35           pubsub_init_header(base, mode, cap, total_size, slots_off, data_off,
506             msg_size, arena_cap);
507 35           flock(fd, LOCK_UN);
508 35           close(fd);
509             }
510              
511 129           PubSubHandle *h = pubsub_init_handle(base, (size_t)total_size, mode, path);
512 129 50         if (!h) { munmap(base, (size_t)total_size); return NULL; }
513 129           return h;
514             }
515              
516 4           static PubSubHandle *pubsub_create_memfd(const char *name, uint32_t capacity,
517             uint32_t mode, uint32_t msg_size,
518             char *errbuf) {
519 4 50         if (errbuf) errbuf[0] = '\0';
520              
521 4           uint32_t cap = pubsub_next_pow2(capacity);
522 4 50         if (cap == 0) { PUBSUB_ERR("invalid capacity"); return NULL; }
    0          
523 4 50         if (mode > PUBSUB_MODE_INT16) { PUBSUB_ERR("unknown mode %u", mode); return NULL; }
    0          
524              
525 4 100         if (mode == PUBSUB_MODE_STR && msg_size == 0)
    50          
526 1           msg_size = PUBSUB_DEFAULT_MSG_SIZE;
527              
528             uint64_t slots_off, data_off, arena_cap, total_size;
529 4 50         if (!pubsub_calc_layout(cap, mode, msg_size, &slots_off, &data_off, &arena_cap, &total_size)) {
530 0 0         PUBSUB_ERR("capacity * (msg_size+8) exceeds 4GB arena limit");
531 0           return NULL;
532             }
533              
534 4 50         int fd = memfd_create(name ? name : "pubsub", MFD_CLOEXEC | MFD_ALLOW_SEALING);
535 4 50         if (fd < 0) { PUBSUB_ERR("memfd_create: %s", strerror(errno)); return NULL; }
    0          
536              
537 4 50         if (ftruncate(fd, (off_t)total_size) < 0) {
538 0 0         PUBSUB_ERR("ftruncate(memfd): %s", strerror(errno));
539 0           close(fd); return NULL;
540             }
541 4           (void)fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW);
542              
543 4           void *base = mmap(NULL, (size_t)total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
544 4 50         if (base == MAP_FAILED) {
545 0 0         PUBSUB_ERR("mmap(memfd): %s", strerror(errno));
546 0           close(fd); return NULL;
547             }
548              
549 4           pubsub_init_header(base, mode, cap, total_size, slots_off, data_off,
550             msg_size, arena_cap);
551              
552 4           PubSubHandle *h = pubsub_init_handle(base, (size_t)total_size, mode, NULL);
553 4 50         if (!h) { munmap(base, (size_t)total_size); close(fd); return NULL; }
554 4           h->backing_fd = fd;
555 4           return h;
556             }
557              
558 4           static PubSubHandle *pubsub_open_fd(int fd, uint32_t mode, char *errbuf) {
559 4 50         if (errbuf) errbuf[0] = '\0';
560              
561             struct stat st;
562 4 50         if (fstat(fd, &st) < 0) {
563 0 0         PUBSUB_ERR("fstat(fd=%d): %s", fd, strerror(errno));
564 0           return NULL;
565             }
566              
567 4 50         if ((uint64_t)st.st_size < sizeof(PubSubHeader)) {
568 0 0         PUBSUB_ERR("fd %d: too small (%lld)", fd, (long long)st.st_size);
569 0           return NULL;
570             }
571              
572 4           size_t map_size = (size_t)st.st_size;
573 4           void *base = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
574 4 50         if (base == MAP_FAILED) {
575 0 0         PUBSUB_ERR("mmap(fd=%d): %s", fd, strerror(errno));
576 0           return NULL;
577             }
578              
579 4 50         if (!pubsub_validate_header((PubSubHeader *)base, mode, (uint64_t)st.st_size)) {
580 0 0         PUBSUB_ERR("fd %d: invalid or incompatible pubsub", fd);
581 0           munmap(base, map_size);
582 0           return NULL;
583             }
584              
585 4           int myfd = fcntl(fd, F_DUPFD_CLOEXEC, 0);
586 4 50         if (myfd < 0) {
587 0 0         PUBSUB_ERR("fcntl(F_DUPFD_CLOEXEC): %s", strerror(errno));
588 0           munmap(base, map_size);
589 0           return NULL;
590             }
591              
592 4           PubSubHandle *h = pubsub_init_handle(base, map_size, mode, NULL);
593 4 50         if (!h) { munmap(base, map_size); close(myfd); return NULL; }
594 4           h->backing_fd = myfd;
595 4           return h;
596             }
597              
598 149           static void pubsub_destroy(PubSubHandle *h) {
599 149 50         if (!h) return;
600 149 100         if (h->notify_fd >= 0) close(h->notify_fd);
601 149 100         if (h->backing_fd >= 0) close(h->backing_fd);
602 149 50         if (h->hdr) munmap(h->hdr, h->mmap_size);
603 149           free(h->path);
604 149           free(h);
605             }
606              
607             /* ================================================================
608             * Subscribe
609             * ================================================================ */
610              
611 311           static PubSubSub *pubsub_subscribe(PubSubHandle *h, int from_oldest) {
612 311           PubSubSub *sub = (PubSubSub *)calloc(1, sizeof(PubSubSub));
613 311 50         if (!sub) return NULL;
614              
615 311           sub->hdr = h->hdr;
616 311           sub->slots = h->slots;
617 311           sub->data = h->data;
618 311           sub->capacity = h->capacity;
619 311           sub->cap_mask = h->cap_mask;
620 311           sub->msg_size = h->msg_size;
621 311           sub->arena_cap = h->arena_cap;
622              
623 311           sub->notify_fd = h->notify_fd;
624              
625 311           uint64_t wp = __atomic_load_n(&h->hdr->write_pos, __ATOMIC_ACQUIRE);
626 311 100         if (from_oldest && wp > h->capacity)
    100          
627 1           sub->cursor = wp - h->capacity;
628 310 100         else if (from_oldest)
629 63           sub->cursor = 0;
630             else
631 247           sub->cursor = wp;
632              
633 311           return sub;
634             }
635              
636 311           static void pubsub_sub_destroy(PubSubSub *sub) {
637 311 50         if (!sub) return;
638 311           free(sub->copy_buf);
639 311           free(sub);
640             }
641              
642             /* ================================================================
643             * Common: lag (shared between Int and Str)
644             * ================================================================ */
645              
646 32           static inline uint64_t pubsub_lag(PubSubSub *sub) {
647 32           uint64_t wp = __atomic_load_n(&sub->hdr->write_pos, __ATOMIC_RELAXED);
648 32 100         return (wp > sub->cursor) ? (wp - sub->cursor) : 0;
649             }
650              
651             /* ================================================================
652             * Int publish/poll macro template
653             *
654             * DEFINE_INT_PUBSUB(prefix, SlotType, ValType, SeqType, DiffType)
655             * generates: pubsub__publish, _publish_multi, _poll, _poll_wait
656             * ================================================================ */
657              
658             #define DEFINE_INT_PUBSUB(PFX, SLOT, VTYPE, STYPE, DTYPE) \
659             \
660             static inline int pubsub_##PFX##_publish(PubSubHandle *h, VTYPE value) { \
661             PubSubHeader *hdr = h->hdr; \
662             SLOT *slots = (SLOT *)h->slots; \
663             uint64_t pos = __atomic_fetch_add(&hdr->write_pos, 1, __ATOMIC_RELAXED); \
664             uint32_t idx = pos & h->cap_mask; \
665             slots[idx].value = value; \
666             __atomic_store_n(&slots[idx].sequence, (STYPE)(pos + 1), __ATOMIC_RELEASE);\
667             __atomic_add_fetch(&hdr->stat_publish_ok, 1, __ATOMIC_RELAXED); \
668             pubsub_wake_subscribers(hdr); \
669             return 1; \
670             } \
671             \
672             static inline uint32_t pubsub_##PFX##_publish_multi(PubSubHandle *h, \
673             const VTYPE *values, uint32_t count) { \
674             PubSubHeader *hdr = h->hdr; \
675             SLOT *slots = (SLOT *)h->slots; \
676             uint32_t mask = h->cap_mask; \
677             uint64_t pos = __atomic_fetch_add(&hdr->write_pos, count, __ATOMIC_RELAXED);\
678             for (uint32_t i = 0; i < count; i++) { \
679             uint32_t idx = (pos + i) & mask; \
680             slots[idx].value = values[i]; \
681             __atomic_store_n(&slots[idx].sequence, \
682             (STYPE)(pos + i + 1), __ATOMIC_RELEASE); \
683             } \
684             __atomic_add_fetch(&hdr->stat_publish_ok, count, __ATOMIC_RELAXED); \
685             pubsub_wake_subscribers(hdr); \
686             return count; \
687             } \
688             \
689             static inline int pubsub_##PFX##_poll(PubSubSub *sub, VTYPE *value) { \
690             PubSubHeader *hdr = sub->hdr; \
691             SLOT *slots = (SLOT *)sub->slots; \
692             for (int attempt = 0; attempt < PUBSUB_POLL_RETRIES; attempt++) { \
693             uint64_t cursor = sub->cursor; \
694             uint64_t wp = __atomic_load_n(&hdr->write_pos, __ATOMIC_ACQUIRE); \
695             if (cursor >= wp) return 0; \
696             if (wp - cursor > sub->capacity) { \
697             sub->overflow_count += wp - cursor - sub->capacity; \
698             sub->cursor = wp - sub->capacity; \
699             continue; \
700             } \
701             uint32_t idx = cursor & sub->cap_mask; \
702             SLOT *slot = &slots[idx]; \
703             STYPE seq1 = __atomic_load_n(&slot->sequence, __ATOMIC_ACQUIRE); \
704             DTYPE diff = (DTYPE)seq1 - (DTYPE)(STYPE)(cursor + 1); \
705             if (diff != 0) { \
706             if (diff > 0) { \
707             uint64_t nc = wp > sub->capacity ? wp - sub->capacity : 0; \
708             if (nc > cursor) sub->overflow_count += nc - cursor; \
709             sub->cursor = nc; \
710             continue; \
711             } \
712             return 0; \
713             } \
714             VTYPE v = slot->value; \
715             STYPE seq2 = __atomic_load_n(&slot->sequence, __ATOMIC_ACQUIRE); \
716             if (seq2 != seq1) continue; \
717             *value = v; \
718             sub->cursor = cursor + 1; \
719             return 1; \
720             } \
721             return 0; \
722             } \
723             \
724             static int pubsub_##PFX##_poll_wait(PubSubSub *sub, VTYPE *value, \
725             double timeout) { \
726             int r = pubsub_##PFX##_poll(sub, value); \
727             if (r != 0) return r; \
728             if (timeout == 0.0) return 0; \
729             PubSubHeader *hdr = sub->hdr; \
730             struct timespec deadline, remaining; \
731             int has_deadline = (timeout > 0); \
732             if (has_deadline) pubsub_make_deadline(timeout, &deadline); \
733             for (;;) { \
734             /* Increment waiters BEFORE loading fseq/polling. SEQ_CST pairs \
735             * with publisher's SEQ_CST fence in pubsub_wake_subscribers so a \
736             * publisher that races our poll() either sees waiters > 0 and \
737             * wakes us, or publishes data we observe in the post-increment \
738             * poll. Without this ordering, we could sleep forever on an \
739             * unchanged fseq while data sits in the ring. */ \
740             __atomic_add_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST); \
741             uint32_t fseq = __atomic_load_n(&hdr->sub_futex, __ATOMIC_ACQUIRE); \
742             r = pubsub_##PFX##_poll(sub, value); \
743             if (r != 0) { \
744             __atomic_sub_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST); \
745             return r; \
746             } \
747             struct timespec *pts = NULL; \
748             if (has_deadline) { \
749             if (!pubsub_remaining_time(&deadline, &remaining)) { \
750             __atomic_sub_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST); \
751             return 0; \
752             } \
753             pts = &remaining; \
754             } \
755             long rc = syscall(SYS_futex, &hdr->sub_futex, FUTEX_WAIT, \
756             fseq, pts, NULL, 0); \
757             __atomic_sub_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST); \
758             r = pubsub_##PFX##_poll(sub, value); \
759             if (r != 0) return r; \
760             if (rc == -1 && errno == ETIMEDOUT) return 0; \
761             } \
762             }
763              
764             /* Instantiate for Int (64-bit seq + 64-bit value = 16 bytes/slot) */
765 1914 100         DEFINE_INT_PUBSUB(int, PubSubIntSlot, int64_t, uint64_t, int64_t)
  17 50          
  1216 50          
  212 50          
  469 50          
    50          
    100          
    50          
    50          
    100          
    100          
    50          
    0          
    0          
    0          
    50          
    50          
    100          
766              
767             /* Instantiate for Int32 (32-bit seq + 32-bit value = 8 bytes/slot) */
768 95 100         DEFINE_INT_PUBSUB(int32, PubSubInt32Slot, int32_t, uint32_t, int32_t)
  3 50          
  38 50          
  4 50          
  50 50          
    50          
    50          
    50          
    50          
    100          
    100          
    50          
    0          
    0          
    0          
    50          
    50          
    100          
769              
770             /* Instantiate for Int16 (32-bit seq + 16-bit value = 8 bytes/slot) */
771 94 100         DEFINE_INT_PUBSUB(int16, PubSubInt16Slot, int16_t, uint32_t, int32_t)
  3 50          
  38 50          
  4 50          
  49 50          
    50          
    50          
    50          
    50          
    100          
    100          
    50          
    0          
    0          
    0          
    50          
    50          
    100          
772              
773             /* ================================================================
774             * Str: mutex-protected publish, lock-free subscribe
775             *
776             * Variable-length messages: each slot owns a fixed dedicated arena region.
777             * The seqlock (sequence double-check) guarantees readers see consistent data.
778             * ================================================================ */
779              
780             /* Publish one Str message while mutex is already held (no lock/wake). */
781 404           static inline int pubsub_str_publish_locked(PubSubHandle *h, const char *str,
782             uint32_t len, bool utf8) {
783 404 50         if (len > PUBSUB_STR_LEN_MASK) return -1;
784 404 100         if (len > h->msg_size) return -1;
785              
786 403           PubSubHeader *hdr = h->hdr;
787 403           PubSubStrSlot *slots = (PubSubStrSlot *)h->slots;
788              
789 403           uint64_t pos = __atomic_load_n(&hdr->write_pos, __ATOMIC_RELAXED);
790 403           uint32_t idx = pos & h->cap_mask;
791 403           PubSubStrSlot *slot = &slots[idx];
792              
793 403           __atomic_store_n(&slot->sequence, 0, __ATOMIC_RELAXED);
794 403           __atomic_thread_fence(__ATOMIC_RELEASE);
795              
796 403           uint32_t alloc = (len + 7) & ~7u;
797 403 100         if (alloc == 0) alloc = 8;
798 403 50         if (alloc > h->arena_cap) return -1;
799              
800             /* Each slot owns a fixed, dedicated arena region
801             * [idx*stride, idx*stride + stride), with
802             * stride = arena_cap/capacity == msg_size+8 > round_up(len).
803             * A publish therefore writes only within its own slot's region and can
804             * never touch another slot's still-live bytes, so the per-slot seqlock
805             * fully guards every message. (The previous circular byte arena could, at
806             * small capacity with variable-length messages, wrap its write frontier
807             * onto a still-current slot's region WITHOUT bumping that slot's sequence,
808             * so a lagging subscriber read corrupted bytes the seqlock never caught.) */
809 403           uint32_t stride = (uint32_t)(h->arena_cap / h->capacity);
810 403           uint32_t apos = idx * stride;
811              
812             /* Bound the arena write position against the cached (open-time-validated)
813             * region size, using cached capacity/arena_cap (not live hdr->) for the
814             * stride: a peer that corrupted the shared header must not be able to make
815             * this publish memcpy past the mapped arena. Never taken for a valid
816             * layout (apos+alloc < arena_cap always). */
817 403 50         if ((uint64_t)apos + alloc > h->arena_cap) return -1;
818              
819 403           memcpy(h->data + apos, str, len);
820              
821 403           slot->arena_off = apos;
822 403 100         slot->packed_len = len | (utf8 ? PUBSUB_STR_UTF8_FLAG : 0);
823              
824 403           __atomic_store_n(&slot->sequence, pos + 1, __ATOMIC_RELEASE);
825 403           __atomic_store_n(&hdr->write_pos, pos + 1, __ATOMIC_RELAXED);
826 403           __atomic_add_fetch(&hdr->stat_publish_ok, 1, __ATOMIC_RELAXED);
827              
828 403           return 1;
829             }
830              
831 393           static inline int pubsub_str_publish(PubSubHandle *h, const char *str,
832             uint32_t len, bool utf8) {
833 393 100         if (len > h->msg_size) return -1;
834 391           pubsub_mutex_lock(h->hdr);
835 391           int r = pubsub_str_publish_locked(h, str, len, utf8);
836 391           pubsub_mutex_unlock(h->hdr);
837 391 50         if (r == 1) pubsub_wake_subscribers(h->hdr);
838 391           return r;
839             }
840              
841             /* Returns: 1 = success, 0 = empty/not-ready */
842 976           static inline int pubsub_str_poll(PubSubSub *sub, const char **out_str,
843             uint32_t *out_len, bool *out_utf8) {
844 976           PubSubHeader *hdr = sub->hdr;
845 976           PubSubStrSlot *slots = (PubSubStrSlot *)sub->slots;
846              
847 978 50         for (int attempt = 0; attempt < PUBSUB_POLL_RETRIES; attempt++) {
848 978           uint64_t cursor = sub->cursor;
849 978           uint64_t wp = __atomic_load_n(&hdr->write_pos, __ATOMIC_ACQUIRE);
850              
851 978 100         if (cursor >= wp) return 0;
852              
853 960 100         if (wp - cursor > sub->capacity) {
854 2           sub->overflow_count += wp - cursor - sub->capacity;
855 2           sub->cursor = wp - sub->capacity;
856 2           continue;
857             }
858              
859 958           uint32_t idx = cursor & sub->cap_mask;
860 958           PubSubStrSlot *slot = &slots[idx];
861              
862 958           uint64_t seq1 = __atomic_load_n(&slot->sequence, __ATOMIC_ACQUIRE);
863 958 50         if (seq1 != cursor + 1) {
864 0 0         if (seq1 > cursor + 1) {
865 0 0         uint64_t new_cursor = wp > sub->capacity ? wp - sub->capacity : 0;
866 0 0         if (new_cursor > cursor)
867 0           sub->overflow_count += new_cursor - cursor;
868 0           sub->cursor = new_cursor;
869 0           continue;
870             }
871 0           return 0;
872             }
873              
874 958           uint32_t plen = slot->packed_len;
875 958           uint32_t aoff = slot->arena_off;
876 958           uint32_t len = plen & PUBSUB_STR_LEN_MASK;
877 958           bool utf8 = (plen & PUBSUB_STR_UTF8_FLAG) != 0;
878              
879             /* Safety: bound the file-stored length and arena offset against the
880             * subscriber's cached (open-time-validated) msg_size / arena_cap
881             * before dereferencing, so a peer that corrupted the shared header
882             * or slot metadata cannot drive this memcpy past the mapped arena.
883             * Cached (not live hdr->) values: a concurrent header corruption
884             * must not be able to widen the bound. Never taken for valid data. */
885 958 50         if (len > sub->msg_size) continue;
886 958 50         if ((uint64_t)aoff + len > sub->arena_cap) continue;
887              
888 958 50         if (!pubsub_ensure_copy_buf(sub, len + 1)) return 0;
889              
890 958 100         if (len > 0)
891 955           memcpy(sub->copy_buf, sub->data + aoff, len);
892 958           sub->copy_buf[len] = '\0';
893              
894 958           uint64_t seq2 = __atomic_load_n(&slot->sequence, __ATOMIC_ACQUIRE);
895 958 50         if (seq2 != seq1) continue;
896              
897 958           *out_str = sub->copy_buf;
898 958           *out_len = len;
899 958           *out_utf8 = utf8;
900 958           sub->cursor = cursor + 1;
901 958           return 1;
902             }
903 0           return 0;
904             }
905              
906 55           static int pubsub_str_poll_wait(PubSubSub *sub, const char **out_str,
907             uint32_t *out_len, bool *out_utf8,
908             double timeout) {
909 55           int r = pubsub_str_poll(sub, out_str, out_len, out_utf8);
910 55 100         if (r != 0) return r;
911 3 50         if (timeout == 0.0) return 0;
912              
913 3           PubSubHeader *hdr = sub->hdr;
914             struct timespec deadline, remaining;
915 3           int has_deadline = (timeout > 0);
916 3 50         if (has_deadline) pubsub_make_deadline(timeout, &deadline);
917              
918 0           for (;;) {
919             /* See pubsub_int_poll_wait above for the SEQ_CST ordering rationale. */
920 3           __atomic_add_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST);
921 3           uint32_t fseq = __atomic_load_n(&hdr->sub_futex, __ATOMIC_ACQUIRE);
922 3           r = pubsub_str_poll(sub, out_str, out_len, out_utf8);
923 3 50         if (r != 0) {
924 0           __atomic_sub_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST);
925 0           return r;
926             }
927              
928 3           struct timespec *pts = NULL;
929 3 50         if (has_deadline) {
930 3 50         if (!pubsub_remaining_time(&deadline, &remaining)) {
931 0           __atomic_sub_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST);
932 0           return 0;
933             }
934 3           pts = &remaining;
935             }
936 3           long rc = syscall(SYS_futex, &hdr->sub_futex, FUTEX_WAIT,
937             fseq, pts, NULL, 0);
938 3           __atomic_sub_fetch(&hdr->sub_waiters, 1, __ATOMIC_SEQ_CST);
939              
940 3           r = pubsub_str_poll(sub, out_str, out_len, out_utf8);
941 3 100         if (r != 0) return r;
942 1 50         if (rc == -1 && errno == ETIMEDOUT) return 0;
    50          
943             }
944             }
945              
946             /* ================================================================
947             * Common operations
948             * ================================================================ */
949              
950 8           static void pubsub_clear(PubSubHandle *h) {
951 8           PubSubHeader *hdr = h->hdr;
952 8 100         if (h->mode == PUBSUB_MODE_STR)
953 2           pubsub_mutex_lock(hdr);
954              
955 8           __atomic_store_n(&hdr->write_pos, 0, __ATOMIC_RELAXED);
956 8           __atomic_store_n(&hdr->stat_publish_ok, 0, __ATOMIC_RELAXED);
957 8 100         if (h->mode == PUBSUB_MODE_STR)
958 2           __atomic_store_n(&hdr->arena_wpos, 0, __ATOMIC_RELAXED);
959              
960             /* Zero all slot sequences */
961 8           uint32_t cap = h->capacity;
962 8 100         if (h->mode == PUBSUB_MODE_INT) {
963 4           PubSubIntSlot *s = (PubSubIntSlot *)h->slots;
964 260 100         for (uint32_t i = 0; i < cap; i++)
965 256           __atomic_store_n(&s[i].sequence, 0, __ATOMIC_RELAXED);
966 4 100         } else if (h->mode == PUBSUB_MODE_INT32) {
967 1           PubSubInt32Slot *s = (PubSubInt32Slot *)h->slots;
968 65 100         for (uint32_t i = 0; i < cap; i++)
969 64           __atomic_store_n(&s[i].sequence, 0, __ATOMIC_RELAXED);
970 3 100         } else if (h->mode == PUBSUB_MODE_INT16) {
971 1           PubSubInt16Slot *s = (PubSubInt16Slot *)h->slots;
972 65 100         for (uint32_t i = 0; i < cap; i++)
973 64           __atomic_store_n(&s[i].sequence, 0, __ATOMIC_RELAXED);
974             } else {
975 2           PubSubStrSlot *s = (PubSubStrSlot *)h->slots;
976 82 100         for (uint32_t i = 0; i < cap; i++)
977 80           __atomic_store_n(&s[i].sequence, 0, __ATOMIC_RELAXED);
978             }
979              
980 8 100         if (h->mode == PUBSUB_MODE_STR)
981 2           pubsub_mutex_unlock(hdr);
982 8           pubsub_wake_subscribers(hdr);
983 8           }
984              
985 7           static inline int pubsub_sync(PubSubHandle *h) {
986 7           return msync(h->hdr, h->mmap_size, MS_SYNC);
987             }
988              
989 13           static inline int pubsub_eventfd_create(PubSubHandle *h) {
990 13 50         if (h->notify_fd >= 0) return h->notify_fd;
991 13           h->notify_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
992 13           return h->notify_fd;
993             }
994              
995 1           static inline void pubsub_eventfd_set(PubSubHandle *h, int fd) {
996 1 50         if (h->notify_fd >= 0 && h->notify_fd != fd)
    50          
997 0           close(h->notify_fd);
998 1           h->notify_fd = fd;
999 1           }
1000              
1001 8           static inline void pubsub_notify(PubSubHandle *h) {
1002 8 50         if (h->notify_fd >= 0) {
1003 8           uint64_t one = 1;
1004 8           ssize_t __attribute__((unused)) rc = write(h->notify_fd, &one, sizeof(one));
1005             }
1006 8           }
1007              
1008 6           static inline int64_t pubsub_eventfd_consume(PubSubHandle *h) {
1009 6 50         if (h->notify_fd < 0) return -1;
1010 6           uint64_t val = 0;
1011 6 50         if (read(h->notify_fd, &val, sizeof(val)) != sizeof(val)) return -1;
1012 6           return (int64_t)val;
1013             }
1014              
1015 4           static inline void pubsub_sub_eventfd_consume(PubSubSub *sub) {
1016 4 50         if (sub->notify_fd >= 0) {
1017             uint64_t val;
1018 4           ssize_t __attribute__((unused)) rc = read(sub->notify_fd, &val, sizeof(val));
1019             }
1020 4           }
1021              
1022             #endif /* PUBSUB_H */