diff --git a/src/cpyrt/CPPDataMember.cxx b/src/cpyrt/CPPDataMember.cxx index c2543dc..da970a6 100644 --- a/src/cpyrt/CPPDataMember.cxx +++ b/src/cpyrt/CPPDataMember.cxx @@ -16,12 +16,43 @@ using namespace cppjit; // Standard #include +#include #include #include #include +#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__) +#error "cpyrt bit-field access assumes a little-endian byte order" +#endif + +// Not conditional on the target's pointer width: "unsigned long long x : 64" +// is legal on a 32-bit target too, so a 9-byte span is reachable everywhere +// and a 64-bit accumulator is never sufficient. +#if !defined(__SIZEOF_INT128__) +#error "cpyrt bit-field access needs unsigned __int128 (a bit-field span can \ +reach 9 bytes); no MSVC/32-bit fallback is implemented" +#endif + namespace cppjit::cpyrt { +// Byte span a bit-field occupies, derived from (bit offset, bit width) -- +// never from the declared type's width, which would over-read a packed +// struct's trailing member. Preconditions, enforced in Set(): fBitWidth is +// in [1,64], so nbytes is in [1,9] and always fits the 16-byte accumulator. +struct BitFieldSpan { + int shift; // bit position within the first byte, 0..7 + int nbytes; // bytes to read/write, 1..9 + unsigned __int128 mask; // fBitWidth low bits set +}; + +static inline BitFieldSpan bitfield_span(intptr_t bit_offset, int bit_width) { + BitFieldSpan s; + s.shift = (int)(bit_offset % 8); + s.nbytes = (s.shift + bit_width + 7) / 8; + s.mask = ((unsigned __int128)1 << bit_width) - 1; + return s; +} + enum ETypeDetails { kNone = 0x0000, kIsStaticData = 0x0001, @@ -29,7 +60,10 @@ enum ETypeDetails { kIsArrayType = 0x0004, kIsEnumPrep = 0x0008, kIsEnumType = 0x0010, - kIsCachable = 0x0020 + kIsCachable = 0x0020, + kIsBitField = 0x0040, + kIsSignedBitField = 0x0080, + kIsBoolBitField = 0x0100 }; //= cpyrt data member as Python property behavior ========================= @@ -98,6 +132,34 @@ static PyObject* dm_get(CPPDataMember* dm, CPPInstance* pyobj, if (!address || (intptr_t)address == -1 /* Cling error */) return nullptr; + if (dm->fFlags & kIsBitField) { + // Read only the bytes this field actually occupies: never the declared + // type's width, which is unknowable from the type name and would + // over-read a packed struct's last member. + const BitFieldSpan span = bitfield_span(dm->fBitOffset, dm->fBitWidth); + unsigned __int128 word = 0; + std::memcpy(&word, address, (size_t)span.nbytes); + + const unsigned __int128 extracted = (word >> span.shift) & span.mask; + + if (dm->fFlags & kIsBoolBitField) + return PyBool_FromLong((long)(extracted != 0)); + + if (dm->fFlags & kIsSignedBitField) { + // sign-extend from fBitWidth; fBitWidth > 0 is enforced in Set(), so + // this shift amount is never negative. + const unsigned __int128 one = 1; + const unsigned __int128 sign_bit = one << (dm->fBitWidth - 1); + if (extracted & sign_bit) { + const long long sval = + (long long)(extracted | ~(span.mask)); // fill above with 1s + return PyLong_FromLongLong(sval); + } + return PyLong_FromLongLong((long long)extracted); + } + return PyLong_FromUnsignedLongLong((unsigned long long)extracted); + } + if (dm->fConverter != 0) { PyObject* result = dm->fConverter->FromMemory( (dm->fFlags & kIsArrayType) ? &address : address); @@ -176,6 +238,58 @@ static int dm_set(CPPDataMember* dm, CPPInstance* pyobj, PyObject* value) { if (!address || address == -1 /* Cling error */) return errret; + if (dm->fFlags & kIsBitField) { + if (dm->fFlags & kIsBoolBitField) { + // Mirror cpyrt_PyLong_AsBool in Converters.cxx exactly: a bool + // member accepts only a bool or the integers 0 and 1, and a float is + // rejected outright even where it would convert. A PyLong_AsLong + // failure returns -1, which is neither 0 nor 1, so it falls into the + // same ValueError -- deliberately replacing the original TypeError or + // OverflowError, so a bit-field reports precisely what a non-bit-field + // bool member reports. + if (!PyBool_Check(value)) { + const long as_long = PyLong_AsLong(value); + if (!(as_long == 0 || as_long == 1) || PyFloat_Check(value)) { + PyErr_SetString(PyExc_ValueError, + "boolean value should be bool, or integer 1 or 0"); + return errret; + } + } + } + + // Documented divergence from the non-bit-field path, not an oversight: the + // ...Mask conversion truncates out-of-range values silently, so "bf : 4 = + // -1" stores 15 and "bf : 4 = 2**100" stores 0, where the same assignment + // to a plain "unsigned" member raises ValueError. Truncation of a negative + // is ordinary C++ bit-field behaviour and test04 codifies it; the 2**100 + // case discards an error Python would otherwise report. Kept as-is because + // masking is what the stored width means, and range-checking here would + // have to pick a signedness the declared type does not settle. + // + // Clearing first is what makes the failure test below trustworthy: the + // sentinel (unsigned long long)-1 is also a legitimate result (that is + // exactly what "= -1" masks to), so a stale error set before dm_set was + // entered would otherwise turn a valid write into a spurious failure. + // Same reasoning as the stale-error handling in CPPScope.cxx. + PyErr_Clear(); + const unsigned long long raw = PyLong_AsUnsignedLongLongMask(value); + if (raw == (unsigned long long)-1 && PyErr_Occurred()) + return errret; + + // fBitWidth is in [1, 64] here -- Set() only sets kIsBitField under that + // precondition -- so bitfield_span's shift is always well-defined; no + // need to guard against a 128-bit field. + const BitFieldSpan span = bitfield_span(dm->fBitOffset, dm->fBitWidth); + + // read-modify-write, so sibling bit-fields sharing these bytes survive + unsigned __int128 word = 0; + std::memcpy(&word, (void*)address, (size_t)span.nbytes); + word &= ~(span.mask << span.shift); + word |= ((unsigned __int128)raw & span.mask) << span.shift; + std::memcpy((void*)address, &word, (size_t)span.nbytes); + return 0; + } + // for fixed size arrays void* ptr = (void*)address; if (dm->fFlags & kIsArrayType) @@ -205,6 +319,8 @@ static CPPDataMember* dm_new(PyTypeObject* pytype, PyObject*, PyObject*) { dm->fEnclosingScope = nullptr; dm->fDescription = nullptr; dm->fDoc = nullptr; + dm->fBitOffset = 0; + dm->fBitWidth = 0; new (&dm->fFullType) std::string{}; @@ -325,12 +441,9 @@ void cpyrt::CPPDataMember::Set(interop::TCppScope_t scope, } fEnclosingScope = scope; - fOffset = interop::GetDatamemberOffset( - fScope, fScope == data - ? scope - : interop::GetScope( - "__cppjit_internal_wrap_g")); // XXX: Check back here // - // TODO: make lazy + const interop::TCppScope_t offset_parent = + fScope == data ? scope : interop::GetScope("__cppjit_internal_wrap_g"); + fOffset = interop::GetDatamemberOffset(fScope, offset_parent); fFlags = interop::IsStaticDatamember(fScope) ? kIsStaticData : 0; const std::string name = interop::GetFinalName(fScope); @@ -359,6 +472,67 @@ void cpyrt::CPPDataMember::Set(interop::TCppScope_t scope, fFlags |= kIsConstData; } + // Bit-fields need masked access: cache the layout facts once here so the + // attribute-access path never has to take the interop lock. A bit-field is + // never static, so fOffset is a genuine byte offset. + if (!(fFlags & kIsStaticData) && interop::IsBitFieldDatamember(fScope)) { + const intptr_t bit_offset = + interop::GetDatamemberBitOffset(fScope, offset_parent); + const int bit_width = interop::GetDatamemberBitWidth(fScope); + // Cap at 64 bits: dm_get's memcpy destination is a 16-byte + // unsigned __int128, and nbytes = ceil((shift + bit_width) / 8) with + // shift in [0, 7] needs bit_width <= 64 to stay within 9 bytes <= 16. + // Gating on width alone (rather than shift + bit_width <= 128) also + // rules out an unpacked "unsigned __int128 x : 128" (shift == 0, so + // that inequality would pass) whose 64-bit extraction would otherwise + // silently truncate. A wider bit-field leaves kIsBitField unset and + // falls through to fConverter, whose base Converter::FromMemory has no + // override for __int128 and raises a clean TypeError -- the + // pre-existing behaviour. + // + // The fOffset == bit_offset / 8 term is the invariant dm_get's masked + // access rests on, checked rather than asserted: it must hold by + // construction, since Cpp::GetVariableBitOffset is defined as + // GetVariableOffset(var, parent) * 8 + getFieldOffset(FD) % 8, so the + // byte offset is baked into the bit offset's high bits for any + // non-negative result. But an assert is compiled out of the Release + // builds this ships as, and if a future CppInterOp change ever + // desynchronises the two the failure mode is a garbage read at a valid + // address -- silent wrong data, not a crash. Declining to treat the + // member as a bit-field instead falls back to the pre-existing + // converter path, which is merely wrong for packed layouts rather than + // arbitrary. One compare per descriptor construction, not per access. + if (bit_offset >= 0 && bit_width > 0 && bit_width <= 64 && + fOffset == bit_offset / 8) { + fFlags |= kIsBitField; + fBitOffset = bit_offset; + fBitWidth = bit_width; + + // Name-based, unlike everything else here: there is no IsBoolType + // query, and IsIntegerType reports bool as an unsigned integer. Exact + // match (not a substring search) is deliberate: a substring search + // would also fire on a typedef like "bool_flags_t" that merely + // contains "bool", turning an integer into True/False. The trade-off + // is the opposite direction -- a bit-field declared through a + // typedef *of* bool still reads back as 0/1 rather than True/False -- + // a presentation difference, not a wrong value. + // + // No equivalent flag exists for char, and that is a real, documented + // divergence: a plain "char c" member goes through CharConverter and + // reads back as a one-character Python str ('A'), while "char c : 5" + // takes the masked path here and reads back an int (-3 for the bits + // 0b11101). signed char and unsigned char bit-fields diverge the same + // way -- int rather than str, unsigned char merely not sign-extending. + // Only bool was given parity; char keeps the integer presentation. + if (fFullType == "bool") + fFlags |= kIsBoolBitField; + + bool is_signed = false; + if (interop::IsIntegerType(type, &is_signed) && is_signed) + fFlags |= kIsSignedBitField; + } + } + auto ldims = interop::GetDimensions(type); std::vector dims(ldims.begin(), ldims.end()); diff --git a/src/cpyrt/CPPDataMember.h b/src/cpyrt/CPPDataMember.h index d3b0d49..5f847ea 100644 --- a/src/cpyrt/CPPDataMember.h +++ b/src/cpyrt/CPPDataMember.h @@ -27,6 +27,10 @@ class CPPDataMember { interop::TCppScope_t fEnclosingScope; PyObject* fDescription; PyObject* fDoc; + // intptr_t, matching fOffset and interop::GetDatamemberBitOffset: an int + // would truncate for a member past 256 MiB into its enclosing object. + intptr_t fBitOffset; // total bit offset in the object; iff kIsBitField + int fBitWidth; // declared bit width, in [1,64]; iff kIsBitField // TODO: data members should have a unique identifier, just like methods, // so that reflection information can be recovered post-initialization diff --git a/src/interop/cppjit_interop.h b/src/interop/cppjit_interop.h index ca7c07e..f3881bf 100644 --- a/src/interop/cppjit_interop.h +++ b/src/interop/cppjit_interop.h @@ -358,6 +358,12 @@ std::string GetTypeAsString(TCppType_t type); RPY_EXPORTED intptr_t GetDatamemberOffset(TCppScope_t var, TCppScope_t klass = nullptr); RPY_EXPORTED +bool IsBitFieldDatamember(TCppScope_t var); +RPY_EXPORTED +intptr_t GetDatamemberBitOffset(TCppScope_t var, TCppScope_t klass = nullptr); +RPY_EXPORTED +int GetDatamemberBitWidth(TCppScope_t var); +RPY_EXPORTED bool CheckDatamember(TCppScope_t scope, const std::string& name); // // data member properties diff --git a/src/interop/interop_wrapper.cxx b/src/interop/interop_wrapper.cxx index 1b5b0eb..c4017b4 100644 --- a/src/interop/interop_wrapper.cxx +++ b/src/interop/interop_wrapper.cxx @@ -1658,6 +1658,21 @@ intptr_t interop::GetDatamemberOffset(TCppScope_t var, TCppScope_t klass) { return Cpp::GetVariableOffset(Cpp::GetUnderlyingScope(var), klass); } +bool interop::IsBitFieldDatamember(TCppScope_t var) { + std::lock_guard Lock(InterOpMutex); + return Cpp::IsBitFieldVariable(Cpp::GetUnderlyingScope(var)); +} + +intptr_t interop::GetDatamemberBitOffset(TCppScope_t var, TCppScope_t klass) { + std::lock_guard Lock(InterOpMutex); + return Cpp::GetVariableBitOffset(Cpp::GetUnderlyingScope(var), klass); +} + +int interop::GetDatamemberBitWidth(TCppScope_t var) { + std::lock_guard Lock(InterOpMutex); + return Cpp::GetVariableBitWidth(Cpp::GetUnderlyingScope(var)); +} + // data member properties ---------------------------------------------------- bool interop::IsPublicData(TCppScope_t datamem) { return Cpp::IsPublicVariable(datamem); diff --git a/test/test_datatypes.py b/test/test_datatypes.py index 2983d4d..992a63b 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -2683,3 +2683,567 @@ def test55_qt_cache_alias_collision(self): ns.take_schar("e") ns.take_int8(101) raises(TypeError, ns.take_int8, "e") + + +class TestBITFIELDS: + def setup_class(cls): + import cppjit + + cppjit.cppdef(r""" + struct BitFieldTest { + unsigned int a : 1; + unsigned int b : 2; + unsigned int c : 4; + unsigned int g :12; + unsigned int d : 1; + unsigned int e : 8; + unsigned int f :16; + + BitFieldTest() + : a(1), b(0x3), c(0xF), g(0xABC), d(0), e(0x33), f(0x5555) {} + }; + """) + + def test01_read_unsigned_bitfields(self): + """Read unsigned bitfield values (cppyy issue #57 reproducer). + + `g` is 12 bits wide and, given `a`+`b`+`c` = 7 bits ahead of it, + starts at a non-byte-aligned bit offset and spans a byte boundary -- + exercising the shift+mask+multi-byte path that byte-aligned, + byte-multiple-width fields (like `e` and `f`) do not. `b` and `c` + are nonzero so a stray fix that is correct only because `nbytes` + happens to bound the read cannot pass by accident. + """ + + import cppjit + + f = cppjit.gbl.BitFieldTest() + assert f.a == 1 + assert f.b == 0x3 + assert f.c == 0xF + assert f.g == 0xABC + assert f.d == 0 + assert f.e == 0x33 + assert f.f == 0x5555 + + def test02_wide_bitfield_raises_cleanly(self): + """A bit-field wider than 64 bits must be refused, not masked. + + `Set()` must refuse to mark this as a masked-read bit-field -- + otherwise dm_get's `unsigned __int128 word` memcpy destination + (16 bytes) would be overrun by the 17-byte `nbytes` a 128-bit + field computes. It should fall through to the ordinary converter path + instead: the base `Converter::FromMemory` has no override for + `__int128` and raises `TypeError` -- observed, not assumed -- so + the access must fail cleanly rather than crash or return a + silently truncated value. + """ + + import cppjit + + cppjit.cppdef(r""" + struct __attribute__((packed)) WideBitField { + unsigned char a : 1; + unsigned __int128 x : 128; + }; + """) + + w = cppjit.gbl.WideBitField() + raises(TypeError, getattr, w, "x") + + def test03_write_unsigned_bitfields(self): + """Write individual bitfield members without corrupting neighbours. + + The fixture's current initialisers are + a=1, b=0x3, c=0xF, g=0xABC, d=0, e=0x33, f=0x5555. + `c` is written to a value it does not already hold, so the write is + not a no-op, and every other field -- including `g`, which starts at + bit 7 and crosses a byte boundary -- is asserted unchanged. `g` is + the most sensitive neighbour: a read-modify-write that used the + declared type's width instead of the field's own byte span would + disturb it. + """ + + import cppjit + + f = cppjit.gbl.BitFieldTest() + + f.c = 0x5 + assert f.c == 0x5 + assert f.a == 1 + assert f.b == 0x3 + assert f.g == 0xABC + assert f.d == 0 + assert f.e == 0x33 + assert f.f == 0x5555 + + def test04_write_truncation(self): + """Writing a value wider than the bitfield truncates to fit. + + This is a deliberate divergence from non-bit-field members, and it + has two halves. Truncating a too-wide positive value, and storing a + negative as its two's-complement low bits (`bf : 4 = -1` gives 15), + is ordinary C++ bit-field behaviour and is what the assertions below + codify. The other half is a genuine loss: dm_set converts through + PyLong_AsUnsignedLongLongMask, which truncates without raising, so + `bf : 4 = 2**100` silently stores 0 where assigning 2**100 to a + plain `unsigned` member raises ValueError. Documented, not fixed -- + range-checking here would have to pick a signedness the declared + type does not settle. + """ + + import cppjit + + f = cppjit.gbl.BitFieldTest() + f.a = 0xFF + assert f.a == 1 # 1-bit field, 0xFF & 1 == 1 + + f.b = 0xFF + assert f.b == 3 # 2-bit field, 0xFF & 3 == 3 + + f.c = 0xF0 + assert f.c == 0 # 4-bit field, 0xF0 & 0xF == 0 + + # a truncating write must still not spill into neighbours + assert f.g == 0xABC + assert f.e == 0x33 + assert f.f == 0x5555 + + f.g = 0xFFFF + assert f.g == 0xFFF # 12-bit field, 0xFFFF & 0xFFF == 0xFFF + assert f.d == 0 + assert f.e == 0x33 + + def test05_signed_bitfields(self): + """Signed bitfields sign-extend on read""" + + import cppjit + + cppjit.cppdef(r""" + struct SignedBitFieldTest { + int x : 3; + int y : 5; + int z : 24; + SignedBitFieldTest() : x(-1), y(-16), z(-0x555555) {} + }; + """) + + f = cppjit.gbl.SignedBitFieldTest() + assert f.x == -1 + assert f.y == -16 + # z is the only signed field here wider than a byte: it starts at bit + # 8 and spans three bytes, so a sign extension driven by the declared + # type's 32 bits rather than the field's 24 would read 0xAAAAAB. + assert f.z == -0x555555 + + f.x = 3 + assert f.x == 3 + + f.x = -2 + assert f.x == -2 + assert f.y == -16 + assert f.z == -0x555555 + + # a wide signed round trip, both signs, with the narrow neighbours + # asserted intact: the write masks to 24 bits and the read + # sign-extends from bit 23 + f.z = 0x7FFFFF + assert f.z == 0x7FFFFF + f.z = -0x800000 + assert f.z == -0x800000 + assert f.x == -2 + assert f.y == -16 + + def test06_typedef_unsigned_not_sign_extended(self): + """uint32_t/uint64_t bitfields must NOT be treated as signed""" + + import cppjit + + cppjit.cppdef(r""" + #include + struct TypedefBitFields { + uint32_t a : 20; + uint64_t b : 40; + TypedefBitFields() : a(0xFFFFF), b(0xFFFFFFFFFFULL) {} + }; + """) + + f = cppjit.gbl.TypedefBitFields() + # the whole point: a name-based signedness guess would return -1 here + assert f.a == 0xFFFFF + assert f.b == 0xFFFFFFFFFF + + def test07_bool_bitfields(self): + """bool bitfields behave like non-bitfield bools, reading AND writing. + + Reads must yield Python bools, and writes must reject non-boolean + values exactly as BoolConverter::ToMemory does. Bypassing the + converter for masked access must not silently widen the contract to + "any truthy value" -- before this was fixed, `p = 2` stored False. + """ + + import cppjit + + cppjit.cppdef(r""" + struct BoolBitFields { + bool p : 1; + bool q : 1; + unsigned int r : 6; + BoolBitFields() : p(true), q(false), r(0x2A) {} + }; + """) + + f = cppjit.gbl.BoolBitFields() + assert f.p is True + assert f.q is False + + f.q = True + assert f.q is True + assert f.p is True + + f.q = False + assert f.q is False + + # integers 0 and 1 are accepted, like a non-bitfield bool member + f.q = 1 + assert f.q is True + f.q = 0 + assert f.q is False + + # anything else is rejected rather than coerced + raises(ValueError, setattr, f, 'q', 2) + raises(ValueError, setattr, f, 'q', -1) + + # non-integers are rejected with the same ValueError a non-bitfield + # bool member gives, not with the underlying TypeError/OverflowError + raises(ValueError, setattr, f, 'q', 2.0) + raises(ValueError, setattr, f, 'q', "x") + raises(ValueError, setattr, f, 'q', None) + raises(ValueError, setattr, f, 'q', 2**100) + + # floats are rejected even when they would convert cleanly + raises(ValueError, setattr, f, 'q', 1.0) + raises(ValueError, setattr, f, 'q', 0.0) + + # every rejected write left the object untouched + assert f.q is False + assert f.p is True + assert f.r == 0x2A + + def test08_enum_bitfields(self): + """enum-typed bitfields resolve to the enum's underlying integer type. + + The underlying type is deliberately signed with a value whose high bit + is set inside the field: an unsigned enum reads the same whether + resolution happened or was skipped, so it cannot tell a working + IsEnumType/ResolveType path from a broken one. + """ + + import cppjit + + cppjit.cppdef(r""" + enum SignedColor : int { SC_NEG = -4, SC_POS = 3 }; + enum UnsignedColor : unsigned int { UC_HIGH = 3 }; + struct EnumBitFields { + SignedColor s : 3; + UnsignedColor u : 2; + unsigned int rest : 6; + EnumBitFields() : s(SC_NEG), u(UC_HIGH), rest(0x2A) {} + }; + """) + + f = cppjit.gbl.EnumBitFields() + # -4 in a signed 3-bit field is 0b100; failing to resolve the enum to + # its signed underlying type would read 4 instead of -4. + assert int(f.s) == -4 + assert int(f.u) == 3 + assert f.rest == 0x2A + + def test09_multi_unit_bitfields(self): + """Bitfields spanning multiple storage units. + + Every one of the five fields here is byte-aligned, so shift == 0 + throughout despite the "multi-unit" name -- this genuinely catches an + unmasked full-width store clobbering a neighbour (the historical bug), + but nonzero-shift and byte-crossing behaviour is exercised by `g` in + test01/test03/test04, not here. + """ + + import cppjit + + cppjit.cppdef(r""" + struct MultiBitFieldUnit { + unsigned int first : 16; + unsigned int second : 16; + unsigned int third : 8; + unsigned int fourth : 8; + unsigned int fifth : 16; + MultiBitFieldUnit() + : first(0xAAAA), second(0x5555), + third(0xBB), fourth(0xCC), fifth(0xDDDD) {} + }; + """) + + m = cppjit.gbl.MultiBitFieldUnit() + assert m.first == 0xAAAA + assert m.second == 0x5555 + assert m.third == 0xBB + assert m.fourth == 0xCC + assert m.fifth == 0xDDDD + + m.first = 0x1234 + assert m.first == 0x1234 + assert m.second == 0x5555 + + def test10_mixed_and_full_width(self): + """Non-bitfield neighbours, full-width and zero-width fields. + + The `unsigned int : 0` separator forces `p` and `q` into different + storage units, and `plain` never shares a unit with `p` either -- so + those neighbour assertions can only catch a grossly wrong `fOffset`, + not a masking or `nbytes` defect. Same-storage-unit sibling + protection is covered by test09 and test12 instead. What this test + does add: the `w : 32` assertion pins `nbytes == 4` at an exact + byte-multiple boundary with the mask spanning all 32 bits, where an + off-by-one `nbytes` would truncate the high bits and fail. + + `m.lead == 'L'` below also pins the one place a `char` divergence is + visible in this file: a plain `char` member reads back as a + one-character Python str, whereas a `char` bit-field takes the masked + integer path and reads back as an int, sign-extended from its own + width: `char bc : 5` holding 0b11101 reads -3, not a str. `signed + char` and `unsigned char` bit-fields diverge the same way (int rather + than str; `unsigned char` simply does not sign-extend). Only + `bool` was given bit-field/non-bit-field parity, via + kIsBoolBitField; `char` keeps the integer presentation by design. + """ + + import cppjit + + cppjit.cppdef(r""" + struct MixedBitFields { + char lead; + unsigned int w : 32; + int plain; + unsigned int p : 3; + unsigned int : 0; // force next field to a new unit + unsigned int q : 3; + MixedBitFields() : lead('L'), w(0xDEADBEEF), plain(-7), + p(5), q(6) {} + }; + """) + + m = cppjit.gbl.MixedBitFields() + assert m.lead == 'L' + assert m.w == 0xDEADBEEF + assert m.plain == -7 + assert m.p == 5 + assert m.q == 6 + + m.p = 2 + assert m.p == 2 + assert m.q == 6 + assert m.w == 0xDEADBEEF + assert m.plain == -7 + + def test11_packed_last_member(self): + """A bitfield as the last member of a packed struct. + + This pins the VALUES: `tail` occupies a 1-byte span at byte offset 1 + of a 2-byte struct, and reading or writing it must not disturb `head`. + + It does NOT, and cannot, detect the over-read itself. Both fields sit + at shift == 0, so reading the declared type's 4 bytes instead of the + field's 1 would extract the same masked value -- the out-of-bounds + bytes land in bit positions the mask discards -- and a masked + read-modify-write writes them back unchanged. An adjacent canary does + not help for the same reason. Nothing in this file asserts it. + + The no-over-read property is guarded solely by running the suite + under valgrind, in the `vg: true` cells of + .github/workflows/ci.yml and .github/workflows/nightly.yml. Any + change that narrows or drops those cells silently deletes the only + check on this, since the value assertions below cannot fail on an + over-read. + """ + + import cppjit + + cppjit.cppdef(r""" + struct __attribute__((packed)) PackedTail { + unsigned int head : 8; + unsigned int tail : 4; + PackedTail() : head(0x7F), tail(0xD) {} + }; + """) + + p = cppjit.gbl.PackedTail() + assert p.head == 0x7F + assert p.tail == 0xD + + p.tail = 0x3 + assert p.tail == 0x3 + assert p.head == 0x7F + + def test12_inherited_and_anonymous(self): + """Bitfields from a base class and inside an anonymous struct""" + + import cppjit + + cppjit.cppdef(r""" + struct BFBase { unsigned int bb : 6; BFBase() : bb(0x2A) {} }; + struct BFDerived : BFBase { + unsigned int dd : 6; + BFDerived() : dd(0x15) {} + }; + struct AnonHolder { + char pad; + struct { unsigned int u : 3; unsigned int v : 5; }; + AnonHolder() : pad('P') { u = 5; v = 20; } + }; + """) + + d = cppjit.gbl.BFDerived() + assert d.bb == 0x2A + assert d.dd == 0x15 + d.dd = 0x0A + assert d.dd == 0x0A + assert d.bb == 0x2A + + a = cppjit.gbl.AnonHolder() + assert a.pad == 'P' + assert a.u == 5 + assert a.v == 20 + a.u = 2 + assert a.u == 2 + assert a.v == 20 + assert a.pad == 'P' + + def test13_const_bitfield_read(self): + """A const bitfield reads correctly and rejects assignment""" + + import cppjit + + cppjit.cppdef(r""" + struct ConstBitField { + const unsigned int cb : 5; + unsigned int other : 3; + ConstBitField() : cb(0x15), other(0x5) {} + }; + """) + + c = cppjit.gbl.ConstBitField() + assert c.cb == 0x15 + assert c.other == 0x5 + raises(TypeError, setattr, c, 'cb', 1) + + # the rejected write must not have touched memory: a guard that fired + # after the read-modify-write would still raise, yet leave cb changed + assert c.cb == 0x15 + assert c.other == 0x5 + + def test14_nine_byte_span(self): + """The widest span the implementation admits: nbytes == 9. + + `b` is 64 bits wide starting at bit offset 7, so + nbytes = (7 + 64 + 7) / 8 == 9 -- the maximum the [1,64] width gate + allows, and the sole reason dm_get/dm_set accumulate into an + `unsigned __int128` rather than a `uint64_t`. A 64-bit accumulator + would drop `b`'s top 7 bits on read and, worse, write back only 8 of + the 9 bytes. Nothing else in this file reaches past nbytes == 8, so + without this test that choice is untested. + """ + + import cppjit + + cppjit.cppdef(r""" + struct __attribute__((packed)) NineByteSpan { + unsigned long long a : 7; + unsigned long long b : 64; + NineByteSpan() : a(0x55), b(0xDEADBEEFCAFEBABEULL) {} + }; + """) + + n = cppjit.gbl.NineByteSpan() + assert n.a == 0x55 + assert n.b == 0xDEADBEEFCAFEBABE + + n.b = 0x1122334455667788 + assert n.b == 0x1122334455667788 + assert n.a == 0x55 + + def test15_union_bitfields(self): + """Bit-fields inside an anonymous union and inside a named union. + + test12 covers an anonymous *struct*; a union member's offset is + computed by a different path (all members share byte offset 0 within + the union), so the anonymous-union case is not implied by it. + """ + + import cppjit + + cppjit.cppdef(r""" + struct UnionHolder { + char pad; + union { unsigned int uu : 5; unsigned int vv : 5; }; + union Named { unsigned int nn : 6; unsigned int mm : 6; }; + Named named; + unsigned int trailer : 4; + UnionHolder() : pad('U'), trailer(0xB) { uu = 21; named.nn = 42; } + }; + """) + + h = cppjit.gbl.UnionHolder() + assert h.pad == 'U' + # uu and vv alias the same bits, so both read the value written + assert h.uu == 21 + assert h.vv == 21 + assert h.named.nn == 42 + assert h.named.mm == 42 + assert h.trailer == 0xB + + h.vv = 10 + assert h.vv == 10 + assert h.uu == 10 + assert h.pad == 'U' + assert h.trailer == 0xB + + h.named.mm = 63 + assert h.named.nn == 63 + + def test16_signed_full_width(self): + """A signed bit-field occupying its declared type's full width. + + `int x : 32` is the signed counterpart of test10's `unsigned int + w : 32`: mask, nbytes and sign-extension width all sit exactly at the + type's boundary, where an off-by-one in any of them shows up as a + wrong sign or a truncated magnitude rather than as a crash. INT_MIN + is the value that catches a sign extension driven by anything other + than the field's own width. + """ + + import cppjit + + cppjit.cppdef(r""" + struct SignedFullWidth { + int x : 32; + unsigned int tail : 8; + SignedFullWidth() : x(-1), tail(0x5A) {} + }; + """) + + s = cppjit.gbl.SignedFullWidth() + assert s.x == -1 + assert s.tail == 0x5A + + s.x = -0x80000000 + assert s.x == -0x80000000 + assert s.tail == 0x5A + + s.x = 0x7FFFFFFF + assert s.x == 0x7FFFFFFF + assert s.tail == 0x5A + + s.x = 0 + assert s.x == 0 + assert s.tail == 0x5A