Skip to content

Commit c025f83

Browse files
Add atomic load/store.
1 parent 3219d0c commit c025f83

3 files changed

Lines changed: 322 additions & 30 deletions

File tree

ext/io/buffer/atomic.c

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@
2424
#error "Atomic operations not available on this platform"
2525
#endif
2626

27+
// Byte swap functions (for endianness conversion)
28+
static inline uint16_t swap16(uint16_t x) {
29+
return ((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8);
30+
}
31+
32+
static inline uint32_t swap32(uint32_t x) {
33+
return ((x & 0xFF000000) >> 24) | ((x & 0x00FF0000) >> 8) |
34+
((x & 0x0000FF00) << 8) | ((x & 0x000000FF) << 24);
35+
}
36+
37+
static inline uint64_t swap64(uint64_t x) {
38+
return ((x & 0xFF00000000000000ULL) >> 56) | ((x & 0x00FF000000000000ULL) >> 40) |
39+
((x & 0x0000FF0000000000ULL) >> 24) | ((x & 0x000000FF00000000ULL) >> 8) |
40+
((x & 0x00000000FF000000ULL) << 8) | ((x & 0x0000000000FF0000ULL) << 24) |
41+
((x & 0x000000000000FF00ULL) << 40) | ((x & 0x00000000000000FFULL) << 56);
42+
}
43+
2744
// Helper to get buffer pointer and validate
2845
static void* get_buffer_pointer(VALUE buffer, size_t offset, size_t size) {
2946
void *base;
@@ -81,6 +98,40 @@ static ID RB_IO_BUFFER_DATA_TYPE_S64;
8198
return val2num(result); \
8299
}
83100

101+
// Helper macro for swap function selection based on size
102+
#define SWAP_BY_SIZE(size_val, value) \
103+
((size_val) == 8 ? (uint8_t)(value) : \
104+
(size_val) == 16 ? swap16((uint16_t)(value)) : \
105+
(size_val) == 32 ? swap32((uint32_t)(value)) : \
106+
swap64((uint64_t)(value)))
107+
108+
// Macro for atomic load (with endianness conversion)
109+
#define ATOMIC_LOAD_IMPL(name, ctype, atomic_type, size_bits, num2val, val2num, endian, swap_func) \
110+
static VALUE atomic_load_##name(VALUE self, size_t offset) { \
111+
void *pointer = get_buffer_pointer(self, offset, size_bits / 8); \
112+
ctype result = atomic_load((atomic_type*)pointer); \
113+
if (endian != RB_IO_BUFFER_HOST_ENDIAN && size_bits > 8) { \
114+
if (size_bits == 16) result = (ctype)swap16((uint16_t)result); \
115+
else if (size_bits == 32) result = (ctype)swap32((uint32_t)result); \
116+
else if (size_bits == 64) result = (ctype)swap64((uint64_t)result); \
117+
} \
118+
return val2num(result); \
119+
}
120+
121+
// Macro for atomic store (with endianness conversion)
122+
#define ATOMIC_STORE_IMPL(name, ctype, atomic_type, size_bits, num2val, val2num, endian, swap_func) \
123+
static VALUE atomic_store_##name(VALUE self, size_t offset, VALUE value) { \
124+
void *pointer = get_buffer_pointer(self, offset, size_bits / 8); \
125+
ctype converted_value = (ctype)num2val(value); \
126+
if (endian != RB_IO_BUFFER_HOST_ENDIAN && size_bits > 8) { \
127+
if (size_bits == 16) converted_value = (ctype)swap16((uint16_t)converted_value); \
128+
else if (size_bits == 32) converted_value = (ctype)swap32((uint32_t)converted_value); \
129+
else if (size_bits == 64) converted_value = (ctype)swap64((uint64_t)converted_value); \
130+
} \
131+
atomic_store((atomic_type*)pointer, converted_value); \
132+
return self; \
133+
}
134+
84135
// Macro for atomic compare and swap
85136
#define ATOMIC_CAS_IMPL(name, ctype, atomic_type, size, num2val, val2num) \
86137
static int atomic_cas_##name(VALUE self, size_t offset, VALUE expected_value, VALUE desired_value) { \
@@ -128,6 +179,46 @@ ATOMIC_BITWISE_IMPL(i32, int32_t, atomic_int_least32_t, 4, NUM2INT, INT2NUM, xor
128179
ATOMIC_BITWISE_IMPL(u64, uint64_t, atomic_uint_least64_t, 8, NUM2ULL, ULL2NUM, xor)
129180
ATOMIC_BITWISE_IMPL(i64, int64_t, atomic_int_least64_t, 8, NUM2LL, LL2NUM, xor)
130181

182+
// 8-bit types: no swap needed (single byte), Ruby only defines U8/S8 (big-endian)
183+
ATOMIC_LOAD_IMPL(U8, uint8_t, atomic_uint_least8_t, 8, NUM2UINT, UINT2NUM, RB_IO_BUFFER_BIG_ENDIAN, NULL)
184+
ATOMIC_LOAD_IMPL(S8, int8_t, atomic_int_least8_t, 8, NUM2INT, INT2NUM, RB_IO_BUFFER_BIG_ENDIAN, NULL)
185+
186+
// 16-bit types
187+
ATOMIC_LOAD_IMPL(u16, uint16_t, atomic_uint_least16_t, 16, NUM2UINT, UINT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap16)
188+
ATOMIC_LOAD_IMPL(s16, int16_t, atomic_int_least16_t, 16, NUM2INT, INT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap16)
189+
ATOMIC_LOAD_IMPL(U16, uint16_t, atomic_uint_least16_t, 16, NUM2UINT, UINT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap16)
190+
ATOMIC_LOAD_IMPL(S16, int16_t, atomic_int_least16_t, 16, NUM2INT, INT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap16)
191+
192+
// 32-bit types
193+
ATOMIC_LOAD_IMPL(u32, uint32_t, atomic_uint_least32_t, 32, NUM2UINT, UINT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap32)
194+
ATOMIC_LOAD_IMPL(s32, int32_t, atomic_int_least32_t, 32, NUM2INT, INT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap32)
195+
ATOMIC_LOAD_IMPL(U32, uint32_t, atomic_uint_least32_t, 32, NUM2UINT, UINT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap32)
196+
ATOMIC_LOAD_IMPL(S32, int32_t, atomic_int_least32_t, 32, NUM2INT, INT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap32)
197+
198+
// 64-bit types
199+
ATOMIC_LOAD_IMPL(u64, uint64_t, atomic_uint_least64_t, 64, NUM2ULL, ULL2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap64)
200+
ATOMIC_LOAD_IMPL(s64, int64_t, atomic_int_least64_t, 64, NUM2LL, LL2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap64)
201+
ATOMIC_LOAD_IMPL(U64, uint64_t, atomic_uint_least64_t, 64, NUM2ULL, ULL2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap64)
202+
ATOMIC_LOAD_IMPL(S64, int64_t, atomic_int_least64_t, 64, NUM2LL, LL2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap64)
203+
204+
ATOMIC_STORE_IMPL(U8, uint8_t, atomic_uint_least8_t, 8, NUM2UINT, UINT2NUM, RB_IO_BUFFER_BIG_ENDIAN, NULL)
205+
ATOMIC_STORE_IMPL(S8, int8_t, atomic_int_least8_t, 8, NUM2INT, INT2NUM, RB_IO_BUFFER_BIG_ENDIAN, NULL)
206+
207+
ATOMIC_STORE_IMPL(u16, uint16_t, atomic_uint_least16_t, 16, NUM2UINT, UINT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap16)
208+
ATOMIC_STORE_IMPL(s16, int16_t, atomic_int_least16_t, 16, NUM2INT, INT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap16)
209+
ATOMIC_STORE_IMPL(U16, uint16_t, atomic_uint_least16_t, 16, NUM2UINT, UINT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap16)
210+
ATOMIC_STORE_IMPL(S16, int16_t, atomic_int_least16_t, 16, NUM2INT, INT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap16)
211+
212+
ATOMIC_STORE_IMPL(u32, uint32_t, atomic_uint_least32_t, 32, NUM2UINT, UINT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap32)
213+
ATOMIC_STORE_IMPL(s32, int32_t, atomic_int_least32_t, 32, NUM2INT, INT2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap32)
214+
ATOMIC_STORE_IMPL(U32, uint32_t, atomic_uint_least32_t, 32, NUM2UINT, UINT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap32)
215+
ATOMIC_STORE_IMPL(S32, int32_t, atomic_int_least32_t, 32, NUM2INT, INT2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap32)
216+
217+
ATOMIC_STORE_IMPL(u64, uint64_t, atomic_uint_least64_t, 64, NUM2ULL, ULL2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap64)
218+
ATOMIC_STORE_IMPL(s64, int64_t, atomic_int_least64_t, 64, NUM2LL, LL2NUM, RB_IO_BUFFER_LITTLE_ENDIAN, swap64)
219+
ATOMIC_STORE_IMPL(U64, uint64_t, atomic_uint_least64_t, 64, NUM2ULL, ULL2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap64)
220+
ATOMIC_STORE_IMPL(S64, int64_t, atomic_int_least64_t, 64, NUM2LL, LL2NUM, RB_IO_BUFFER_BIG_ENDIAN, swap64)
221+
131222
ATOMIC_CAS_IMPL(u8, uint8_t, atomic_uint_least8_t, 1, NUM2UINT, UINT2NUM)
132223
ATOMIC_CAS_IMPL(i8, int8_t, atomic_int_least8_t, 1, NUM2INT, INT2NUM)
133224
ATOMIC_CAS_IMPL(u16, uint16_t, atomic_uint_least16_t, 2, NUM2UINT, UINT2NUM)
@@ -242,6 +333,58 @@ static VALUE atomic_xor_impl(VALUE self, ID type_identifier, size_t offset, long
242333
return Qnil;
243334
}
244335

336+
static VALUE atomic_load_impl(VALUE self, ID type_identifier, size_t offset) {
337+
#define ATOMIC_LOAD_CASE(name, impl_name) \
338+
if (type_identifier == RB_IO_BUFFER_DATA_TYPE_##name) { \
339+
return atomic_load_##impl_name(self, offset); \
340+
}
341+
342+
ATOMIC_LOAD_CASE(U8, U8)
343+
ATOMIC_LOAD_CASE(S8, S8)
344+
ATOMIC_LOAD_CASE(u16, u16)
345+
ATOMIC_LOAD_CASE(s16, s16)
346+
ATOMIC_LOAD_CASE(U16, U16)
347+
ATOMIC_LOAD_CASE(S16, S16)
348+
ATOMIC_LOAD_CASE(u32, u32)
349+
ATOMIC_LOAD_CASE(s32, s32)
350+
ATOMIC_LOAD_CASE(U32, U32)
351+
ATOMIC_LOAD_CASE(S32, S32)
352+
ATOMIC_LOAD_CASE(u64, u64)
353+
ATOMIC_LOAD_CASE(s64, s64)
354+
ATOMIC_LOAD_CASE(U64, U64)
355+
ATOMIC_LOAD_CASE(S64, S64)
356+
#undef ATOMIC_LOAD_CASE
357+
358+
rb_raise(rb_eArgError, "Unsupported type for atomic operations: %"PRIsVALUE, rb_id2str(type_identifier));
359+
return Qnil;
360+
}
361+
362+
static VALUE atomic_store_impl(VALUE self, ID type_identifier, size_t offset, VALUE value) {
363+
#define ATOMIC_STORE_CASE(name, impl_name) \
364+
if (type_identifier == RB_IO_BUFFER_DATA_TYPE_##name) { \
365+
return atomic_store_##impl_name(self, offset, value); \
366+
}
367+
368+
ATOMIC_STORE_CASE(U8, U8)
369+
ATOMIC_STORE_CASE(S8, S8)
370+
ATOMIC_STORE_CASE(u16, u16)
371+
ATOMIC_STORE_CASE(s16, s16)
372+
ATOMIC_STORE_CASE(U16, U16)
373+
ATOMIC_STORE_CASE(S16, S16)
374+
ATOMIC_STORE_CASE(u32, u32)
375+
ATOMIC_STORE_CASE(s32, s32)
376+
ATOMIC_STORE_CASE(U32, U32)
377+
ATOMIC_STORE_CASE(S32, S32)
378+
ATOMIC_STORE_CASE(u64, u64)
379+
ATOMIC_STORE_CASE(s64, s64)
380+
ATOMIC_STORE_CASE(U64, U64)
381+
ATOMIC_STORE_CASE(S64, S64)
382+
#undef ATOMIC_STORE_CASE
383+
384+
rb_raise(rb_eArgError, "Unsupported type for atomic operations: %"PRIsVALUE, rb_id2str(type_identifier));
385+
return Qnil;
386+
}
387+
245388
static VALUE atomic_compare_and_swap_impl(VALUE self, ID type_identifier, size_t offset, VALUE expected_value, VALUE desired_value) {
246389
#define ATOMIC_CAS_CASE(name, impl_name) \
247390
if (type_identifier == RB_IO_BUFFER_DATA_TYPE_##name) { \
@@ -347,6 +490,20 @@ static VALUE atomic_xor(VALUE self, VALUE type_symbol, VALUE offset_value, VALUE
347490
return atomic_xor_impl(self, type_identifier, offset, value);
348491
}
349492

493+
static VALUE atomic_load_method(VALUE self, VALUE type_symbol, VALUE offset_value) {
494+
ID type_identifier = get_type_id(type_symbol);
495+
size_t offset = NUM2SIZET(offset_value);
496+
497+
return atomic_load_impl(self, type_identifier, offset);
498+
}
499+
500+
static VALUE atomic_store_method(VALUE self, VALUE type_symbol, VALUE offset_value, VALUE value) {
501+
ID type_identifier = get_type_id(type_symbol);
502+
size_t offset = NUM2SIZET(offset_value);
503+
504+
return atomic_store_impl(self, type_identifier, offset, value);
505+
}
506+
350507
static VALUE atomic_compare_and_swap(VALUE self, VALUE type_symbol, VALUE offset_value, VALUE expected_value, VALUE desired_value) {
351508
ID type_identifier = get_type_id(type_symbol);
352509
size_t offset = NUM2SIZET(offset_value);
@@ -371,7 +528,7 @@ void Init_IO_Buffer_Atomic(void) {
371528

372529
VALUE IO_Buffer = rb_const_get(rb_cIO, rb_intern("Buffer"));
373530

374-
// Initialize type IDs (matching Ruby's pattern)
531+
// Initialize type IDs (support both forms for compatibility, but atomic operations work on raw bytes in host endian)
375532
#define IO_BUFFER_DEFINE_DATA_TYPE(name) RB_IO_BUFFER_DATA_TYPE_##name = rb_intern_const(#name)
376533
IO_BUFFER_DEFINE_DATA_TYPE(U8);
377534
IO_BUFFER_DEFINE_DATA_TYPE(S8);
@@ -396,5 +553,7 @@ void Init_IO_Buffer_Atomic(void) {
396553
DEFINE_METHOD_IF_NOT_EXISTS(IO_Buffer, "atomic_and", atomic_and, 3);
397554
DEFINE_METHOD_IF_NOT_EXISTS(IO_Buffer, "atomic_or", atomic_or, 3);
398555
DEFINE_METHOD_IF_NOT_EXISTS(IO_Buffer, "atomic_xor", atomic_xor, 3);
556+
DEFINE_METHOD_IF_NOT_EXISTS(IO_Buffer, "atomic_load", atomic_load_method, 2);
557+
DEFINE_METHOD_IF_NOT_EXISTS(IO_Buffer, "atomic_store", atomic_store_method, 3);
399558
DEFINE_METHOD_IF_NOT_EXISTS(IO_Buffer, "atomic_compare_and_swap", atomic_compare_and_swap, 4);
400559
}

guides/getting-started/readme.md

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ $ bundle add io-buffer-atomic
1414

1515
`io-buffer-atomic` extends `IO::Buffer` with atomic operations that are safe for concurrent access across threads and processes. When multiple threads or processes share memory (via `IO::Buffer`), you need atomic operations to prevent race conditions and ensure data consistency.
1616

17+
**Important**: Atomic operations work on raw memory bytes in host endian (native byte order). The endianness distinction in Ruby's type system (`:u32` vs `:U32`) affects how Ruby interprets bytes when reading/writing, but atomic operations themselves operate directly on raw memory in host byte order.
18+
1719
Use atomic operations when you need:
1820
- **Thread-safe counters**: Multiple threads updating shared counters without locks.
1921
- **Process-safe coordination**: Multiple processes coordinating via shared memory.
@@ -77,6 +79,36 @@ result = buffer.atomic_xor(:u32, 0, 0b1111)
7779
# => 0b0000
7880
```
7981

82+
### Atomic Load
83+
84+
Atomic load operations ensure you read a complete, consistent value from shared memory, preventing torn reads:
85+
86+
```ruby
87+
# Set a value:
88+
buffer.set_value(:u32, 0, 42)
89+
90+
# Atomically read the value (ensures no torn reads):
91+
result = buffer.atomic_load(:u32, 0)
92+
# => 42
93+
```
94+
95+
Use `atomic_load` instead of `get_value` when reading values that may be modified by other threads or processes, as it provides memory ordering guarantees and prevents data races.
96+
97+
### Atomic Store
98+
99+
Atomic store operations ensure you write a complete value atomically to shared memory:
100+
101+
```ruby
102+
# Atomically write a value:
103+
buffer.atomic_store(:u32, 0, 42)
104+
105+
# Read it back:
106+
result = buffer.atomic_load(:u32, 0)
107+
# => 42
108+
```
109+
110+
Use `atomic_store` instead of `set_value` when writing values that may be read by other threads or processes, as it provides memory ordering guarantees and prevents torn writes.
111+
80112
### Compare and Swap
81113

82114
Compare-and-swap operations enable lock-free algorithms and optimistic concurrency:
@@ -88,7 +120,7 @@ buffer.set_value(:u32, 0, 10)
88120
# Atomically swap if current value matches expected:
89121
swapped = buffer.atomic_compare_and_swap(:u32, 0, 10, 20)
90122
# => true
91-
buffer.get_value(:u32, 0)
123+
buffer.atomic_load(:u32, 0)
92124
# => 20
93125

94126
# If value doesn't match, swap fails:
@@ -98,6 +130,8 @@ swapped = buffer.atomic_compare_and_swap(:u32, 0, 10, 30)
98130

99131
## Supported Operations
100132

133+
- `atomic_load(type, offset)` - Atomically read a value (prevents torn reads).
134+
- `atomic_store(type, offset, value)` - Atomically write a value (prevents torn writes).
101135
- `atomic_increment(type, offset, value = 1)` - Atomically increment a value.
102136
- `atomic_decrement(type, offset, value = 1)` - Atomically decrement a value.
103137
- `atomic_add(type, offset, value)` - Atomically add a value.
@@ -106,8 +140,3 @@ swapped = buffer.atomic_compare_and_swap(:u32, 0, 10, 30)
106140
- `atomic_or(type, offset, value)` - Atomically perform bitwise OR.
107141
- `atomic_xor(type, offset, value)` - Atomically perform bitwise XOR.
108142
- `atomic_compare_and_swap(type, offset, expected, desired)` - Atomically compare and swap.
109-
110-
## Requirements
111-
112-
- Ruby \>= 3.2.6.
113-
- `IO::Buffer` support (available in Ruby 3.2+).

0 commit comments

Comments
 (0)