-
Notifications
You must be signed in to change notification settings - Fork 778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement wrapper for PyASCIIObject.state
bitfield accesses
#3015
Conversation
If the only difference between those C files is the include path, then this should just be an |
Personally I would prefer putting this behind a feature a) so a C compiler isn't needed if you don't use this b) there is a bit of a hurdle to jump over if you do need it
Is anything like that in the works? It would be nice to be able to get rid of this shim eventually :) Also; is there any prior art on how C extensions handle this? It is an issue for them too. |
The hurdle for users is already there because the functions are unsafe. The current little-endian-only implementation also relies on UB, it's just coincidence that most C compilers generate the same memory layout for this struct and bitfield ... so the C shim is not actually making things worse. :) But I understand that you'd want to make the dependency on
Not as far as I know. It appears that CPython upstream would like to hide the internals of this struct rather than expose more of it publicly: python/cpython#89188 (comment)
The C headers do provide macros that provide access to the bitfield, but C macros are obviously not usable via FFI. |
second this, it will make cross compilation harder now that it requires python development header files. |
I do not want to side track this, but what does |
Here are excerpts of
|
So this seems to boil down to #[repr(C)]
#[repr(align(4))]
#[derive(Debug, Copy, Clone)]
pub struct PyASCIIObject__bindgen_ty_1 {
pub _bitfield_align_1: [u8; 0],
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize]>,
} and let bit_index = if cfg!(target_endian = "big") {
7 - (index % 8)
} else {
index % 8
}; which would seem palatable enough. But indeed, we'd need to check the C standard legalese on whether this is mandated or whether it is just Clang doing this way. To be honest, if I try to be in a very very pragmatic mood, the above might be considered an improvement over the status quo and allow us to support big endian targets. I also suspect that the only way to be truly correct is the shim going through the actual C target compiler, i.e. we are not really correct ATM as well, which is why I could personally live with this. |
I don't think the C standard mandates anything, but in practice we can examine what "implementation defined" actually does on targets we care about. It should still be part of the platform ABI, so we wouldn't have to worry about GCC vs Clang (on a given target). |
If bindgen produces usable functions to access these fields that would of course be perfect, as it would mean including C files is not necessary at all. The only thing that would need to be taken into account is possible differences between Python versions. It appears that the exact bitfield layout sometimes changes between major versions (like here), so it might be necessary to keep copies around for different target CPython versions. |
Keeping multiple structure definitions around to support the few Python major versions we do seems preferable to having to involve a C compiler IMHO. |
PyASCIIObject.state
bitfield accessesPyASCIIObject.state
bitfield accesses
Ok, I've changed this PR to no longer depend on the The I checked with all versions of Python that I have access to (3.7, 3.9, 3.10, 3.11, and 3.12) and bindgen generated the same code for all of them (which is why there's no copies for different versions any longer) - except for Python 3.12, where the "ready" flag was apparently removed from the bitfield (but Python 3.12 is not yet supported by PyO3). I have adapted the tests to the changed API, removed |
I don't really understand some of the test failures in CI, especially the Also, can I add |
I think there are no distinct failures here: Our FFI checking macros do not yet support generics as we never used them in the FFI definitions before. Personally, I would suggest just folding the bindgen-generated wrapper into the concrete struct to avoid them outright but see below. The other issue is that your type is named
I would really be interested what the other maintainers think, but the first impulse for me personally would be to slim down the generated code to what we actually need. But I also understand the idea of really delegating this to bindgen and thus being able to simply import an updated version of these bindings in the future. Let's see what other people think...
I think we should take the pragmatic way as the change is proposed now as it is definitely an improvement over the status quo, I just wanted to mention that I think the choice of target is the bigger problem than GCC versus Clang. Or put differently, if we really want to rely on the ABI specifications we would need to limit the availability of the type definitions to the finite set of targets which we have checked whereas now (and in the pragmatic approach), one could at least compile the code on targets we did not even consider. |
@@ -47,39 +252,35 @@ pub struct PyASCIIObject { | |||
/// unsigned int ascii:1; | |||
/// unsigned int ready:1; | |||
/// unsigned int :24; | |||
pub state: u32, | |||
pub state: PyASCIIObjectState, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't necessarily need to be a public change. If you move the generated PyASCIIObjectState
to a private module, it would be trivial to add From<u32>
for it using u32::to_ne_bytes
. Then the accessors would look something like PyASCIIObjectState::from(self.state).interned()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would work for now, but I think only as long as u32
still yields a PyASCIIObject
with the right size and alignment.
That said, I don't this breakage would be something we would refrain from for the next release 0.19.0 where it would be within semver bounds. My personal expectation is that this is mainly used via the existing accessors or opaquely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, it's up to you. Bindgen's output looks like a lot more pub
surface than I would want, so u32
keeps that opaque, or a newtype could accomplish that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bindgen's output looks like a lot more pub surface than I would want, so u32 keeps that opaque, or a newtype could accomplish that too.
Yes, as written above, I would also condense that into the minimum we actually need, i.e. the layout-relevant parts and the few accessors we want to provide. Let's see what the others think about that though.
Right, this can only hold up for known target ABIs -- outside that is still a big question mark. They may follow the same approach, as it's a pretty straightforward way to implement bitfields, but it's not necessarily the only way. |
I can try to do that, but it would make it harder to incorporate changes from bindgen when updating the definitions for future Python releases (or if a new bindgen version produced better code).
I think you meant to say that the bitfield wrapper type is named
That was part of the reason why I didn't change the function bodies at all - it would make it easier to update / compare them with things generated by bindgen in the future. The only things I did change from what bindgen produced was to give the anonymous |
FYI I noticed that it might be possible to remove the bitfield in Python 3.12, if that lands upstream we might be able to simplify this eventually. python/cpython#102589 |
Alright, I managed to simplify the code a little, and I've reverted the I silenced the I think these changes should address most or all issues that have been mentioned in previous comments. The CI also looks (mostly) happy and green now. I also ran the pyo3 test suite on all architectures that are supported by Fedora (x86_64, i686, aarch64, powerpc64le, and 390x) and everything passed there, as well. Please let me know if there's anything else that I can do to move this forward. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this seems good to me with a couple of tiny suggestions to remove some derived traits from the private types.
Final step is to add a newsfragment - I suggest something like 3015.added.md with "Support PyStringData
on big-endian targets"
Great, I removed all the unnecessary derived trait implementations (it turns out, it was all of them) and added a changelog fragment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me. Thanks very much!
I'll continue to try to get something simpler in for 3.12 or another future Python, however looks like my original simplification suggested above won't stick ;)
bors r+
Build succeeded: |
Awesome. Thanks for merging! |
This is a first draft of my attempt to fix #1824 "properly" by writing a C wrapper for the
PyASCIIObject.state
bitfield accesses, as proposed here: #1824 (comment)The original argument for making these functions
unsafe
is still valid, though - bitfield memory layout is not guaranteed to be stable across different C compilers, as it is "implementation defined" in the C standard. However, short of having CPython upstream provide non-inlined public functions to access this bitfield, this is the next best thing, as far as I can tell.I've removed the
#[cfg(target_endian = "little")]
attributes from all things that are un-blocked by fixing this issue on big-endian systems, except for three tests, which look like expected failures considering that they do not take bit/byte order into account (for example, when writing to the bitfield).ffi::tests::ascii_object_bitfield
types::string::tests::test_string_data_ucs2_invalid
types::string::tests::test_string_data_ucs4_invalid
All other tests now pass on both little-endian and big-endian systems.
I am aware that some parts of this PR are probably not in a state that's acceptable for merging as-is, which is why I'm filing this as a draft. Feedback about how to better integrate this change with pyo3-ffi would be great. :)
In particular, I'm unsure whether the
#include
statements in the C files are actually correct across different systems. I have only tested this on Fedora Linux so far.I'm also open to changing the names of the C functions that are implemented in the wrapper. For now I chose the most obvious names that shouldn't cause collisions with other symbols.