Skip to content

Commit 9636e35

Browse files
authored
Improve handling of bitfields in ctypes (#1441)
* Support bool values on ctypes bitfields * Support large int values on ctypes bitfields * Support multiple c_bool bitfields (cpython gh90914) * Align __repr__ of bitfields with CPython * Align reported "size" of bitfields with CPython * Add tests * Support big endian bitfields * Enable CPython.ctypes.test_bitfields * Update gen_stdlib_test to handle tests in packages * Enable select tests * Disable ctypes test on macOS * Update after review
1 parent 935b694 commit 9636e35

8 files changed

Lines changed: 338 additions & 103 deletions

File tree

Src/IronPython.Modules/_ctypes/Field.cs

Lines changed: 115 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public static partial class CTypes {
2626
[PythonType, PythonHidden]
2727
public sealed class Field : PythonTypeDataSlot, ICodeFormattable {
2828
private readonly INativeType _fieldType;
29-
private readonly int _offset, _index, _bits = -1, _bitsOffset;
29+
private readonly int _offset, _index, _bits, _bitsOffset;
3030
private readonly string _fieldName;
3131

3232
internal Field(string fieldName, INativeType fieldType, int offset, int index) {
@@ -42,51 +42,41 @@ internal Field(string fieldName, INativeType fieldType, int offset, int index, i
4242
_index = index;
4343
_fieldName = fieldName;
4444

45-
// if the number of bits is the full type width, we don't
46-
// need to do any masking anywhere, we may want to revisit this
47-
// if the __repr__ is an issue
48-
if (bits != null && bits.Value != (_fieldType.Size * 8)) {
45+
if (bits != null) {
46+
Debug.Assert(bitOffset != null);
47+
Debug.Assert(_fieldType is SimpleType);
4948
_bits = bits.Value;
5049
_bitsOffset = bitOffset.Value;
50+
if (((SimpleType)_fieldType)._swap) {
51+
_bitsOffset = _fieldType.Size * 8 - _bitsOffset - _bits;
52+
}
5153
}
5254
}
5355

54-
public int offset {
55-
get {
56-
return _offset;
57-
}
58-
}
56+
public int offset => _offset;
5957

60-
public int size {
61-
get {
62-
return _fieldType.Size;
63-
}
64-
}
58+
public int size => _bits == 0 ? _fieldType.Size : (_bits << 16) + _bitsOffset;
6559

66-
#region Internal APIs
60+
#region Internal APIs
6761

6862
internal override bool TryGetValue(CodeContext context, object instance, PythonType owner, out object value) {
6963
if (instance != null) {
7064
CData inst = (CData)instance;
71-
value = _fieldType.GetValue(inst.MemHolder, inst, _offset, false);
72-
if (_bits == -1) {
65+
if (!IsProperBitfield) {
66+
value = _fieldType.GetValue(inst.MemHolder, inst, _offset, false);
67+
return true;
68+
} else {
69+
value = ExtractBits(inst);
7370
return true;
7471
}
75-
76-
value = ExtractBits(value);
77-
return true;
7872
}
7973

8074
value = this;
8175
return true;
8276
}
8377

8478

85-
internal override bool GetAlwaysSucceeds {
86-
get {
87-
return true;
88-
}
89-
}
79+
internal override bool GetAlwaysSucceeds => true;
9080

9181
internal override bool TrySetValue(CodeContext context, object instance, PythonType owner, object value) {
9282
if (instance != null) {
@@ -98,7 +88,7 @@ internal override bool TrySetValue(CodeContext context, object instance, PythonT
9888
}
9989

10090
internal void SetValue(MemoryHolder address, int baseOffset, object value) {
101-
if (_bits == -1) {
91+
if (!IsProperBitfield) {
10292
object keepAlive = _fieldType.SetValue(address, baseOffset + _offset, value);
10393
if (keepAlive != null) {
10494
address.AddObject(_index.ToString(), keepAlive);
@@ -112,110 +102,134 @@ internal override bool TryDeleteValue(CodeContext context, object instance, Pyth
112102
throw PythonOps.TypeError("cannot delete fields in ctypes structures/unions");
113103
}
114104

115-
internal INativeType NativeType {
116-
get {
117-
return _fieldType;
118-
}
119-
}
105+
internal INativeType NativeType => _fieldType;
120106

121-
internal int? BitCount {
122-
get {
123-
if (_bits == -1) {
124-
return null;
125-
}
126-
127-
return _bits;
128-
}
129-
}
107+
internal int? BitCount => _bits == 0 ? null : _bits;
130108

131-
internal string FieldName {
132-
get {
133-
return _fieldName;
134-
}
135-
}
109+
internal string FieldName => _fieldName;
136110

137-
#endregion
111+
#endregion
138112

139-
#region ICodeFormattable Members
113+
#region ICodeFormattable Members
140114

141115
public string __repr__(CodeContext context) {
142-
if (_bits == -1) {
143-
return String.Format("<Field type={0}, ofs={1}, size={2}>", ((PythonType)_fieldType).Name, offset, size);
116+
if (_bits == 0) {
117+
return string.Format("<Field type={0}, ofs={1}, size={2}>", ((PythonType)_fieldType).Name, offset, size);
144118
}
145-
return String.Format("<Field type={0}, ofs={1}:{2}, bits={3}>", ((PythonType)_fieldType).Name, offset, _bitsOffset, _bits);
119+
return string.Format("<Field type={0}, ofs={1}:{2}, bits={3}>", ((PythonType)_fieldType).Name, offset, _bitsOffset, _bits);
146120
}
147121

148-
#endregion
122+
#endregion
149123

150-
#region Private implementation
124+
#region Private implementation
151125

152126
/// <summary>
153127
/// Called for fields which have been limited to a range of bits. Given the
154128
/// value for the full type this extracts the individual bits.
155129
/// </summary>
156-
private object ExtractBits(object value) {
157-
if (value is int) {
158-
int validBits = ((1 << _bits) - 1);
130+
private object ExtractBits(CData instance) {
131+
Debug.Assert(_bits > 0);
132+
Debug.Assert(_bits < sizeof(ulong) * 8); // Avoid unspecified shifts: ECMA 335: III.3.58-59
133+
Debug.Assert(instance != null);
134+
135+
object value;
136+
if (IsBoolType) {
137+
int valueBits = instance.MemHolder.ReadByte(offset);
138+
int ibVal = ExtractBitsFromInt(valueBits);
139+
value = ScriptingRuntimeHelpers.BooleanToObject(ibVal != 0);
159140

160-
int iVal = (int)value;
161-
iVal = (iVal >> _bitsOffset) & validBits;
162-
if (IsSignedType) {
163-
// need to sign extend if high bit is set
164-
if ((iVal & (1 << (_bits - 1))) != 0) {
165-
iVal |= (-1) ^ validBits;
166-
}
167-
}
168-
value = ScriptingRuntimeHelpers.Int32ToObject(iVal);
169141
} else {
170-
Debug.Assert(value is BigInteger); // we only return ints or big ints from GetValue
171-
ulong validBits = (1UL << _bits) - 1;
142+
value = _fieldType.GetValue(instance.MemHolder, instance, _offset, false);
143+
if (value is int iVal) {
144+
iVal = ExtractBitsFromInt(iVal);
145+
value = ScriptingRuntimeHelpers.Int32ToObject(iVal);
146+
} else if (value is BigInteger biVal) {
147+
ulong validBits = (1UL << _bits) - 1;
148+
149+
ulong bits;
150+
if (IsSignedType) {
151+
bits = (ulong)(long)biVal;
152+
} else {
153+
bits = (ulong)biVal;
154+
}
155+
156+
bits = (bits >> _bitsOffset) & validBits;
157+
158+
if (IsSignedType) {
159+
// need to sign extend if high bit is set
160+
if ((bits & (1UL << (_bits - 1))) != 0) {
161+
bits |= ulong.MaxValue ^ validBits;
162+
}
163+
value = (BigInteger)(long)bits;
164+
} else {
165+
value = (BigInteger)bits;
166+
}
172167

173-
BigInteger biVal = (BigInteger)value;
174-
ulong bits;
175-
if (IsSignedType) {
176-
bits = (ulong)(long)biVal;
177168
} else {
178-
bits = (ulong)biVal;
169+
throw new InvalidOperationException("we only return int, bigint from GetValue");
179170
}
171+
}
172+
return value;
180173

181-
bits = (bits >> _bitsOffset) & validBits;
174+
int ExtractBitsFromInt(int iVal) {
175+
int validBits = ((1 << _bits) - 1);
182176

177+
iVal = (iVal >> _bitsOffset) & validBits;
183178
if (IsSignedType) {
184179
// need to sign extend if high bit is set
185-
if ((bits & (1UL << (_bits - 1))) != 0) {
186-
bits |= ulong.MaxValue ^ validBits;
180+
if ((iVal & (1 << (_bits - 1))) != 0) {
181+
iVal |= (-1) ^ validBits;
187182
}
188-
value = (BigInteger)(long)bits;
189-
} else {
190-
value = (BigInteger)bits;
191183
}
184+
185+
return iVal;
192186
}
193-
return value;
194187
}
195188

196189
/// <summary>
197-
/// Called for fields which have been limited to a range of bits. Sets the
190+
/// Called for fields which have been limited to a range of bits. Sets the
198191
/// specified value into the bits for the field.
199192
/// </summary>
200193
private void SetBitsValue(MemoryHolder address, int baseOffset, object value) {
201194
// get the value in the form of a ulong which can contain the biggest bitfield
202195
ulong newBits;
203-
if (value is int) {
204-
newBits = (ulong)(int)value;
205-
} else if (value is BigInteger) {
206-
newBits = (ulong)(long)(BigInteger)value;
196+
if (IsBoolType) {
197+
newBits = PythonOps.IsTrue(value) ? 1ul : 0ul;
207198
} else {
208-
throw PythonOps.TypeErrorForTypeMismatch("int or long", value);
199+
value = PythonOps.Index(value); // since 3.8
200+
if (value is int iVal) {
201+
newBits = (ulong)iVal;
202+
} else if (value is BigInteger biVal) {
203+
if (biVal.Sign >= 0) {
204+
if (biVal <= ulong.MaxValue) {
205+
newBits = (ulong)biVal;
206+
} else {
207+
newBits = (ulong)(biVal & ulong.MaxValue);
208+
}
209+
} else {
210+
if (biVal >= long.MinValue) {
211+
newBits = (ulong)(long)biVal;
212+
} else {
213+
newBits = (ulong)(biVal & ulong.MaxValue);
214+
}
215+
}
216+
} else {
217+
throw new InvalidOperationException("unreachable");
218+
}
209219
}
210220

211221
// do the same for the existing value
212222
int offset = checked(_offset + baseOffset);
213-
object curValue = _fieldType.GetValue(address, null, offset, false);
214223
ulong valueBits;
215-
if (curValue is int) {
216-
valueBits = (ulong)(int)curValue;
224+
if (IsBoolType) {
225+
valueBits = address.ReadByte(offset);
217226
} else {
218-
valueBits = (ulong)(long)(BigInteger)curValue;
227+
object curValue = _fieldType.GetValue(address, null, offset, false);
228+
if (curValue is int iCurVal) {
229+
valueBits = (ulong)iCurVal;
230+
} else {
231+
valueBits = (ulong)(long)(BigInteger)curValue;
232+
}
219233
}
220234

221235
// get a mask for the bits this field owns
@@ -225,8 +239,10 @@ private void SetBitsValue(MemoryHolder address, int baseOffset, object value) {
225239
// or in the new bits provided by the user
226240
valueBits |= (newBits << _bitsOffset) & targetBits;
227241

228-
// and set the value
229-
if (IsSignedType) {
242+
// and set the value
243+
if (IsBoolType) {
244+
address.WriteByte(offset, (byte)valueBits);
245+
} else if (IsSignedType) {
230246
if (_fieldType.Size <= 4) {
231247
_fieldType.SetValue(address, offset, (int)(long)valueBits);
232248
} else {
@@ -255,7 +271,14 @@ private bool IsSignedType {
255271
}
256272
}
257273

258-
#endregion
274+
private bool IsBoolType => ((SimpleType)_fieldType)._type is SimpleTypeKind.Boolean;
275+
276+
/// <summary>
277+
/// Returns true if the field is a bitfield that does not span the whole width of the underlying field type.
278+
/// </summary>
279+
private bool IsProperBitfield => 0 < _bits && _bits < _fieldType.Size * 8;
280+
281+
#endregion
259282
}
260283
}
261284
}

Src/IronPython.Modules/_ctypes/SimpleType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class SimpleType : PythonType, INativeType {
3434
internal readonly SimpleTypeKind _type;
3535
private readonly char _charType;
3636
private readonly string _format;
37-
private readonly bool _swap;
37+
internal readonly bool _swap;
3838

3939
public SimpleType(CodeContext/*!*/ context, string name, PythonTuple bases, PythonDictionary dict) : base(context, name, bases, dict) {
4040
string sVal;

Src/IronPython.Modules/_ctypes/Structure.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ protected _Structure() {
4444
MemHolder = new MemoryHolder(NativeType.Size);
4545
}
4646

47-
public void __init__(params object[] args) {
47+
public void __init__([NotNone] params object[] args) {
4848
CheckAbstract();
4949

5050
INativeType nativeType = NativeType;

Src/IronPythonTest/Cases/CPythonCasesManifest.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ RunCondition=NOT $(IS_OSX) # TODO: debug
1010
[CPython.ctypes.test_as_parameter]
1111
Ignore=true
1212

13-
[CPython.ctypes.test_bitfields]
14-
Ignore=true # blocked by https://github.com/IronLanguages/ironpython3/issues/1297
13+
[CPython.ctypes.test_bitfields] # IronPython.modules.type_related.test_bitfields_ctypes_stdlib
14+
Ignore=true
1515

1616
[CPython.ctypes.test_errno]
1717
Ignore=true

Src/IronPythonTest/Cases/IronPythonCasesManifest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ NotParallelSafe=true # Uses fixed file, directory, and environment variable name
152152
[IronPython.modules.io_related.test_cPickle]
153153
NotParallelSafe=true # Uses a temporary module with a fixed name
154154

155+
[IronPython.modules.type_related.test_bitfields_ctypes_stdlib]
156+
RunCondition=NOT $(IS_OSX) # ctypes tests not prepared for macOS
157+
155158
[IronPython.modules.system_related.test_sys_getframe]
156159
IsolationLevel=PROCESS # https://github.com/IronLanguages/ironpython3/issues/489
157160
FullFrames=true

0 commit comments

Comments
 (0)