之前要调用海康的sdk,尝试过使用ctypes,但是自己没有潜心学习,以为这东西不好用。等最近看arcface的pythondemo的时候,发现都是用ctypes做的接口,感动的泪流满面。。。。
深刻体会:数据出错的时候,请先检查结构体是否对齐,数据是否正确!
先讲数据类型的问题。大多数不知道怎么处理的问题其实都在这里。
具体的数据类型在linux和windows上可能有不同,比如c_int在不同的系统上可能长度都不同。
如果遇到不知道是啥的数据类型,先百度,比如DWORD类型,百度一下就知道其实是unsigned int32,所以我们使用c_uint32就ok。
ctypes类型 | C类型 | Python类型 | 备注 |
---|---|---|---|
c_bool | _Bool | bool | - |
c_char | char | 1个字符的字节对象 | - |
c_wchar | wchar_t | 1个字符的字符串 | - |
c_byte | char | int | - |
c_ubyte | unsigned char | int | - |
c_short | short | int | - |
c_ushort | unsigned short | int | - |
c_int | int | int | - |
c_uint | unsigned int | int | - |
c_long | long | int | - |
c_ulong | unsigned long | int | - |
c_longlong | __int64 或者 long long | int | - |
c_ulonglong | unsigned __int64 或者 unsigned long long | int | - |
c_size_t | size_t | int | - |
c_ssize_t | ssize_t 要么 Py_ssize_t | int | - |
c_float | float | float | - |
c_double | double | float | - |
c_longdouble | long double | float | - |
c_char_p | char * ((NUL terminated)) | bytes对象或 None | - |
c_wchar_p | wchar_t * ((NUL terminated)) | 字符串或 None | - |
c_void_p | void * | int或 None | 通用指针,不指定数据类型,使用指针内容的时候需要强制转换。 |
c_uint32或c_uint | DWORD | int | 根据x32或x64决定该值 |
日常使用的时候,注意指针漂移问题: 在使用x64编程的时候,可能会遇到系统报错,如果出现地址读取错误或者地址冲突等,首先给方法强制定义输入参数和输出参数,然后检查参数是否存在32位或64位的区分,特别是涉及到指针,一定要强制定义,不然就会报错。
日常中,除了以上类型,还有一些值得注意的地方:一般情况下数据传递通过结构体。而结构体很多时候都使用的是指针进行传递数据。数据传递主要分为两种:文本数据和二进制数据。
文本数据很简单,一般不会有指针, 即使有,使用的方式也是POINTER(c_int)大概这样的结构。如果是结构体指针,先定义结构体,再使用指针。
二进制数据:包括从内存读取数据和通过指针传递二进制数据。
#从二进制获取数据
import ctypes
#x_id为某个指定的地址,该数据为结构体BASE_STRUCT
base=BASE_STRUCT()
ctypes.memmove(base,x_id,len(base))#从内存读取结构体数据
通过如上方法就可从内存地址加载该结构体了.这里要注意一个很重要的问题,结构体的创建者。结构体在c++或python里面创建的是不一样的。python对结构体进行了翻译,但并不是创建了c++的结构体。所以:该方法仅适用于python创建的结构体。(c++创建的结构体字段长度比python的更多,因为python创建的结构体只有数据,没有基础的struct包含的其他东西,当然不知道c语言里面是不是也是这样。。)
#include
#define DLLEXPORT extern "C" __declspec(dllexport)
typedef struct Sa {
char x;
int y;
char *z[10];
}TEST_STRUCT;
DLLEXPORT void *w(TEST_STRUCT *tt){
tt->y = -101;
std::cout<<"c:"<< sizeof(tt)<<"\n";
//std::cout << "cs:" << sizeof(test_struct) << "\n";
return (void *)tt;
}
对应的python代码
from ctypes import *
dll = CDLL('Project1.dll')
dllc=cdll.msvcrt#c语言自带的一些方法
class TEST_STRUCT(Structure):
_fields_=[
("x",c_char),
("y",c_int),
("z",c_char*10)
]
www=TEST_STRUCT()
id=dll.w
id.restype=c_void_p
id.argtypes=(POINTER(TEST_STRUCT),)
w2=TEST_STRUCT()
w2.y=-101
print("w2-size:",sizeof(w2))
res=id(byref(www))
print(res)
print("p:",sizeof(w2))
memmove(addressof(w2),res,sizeof(w2))
print("w2:",w2.y)
print(www.y)
在ctypes里面,不要对ctypes的数据进行地址比对判断是否为同一对象,要比较也要比较指针的值。因为ctypes每次调用返回的对象的地址都是不一样的。
函数调用根据垃圾回收机制主要分为4种调用方式:CDLL,OleDLL,WinDLL,PyDLL。
CDLL:这些库中的函数使用标准C调用约定,并假定返回 int。(即垃圾回收由c++端进行)
OleDLL:仅限Windows。此类的实例表示已加载的共享库,这些库中的函数使用stdcall调用约定,并假定返回特定于Windows的HRESULT代码。 HRESULT values包含指定函数调用是否失败或成功的信息,以及其他错误代码。如果返回值表示失败,OSError则自动引发a。
由于种种原因,很多东西还是不能直接使用的,ctypes只支持c语言的结构,不支持c++,所以编译之前,需要对要使用的函数使用如下的前缀,才能调用成功:
//必不可少的东西
#define DLLEXPORT extern "C" __declspec(dllexport)
typedef void (*lpFunc)(char, int, TEST_STRUCT*);
DLLEXPORT int a() {
return 1;
}
//所有要导出的方法均需要加这个,
DLLEXPORT void *w(TEST_STRUCT *tt){
tt->y = -101;
std::cout<<"c:"<< sizeof(tt)<<"\n";
//std::cout << "cs:" << sizeof(test_struct) << "\n";
return (void *)tt;
}
一般情况下,给的sdk均为一个头文件(.h)和一堆的dll文件,这里使用的时候,注意可能需要重新编译。
由于c/c++和python对内存回收的机制不一样,导致了学习python的人使用ctypes极易忽略的一个错误就是,通过指针把函数或方法里面的参数值返回出来了。这是一个很愚蠢的方式,当该函数运行结束的时候,c/c++就会回收占用的内存,导致内部的变量的值被回收为空。所以这里一定要注意这个问题:不要返回局部变量的指针。
参考代码如下:
//ctypes测试
#define DLLEXPORT extern "C" __declspec(dllexport)
#include
DLLEXPORT const char * get_char() {
const char * x = "hello ctypes.";
return x;
}
from ctypes import *
dll=cdll.LoadLibrary("ctypes测试")
dllc = cdll.msvcrt
get_char=dll.get_char
get_char.restype=c_char_p
x=get_char( )
print(x)
#print(get_char())
关于返回值每次获取的时候得到的地址都不一样的问题:由于ctypes从局部变量返回了值,而2原来的函数已经被c/c++回收了,所以局部变量的根已经没有了,只能由python代为托管,每次获取的值都是复制一份参数给用户,所以这里不会相等,因为引用的是c的参数,内存回收方式与python不一样。
ctypes使用结构体作为参数传递,是一个比较容易出错的事情。大部分数据上的问题都是结构体的问题。
结构体定义方式如下:
class NET_VCA_DEV_INFO(Structure):
_pack_=4 #这里是定义结构体字节码的地方
_fields_ = [
("struDevIP", NET_DVR_IPADDR),
("wPort", c_uint32),
("byChannel", POINTER(c_byte)),
("byIvmsChannel", c_byte)
]
当数据传输不通畅,或者结构体参数值与预期不符合的时候,注意以下几个问题:
最后经过写c/c++的替代函数,发现指针数据和python的数据(指针头的数据)没有异常。
然后判断指针地址,发现地址也正常。
这就很尴尬了,我就把指针转结构体的操作放在c/c++进行,发现nm居然还是有问题。
我猜测是回调函数调用c方法的时候,指针(c_char_p)发生了一些奇怪的改变。
最后查看别人的范例和百度,找到这么一种方法进行数据转指针:
# pAlarmInfo是一个char*指针
x = string_at(pAlarmInfo, sizeof(NET_VCA_FACESNAP_RESULT))
stru=cast(x,POINTER(NET_VCA_FACESNAP_RESULT))
字节码问题
结构体的数据是根据顺序排列在同一段内存里的,取值的时候按照顺序取。而为了方便cpu取值,所有参数总是在自己字段所占长度的整倍数的位置或在字节码长度的位置。
总之就是,一定要检查python的结构体和c的结构体是否长度一致,不一致要检查字节码问题。