将VBA类型/C结构移植到Python ctypes.Structure:固定长度字符串数组
我正在尝试把一段VBA代码移植到Python。这项工作包括调用一个Windows DLL中的函数。这个函数需要一个指向C语言结构体的指针(在VBA中,这种结构体叫“Type”)作为参数。这个结构体里包含了固定长度的字符串和固定长度的字符串数组。我在用ctypes库在Python中表达这个结构体时遇到了困难。
原来的VBA代码里有这样的语句:
Public Type elements
elementA As String * 48
elementB(3) As String * 12
End Type
我认为在C语言中可以这样表示:
struct elements
{
char elementA[48];
char elementB[4][12];
}
我在Python中尝试过的内容:
import ctypes
class elements(ctypes.Structure):
_fields_ = [
("elementA", ctypes.create_string_buffer(48)),
("elementB", ctypes.create_string_buffer(12) * 4)
]
我能成功声明elementA,但声明elementB时失败,出现了
“TypeError: unsupported operand type(s) for *: 'c_char_Array_12' and 'int'”
我该如何正确地做到这一点呢?
更新 #1
我能成功声明以下内容:
import ctypes
class elements(ctypes.Structure):
_fields_ = [
("elementA", ctypes.c_char * 48),
("elementB", ctypes.c_char * 12 * 4)
]
elementA有一个“value”属性,但我找不到处理elementB的方法。我该如何读取或更改它的内容呢?
更新 #2
我想我理解了这个行为。
>>> e = elements()
>>> e.elementA
''
>>> e.elementA = 'test'
>>> e.elementA
'test'
>>> e.elementB
<__main__.c_char_Array_12_Array_4 object at 0x9878ecc>
>>> e.elementB[0][:] == '\x00' * 12
True
>>> e.elementB[0][:]
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> e.elementB[0][:] = 'test'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Can only assign sequence of same size
>>> e.elementB[0][:] = 'test' + '\x00' * 8
>>> e.elementB[0][:]
'test\x00\x00\x00\x00\x00\x00\x00\x00'
>>> testB = 'abcde'
>>> e.elementB[0][:] = testB + '\x00' * ( ctypes.sizeof(e.elementB[0]) - len(testB) )
>>> e.elementB[0][:]
'abcde\x00\x00\x00\x00\x00\x00\x00'
>>> e.elementB[0][:].rstrip('\x00')
'abcde'
>>> e.elementB[0].value
'abcde'
>>> e.elementB[0].value = 'abcdef'
>>> e.elementB[0][:]
'abcdef\x00\x00\x00\x00\x00\x00'
(这个问题是关于Python 2.6和2.7的。)
1 个回答
create_string_buffer
是一个方便的函数,用来创建一个 c_char
数组的实例。不过,字段定义需要的是 C 类型,而不是实例。举个例子:
import ctypes
class elements(ctypes.Structure):
_fields_ = [("elementA", ctypes.c_char * 48),
("elementB", ctypes.c_char * 12 * 4)]
假设你有一个 C 函数,定义如下:
lib.func.argtypes = [ctypes.POINTER(elements)]
要调用这个函数,你需要用 byref
传递一个 elements
的实例:
e = elements()
lib.func(ctypes.byref(e))
访问一维的 c_char
数组字段,比如 elementA
,会特别处理,返回一个 Python 字符串。但访问二维数组,比如 elementB
,则返回一个 ctypes 的 Array
实例。在 elementB
的情况下,有 4 行,每行包含 12 列。
>>> len(e.elementB)
4
>>> map(len, e.elementB)
[12, 12, 12, 12]
sizeof
函数返回数组的字节大小。例如,elementB
的缓冲区由 48 个 c_char
元素组成,每个元素占 1 个字节:
>>> ctypes.sizeof(e.elementB)
48
作为字符数组的 elementB
的 c_char
数组,特别处理后有 value
和 raw
属性。获取 value
属性会创建一个 Python 字符串,把数组当作一个以 null 结尾的 C 字符串。raw
属性则返回整个长度。你也可以通过这些属性赋值 Python 字符串,两个属性都可以接受包含 null 的字符串。
>>> e.elementB[3].value = 'abc\x00def'
>>> e.elementB[3].value
'abc'
>>> e.elementB[3].raw
'abc\x00def\x00\x00\x00\x00\x00'
或者你可以切片数组来获取子字符串:
>>> e.elementB[3][:]
'abc\x00def\x00\x00\x00\x00\x00'
>>> e.elementB[3][4:7]
'def'
c_wchar
数组只有 value
属性,它返回一个 unicode
字符串。你可以用 unicode
字符串或(在 Python 2 中)8 位字符串来设置 value
。8 位字符串会根据当前的 ctypes 编码进行解码,Windows 默认是 'mbcs'
,其他情况则是 'ascii'
。set_conversion_mode
(Python 2)可以设置默认编码:
>>> s = (ctypes.c_wchar * 12)()
>>> v = u'\u0100'.encode('utf-8')
>>> v
'\xc4\x80'
>>> s.value = v
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 0:
ordinal not in range(128)
>>> old_mode = ctypes.set_conversion_mode('utf-8', 'strict')
>>> old_mode
('ascii', 'strict')
现在,分配 '\xc4\x80'
是有效的,因为转换编码设置为 UTF-8:
>>> s.value = v
>>> s.value
u'\u0100'
>>> s[:]
u'\u0100\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
数组是可迭代的:
for row in e.elementB:
row[:] = 'abcdefghijkl'
>>> print '\n'.join(row[::-1] for row in e.elementB)
lkjihgfedcba
lkjihgfedcba
lkjihgfedcba
lkjihgfedcba
ctypes 数据类型支持 Python 的缓冲协议,可以与其他类型进行交互:
>>> bytearray(e.elementB)
bytearray(b'abcdefghijklabcdefghijklabcdefghijklabcdefghijkl')
>>> import numpy as np
>>> np.frombuffer(e.elementB, dtype='uint8')
array([ 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 97,
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 97, 98,
99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 97, 98, 99,
100, 101, 102, 103, 104, 105, 106, 107, 108], dtype=uint8)