-
Notifications
You must be signed in to change notification settings - Fork 638
Fix stuct packing in build_bcm_header on 32-bit #573
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
Fix stuct packing in build_bcm_header on 32-bit #573
Conversation
Previously build_bcm_header relied upon adding a "0q" at the end of the struct format string in order to force a 8-byte wide alignment on the struct, in order to interop with the structure defined in the Linux headers: struct bcm_msg_head { __u32 opcode; __u32 flags; __u32 count; struct bcm_timeval ival1, ival2; canid_t can_id; __u32 nframes; struct can_frame frames[0]; }; The Python code assumed that alignof(long long) == 8 bytes in order to accomplish this. However, on certain 32-bit platforms, this assumption is not true, and alignof(long long) == 4. As the struct module does not provide logic to derive alignments of its types, this changes the packing logic to use ctypes. This exposes the alignment of the struct members, allowing us to determine whether padding bytes are necessary. Fixes hardbyte#470
Codecov Report
@@ Coverage Diff @@
## develop #573 +/- ##
===========================================
+ Coverage 63.99% 64.16% +0.17%
===========================================
Files 63 63
Lines 5699 5718 +19
===========================================
+ Hits 3647 3669 +22
+ Misses 2052 2049 -3 |
I've tested this by installing
|
Oh, I also had tried writing a unit test by mocking the return values of I ended up hacking around it by poking at the It looks something like this: import unittest
try:
from unittest.mock import Mock
from unittest.mock import patch
from unittest.mock import call
except ImportError:
from mock import Mock
from mock import patch
from mock import call
import ctypes
from can.interfaces.socketcan.socketcan import bcm_header_factory
class BcmMsgHeadTest(unittest.TestCase):
def setUp(self):
self._ctypes_sizeof = ctypes.sizeof
self._ctypes_alignment = ctypes.alignment
@patch("ctypes.sizeof")
@patch("ctypes.alignment")
def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_4(
self, ctypes_sizeof, ctypes_alignment
):
def side_effect_ctypes_sizeof(value):
if value == ctypes.c_long:
return 4
return self._ctypes_sizeof(value)
def side_effect_ctypes_alignment(value):
if value == ctypes.c_long:
return 4
return self._ctypes_alignment(value)
ctypes_sizeof.side_effect = side_effect_ctypes_sizeof
ctypes_alignment.side_effect = side_effect_ctypes_alignment
fields = [
("opcode", ctypes.c_uint32),
("flags", ctypes.c_uint32),
("count", ctypes.c_uint32),
("ival1_tv_sec", ctypes.c_long),
("ival1_tv_usec", ctypes.c_long),
("ival2_tv_sec", ctypes.c_long),
("ival2_tv_usec", ctypes.c_long),
("can_id", ctypes.c_uint32),
("nframes", ctypes.c_uint32),
]
BcmMsgHead = bcm_header_factory(fields)
# TODO: Figure out if we can just use ctypes.sizeof here?
expected_fields = [
("opcode", ctypes.c_uint32),
("flags", ctypes.c_uint32),
("count", ctypes.c_uint32),
("ival1_tv_sec", ctypes.c_long),
("ival1_tv_usec", ctypes.c_long),
("ival2_tv_sec", ctypes.c_long),
("ival2_tv_usec", ctypes.c_long),
("can_id", ctypes.c_uint32),
("nframes", ctypes.c_uint32),
# We expect 4 bytes of padding
("pad_0", ctypes.c_uint8),
("pad_1", ctypes.c_uint8),
("pad_2", ctypes.c_uint8),
("pad_3", ctypes.c_uint8),
]
self.assertEqual(expected_fields, BcmMsgHead._fields_)
# And then a similar test for 64-bit results
if __name__ == "__main__":
unittest.main() Would you want me to add this somewhere? |
("can_id", ctypes.c_uint32), | ||
("nframes", ctypes.c_uint32), | ||
] | ||
BcmMsgHead = bcm_header_factory(fields) |
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.
Consider calling the factory once at import time or socketcan bus instantiation time.
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.
Done
Your test would be a good addition too. I assume your testing was with Python 2 and 3? We could also see if we can carry out testing on 32bit systems on travis. |
Sure! AFAIK, Travis only supports 64-bit, so presumably the only way to get 32-bit runners is to run the tests in a 32-bit container via Docker. Not sure if that's something we want to do as part of this change? |
In order to prevent the BCM factory method from being called each time a BCM header struct is created, we create the class when the module gets created.
Add a SocketCAN interface test file with tests for validating the BcmMsgHead class created by bcm_header_factory on various platforms.
Re-tested using the cyclic example on the same platforms using a variant of Python 2 and 3 for each, and it doesn't seem like I've broken anything. 😂 Would you like me to squash my commits? Or would you rather do that when merging (or not at all)? |
Clean up the old comment in build_bcm_header referencing the old packing code that called into Python's struct library. This now uses ctypes, so and the class is created via the bcm_header_factory factory function, so we move the comment there instead.
Thanks for fixing this 👍 |
Previously
build_bcm_header
relied upon adding a "0q" at the end of thestruct format string in order to force a 8-byte wide alignment on the
struct, in order to interop with the structure defined in the Linux
headers:
The Python code assumed that alignof(long long) == 8 bytes in order to
accomplish this. However, on certain 32-bit platforms, this assumption
is not true, and alignof(long long) == 4.
As the struct module does not provide logic to derive alignments of its
types, this changes the packing logic to use ctypes. This exposes the
alignment of the struct members, allowing us to determine whether padding
bytes are necessary.
Fixes #470