diff options
| author | Samuel Williams <samuel.williams@shopify.com> | 2025-12-06 15:55:32 +1300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-06 15:55:32 +1300 |
| commit | a7dc53b91c8475323b34d5a332fdb25d190e277d (patch) | |
| tree | 0a3ae38b9f91bf841b0ee76d877da1711ff4791d | |
| parent | 8f9838476dc8cc857859a0a93da285d792be7d3b (diff) | |
Add support for `u128`, `U128`, `s128` and `S128` integers to `IO::Buffer`. (#15399)
| -rw-r--r-- | bignum.c | 6 | ||||
| -rw-r--r-- | internal/bignum.h | 6 | ||||
| -rw-r--r-- | internal/numeric.h | 43 | ||||
| -rw-r--r-- | io_buffer.c | 70 | ||||
| -rw-r--r-- | numeric.c | 223 | ||||
| -rw-r--r-- | test/ruby/test_io_buffer.rb | 147 |
6 files changed, 492 insertions, 3 deletions
@@ -4515,7 +4515,7 @@ rb_str2big_gmp(VALUE arg, int base, int badcheck) #if HAVE_LONG_LONG -static VALUE +VALUE rb_ull2big(unsigned LONG_LONG n) { long i; @@ -4537,7 +4537,7 @@ rb_ull2big(unsigned LONG_LONG n) return big; } -static VALUE +VALUE rb_ll2big(LONG_LONG n) { long neg = 0; @@ -4575,7 +4575,7 @@ rb_ll2inum(LONG_LONG n) #endif /* HAVE_LONG_LONG */ #ifdef HAVE_INT128_T -static VALUE +VALUE rb_uint128t2big(uint128_t n) { long i; diff --git a/internal/bignum.h b/internal/bignum.h index e5b6b42563..0692bafed3 100644 --- a/internal/bignum.h +++ b/internal/bignum.h @@ -169,7 +169,13 @@ VALUE rb_str2big_gmp(VALUE arg, int base, int badcheck); VALUE rb_int_parse_cstr(const char *str, ssize_t len, char **endp, size_t *ndigits, int base, int flags); RUBY_SYMBOL_EXPORT_END +#if HAVE_LONG_LONG +VALUE rb_ull2big(unsigned LONG_LONG n); +VALUE rb_ll2big(LONG_LONG n); +#endif + #if defined(HAVE_INT128_T) +VALUE rb_uint128t2big(uint128_t n); VALUE rb_int128t2big(int128_t n); #endif diff --git a/internal/numeric.h b/internal/numeric.h index 58f42f41ac..75181a7f16 100644 --- a/internal/numeric.h +++ b/internal/numeric.h @@ -127,6 +127,49 @@ VALUE rb_int_bit_length(VALUE num); VALUE rb_int_uminus(VALUE num); VALUE rb_int_comp(VALUE num); +// Unified 128-bit integer structures that work with or without native support: +union rb_uint128 { +#ifdef WORDS_BIGENDIAN + struct { + uint64_t high; + uint64_t low; + } parts; +#else + struct { + uint64_t low; + uint64_t high; + } parts; +#endif +#ifdef HAVE_UINT128_T + uint128_t value; +#endif +}; +typedef union rb_uint128 rb_uint128_t; + +union rb_int128 { +#ifdef WORDS_BIGENDIAN + struct { + uint64_t high; + uint64_t low; + } parts; +#else + struct { + uint64_t low; + uint64_t high; + } parts; +#endif +#ifdef HAVE_UINT128_T + int128_t value; +#endif +}; +typedef union rb_int128 rb_int128_t; + +// Conversion functions for 128-bit integers: +rb_uint128_t rb_numeric_to_uint128(VALUE x); +rb_int128_t rb_numeric_to_int128(VALUE x); +VALUE rb_uint128_to_numeric(rb_uint128_t n); +VALUE rb_int128_to_numeric(rb_int128_t n); + static inline bool INT_POSITIVE_P(VALUE num) { diff --git a/io_buffer.c b/io_buffer.c index 89f169176f..0d2cbdb4b7 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1864,6 +1864,9 @@ io_buffer_validate_type(size_t size, size_t offset) // :u64, :U64 | unsigned 64-bit integer. // :s64, :S64 | signed 64-bit integer. // +// :u128, :U128 | unsigned 128-bit integer. +// :s128, :S128 | signed 128-bit integer. +// // :f32, :F32 | 32-bit floating point number. // :f64, :F64 | 64-bit floating point number. @@ -1895,6 +1898,45 @@ ruby_swapf64(double value) return swap.value; } +// Structures and conversion functions are now in numeric.h/numeric.c +// Unified swap function for 128-bit integers (works with both signed and unsigned) +// Since both rb_uint128_t and rb_int128_t have the same memory layout, +// we can use a union to make the swap function work with both types +static inline rb_uint128_t +ruby_swap128_uint(rb_uint128_t x) +{ + rb_uint128_t result; +#ifdef HAVE_UINT128_T +#if __has_builtin(__builtin_bswap128) + result.value = __builtin_bswap128(x.value); +#else + // Manual byte swap for 128-bit integers + uint64_t low = (uint64_t)x.value; + uint64_t high = (uint64_t)(x.value >> 64); + low = ruby_swap64(low); + high = ruby_swap64(high); + result.value = ((uint128_t)low << 64) | high; +#endif +#else + // Fallback swap function using two 64-bit integers + // For big-endian data on little-endian host (or vice versa): + // 1. Swap bytes within each 64-bit part + // 2. Swap the order of the parts (since big-endian stores high first, little-endian stores low first) + result.parts.low = ruby_swap64(x.parts.high); + result.parts.high = ruby_swap64(x.parts.low); +#endif + return result; +} + +static inline rb_int128_t +ruby_swap128_int(rb_int128_t x) +{ + // Cast to unsigned, swap, then cast back + rb_uint128_t u = *(rb_uint128_t*)&x; + rb_uint128_t swapped = ruby_swap128_uint(u); + return *(rb_int128_t*)&swapped; +} + #define IO_BUFFER_DECLARE_TYPE(name, type, endian, wrap, unwrap, swap) \ static ID RB_IO_BUFFER_DATA_TYPE_##name; \ \ @@ -1941,6 +1983,11 @@ IO_BUFFER_DECLARE_TYPE(U64, uint64_t, RB_IO_BUFFER_BIG_ENDIAN, RB_ULL2NUM, RB_NU IO_BUFFER_DECLARE_TYPE(s64, int64_t, RB_IO_BUFFER_LITTLE_ENDIAN, RB_LL2NUM, RB_NUM2LL, ruby_swap64) IO_BUFFER_DECLARE_TYPE(S64, int64_t, RB_IO_BUFFER_BIG_ENDIAN, RB_LL2NUM, RB_NUM2LL, ruby_swap64) +IO_BUFFER_DECLARE_TYPE(u128, rb_uint128_t, RB_IO_BUFFER_LITTLE_ENDIAN, rb_uint128_to_numeric, rb_numeric_to_uint128, ruby_swap128_uint) +IO_BUFFER_DECLARE_TYPE(U128, rb_uint128_t, RB_IO_BUFFER_BIG_ENDIAN, rb_uint128_to_numeric, rb_numeric_to_uint128, ruby_swap128_uint) +IO_BUFFER_DECLARE_TYPE(s128, rb_int128_t, RB_IO_BUFFER_LITTLE_ENDIAN, rb_int128_to_numeric, rb_numeric_to_int128, ruby_swap128_int) +IO_BUFFER_DECLARE_TYPE(S128, rb_int128_t, RB_IO_BUFFER_BIG_ENDIAN, rb_int128_to_numeric, rb_numeric_to_int128, ruby_swap128_int) + IO_BUFFER_DECLARE_TYPE(f32, float, RB_IO_BUFFER_LITTLE_ENDIAN, DBL2NUM, NUM2DBL, ruby_swapf32) IO_BUFFER_DECLARE_TYPE(F32, float, RB_IO_BUFFER_BIG_ENDIAN, DBL2NUM, NUM2DBL, ruby_swapf32) IO_BUFFER_DECLARE_TYPE(f64, double, RB_IO_BUFFER_LITTLE_ENDIAN, DBL2NUM, NUM2DBL, ruby_swapf64) @@ -1965,6 +2012,10 @@ io_buffer_buffer_type_size(ID buffer_type) IO_BUFFER_DATA_TYPE_SIZE(U64) IO_BUFFER_DATA_TYPE_SIZE(s64) IO_BUFFER_DATA_TYPE_SIZE(S64) + IO_BUFFER_DATA_TYPE_SIZE(u128) + IO_BUFFER_DATA_TYPE_SIZE(U128) + IO_BUFFER_DATA_TYPE_SIZE(s128) + IO_BUFFER_DATA_TYPE_SIZE(S128) IO_BUFFER_DATA_TYPE_SIZE(f32) IO_BUFFER_DATA_TYPE_SIZE(F32) IO_BUFFER_DATA_TYPE_SIZE(f64) @@ -2021,6 +2072,11 @@ rb_io_buffer_get_value(const void* base, size_t size, ID buffer_type, size_t *of IO_BUFFER_GET_VALUE(s64) IO_BUFFER_GET_VALUE(S64) + IO_BUFFER_GET_VALUE(u128) + IO_BUFFER_GET_VALUE(U128) + IO_BUFFER_GET_VALUE(s128) + IO_BUFFER_GET_VALUE(S128) + IO_BUFFER_GET_VALUE(f32) IO_BUFFER_GET_VALUE(F32) IO_BUFFER_GET_VALUE(f64) @@ -2050,6 +2106,10 @@ rb_io_buffer_get_value(const void* base, size_t size, ID buffer_type, size_t *of * * +:U64+: unsigned integer, 8 bytes, big-endian * * +:s64+: signed integer, 8 bytes, little-endian * * +:S64+: signed integer, 8 bytes, big-endian + * * +:u128+: unsigned integer, 16 bytes, little-endian + * * +:U128+: unsigned integer, 16 bytes, big-endian + * * +:s128+: signed integer, 16 bytes, little-endian + * * +:S128+: signed integer, 16 bytes, big-endian * * +:f32+: float, 4 bytes, little-endian * * +:F32+: float, 4 bytes, big-endian * * +:f64+: double, 8 bytes, little-endian @@ -2287,6 +2347,11 @@ rb_io_buffer_set_value(const void* base, size_t size, ID buffer_type, size_t *of IO_BUFFER_SET_VALUE(s64); IO_BUFFER_SET_VALUE(S64); + IO_BUFFER_SET_VALUE(u128); + IO_BUFFER_SET_VALUE(U128); + IO_BUFFER_SET_VALUE(s128); + IO_BUFFER_SET_VALUE(S128); + IO_BUFFER_SET_VALUE(f32); IO_BUFFER_SET_VALUE(F32); IO_BUFFER_SET_VALUE(f64); @@ -3859,6 +3924,11 @@ Init_IO_Buffer(void) IO_BUFFER_DEFINE_DATA_TYPE(s64); IO_BUFFER_DEFINE_DATA_TYPE(S64); + IO_BUFFER_DEFINE_DATA_TYPE(u128); + IO_BUFFER_DEFINE_DATA_TYPE(U128); + IO_BUFFER_DEFINE_DATA_TYPE(s128); + IO_BUFFER_DEFINE_DATA_TYPE(S128); + IO_BUFFER_DEFINE_DATA_TYPE(f32); IO_BUFFER_DEFINE_DATA_TYPE(F32); IO_BUFFER_DEFINE_DATA_TYPE(f64); @@ -3420,6 +3420,229 @@ rb_num2ull(VALUE val) #endif /* HAVE_LONG_LONG */ +// Conversion functions for unified 128-bit integer structures, +// These work with or without native 128-bit integer support. + +#ifndef HAVE_UINT128_T +// Helper function to build 128-bit value from bignum digits (fallback path). +static inline void +rb_uint128_from_bignum_digits_fallback(rb_uint128_t *result, BDIGIT *digits, size_t length) +{ + // Build the 128-bit value from bignum digits: + for (long i = length - 1; i >= 0; i--) { + // Shift both low and high parts: + uint64_t carry = result->parts.low >> (64 - (SIZEOF_BDIGIT * CHAR_BIT)); + result->parts.low = (result->parts.low << (SIZEOF_BDIGIT * CHAR_BIT)) | digits[i]; + result->parts.high = (result->parts.high << (SIZEOF_BDIGIT * CHAR_BIT)) | carry; + } +} + +// Helper function to convert absolute value of negative bignum to two's complement. +// Ruby stores negative bignums as absolute values, so we need to convert to two's complement. +static inline void +rb_uint128_twos_complement_negate(rb_uint128_t *value) +{ + if (value->parts.low == 0) { + value->parts.high = ~value->parts.high + 1; + } + else { + value->parts.low = ~value->parts.low + 1; + value->parts.high = ~value->parts.high + (value->parts.low == 0 ? 1 : 0); + } +} +#endif + +rb_uint128_t +rb_numeric_to_uint128(VALUE x) +{ + rb_uint128_t result = {0}; + if (RB_FIXNUM_P(x)) { + long value = RB_FIX2LONG(x); + if (value < 0) { + rb_raise(rb_eRangeError, "negative integer cannot be converted to unsigned 128-bit integer"); + } +#ifdef HAVE_UINT128_T + result.value = (uint128_t)value; +#else + result.parts.low = (uint64_t)value; + result.parts.high = 0; +#endif + return result; + } + else if (RB_BIGNUM_TYPE_P(x)) { + if (BIGNUM_NEGATIVE_P(x)) { + rb_raise(rb_eRangeError, "negative integer cannot be converted to unsigned 128-bit integer"); + } + size_t length = BIGNUM_LEN(x); +#ifdef HAVE_UINT128_T + if (length > roomof(SIZEOF_INT128_T, SIZEOF_BDIGIT)) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'unsigned 128-bit integer'"); + } + BDIGIT *digits = BIGNUM_DIGITS(x); + result.value = 0; + for (long i = length - 1; i >= 0; i--) { + result.value = (result.value << (SIZEOF_BDIGIT * CHAR_BIT)) | digits[i]; + } +#else + // Check if bignum fits in 128 bits (16 bytes) + if (length > roomof(16, SIZEOF_BDIGIT)) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'unsigned 128-bit integer'"); + } + BDIGIT *digits = BIGNUM_DIGITS(x); + rb_uint128_from_bignum_digits_fallback(&result, digits, length); +#endif + return result; + } + else { + rb_raise(rb_eTypeError, "not an integer"); + } +} + +rb_int128_t +rb_numeric_to_int128(VALUE x) +{ + rb_int128_t result = {0}; + if (RB_FIXNUM_P(x)) { + long value = RB_FIX2LONG(x); +#ifdef HAVE_UINT128_T + result.value = (int128_t)value; +#else + if (value < 0) { + // Two's complement representation: for negative values, sign extend + // Convert to unsigned: for -1, we want all bits set + result.parts.low = (uint64_t)value; // This will be the two's complement representation + result.parts.high = UINT64_MAX; // Sign extend: all bits set for negative + } + else { + result.parts.low = (uint64_t)value; + result.parts.high = 0; + } +#endif + return result; + } + else if (RB_BIGNUM_TYPE_P(x)) { + size_t length = BIGNUM_LEN(x); +#ifdef HAVE_UINT128_T + if (length > roomof(SIZEOF_INT128_T, SIZEOF_BDIGIT)) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'signed 128-bit integer'"); + } + BDIGIT *digits = BIGNUM_DIGITS(x); + uint128_t unsigned_result = 0; + for (long i = length - 1; i >= 0; i--) { + unsigned_result = (unsigned_result << (SIZEOF_BDIGIT * CHAR_BIT)) | digits[i]; + } + if (BIGNUM_NEGATIVE_P(x)) { + // Convert from two's complement + // Maximum negative value is 2^127 + if (unsigned_result > ((uint128_t)1 << 127)) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'signed 128-bit integer'"); + } + result.value = -(int128_t)(unsigned_result - 1) - 1; + } + else { + // Maximum positive value is 2^127 - 1 + if (unsigned_result > (((uint128_t)1 << 127) - 1)) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'signed 128-bit integer'"); + } + result.value = (int128_t)unsigned_result; + } +#else + if (length > roomof(16, SIZEOF_BDIGIT)) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'signed 128-bit integer'"); + } + BDIGIT *digits = BIGNUM_DIGITS(x); + rb_uint128_t unsigned_result = {0}; + rb_uint128_from_bignum_digits_fallback(&unsigned_result, digits, length); + if (BIGNUM_NEGATIVE_P(x)) { + // Check if value fits in signed 128-bit (max negative is 2^127) + uint64_t max_neg_high = (uint64_t)1 << 63; + if (unsigned_result.parts.high > max_neg_high || (unsigned_result.parts.high == max_neg_high && unsigned_result.parts.low > 0)) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'signed 128-bit integer'"); + } + // Convert from absolute value to two's complement (Ruby stores negative as absolute value) + rb_uint128_twos_complement_negate(&unsigned_result); + result.parts.low = unsigned_result.parts.low; + result.parts.high = (int64_t)unsigned_result.parts.high; // Sign extend + } + else { + // Check if value fits in signed 128-bit (max positive is 2^127 - 1) + // Max positive: high = 0x7FFFFFFFFFFFFFFF, low = 0xFFFFFFFFFFFFFFFF + uint64_t max_pos_high = ((uint64_t)1 << 63) - 1; + if (unsigned_result.parts.high > max_pos_high) { + rb_raise(rb_eRangeError, "bignum too big to convert into 'signed 128-bit integer'"); + } + result.parts.low = unsigned_result.parts.low; + result.parts.high = unsigned_result.parts.high; + } +#endif + return result; + } + else { + rb_raise(rb_eTypeError, "not an integer"); + } +} + +VALUE +rb_uint128_to_numeric(rb_uint128_t n) +{ +#ifdef HAVE_UINT128_T + if (n.value <= (uint128_t)RUBY_FIXNUM_MAX) { + return LONG2FIX((long)n.value); + } + return rb_uint128t2big(n.value); +#else + // If high part is zero and low part fits in fixnum + if (n.parts.high == 0 && n.parts.low <= (uint64_t)RUBY_FIXNUM_MAX) { + return LONG2FIX((long)n.parts.low); + } + // Convert to bignum by building it from the two 64-bit parts + VALUE bignum = rb_ull2big(n.parts.low); + if (n.parts.high > 0) { + VALUE high_bignum = rb_ull2big(n.parts.high); + // Multiply high part by 2^64 and add to low part + VALUE shifted_value = rb_int_lshift(high_bignum, INT2FIX(64)); + bignum = rb_int_plus(bignum, shifted_value); + } + return bignum; +#endif +} + +VALUE +rb_int128_to_numeric(rb_int128_t n) +{ +#ifdef HAVE_UINT128_T + if (FIXABLE(n.value)) { + return LONG2FIX((long)n.value); + } + return rb_int128t2big(n.value); +#else + int64_t high = (int64_t)n.parts.high; + // If it's a small positive value that fits in fixnum + if (high == 0 && n.parts.low <= (uint64_t)RUBY_FIXNUM_MAX) { + return LONG2FIX((long)n.parts.low); + } + // Check if it's negative (high bit of high part is set) + if (high < 0) { + // Negative value - convert from two's complement to absolute value + rb_uint128_t unsigned_value = {0}; + if (n.parts.low == 0) { + unsigned_value.parts.low = 0; + unsigned_value.parts.high = ~n.parts.high + 1; + } + else { + unsigned_value.parts.low = ~n.parts.low + 1; + unsigned_value.parts.high = ~n.parts.high + (unsigned_value.parts.low == 0 ? 1 : 0); + } + VALUE bignum = rb_uint128_to_numeric(unsigned_value); + return rb_int_uminus(bignum); + } + else { + // Positive value + return rb_uint128_to_numeric(*(rb_uint128_t*)&n); + } +#endif +} + /******************************************************************** * * Document-class: Integer diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 1e4a6e2fd8..9ff22b4bb3 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -405,6 +405,11 @@ class TestIOBuffer < Test::Unit::TestCase :u64 => [0, 2**64-1], :s64 => [-2**63, 0, 2**63-1], + :U128 => [0, 2**64, 2**127-1, 2**128-1], + :S128 => [-2**127, -2**63-1, -1, 0, 2**63, 2**127-1], + :u128 => [0, 2**64, 2**127-1, 2**128-1], + :s128 => [-2**127, -2**63-1, -1, 0, 2**63, 2**127-1], + :F32 => [-1.0, 0.0, 0.5, 1.0, 128.0], :F64 => [-1.0, 0.0, 0.5, 1.0, 128.0], } @@ -759,4 +764,146 @@ class TestIOBuffer < Test::Unit::TestCase assert_predicate buf, :valid? end + + def test_128_bit_integers + buffer = IO::Buffer.new(32) + + # Test unsigned 128-bit integers + test_values_u128 = [ + 0, + 1, + 2**64 - 1, + 2**64, + 2**127 - 1, + 2**128 - 1, + ] + + test_values_u128.each do |value| + buffer.set_value(:u128, 0, value) + assert_equal value, buffer.get_value(:u128, 0), "u128: #{value}" + + buffer.set_value(:U128, 0, value) + assert_equal value, buffer.get_value(:U128, 0), "U128: #{value}" + end + + # Test signed 128-bit integers + test_values_s128 = [ + -2**127, + -2**63 - 1, + -1, + 0, + 1, + 2**63, + 2**127 - 1, + ] + + test_values_s128.each do |value| + buffer.set_value(:s128, 0, value) + assert_equal value, buffer.get_value(:s128, 0), "s128: #{value}" + + buffer.set_value(:S128, 0, value) + assert_equal value, buffer.get_value(:S128, 0), "S128: #{value}" + end + + # Test size_of + assert_equal 16, IO::Buffer.size_of(:u128) + assert_equal 16, IO::Buffer.size_of(:U128) + assert_equal 16, IO::Buffer.size_of(:s128) + assert_equal 16, IO::Buffer.size_of(:S128) + assert_equal 32, IO::Buffer.size_of([:u128, :u128]) + end + + def test_integer_endianness_swapping + # Test that byte order is swapped correctly for all signed and unsigned integers > 1 byte + host_is_le = IO::Buffer::HOST_ENDIAN == IO::Buffer::LITTLE_ENDIAN + host_is_be = IO::Buffer::HOST_ENDIAN == IO::Buffer::BIG_ENDIAN + + # Test values that will produce different byte patterns when swapped + # Format: [little_endian_type, big_endian_type, test_value, expected_swapped_value] + # expected_swapped_value is the result when writing as le_type and reading as be_type + # (or vice versa) on a little-endian host + test_cases = [ + [:u16, :U16, 0x1234, 0x3412], + [:s16, :S16, 0x1234, 0x3412], + [:u32, :U32, 0x12345678, 0x78563412], + [:s32, :S32, 0x12345678, 0x78563412], + [:u64, :U64, 0x0123456789ABCDEF, 0xEFCDAB8967452301], + [:s64, :S64, 0x0123456789ABCDEF, -1167088121787636991], + [:u128, :U128, 0x0123456789ABCDEF0123456789ABCDEF, 0xEFCDAB8967452301EFCDAB8967452301], + [:u128, :U128, 0x0123456789ABCDEFFEDCBA9876543210, 0x1032547698BADCFEEFCDAB8967452301], + [:u128, :U128, 0xFEDCBA98765432100123456789ABCDEF, 0xEFCDAB89674523011032547698BADCFE], + [:u128, :U128, 0x123456789ABCDEF0FEDCBA9876543210, 0x1032547698BADCFEF0DEBC9A78563412], + [:s128, :S128, 0x0123456789ABCDEF0123456789ABCDEF, -21528975894082904073953971026863512831], + [:s128, :S128, 0x0123456789ABCDEFFEDCBA9876543210, 0x1032547698BADCFEEFCDAB8967452301], + ] + + test_cases.each do |le_type, be_type, value, expected_swapped| + buffer_size = IO::Buffer.size_of(le_type) + buffer = IO::Buffer.new(buffer_size * 2) + + # Test little-endian round-trip + buffer.set_value(le_type, 0, value) + result_le = buffer.get_value(le_type, 0) + assert_equal value, result_le, "#{le_type}: round-trip failed" + + # Test big-endian round-trip + buffer.set_value(be_type, buffer_size, value) + result_be = buffer.get_value(be_type, buffer_size) + assert_equal value, result_be, "#{be_type}: round-trip failed" + + # Verify byte patterns are different when endianness differs from host + le_bytes = buffer.get_string(0, buffer_size) + be_bytes = buffer.get_string(buffer_size, buffer_size) + + if host_is_le + # On little-endian host: le_type should match host, be_type should be swapped + # So the byte patterns should be different (unless value is symmetric) + # Read back with opposite endianness to verify swapping + result_le_read_as_be = buffer.get_value(be_type, 0) + result_be_read_as_le = buffer.get_value(le_type, buffer_size) + + # The swapped reads should NOT equal the original value (unless it's symmetric) + # For most values, this will be different + if value != 0 && value != -1 && value.abs != 1 + refute_equal value, result_le_read_as_be, "#{le_type} written, read as #{be_type} should be swapped on LE host" + refute_equal value, result_be_read_as_le, "#{be_type} written, read as #{le_type} should be swapped on LE host" + end + + # Verify that reading back with correct endianness works + assert_equal value, buffer.get_value(le_type, 0), "#{le_type} should read correctly on LE host" + assert_equal value, buffer.get_value(be_type, buffer_size), "#{be_type} should read correctly on LE host (with swapping)" + elsif host_is_be + # On big-endian host: be_type should match host, le_type should be swapped + result_le_read_as_be = buffer.get_value(be_type, 0) + result_be_read_as_le = buffer.get_value(le_type, buffer_size) + + # The swapped reads should NOT equal the original value (unless it's symmetric) + if value != 0 && value != -1 && value.abs != 1 + refute_equal value, result_le_read_as_be, "#{le_type} written, read as #{be_type} should be swapped on BE host" + refute_equal value, result_be_read_as_le, "#{be_type} written, read as #{le_type} should be swapped on BE host" + end + + # Verify that reading back with correct endianness works + assert_equal value, buffer.get_value(be_type, buffer_size), "#{be_type} should read correctly on BE host" + assert_equal value, buffer.get_value(le_type, 0), "#{le_type} should read correctly on BE host (with swapping)" + end + + # Verify that when we write with one endianness and read with the opposite, + # we get the expected swapped value + buffer.set_value(le_type, 0, value) + swapped_value_le_to_be = buffer.get_value(be_type, 0) + assert_equal expected_swapped, swapped_value_le_to_be, "#{le_type} written, read as #{be_type} should produce expected swapped value" + + # Also verify the reverse direction + buffer.set_value(be_type, buffer_size, value) + swapped_value_be_to_le = buffer.get_value(le_type, buffer_size) + assert_equal expected_swapped, swapped_value_be_to_le, "#{be_type} written, read as #{le_type} should produce expected swapped value" + + # Verify that writing the swapped value back and reading with original endianness + # gives us the original value (double-swap should restore original) + buffer.set_value(be_type, 0, swapped_value_le_to_be) + round_trip_value = buffer.get_value(le_type, 0) + assert_equal value, round_trip_value, "#{le_type}/#{be_type}: double-swap should restore original value" + end + end end |
