io-event 1.22.0 → 1.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,489 @@
1
+ // Released under the MIT License.
2
+ // Copyright, 2026, by Samuel Williams.
3
+
4
+ #include "futex.h"
5
+
6
+ #ifdef IO_EVENT_FUTEX
7
+
8
+ #include <errno.h>
9
+ #include <stdbool.h>
10
+ #include <sys/syscall.h>
11
+ #include <time.h>
12
+ #include <unistd.h>
13
+
14
+ #include <ruby/fiber/scheduler.h>
15
+ #include <ruby/io/buffer.h>
16
+ #include <ruby/thread.h>
17
+
18
+ // The finalizer owns the allocation lock independently of the Futex. It must
19
+ // never retain the Futex or its C struct: either may be reclaimed before the
20
+ // finalizer runs. Keeping the buffer here preserves it until it is unlocked.
21
+ struct IO_Event_Futex_Finalizer {
22
+ VALUE buffer;
23
+ uint32_t *address;
24
+ size_t waits;
25
+ };
26
+
27
+ static VALUE IO_Event_Futex_Finalizer_Class;
28
+
29
+ static void IO_Event_Futex_Finalizer_mark(void *_finalizer) {
30
+ struct IO_Event_Futex_Finalizer *finalizer = _finalizer;
31
+ rb_gc_mark_movable(finalizer->buffer);
32
+ }
33
+
34
+ static void IO_Event_Futex_Finalizer_compact(void *_finalizer) {
35
+ struct IO_Event_Futex_Finalizer *finalizer = _finalizer;
36
+ finalizer->buffer = rb_gc_location(finalizer->buffer);
37
+ }
38
+
39
+ static size_t IO_Event_Futex_Finalizer_size(const void *_finalizer) {
40
+ return sizeof(struct IO_Event_Futex_Finalizer);
41
+ }
42
+
43
+ static const rb_data_type_t IO_Event_Futex_Finalizer_Type = {
44
+ .wrap_struct_name = "IO::Event::Futex::Finalizer",
45
+ .function = {
46
+ .dmark = IO_Event_Futex_Finalizer_mark,
47
+ .dcompact = IO_Event_Futex_Finalizer_compact,
48
+ .dfree = RUBY_TYPED_DEFAULT_FREE,
49
+ .dsize = IO_Event_Futex_Finalizer_size,
50
+ },
51
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
52
+ };
53
+
54
+ static VALUE IO_Event_Futex_Finalizer_release(VALUE self) {
55
+ struct IO_Event_Futex_Finalizer *finalizer = NULL;
56
+ TypedData_Get_Struct(self, struct IO_Event_Futex_Finalizer, &IO_Event_Futex_Finalizer_Type, finalizer);
57
+ if (!NIL_P(finalizer->buffer)) {
58
+ rb_io_buffer_unlock(finalizer->buffer);
59
+ RB_OBJ_WRITE(self, &finalizer->buffer, Qnil);
60
+ finalizer->address = NULL;
61
+ }
62
+ return Qnil;
63
+ }
64
+
65
+ static VALUE IO_Event_Futex_Finalizer_call(VALUE self, VALUE object_id) {
66
+ struct IO_Event_Futex_Finalizer *finalizer = NULL;
67
+ TypedData_Get_Struct(self, struct IO_Event_Futex_Finalizer, &IO_Event_Futex_Finalizer_Type, finalizer);
68
+ // Ruby also invokes finalizers at shutdown, even for reachable objects.
69
+ // Never unlock memory still used by an outstanding kernel operation.
70
+ if (finalizer->waits) return Qnil;
71
+ return IO_Event_Futex_Finalizer_release(self);
72
+ }
73
+
74
+ struct IO_Event_Futex {
75
+ VALUE finalizer;
76
+ };
77
+
78
+ static void IO_Event_Futex_mark(void *_futex) {
79
+ struct IO_Event_Futex *futex = _futex;
80
+ rb_gc_mark_movable(futex->finalizer);
81
+ }
82
+
83
+ static void IO_Event_Futex_compact(void *_futex) {
84
+ struct IO_Event_Futex *futex = _futex;
85
+ futex->finalizer = rb_gc_location(futex->finalizer);
86
+ }
87
+
88
+ static size_t IO_Event_Futex_size(const void *_futex) {
89
+ return sizeof(struct IO_Event_Futex);
90
+ }
91
+
92
+ static const rb_data_type_t IO_Event_Futex_Type = {
93
+ .wrap_struct_name = "IO::Event::Futex",
94
+ .function = {
95
+ .dmark = IO_Event_Futex_mark,
96
+ .dcompact = IO_Event_Futex_compact,
97
+ .dfree = RUBY_TYPED_DEFAULT_FREE,
98
+ .dsize = IO_Event_Futex_size,
99
+ },
100
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
101
+ };
102
+
103
+ static VALUE IO_Event_Futex_allocate(VALUE klass) {
104
+ struct IO_Event_Futex *futex = NULL;
105
+ VALUE instance = TypedData_Make_Struct(klass, struct IO_Event_Futex, &IO_Event_Futex_Type, futex);
106
+ futex->finalizer = Qnil;
107
+ return instance;
108
+ }
109
+
110
+ static ID id_offset;
111
+ static ID id_futex_wait;
112
+ static ID id_futex_waitv;
113
+
114
+ static VALUE IO_Event_Futex_initialize(int argc, VALUE *argv, VALUE self) {
115
+ rb_check_frozen(self);
116
+ struct IO_Event_Futex *futex = NULL;
117
+ TypedData_Get_Struct(self, struct IO_Event_Futex, &IO_Event_Futex_Type, futex);
118
+ if (!NIL_P(futex->finalizer)) rb_raise(rb_eRuntimeError, "Futex is already initialized!");
119
+
120
+ VALUE buffer, options;
121
+ rb_scan_args(argc, argv, "1:", &buffer, &options);
122
+
123
+ VALUE offset_value = Qundef;
124
+ if (!NIL_P(options)) {
125
+ ID keys[] = {id_offset};
126
+ VALUE values[1];
127
+ rb_get_kwargs(options, keys, 0, 1, values);
128
+ offset_value = values[0];
129
+ }
130
+
131
+ size_t offset = offset_value == Qundef ? 0 : NUM2SIZET(offset_value);
132
+ // Numeric coercion may have invoked initialize recursively.
133
+ if (!NIL_P(futex->finalizer)) rb_raise(rb_eRuntimeError, "Futex is already initialized!");
134
+
135
+ // Register cleanup before acquiring the lock. All allocating operations are
136
+ // performed before extracting the pointer; failures leave an inert finalizer.
137
+ struct IO_Event_Futex_Finalizer *finalizer = NULL;
138
+ VALUE finalizer_value = TypedData_Make_Struct(IO_Event_Futex_Finalizer_Class, struct IO_Event_Futex_Finalizer, &IO_Event_Futex_Finalizer_Type, finalizer);
139
+ finalizer->buffer = Qnil;
140
+ finalizer->address = NULL;
141
+ finalizer->waits = 0;
142
+ rb_define_finalizer(self, finalizer_value);
143
+
144
+ void *base = NULL;
145
+ size_t size = 0;
146
+ rb_io_buffer_get_bytes_for_writing(buffer, &base, &size);
147
+
148
+ if (offset > size || size - offset < sizeof(uint32_t)) {
149
+ rb_raise(rb_eRangeError, "Futex offset exceeds the buffer size!");
150
+ }
151
+
152
+ uint32_t *address = (uint32_t *)((char *)base + offset);
153
+ if ((uintptr_t)address % sizeof(uint32_t) != 0) {
154
+ rb_raise(rb_eArgError, "Futex address must be aligned to 4 bytes!");
155
+ }
156
+
157
+ // Nothing after a successful lock can raise or invoke Ruby code.
158
+ rb_io_buffer_lock(buffer);
159
+ RB_OBJ_WRITE(finalizer_value, &finalizer->buffer, buffer);
160
+ finalizer->address = address;
161
+ RB_OBJ_WRITE(self, &futex->finalizer, finalizer_value);
162
+
163
+ return self;
164
+ }
165
+
166
+ static VALUE IO_Event_Futex_value(VALUE self) {
167
+ return UINT2NUM(__atomic_load_n(IO_Event_Futex_address(self), __ATOMIC_ACQUIRE));
168
+ }
169
+
170
+ static VALUE IO_Event_Futex_set_value(VALUE self, VALUE value) {
171
+ uint32_t converted = NUM2UINT(value);
172
+ __atomic_store_n(IO_Event_Futex_address(self), converted, __ATOMIC_RELEASE);
173
+ return value;
174
+ }
175
+
176
+ static VALUE IO_Event_Futex_increment(int argc, VALUE *argv, VALUE self) {
177
+ VALUE amount_value;
178
+ rb_scan_args(argc, argv, "01", &amount_value);
179
+ uint32_t amount = NIL_P(amount_value) ? 1 : NUM2UINT(amount_value);
180
+
181
+ uint32_t value = __atomic_add_fetch(IO_Event_Futex_address(self), amount, __ATOMIC_ACQ_REL);
182
+ return UINT2NUM(value);
183
+ }
184
+
185
+ static VALUE IO_Event_Futex_decrement(int argc, VALUE *argv, VALUE self) {
186
+ VALUE amount_value;
187
+ rb_scan_args(argc, argv, "01", &amount_value);
188
+ uint32_t amount = NIL_P(amount_value) ? 1 : NUM2UINT(amount_value);
189
+
190
+ uint32_t value = __atomic_sub_fetch(IO_Event_Futex_address(self), amount, __ATOMIC_ACQ_REL);
191
+ return UINT2NUM(value);
192
+ }
193
+
194
+ static VALUE IO_Event_Futex_compare_exchange(VALUE self, VALUE expected_value, VALUE desired_value) {
195
+ uint32_t expected = NUM2UINT(expected_value);
196
+ uint32_t desired = NUM2UINT(desired_value);
197
+
198
+ bool exchanged = __atomic_compare_exchange_n(
199
+ IO_Event_Futex_address(self),
200
+ &expected,
201
+ desired,
202
+ false,
203
+ __ATOMIC_ACQ_REL,
204
+ __ATOMIC_ACQUIRE
205
+ );
206
+
207
+ return exchanged ? Qtrue : Qfalse;
208
+ }
209
+
210
+ static VALUE IO_Event_Futex_wake(int argc, VALUE *argv, VALUE self) {
211
+ VALUE count_value;
212
+ rb_scan_args(argc, argv, "01", &count_value);
213
+ int count = NIL_P(count_value) ? 1 : NUM2INT(count_value);
214
+ if (count < 0) rb_raise(rb_eArgError, "Wake count must be non-negative!");
215
+
216
+ uint32_t *address = IO_Event_Futex_address(self);
217
+ // Legacy FUTEX_WAKE can wake one waiter even when count is zero.
218
+ if (count == 0) return INT2NUM(0);
219
+
220
+ int result = syscall(SYS_futex, address, FUTEX_WAKE, count, NULL, NULL, 0);
221
+ if (result < 0) rb_sys_fail("IO_Event_Futex_wake:futex");
222
+ return INT2NUM(result);
223
+ }
224
+
225
+ static VALUE IO_Event_Futex_signal(int argc, VALUE *argv, VALUE self) {
226
+ VALUE count_value;
227
+ rb_scan_args(argc, argv, "01", &count_value);
228
+
229
+ int count = NIL_P(count_value) ? 1 : NUM2INT(count_value);
230
+ if (count < 0) rb_raise(rb_eArgError, "Wake count must be non-negative!");
231
+ uint32_t *address = IO_Event_Futex_address(self);
232
+ // A zero-count signal neither changes the word nor wakes any waiters.
233
+ if (count == 0) return UINT2NUM(__atomic_load_n(address, __ATOMIC_ACQUIRE));
234
+
235
+ uint32_t value = __atomic_add_fetch(address, 1, __ATOMIC_ACQ_REL);
236
+ int result = syscall(SYS_futex, address, FUTEX_WAKE, count, NULL, NULL, 0);
237
+ if (result < 0) rb_sys_fail("IO_Event_Futex_signal:futex");
238
+ return UINT2NUM(value);
239
+ }
240
+
241
+ static struct IO_Event_Futex_Finalizer *IO_Event_Futex_get(VALUE self) {
242
+ struct IO_Event_Futex *futex = NULL;
243
+ TypedData_Get_Struct(self, struct IO_Event_Futex, &IO_Event_Futex_Type, futex);
244
+ if (NIL_P(futex->finalizer)) rb_raise(rb_eIOError, "Futex is uninitialized!");
245
+ struct IO_Event_Futex_Finalizer *finalizer = NULL;
246
+ TypedData_Get_Struct(futex->finalizer, struct IO_Event_Futex_Finalizer, &IO_Event_Futex_Finalizer_Type, finalizer);
247
+ if (!finalizer->address) rb_raise(rb_eIOError, "Futex is closed!");
248
+ return finalizer;
249
+ }
250
+
251
+ uint32_t *IO_Event_Futex_address(VALUE self) {
252
+ return IO_Event_Futex_get(self)->address;
253
+ }
254
+
255
+ uint32_t *IO_Event_Futex_acquire(VALUE self) {
256
+ struct IO_Event_Futex_Finalizer *finalizer = IO_Event_Futex_get(self);
257
+ if (finalizer->waits == SIZE_MAX) rb_raise(rb_eRuntimeError, "Too many futex waits!");
258
+ finalizer->waits += 1;
259
+ return finalizer->address;
260
+ }
261
+
262
+ void IO_Event_Futex_release(VALUE self) {
263
+ struct IO_Event_Futex_Finalizer *finalizer = IO_Event_Futex_get(self);
264
+ if (!finalizer->waits) rb_bug("IO_Event_Futex_release: no pending waits");
265
+ finalizer->waits -= 1;
266
+ }
267
+
268
+ static VALUE IO_Event_Futex_close(VALUE self) {
269
+ struct IO_Event_Futex *futex = NULL;
270
+ TypedData_Get_Struct(self, struct IO_Event_Futex, &IO_Event_Futex_Type, futex);
271
+ if (NIL_P(futex->finalizer)) return Qnil;
272
+ struct IO_Event_Futex_Finalizer *finalizer = NULL;
273
+ TypedData_Get_Struct(futex->finalizer, struct IO_Event_Futex_Finalizer, &IO_Event_Futex_Finalizer_Type, finalizer);
274
+ if (finalizer->waits) rb_raise(rb_eIOError, "Cannot close a futex with pending waits!");
275
+ return IO_Event_Futex_Finalizer_release(futex->finalizer);
276
+ }
277
+
278
+ static VALUE IO_Event_Futex_closed_p(VALUE self) {
279
+ struct IO_Event_Futex *futex = NULL;
280
+ TypedData_Get_Struct(self, struct IO_Event_Futex, &IO_Event_Futex_Type, futex);
281
+ if (NIL_P(futex->finalizer)) return Qtrue;
282
+ struct IO_Event_Futex_Finalizer *finalizer = NULL;
283
+ TypedData_Get_Struct(futex->finalizer, struct IO_Event_Futex_Finalizer, &IO_Event_Futex_Finalizer_Type, finalizer);
284
+ return finalizer->address ? Qfalse : Qtrue;
285
+ }
286
+
287
+ static VALUE IO_Event_Futex_initialize_copy(VALUE self, VALUE other) {
288
+ struct IO_Event_Futex *futex = NULL;
289
+ TypedData_Get_Struct(self, struct IO_Event_Futex, &IO_Event_Futex_Type, futex);
290
+ // Ruby copies finalizers before invoking initialize_copy. The rejected copy
291
+ // must not release the original's allocation lock when it is collected.
292
+ if (NIL_P(futex->finalizer)) rb_undefine_finalizer(self);
293
+ rb_raise(rb_eTypeError, "Cannot copy a futex!");
294
+ }
295
+
296
+ struct IO_Event_Futex_BlockingWait {
297
+ VALUE futex;
298
+ uint32_t *address;
299
+ uint32_t expected;
300
+ int result;
301
+ int error;
302
+ };
303
+
304
+ static void *IO_Event_Futex_blocking_wait_without_gvl(void *_arguments) {
305
+ struct IO_Event_Futex_BlockingWait *arguments = _arguments;
306
+ arguments->result = syscall(SYS_futex, arguments->address, FUTEX_WAIT, arguments->expected, NULL, NULL, 0);
307
+ arguments->error = arguments->result < 0 ? errno : 0;
308
+ return NULL;
309
+ }
310
+
311
+ static VALUE IO_Event_Futex_blocking_wait_body(VALUE _arguments) {
312
+ struct IO_Event_Futex_BlockingWait *arguments = (void *)_arguments;
313
+ arguments->address = IO_Event_Futex_acquire(arguments->futex);
314
+ rb_thread_call_without_gvl(IO_Event_Futex_blocking_wait_without_gvl, arguments, RUBY_UBF_IO, 0);
315
+
316
+ if (arguments->result == 0) {
317
+ return Qtrue;
318
+ } else if (arguments->error == EAGAIN) {
319
+ return Qfalse;
320
+ } else {
321
+ rb_syserr_fail(arguments->error, "IO_Event_Futex_blocking_wait:futex");
322
+ }
323
+
324
+ return Qfalse;
325
+ }
326
+
327
+ static VALUE IO_Event_Futex_blocking_wait_ensure(VALUE _arguments) {
328
+ struct IO_Event_Futex_BlockingWait *arguments = (void *)_arguments;
329
+ if (arguments->address) IO_Event_Futex_release(arguments->futex);
330
+ return Qnil;
331
+ }
332
+
333
+ static VALUE IO_Event_Futex_blocking_wait(VALUE self, VALUE expected_value) {
334
+ struct IO_Event_Futex_BlockingWait arguments = {
335
+ .futex = self,
336
+ .expected = NUM2UINT(expected_value),
337
+ };
338
+ VALUE result = rb_ensure(IO_Event_Futex_blocking_wait_body, (VALUE)&arguments, IO_Event_Futex_blocking_wait_ensure, (VALUE)&arguments);
339
+ RB_GC_GUARD(self);
340
+ return result;
341
+ }
342
+
343
+ #ifdef IO_EVENT_FUTEX_WAITV
344
+
345
+ // Coerce all arguments before retaining native addresses. The returned array
346
+ // owns a private snapshot of the futex references: mutating the caller's entries
347
+ // while a wait is pending must not make its futexes collectible.
348
+ VALUE IO_Event_Futex_prepare_waitv(VALUE entries, struct futex_waitv *vector) {
349
+ entries = rb_ary_dup(rb_Array(entries));
350
+ long count = RARRAY_LEN(entries);
351
+ if (count < 1 || count > FUTEX_WAITV_MAX) {
352
+ rb_raise(rb_eArgError, "Futex vector must contain between 1 and %d entries!", FUTEX_WAITV_MAX);
353
+ }
354
+ VALUE futexes = rb_ary_new_capa(count);
355
+ for (long index = 0; index < count; index++) {
356
+ VALUE entry = rb_Array(RARRAY_AREF(entries, index));
357
+ if (RARRAY_LEN(entry) != 2) {
358
+ rb_raise(rb_eArgError, "Each futex vector entry must contain a futex and its expected value!");
359
+ }
360
+ rb_ary_push(futexes, RARRAY_AREF(entry, 0));
361
+ vector[index].val = NUM2UINT(RARRAY_AREF(entry, 1));
362
+ vector[index].flags = FUTEX_32;
363
+ vector[index].__reserved = 0;
364
+ }
365
+ return futexes;
366
+ }
367
+
368
+ struct IO_Event_Futex_BlockingWaitV {
369
+ VALUE futexes;
370
+ struct futex_waitv *vector;
371
+ size_t count;
372
+ size_t acquired;
373
+ int result;
374
+ int error;
375
+ };
376
+
377
+ static void *IO_Event_Futex_blocking_waitv_without_gvl(void *_arguments) {
378
+ struct IO_Event_Futex_BlockingWaitV *arguments = _arguments;
379
+ arguments->result = syscall(SYS_futex_waitv, arguments->vector, arguments->count, 0, NULL, CLOCK_MONOTONIC);
380
+ arguments->error = arguments->result < 0 ? errno : 0;
381
+ return NULL;
382
+ }
383
+
384
+ static VALUE IO_Event_Futex_blocking_waitv_body(VALUE _arguments) {
385
+ struct IO_Event_Futex_BlockingWaitV *arguments = (void *)_arguments;
386
+ while (arguments->acquired < arguments->count) {
387
+ size_t index = arguments->acquired;
388
+ arguments->vector[index].uaddr = (uintptr_t)IO_Event_Futex_acquire(RARRAY_AREF(arguments->futexes, index));
389
+ arguments->acquired += 1;
390
+ }
391
+ rb_thread_call_without_gvl(IO_Event_Futex_blocking_waitv_without_gvl, arguments, RUBY_UBF_IO, 0);
392
+
393
+ if (arguments->result >= 0) {
394
+ return INT2NUM(arguments->result);
395
+ } else if (arguments->error == EAGAIN) {
396
+ return Qnil;
397
+ } else {
398
+ rb_syserr_fail(arguments->error, "IO_Event_Futex_blocking_waitv:futex_waitv");
399
+ }
400
+
401
+ return Qnil;
402
+ }
403
+
404
+ static VALUE IO_Event_Futex_blocking_waitv_ensure(VALUE _arguments) {
405
+ struct IO_Event_Futex_BlockingWaitV *arguments = (void *)_arguments;
406
+ while (arguments->acquired) {
407
+ IO_Event_Futex_release(RARRAY_AREF(arguments->futexes, --arguments->acquired));
408
+ }
409
+ return Qnil;
410
+ }
411
+
412
+ static VALUE IO_Event_Futex_blocking_waitv(VALUE entries) {
413
+ struct futex_waitv vector[FUTEX_WAITV_MAX];
414
+ VALUE futexes = IO_Event_Futex_prepare_waitv(entries, vector);
415
+ struct IO_Event_Futex_BlockingWaitV arguments = {
416
+ .futexes = futexes,
417
+ .vector = vector,
418
+ .count = RARRAY_LEN(futexes),
419
+ };
420
+ VALUE result = rb_ensure(IO_Event_Futex_blocking_waitv_body, (VALUE)&arguments, IO_Event_Futex_blocking_waitv_ensure, (VALUE)&arguments);
421
+ RB_GC_GUARD(futexes);
422
+ return result;
423
+ }
424
+
425
+ #endif
426
+
427
+ static VALUE IO_Event_Futex_wait(VALUE self, VALUE expected_value) {
428
+ VALUE scheduler = rb_fiber_scheduler_current();
429
+ if (NIL_P(scheduler)) {
430
+ return IO_Event_Futex_blocking_wait(self, expected_value);
431
+ }
432
+
433
+ if (!rb_respond_to(scheduler, id_futex_wait)) {
434
+ rb_raise(rb_eNotImpError, "The current fiber scheduler does not support futex waits!");
435
+ }
436
+
437
+ return rb_funcall(scheduler, id_futex_wait, 2, self, expected_value);
438
+ }
439
+
440
+ #ifdef IO_EVENT_FUTEX_WAITV
441
+
442
+ static VALUE IO_Event_Futex_wait_any(VALUE klass, VALUE entries) {
443
+ (void)klass;
444
+ VALUE scheduler = rb_fiber_scheduler_current();
445
+ if (NIL_P(scheduler)) {
446
+ return IO_Event_Futex_blocking_waitv(entries);
447
+ }
448
+
449
+ if (!rb_respond_to(scheduler, id_futex_waitv)) {
450
+ rb_raise(rb_eNotImpError, "The current fiber scheduler does not support vector futex waits!");
451
+ }
452
+
453
+ return rb_funcall(scheduler, id_futex_waitv, 1, entries);
454
+ }
455
+
456
+ #endif
457
+
458
+ void Init_IO_Event_Futex(VALUE IO_Event) {
459
+ IO_Event_Futex_Finalizer_Class = rb_class_new(rb_cObject);
460
+ rb_gc_register_mark_object(IO_Event_Futex_Finalizer_Class);
461
+ rb_undef_alloc_func(IO_Event_Futex_Finalizer_Class);
462
+ rb_define_method(IO_Event_Futex_Finalizer_Class, "call", IO_Event_Futex_Finalizer_call, 1);
463
+
464
+ VALUE IO_Event_Futex = rb_define_class_under(IO_Event, "Futex", rb_cObject);
465
+ rb_define_alloc_func(IO_Event_Futex, IO_Event_Futex_allocate);
466
+ rb_define_method(IO_Event_Futex, "initialize", IO_Event_Futex_initialize, -1);
467
+ rb_define_method(IO_Event_Futex, "initialize_copy", IO_Event_Futex_initialize_copy, 1);
468
+ rb_define_method(IO_Event_Futex, "close", IO_Event_Futex_close, 0);
469
+ rb_define_method(IO_Event_Futex, "closed?", IO_Event_Futex_closed_p, 0);
470
+ rb_define_method(IO_Event_Futex, "value", IO_Event_Futex_value, 0);
471
+ rb_define_method(IO_Event_Futex, "value=", IO_Event_Futex_set_value, 1);
472
+ rb_define_method(IO_Event_Futex, "increment", IO_Event_Futex_increment, -1);
473
+ rb_define_method(IO_Event_Futex, "decrement", IO_Event_Futex_decrement, -1);
474
+ rb_define_method(IO_Event_Futex, "compare_exchange", IO_Event_Futex_compare_exchange, 2);
475
+ rb_define_method(IO_Event_Futex, "wake", IO_Event_Futex_wake, -1);
476
+ rb_define_method(IO_Event_Futex, "signal", IO_Event_Futex_signal, -1);
477
+ rb_define_method(IO_Event_Futex, "wait", IO_Event_Futex_wait, 1);
478
+
479
+ #ifdef IO_EVENT_FUTEX_WAITV
480
+ rb_define_const(IO_Event_Futex, "WAITV_LIMIT", INT2NUM(FUTEX_WAITV_MAX));
481
+ rb_define_singleton_method(IO_Event_Futex, "wait_any", IO_Event_Futex_wait_any, 1);
482
+ #endif
483
+
484
+ id_offset = rb_intern("offset");
485
+ id_futex_wait = rb_intern("futex_wait");
486
+ id_futex_waitv = rb_intern("futex_waitv");
487
+ }
488
+
489
+ #endif
@@ -0,0 +1,42 @@
1
+ // Released under the MIT License.
2
+ // Copyright, 2026, by Samuel Williams.
3
+
4
+ #pragma once
5
+
6
+ #include <ruby.h>
7
+
8
+ #ifdef HAVE_RUBY_IO_BUFFER_H
9
+ #include <ruby/io/buffer.h>
10
+ #endif
11
+
12
+ // Version 3 (Ruby 4.1) provides counted allocation locks, allowing independent
13
+ // futex words to retain the same buffer without unlocking each other.
14
+ #if defined(__linux__) && RUBY_IO_BUFFER_VERSION >= 3 && defined(HAVE_LINUX_FUTEX_H) && defined(HAVE_SYS_SYSCALL_H)
15
+
16
+ #define IO_EVENT_FUTEX
17
+
18
+ #include <linux/futex.h>
19
+ #include <stdint.h>
20
+ #include <sys/syscall.h>
21
+
22
+ #ifndef FUTEX2_SIZE_U32
23
+ #define FUTEX2_SIZE_U32 2
24
+ #endif
25
+
26
+ #ifndef FUTEX_32
27
+ #define FUTEX_32 2
28
+ #endif
29
+
30
+ #if defined(FUTEX_WAITV_MAX) && defined(SYS_futex_waitv)
31
+ #define IO_EVENT_FUTEX_WAITV
32
+ #endif
33
+
34
+ uint32_t *IO_Event_Futex_address(VALUE self);
35
+ uint32_t *IO_Event_Futex_acquire(VALUE self);
36
+ void IO_Event_Futex_release(VALUE self);
37
+ #ifdef IO_EVENT_FUTEX_WAITV
38
+ VALUE IO_Event_Futex_prepare_waitv(VALUE entries, struct futex_waitv *vector);
39
+ #endif
40
+ void Init_IO_Event_Futex(VALUE IO_Event);
41
+
42
+ #endif
@@ -437,7 +437,7 @@ VALUE IO_Event_Selector_EPoll_ready_p(VALUE self) {
437
437
  struct IO_Event_Selector_EPoll *selector = NULL;
438
438
  TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector);
439
439
 
440
- return selector->backend.ready ? Qtrue : Qfalse;
440
+ return IO_Event_Selector_ready_p(&selector->backend) ? Qtrue : Qfalse;
441
441
  }
442
442
 
443
443
  struct process_wait_arguments {
@@ -1101,7 +1101,7 @@ VALUE IO_Event_Selector_EPoll_select(VALUE self, VALUE duration) {
1101
1101
  // 2. Didn't process any events from non-blocking select (above), and
1102
1102
  // 3. There are no items in the ready list,
1103
1103
  // then we can perform a blocking select.
1104
- if (!ready && !result && !selector->backend.ready) {
1104
+ if (!ready && !result && !IO_Event_Selector_ready_p(&selector->backend)) {
1105
1105
  arguments.timeout = make_timeout(duration, &arguments.storage);
1106
1106
 
1107
1107
  if (select_blocking_allowed(arguments.timeout)) {
@@ -425,7 +425,7 @@ VALUE IO_Event_Selector_KQueue_ready_p(VALUE self) {
425
425
  struct IO_Event_Selector_KQueue *selector = NULL;
426
426
  TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector);
427
427
 
428
- return selector->backend.ready ? Qtrue : Qfalse;
428
+ return IO_Event_Selector_ready_p(&selector->backend) ? Qtrue : Qfalse;
429
429
  }
430
430
 
431
431
  struct process_wait_arguments {
@@ -1080,7 +1080,7 @@ VALUE IO_Event_Selector_KQueue_select(VALUE self, VALUE duration) {
1080
1080
  // 2. Didn't process any events from non-blocking select (above), and
1081
1081
  // 3. There are no items in the ready list,
1082
1082
  // then we can perform a blocking select.
1083
- if (!ready && !result && !selector->backend.ready) {
1083
+ if (!ready && !result && !IO_Event_Selector_ready_p(&selector->backend)) {
1084
1084
  arguments.timeout = make_timeout(duration, &arguments.storage);
1085
1085
 
1086
1086
  if (select_blocking_allowed(arguments.timeout)) {
@@ -354,27 +354,71 @@ void IO_Event_Selector_ready_pop(struct IO_Event_Selector *backend, struct IO_Ev
354
354
  IO_Event_Selector_loop_resume(backend, fiber, 0, NULL);
355
355
  }
356
356
 
357
- int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend)
357
+ // State shared by the rb_ensure body and cleanup callbacks. Each flush owns a distinct placeholder on its C stack, including when flushes are nested.
358
+ struct ready_flush_arguments {
359
+ struct IO_Event_Selector *backend;
360
+ struct IO_Event_Selector_Queue placeholder;
361
+ int count;
362
+ };
363
+
364
+ // rb_ensure requires VALUE(VALUE) callbacks. IO_Event_Selector_ready_flush wraps this loop and returns the count stored in the shared arguments.
365
+ static VALUE IO_Event_Selector_ready_flush_begin(VALUE _arguments)
358
366
  {
359
- int count = 0;
367
+ struct ready_flush_arguments *arguments = (struct ready_flush_arguments *)_arguments;
368
+ struct IO_Event_Selector *backend = arguments->backend;
360
369
 
361
- // During iteration of the queue, the same item may be re-queued. If we don't handle this correctly, we may end up in an infinite loop. So, to avoid this situation, we keep note of the current head of the queue and break the loop if we reach the same item again.
362
-
363
- // Get the current tail and head of the queue:
364
- struct IO_Event_Selector_Queue *waiting = backend->waiting;
365
- if (DEBUG) fprintf(stderr, "IO_Event_Selector_ready_flush waiting = %p\n", waiting);
370
+ // rb_ensure has installed cleanup before entering this callback. Link the placeholder before any operation that can raise or yield.
371
+ //
372
+ // Saving the last existing entry is unsafe: another fiber can remove it. Counting entries instead can let newly queued work replace removed entries. This placeholder stays linked until its owning flush finishes, preserving:
373
+ // existing entries -> placeholder -> newly queued entries
374
+ queue_push(backend, &arguments->placeholder);
366
375
 
367
- // Process from head to tail in order:
368
- // During this, more items may be appended to tail.
369
376
  while (backend->ready) {
370
- if (DEBUG) fprintf(stderr, "backend->ready = %p\n", backend->ready);
371
377
  struct IO_Event_Selector_Queue *ready = backend->ready;
372
378
 
373
- count += 1;
374
- IO_Event_Selector_ready_pop(backend, ready);
379
+ // Each flush stops at its own placeholder. Entries beyond an outer placeholder may already have been queued when this flush started. Skip other placeholders without unlinking them: their owners still need them as boundaries and will remove them in their ensure callbacks.
380
+ while (ready && ready != &arguments->placeholder && (ready->flags & IO_EVENT_SELECTOR_QUEUE_PLACEHOLDER)) {
381
+ ready = ready->head;
382
+ }
375
383
 
376
- if (ready == waiting) break;
384
+ if (!ready || ready == &arguments->placeholder) break;
385
+
386
+ // Resuming a fiber can unlink other entries, so read backend->ready again on the next iteration instead of retaining a neighbour pointer.
387
+ arguments->count += 1;
388
+ IO_Event_Selector_ready_pop(backend, ready);
377
389
  }
378
390
 
379
- return count;
391
+ return Qnil;
392
+ }
393
+
394
+ static VALUE IO_Event_Selector_ready_flush_ensure(VALUE _arguments)
395
+ {
396
+ struct ready_flush_arguments *arguments = (struct ready_flush_arguments *)_arguments;
397
+
398
+ // Only the owning flush removes this placeholder, exactly once. Unlinking it reconnects its neighbours without removing any other placeholders.
399
+ queue_pop(arguments->backend, &arguments->placeholder);
400
+
401
+ return Qnil;
402
+ }
403
+
404
+ int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend)
405
+ {
406
+ if (!backend->ready) return 0;
407
+
408
+ struct ready_flush_arguments arguments = {
409
+ .backend = backend,
410
+ .placeholder = {
411
+ .head = NULL,
412
+ .tail = NULL,
413
+ .flags = IO_EVENT_SELECTOR_QUEUE_PLACEHOLDER,
414
+ // Queue marking and compaction visit every node, including this one.
415
+ .fiber = Qnil,
416
+ },
417
+ .count = 0,
418
+ };
419
+
420
+ // Always unlink the placeholder on normal return or exception, before the arguments leave scope, so the queue cannot retain a pointer into this stack.
421
+ rb_ensure(IO_Event_Selector_ready_flush_begin, (VALUE)&arguments, IO_Event_Selector_ready_flush_ensure, (VALUE)&arguments);
422
+
423
+ return arguments.count;
380
424
  }
@@ -77,6 +77,8 @@ void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags);
77
77
  enum IO_Event_Selector_Queue_Flags {
78
78
  IO_EVENT_SELECTOR_QUEUE_FIBER = 1,
79
79
  IO_EVENT_SELECTOR_QUEUE_INTERNAL = 2,
80
+ // A stack-allocated flush boundary; never dispatched or freed as a fiber entry.
81
+ IO_EVENT_SELECTOR_QUEUE_PLACEHOLDER = 4,
80
82
  };
81
83
 
82
84
  struct IO_Event_Selector_Queue {
@@ -107,6 +109,18 @@ struct IO_Event_Selector {
107
109
 
108
110
  void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self, VALUE loop);
109
111
 
112
+ // Whether the queue contains any entries other than flush placeholders.
113
+ static inline
114
+ int IO_Event_Selector_ready_p(struct IO_Event_Selector *backend) {
115
+ struct IO_Event_Selector_Queue *ready = backend->ready;
116
+ // Nested flushes can leave several adjacent placeholders at the front. Look past them for real entries, including work deferred to the next flush.
117
+ while (ready && (ready->flags & IO_EVENT_SELECTOR_QUEUE_PLACEHOLDER)) {
118
+ ready = ready->head;
119
+ }
120
+
121
+ return ready != NULL;
122
+ }
123
+
110
124
  static inline
111
125
  void IO_Event_Selector_mark(struct IO_Event_Selector *backend) {
112
126
  rb_gc_mark_movable(backend->self);