Is there a way to convert a Ctypes Structure including a pointer to a bytearray?
class SRamAccess(ctypes.Structure):
_fields_ = [('channel', ctypes.c_uint), ('offset', ctypes.c_uint), ('len', ctypes.c_uint), ('data', ctypes.c_char_p)]
Is there a way to convert a Ctypes Structure including a pointer to a bytearray?
class SRamAccess(ctypes.Structure):
_fields_ = [('channel', ctypes.c_uint), ('offset', ctypes.c_uint), ('len', ctypes.c_uint), ('data', ctypes.c_char_p)]
Just pass it to bytearray():
>>> import ctypes
>>> class SRamAccess(ctypes.Structure):
... _fields_ = [('channel', ctypes.c_uint), ('offset', ctypes.c_uint), ('len', ctypes.c_uint), ('data', ctypes.c_char_p)]
...
>>> s = SRamAccess(1,2,3,b'blah')
>>> bytearray(s)
bytearray(b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\xf0 .\x1a\xed\x01\x00\x00')
# \______________/\______________/\______________/\______________/\________________________/
# 1 2 3 padding pointer64
Note that 'blah' is not part of the structure. The last 8 bytes (64-bit Python) are the pointer address, and the 4 bytes before that are padding to align the 8-byte pointer to an 8-byte offset in the structure.
You would need an array in the structure to see array contents:
>>> class SRamAccess(ctypes.Structure):
... _fields_ = [('channel', ctypes.c_uint), ('offset', ctypes.c_uint), ('len', ctypes.c_uint), ('data', ctypes.c_char * 5)]
...
>>> s = SRamAccess(1,2,3,b'blah')
>>> bytearray(s)
bytearray(b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00blah\x00\x00\x00\x00')
# \______________/\______________/\______________/\______/\__________/
# 1 2 3 blah padding
Note that last 3 bytes are padding to make the whole structure a multiple of 4 bytes in size so an array of such structures keeps the 4-byte integers aligned on 4-byte boundaries.
Listing [Python 3.Docs]: ctypes - A foreign function library for Python.
One way would be to create a helper method that does the conversion. Below is such an example, which also does the conversion back (as I imagine that converting one way only would be pretty much useless).
code00.py:
#!/usr/bin/env python
import sys
import ctypes as ct
class SRamAccess(ct.Structure):
_fields_ = [
("channel", ct.c_uint),
("offset", ct.c_uint),
("len", ct.c_uint),
("data", ct.c_char_p),
]
@classmethod
def deserialize(cls, buf):
inst = cls.from_buffer(buf)
return inst
def serialize(self):
struct_size = ct.sizeof(SRamAccess)
size = struct_size + self.len
buf = (ct.c_char * size)()
ct.memmove(ct.addressof(buf), ct.addressof(self), struct_size)
ct.memmove(ct.addressof(buf) + struct_size, self.data, self.len)
return bytearray(buf)
def __str__(self):
s = self.__repr__()
for field_name, _ in self._fields_[:-1]:
s += "\n {0:s}: {1:}".format(field_name, getattr(self, field_name))
s += "\n {0:s}:".format(self._fields_[-1][0])
for i in range(self.len):
s += " 0x{0:02X}".format(self.data[i])
return "{0:s}\n".format(s)
def main(*argv):
text = b"abcd1234"
ssrc = SRamAccess(1, 2, len(text), text)
print("Src:", ssrc)
buf = ssrc.serialize()
print("Buf:", buf)
sdst = SRamAccess.deserialize(buf)
print("\nDst:", sdst)
if __name__ == "__main__":
print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
main(*sys.argv[1:])
print("\nDone.")
Output:
e:\Work\Dev\StackOverflow\q060926139>"e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code00.py Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32 Src: <__main__.SRamAccess object at 0x00000298F25553C8> channel: 1 offset: 2 len: 8 data: 0x61 0x62 0x63 0x64 0x31 0x32 0x33 0x34 Buf: bytearray(b'\x01\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00 \xc6V\xf2\x98\x02\x00\x00abcd1234') Dst: <__main__.SRamAccess object at 0x00000298F2555448> channel: 1 offset: 2 len: 8 data: 0x61 0x62 0x63 0x64 0x31 0x32 0x33 0x34 Done.
As a note, this representation is also storing the last field (pointer) address which is irrelevant (as the contents is also stored).
Previous variant doesn't behave well when there are NUL (0, \x00) chars in the string (text = b"abcd\x001234"), because ctypes.c_char_p is used for NUL terminated strings. I think that the len field presence automatically implies the above scenario.
code01.py:
#!/usr/bin/env python
import sys
import ctypes as ct
import struct
CharPtr = ct.POINTER(ct.c_char)
class SRamAccess(ct.Structure):
_fields_ = [
("channel", ct.c_uint),
("offset", ct.c_uint),
("len", ct.c_uint),
("data", CharPtr),
]
@classmethod
def deserialize(cls, buf):
fmt_prefix = "II"
data_len = len(buf) - struct.calcsize(fmt_prefix)
fmt = fmt_prefix + "b" * data_len
unpacked = struct.unpack(fmt, buf)
return cls(unpacked[0], unpacked[1], 0, unpacked[2:])
def serialize(self):
buf = struct.pack("II" + "b" * self.len, self.channel, self.offset, *self.data[:self.len])
return bytearray(buf)
def __setattr__(self, name, value):
if name == "data":
self.len = len(value)
buf = (ct.c_char * len(value))(*value)
super().__setattr__(name, buf)
else:
super().__setattr__(name, value)
def __str__(self):
s = self.__repr__()
for field_name, _ in self._fields_[:-1]:
s += "\n {0:s}: {1:}".format(field_name, getattr(self, field_name))
s += "\n {0:s}:".format(self._fields_[-1][0])
for i in range(self.len):
s += " 0x{0:02X}".format(ord(self.data[i]))
return "{0:s}\n".format(s)
def main(*argv):
text = b"abcd\x00123"
ssrc = SRamAccess(1, 2, 12345, text)
print("Src:", ssrc)
buf = ssrc.serialize()
print("Buf:", buf)
sdst = SRamAccess.deserialize(buf)
print("\nDst:", sdst)
if __name__ == "__main__":
print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
main(*sys.argv[1:])
print("\nDone.")
Output:
e:\Work\Dev\StackOverflow\q060926139>"e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code01.py Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32 Src: <__main__.SRamAccess object at 0x0000025277C85348> channel: 1 offset: 2 len: 8 data: 0x61 0x62 0x63 0x64 0x00 0x31 0x32 0x33 Buf: bytearray(b'\x01\x00\x00\x00\x02\x00\x00\x00abcd\x00123') Dst: <__main__.SRamAccess object at 0x0000025277C85448> channel: 1 offset: 2 len: 8 data: 0x61 0x62 0x63 0x64 0x00 0x31 0x32 0x33 Done.
Notes (changes from the previous version):