Python和Ctypes:将结构体作为指针传递给函数以获取数据

28
我浏览了其他答案,但好像无法让这个功能正常工作。我正在尝试调用一个与SMBus设备通信的DLL中的函数。此函数需要一个结构体指针作为参数,其中包含一个数组作为其字段之一。所以...
在C语言中:
typedef struct _SMB_REQUEST
{
    unsigned char Address;
    unsigned char Command;
    unsigned char BlockLength;
    unsigned char Data[SMB_MAX_DATA_SIZE];
} SMB_REQUEST;

我认为在DLL填充数据数组时,需要为地址、命令和块长度设置值。该函数将此结构作为指针传递。

SMBUS_API int SmBusReadByte( SMBUS_HANDLE handle, SMB_REQUEST *request );

所以我已经在Python中设置了结构体,如下所示:

class SMB_REQUEST(ctypes.Structure):
    _fields_ = [("Address", c_char),
            ("Command", c_char),
            ("BlockLength", c_char),
            ("Data", type(create_string_buffer(SMB_MAX_DATA_SIZE))]

*注意:我也尝试过使用ctypes.c_char*SMB_MAX_DATA_SIZE作为数据类型*

要将指向此类型结构的指针传递给函数,我尝试首先将其初始化如下:

data = create_string_buffer(SMB_MAX_DATA_SIZE)
smb_request = SMB_REQUEST('\x53', \x00', 1, data)

这将回应以下内容:
TypeError: expected string or Unicode object, c_char_Array_32 found

如果我尝试省略数据数组,就像这样:

smb_request = SMB_REQUEST('\x53', \x00', 1)

不,没有错误。然而,当我尝试将此传递给函数时:
int_response =  smbus_read_byte(smbus_handle, smb_request))

我能理解:

ArgumentError: argument 2: <type 'exceptions.TypeError'>: expected LP_SMB_REQUES
T instance instead of SMB_REQUEST

我尝试将它作为指针传递:

int_response =  smbus_read_byte(smbus_handle, ctypes.POINTER(smb_request))

然后我得到:

----> 1
      2
      3
      4
      5

TypeError: must be a ctypes type

以下是我设置艺术类型的方式:

smbus_read_byte.argtypes = (ctypes.c_void_p, ctypes.POINTER(SMB_REQUEST))

我尝试了转换,但仍然不起作用。有人能为我解释一下吗?
更新:
如果我首先像这样初始化结构体:
smb_request = SMB_REQUEST('\xA6', '\x00', chr(1), 'a test string')

然后通过引用传递低音:

int_response =  smbus_receive_byte(smbus_handle, ctypes.byref(smb_request))

我没有收到错误信息。但是,当函数成功时应该返回'0',失败时应该返回非零值,但它返回了'-1'。检查smb_request.Data的值仍然是'a test string',因此没有任何变化。如果您有任何建议,请告诉我,谢谢。
更新:由于有一些人询问我的句柄是否正确,所以我在这里解释一下。DLL的头文件声明如下:
typedef void *SMBUS_HANDLE;

//
// This function call initializes the SMBus, opens the driver and 
// allocates the resources associated with the SMBus.
// All SMBus API calls are valid 
// after making this call except to re-open the SMBus.
//
SMBUS_API SMBUS_HANDLE OpenSmbus(void);

这是我在Python中实现的方法:

smbus_handle = c_void_p() # NOTE: I have also tried it without this line but same result

open_smbus = CDLL('smbus.dll').OpenSmbus
smbus_handle =  open_smbus()
print 'SMBUS_API SMBUS_HANDLE OpenSmbus(void): ' + str(smbus_handle)

在调用 smbus_read_byte() 之前,我会先执行这段代码。我试过设置 open_smbus.restype = c_void_p() 但是出现错误:TypeError: restype must be a type, a callable, or None

嗨Mark/Adam,抱歉回复你们的有用答案有所延迟。我终于拿到了逻辑分析仪,并发现DLL的行为与预期不符。在你们的帮助下,我现在已经让代码工作了。我是SO的新手,我知道留下正确答案未核对是很粗鲁的,但我想将你们两个的答案都标记为正确,因为你们都付出了很多帮助我的努力,而且你们的代码现在也可以运行。我只能标记一个答案为正确。你们有什么建议? - Jack
没问题。很高兴你解决了它。不幸的是,你只能投一票。由你决定。你也可以给两个都点赞;^) - Mark Tolonen
3个回答

37

这是一个可工作的示例。看起来您正在向函数传递错误的类型。

测试DLL代码(在Windows上执行“cl /W4 /LD x.c”)

#include <stdio.h>

#define SMBUS_API __declspec(dllexport)
#define SMB_MAX_DATA_SIZE 5

typedef void* SMBUS_HANDLE;

typedef struct _SMB_REQUEST
{
    unsigned char Address;
    unsigned char Command;
    unsigned char BlockLength;
    unsigned char Data[SMB_MAX_DATA_SIZE];
} SMB_REQUEST;

SMBUS_API int SmBusReadByte(SMBUS_HANDLE handle,SMB_REQUEST *request)
{
    unsigned char i;
    for(i = 0; i < request->BlockLength; i++)
        request->Data[i] = i;
    return request->BlockLength;
}

SMBUS_API SMBUS_HANDLE OpenSmbus(void)
{
    return (void*)0x12345678;
}

Python 代码

from ctypes import *
SMB_MAX_DATA_SIZE = 5
ARRAY5 = c_ubyte * SMB_MAX_DATA_SIZE

class SMB_REQUEST(Structure):
    _fields_ = [
        ("Address", c_ubyte),
        ("Command", c_ubyte),
        ("BlockLength", c_ubyte),
        ("Data", ARRAY5)]

smbus_read_byte = CDLL('x').SmBusReadByte
smbus_read_byte.argtypes = [c_void_p,POINTER(SMB_REQUEST)]
smbus_read_byte.restype = c_int
open_smbus = CDLL('x').OpenSmbus
open_smbus.argtypes = []
open_smbus.restype = c_void_p

handle = open_smbus()
print 'handle = %08Xh' % handle

smb_request = SMB_REQUEST(1,2,5)

print 'returned =',smbus_read_byte(handle,byref(smb_request))
print 'Address =',smb_request.Address
print 'Command =',smb_request.Command
print 'BlockLength =',smb_request.BlockLength
for i,b in enumerate(smb_request.Data):
    print 'Data[%d] = %02Xh' % (i,b)

输出

handle = 12345678h
returned = 5
Address = 1
Command = 2
BlockLength = 5
Data[0] = 00h
Data[1] = 01h
Data[2] = 02h
Data[3] = 03h
Data[4] = 04h

嗨,马克。感谢你为此付出了这么多努力。非常感激!尝试你给我的东西后,我得到了相同的结果(即从函数返回-1,并且ARRAY5()数据元素保持不变)。我注意到的一件事是,你正在使用c_int作为要传递给SmBusReadByte()的第一个参数的smb_handle类型。在头文件中,它被定义为:typedef void *SMBUS_HANDLE; 这就是为什么我将其作为ctypes.c_void_p传递的原因。这在这里有很大的区别吗?再次感谢你的帮助! - Jack
抱歉,我的错...我会为读取进行更正。我模拟了一次写入。如果该函数返回-1,您的句柄是否有效? - Mark Tolonen
“Structures”是可变的。 byref会传递结构体的指针,因此C代码可以修改它。不需要创建字符串缓冲区。 - Mark Tolonen
还将 SMB_HANDLE 更改为 void*,这对您的情况更加正确。 - Mark Tolonen
1
我已经针对OpenSmbus进行了更新。如果您仍然得到-1,请检查其他参数是否正确(地址,命令,块长度)。由于这是读取操作,我怀疑您不必初始化数据。 - Mark Tolonen
显示剩余4条评论

7

你已经接近成功了。对于Data的定义,应该使用c_char * SMB_MAX_DATA_SIZE作为类型。在Mac OS X上,这对我有效:

共享库:

$ cat test.c
#include <stdio.h>

#define SMB_MAX_DATA_SIZE 16

typedef struct _SMB_REQUEST
{
  unsigned char Address;
  unsigned char Command;
  unsigned char BlockLength;
  unsigned char Data[SMB_MAX_DATA_SIZE];
} SMB_REQUEST;

int SmBusReadByte(void *handle, SMB_REQUEST *request)
{
  printf("SmBusReadByte: handle=%p request=[%d %d %d %s]\n", handle, 
      request->Address, request->Command, request->BlockLength, request->Data);
  return 13;
}

$ gcc test.c -fPIC -shared -o libtest.dylib

Python 驱动程序:

$ cat test.py
import ctypes

SMB_MAX_DATA_SIZE = 16

class SMB_REQUEST(ctypes.Structure):
    _fields_ = [("Address", ctypes.c_ubyte),
                ("Command", ctypes.c_ubyte),
                ("BlockLength", ctypes.c_ubyte),
                ("Data", ctypes.c_char * SMB_MAX_DATA_SIZE)]

libtest = ctypes.cdll.LoadLibrary('libtest.dylib')

req = SMB_REQUEST(1, 2, 3, 'test')

result = libtest.SmBusReadByte(ctypes.c_voidp(0x12345678), ctypes.byref(req))

print 'result: %d' % result

$ python test.py
SmBusReadByte: handle=0x12345678 request=[1 2 3 test]
result: 13

更新

您遇到问题的原因是需要将open_smbus函数的结果类型设置为void*。默认情况下,ctypes假定函数返回int。您需要这样说明:

open_smbus.restype = ctypes.c_void_p

你之前出现错误是因为你使用了c_void_p()(注意多余的括号)。c_void_pc_void_p()这两个名称之间有很大的区别。前者是一种类型,而后者是该类型的实例c_void_p代表C语言中的void*类型,而c_void_p()表示实际的指针实例(默认值为0)。

嗨,亚当,哇,你在那里付出了很多努力。非常感谢。不幸的是,它出现了以下错误:ArgumentError: argument 2:<type 'exceptions.TypeError'>:期望LP_SMB_REQUEST实例而不是指向SMB_REQUEST的指针。有什么想法吗?谢谢。 - Jack
我基本上使用了和Mark一样的方法,就像你建议的那样,但是我已经更新了上面的内容,包括我如何初始化句柄。 - Jack
请看我在上面问题后面留下的注释。 - Jack

1

尝试更改

("Data", type(create_string_buffer(SMB_MAX_DATA_SIZE))

("Data", (c_char * SMB_MAX_DATA_SIZE)]

嗨Gabi,感谢您的回复。我已经在上面尝试过了,但是出现了错误。 - Jack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接