ctypes学习历程-定期更新

ctypes笔记

概述

之前要调用海康的sdk,尝试过使用ctypes,但是自己没有潜心学习,以为这东西不好用。等最近看arcface的pythondemo的时候,发现都是用ctypes做的接口,感动的泪流满面。。。。
深刻体会:数据出错的时候,请先检查结构体是否对齐,数据是否正确!

数据基础

先讲数据类型的问题。大多数不知道怎么处理的问题其实都在这里。
具体的数据类型在linux和windows上可能有不同,比如c_int在不同的系统上可能长度都不同。
如果遇到不知道是啥的数据类型,先百度,比如DWORD类型,百度一下就知道其实是unsigned int32,所以我们使用c_uint32就ok。

基础数据类型

ctypes类型C类型Python类型备注
c_bool_Boolbool-
c_charchar1个字符的字节对象-
c_wcharwchar_t1个字符的字符串-
c_bytecharint-
c_ubyteunsigned charint-
c_shortshortint-
c_ushortunsigned shortint-
c_intintint-
c_uintunsigned intint-
c_longlongint-
c_ulongunsigned longint-
c_longlong__int64 或者 long longint-
c_ulonglongunsigned __int64 或者 unsigned long longint-
c_size_tsize_tint-
c_ssize_tssize_t 要么 Py_ssize_tint-
c_floatfloatfloat-
c_doubledoublefloat-
c_longdoublelong doublefloat-
c_char_pchar * ((NUL terminated))bytes对象或 None-
c_wchar_pwchar_t * ((NUL terminated))字符串或 None-
c_void_pvoid *int或 None通用指针,不指定数据类型,使用指针内容的时候需要强制转换。
c_uint32或c_uintDWORDint根据x32或x64决定该值

日常使用的时候,注意指针漂移问题: 在使用x64编程的时候,可能会遇到系统报错,如果出现地址读取错误或者地址冲突等,首先给方法强制定义输入参数和输出参数,然后检查参数是否存在32位或64位的区分,特别是涉及到指针,一定要强制定义,不然就会报错。
日常中,除了以上类型,还有一些值得注意的地方:一般情况下数据传递通过结构体。而结构体很多时候都使用的是指针进行传递数据。数据传递主要分为两种:文本数据和二进制数据。
文本数据很简单,一般不会有指针, 即使有,使用的方式也是POINTER(c_int)大概这样的结构。如果是结构体指针,先定义结构体,再使用指针。
二进制数据:包括从内存读取数据和通过指针传递二进制数据。

  1. 知道文件地址,从内存读取数据。
    假设,我们知道某个指针指向一段内存,也知道这段内存是什么数据结构,可以通过如下方式将数据结构化:
#从二进制获取数据
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 <iostream>
#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。

对c++的要求

由于种种原因,很多东西还是不能直接使用的,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 <iostream>
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)
    ]

当数据传输不通畅,或者结构体参数值与预期不符合的时候,注意以下几个问题:

  1. 结构体内结构的数据排列顺序与c/c++结构体应一致
  2. 结构体的实例与c/c++实例得到的sizeof长度应一致
  3. 读取结构体的方式应注意
    这里记录一个自己遇到的问题:
    在做和海康sdk对接的时候,回调函数返回的参数值总是不发生改变,而且均为错误的值。经过判断,可能为回调函数的参数异常,或者根据数据生成的结构体数据异常。但是使用c++自己的回调函数就没问题。
    猜测问题
  4. 结构体指针数据异常
  5. 结构体指针地址异常

最后经过写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的结构体是否长度一致,不一致要检查字节码问题。

读取二进制数据

  1. 调用约定,我tm发现海康的调用约定都是windll,我tm用cdll,不报错才怪
  2. 对于回调函数,在使用之前先引用一下,免得被python垃圾回收了,这样c++调用的时候会报错。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值