classification
Title: ctypes.Structure bit order is reversed - counts from right
Type: behavior Stage: resolved
Components: ctypes Versions: Python 3.4
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: eryksun, martin.panter, zeero
Priority: normal Keywords:

Created on 2015-08-13 19:20 by zeero, last changed 2015-08-15 21:29 by eryksun. This issue is now closed.

Files
File name Uploaded Description Edit
test_structure.py zeero, 2015-08-13 19:20 Good_ Bad_Test_Script
Messages (6)
msg248536 - (view) Author: (zeero) Date: 2015-08-13 19:20
I'm implementing a CAN SAEJ1939 stack in Python and convert CAN message ids in between integers and structures, tunneling them through a Union object with 4 bytes like what i would do in C.

I was checking that particular function and the 3 priority bits were at the wrong position. Some trials later i concluded the bits are reversed and inverted the order in my structure. Now it works fine.

I'm not sure if it's a bug but i would expect ctypes.Structure to
store the fields in the order i provide and not the other way around.
On the other hand the bytes are in the order i provided.

The System I'm running on is 64Bit Ubuntu AMD A1046 but it shows the same behaviour an a 64bit Windows 7 Intel I5
msg248552 - (view) Author: Martin Panter (martin.panter) * (Python committer) Date: 2015-08-14 02:38
It would be helpful if you could trim down your example code a bit. Without studying the whole file, it is hard to see exactly what order you are seeing and what order you expect, since there are two versions with different orders in the code.

My understanding of the “ctypes” module is that it is for interacting with the local OS, ABI, compiler, etc, which could use various layouts depending on the platform. According to the Linux x86-64 ABI <http:/www.x86-64.org/documentation/abi.pdf>, page 14, “bit-fields are allocated from right to left”, which I interpret to mean from least-significant to most-significant bit. Not so sure about Windows, but <https://msdn.microsoft.com/en-us/library/yszfawxh.aspx> suggests a similar story (LSB first). This behaviour agrees with my experiments on Linux and Wine:

>>> class Bitfield(Structure):
...     _fields_ = (("a", c_uint8, 4), ("b", c_uint8, 4))
... 
>>> bytes(Bitfield(0xA, 0xB))
b'\xba'

Does this agree with what you expect? Otherwise, what leads you to expect something different?

Also:
* bytes(saej1939_message_id) should copy the bytes directly; no need for a union.
* struct.unpack() should also accept a “ctypes” object directly; no need for the copy.
msg248581 - (view) Author: (zeero) Date: 2015-08-14 11:00
Sorry for the inconvenience.

The format specification can be found in chapter 2.1 in 

http://vector.com/portal/medien/cmc/application_notes/AN-ION-1-3100_Introduction_to_J1939.pdf

So I would write down the field contents in that order

    _fields_ = [('reserved',c_uint8,3),
                ('priority',c_uint8,3),
                ('extended_data_page',c_uint8,1),
                ('data_page',c_uint8,1)
                ...
                ]

I expect the first Byte to be 0x1C when I set priority to 7 but it came out as 0x38, after tunneling it through the Union object.

Reversing the order of the bit fields makes it work like expected.
msg248637 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2015-08-15 12:55
It seems you want a BigEndianStructure:

    from ctypes import *

    class SAEJ1939MsgId(BigEndianStructure):

        _fields_ = (('reserved',           c_uint8, 3),
                    ('priority',           c_uint8, 3),
                    ('extended_data_page', c_uint8, 1),
                    ('data_page',          c_uint8, 1),
                    ('pdu_format',         c_uint8),
                    ('pdu_specific',       c_uint8),
                    ('source_address',     c_uint8))
        
        def __init__(self, *args, pgn=0, **kwds):
            super().__init__(*args, **kwds)
            if pgn > 0:
                self.pgn = pgn

        def __int__(self):
            return int.from_bytes(self, 'big')

        @property
        def pgn(self):
            """pgn is an 18-bit number consisting of EDP, DP, PF, and PS"""
            return (int(self) >> 8) & 0x3FFFF

        @pgn.setter
        def pgn(self, value):
            value |= self.priority << 18
            view = (c_char * 3).from_buffer(self)
            view[:] = value.to_bytes(3, 'big')

        @classmethod
        def from_bytes(cls, msg_id):
            return cls.from_buffer_copy(msg_id)

        @classmethod
        def from_integer(cls, msg_id):
            msg_id_bytes = msg_id.to_bytes(sizeof(cls), 'big')
            return cls.from_buffer_copy(msg_id_bytes)

Example:

    >>> a = SAEJ1939MsgId(priority=7, source_address=3)
    >>> hex(int(a))
    '0x1c000003'

    >>> b = SAEJ1939MsgId(pgn=0xf004, priority=7, source_address=3)
    >>> hex(int(b))
    '0x1cf00403'
    >>> b.priority
    7
    >>> b.pdu_format
    240
    >>> b.pdu_specific
    4
    >>> b.source_address
    3

    >>> c = SAEJ1939MsgId.from_integer(int(b))
    >>> hex(int(c))
    '0x1cf00403'
msg248642 - (view) Author: (zeero) Date: 2015-08-15 16:53
Thanks for the advise. I'll give it a try.

So ctypes.Structure is always little endian regardless from the underlying architecture. 
I just checked on a raspberry pi 2 that should be a big endian device, and got the same results as before.

I'm still not sure if it's right that ctypes.Structure swaps the order of the bit items but apparently it's the intended behaviour.

I'll close the issue then. Thanks again.
msg248661 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2015-08-15 21:29
No, ctypes.Structure should use native endianness. So on a little-endian it's the same as ctypes.LittleEndianStructure, and on a big-endian system it's the same as ctypes.BigEndianStructure. ARM processors can work in either mode. IIRC, the Raspberry Pi is built as a little-endian system. Check sys.byteorder to be sure.
History
Date User Action Args
2015-08-15 21:29:10eryksunsetmessages: + msg248661
stage: resolved
2015-08-15 16:53:22zeerosetstatus: open -> closed
resolution: not a bug
messages: + msg248642
2015-08-15 12:55:45eryksunsetnosy: + eryksun
messages: + msg248637
2015-08-14 11:00:05zeerosetmessages: + msg248581
2015-08-14 02:38:21martin.pantersetnosy: + martin.panter
messages: + msg248552
2015-08-13 19:20:46zeerocreate