Python调用c/c++动态库(一)

6 篇文章 0 订阅
4 篇文章 2 订阅

2020.6.22更新:
增加了部分案例,并在python2和python3下都进行了调试。
——————————————————————————————————————————
因为工作需求,最近要使用python在linux环境下调用c/c++的动态库,执行动态库中的函数。这种没接触过的内容,自然首先开启百度谷歌大法。经过一番搜索,尝试使用python的ctypes模块。

一、初识
首先自然是查询文档了。
附文档链接:
python2.7 ctypes文档
python3.6 ctypes文档
python2.7文档描述:“ctypes is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python.”
大意是ctypes是python的一个外部函数库,提供了c的兼容数据类型,允许调用DLL或者共享库中的函数。通过该模块能以纯python的代码对这些库进行封装(最后这句话不理解 =_=,看别人的文章,说是这样你就可以直接 import xxx 来使用这些函数库了)。
可知ctypes只提供了c的兼容,因此若是c++代码,需要使其以c的方式进行编译。
(想了解如何用c的方式编译c++代码,见链接:c方式编译c++

二、使用
注明:本文均采用python2.7版本
新增了python3的调试,除了str编码内容,其余无需变动

看了文档,就要开始使用了。根据我遇到的使用场景,主要有3个步骤需要处理。
0_0
1.动态库的引入
这一步操作很简单,只需调用方法LoadLibrary,查看文档:
LoadLibrary(name):Load a shared library into the process and return it. This method always returns a new instance of the library.
加载动态库到进程中并返回其实例对象,该方法每次都会返回一个新的实例。
举个栗子,编写如下代码:

# -*- coding: utf-8 -*-
from ctypes import *

#引入动态库libDemo.so
library = cdll.LoadLibrary("./libDemo.so")

2.函数的声明和调用
因为ctypes只能调用C编译成的库,因此不支持重载,需要在程序中显式定义函数的参数类型和返回类型。
不过在介绍前,需要先了解ctypes的基本数据类型
ctypes基本数据类型
该表格列举了ctypes、c和python之间基本数据的对应关系,在定义函数的参数和返回值时,需记住几点:

  • 必须使用ctypes的数据类型
  • 参数类型用关键字argtypes定义,返回类型用restype定义,其中argtypes必须是一个序列,如tuple或list,否则会报错
  • 若没有显式定义参数类型和返回类型,python默认为int型

举个栗子:

/******C端代码*********/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

#include "demo.h"

int hello()
{
    printf("Hello world\n");
    return 0;    
}

编写c端代码demo.c, 用GCC指令编译成动态库libDemo.so :gcc -fPIC -shared -o libDemo.so demo.c

编写python端代码linkTest.py进行调用:

# -*- coding: utf-8 -*-
from ctypes import *

#引入动态库libDemo.so
    library = cdll.LoadLibrary("./libDemo.so")

    library.hello()

执行结果:
输出hello
3.C语言和python之间数据类型的转换
这部分内容是当初学习的重点,踩了不少坑。

3.1 基本数据类型
这部分已经在上面提到过了,根据对应表格进行转换使用即可。
样例代码在上述demo.c和linkTest.py中继续添加:
C端代码:

......(省略上述代码)
int basicTest(int a, float b)
{
    printf("a=%d\n", a);
    printf("b=%f\n", b);
    return 100;
}

python端代码

def c_basic_test():
    library.basicTest.argtypes = [c_int, c_float]
    library.basicTest.restype = c_void_p

    a = c_int(10)
    b = c_float(12.34)
    library.basicTest(a, b)

执行结果:
result
3.1-增加 字符指针及数组类型

c代码:

void arrayTest(char * pStr, unsigned char *puStr)
{
    int i = 0;
    printf("%s\n", pStr);
    for (i = 0; i < 10; i++) {
        printf("%c ", puStr[i]);
    }
    printf("\n");
}

python代码:
该样例有几个注意点:
<1>使用了ctypes的create_string_buffer和from_buffer_copy函数。
create_string_buffer函数会分配一段内存,产生一个c_char类型的字符串,并以NULL结尾。
同理有个类似函数create_unicode_buffer函数,返回的是c_wchar类型
create_string_buffer
from_buffer_copy函数则是创建一个ctypes实例,并将source参数内容拷贝进去
在这里插入图片描述
<2>python2与python3的字符差异:
python2默认都是ascii编码,python3中str类型默认是unicode类型,而ctypes参数需传入bytes-like object(提示这么说的)。因此python3中的字符串都需转换编码,如样例所示。

def c_array_test():
    # 以下两方法皆可,但需显式说明c_ubyte数组的大小,个人感觉不方便,求指导
    # library.arrayTest.argtypes = [c_char_p, c_ubyte*16]
    library.arrayTest.argtypes = [c_char_p, POINTER(c_ubyte * 16)]
    library.arrayTest.restype = c_void_p

    # python2
    # str_info = create_string_buffer("Fine,thank you")

    # python3,str都是unicode格式,需转为其余编码,可使用encode函数或b前缀
    # 以下两种方法皆可
    str_info = create_string_buffer(b"Fine,thank you")
    str_info = create_string_buffer("Fine,thank you".encode('utf-8'))
    # 调用类型需配套
    u_str_info = (c_ubyte * 16).from_buffer_copy(b'0123456789abcdef')
    # library.arrayTest(str_info, u_str_info)
    library.arrayTest(str_info, byref(u_str_info))

执行结果:
result

3.2 指针

  • 一级指针

指针是c语言中的重要内容,难免要经常使用。因为我处理的主要是api接口的转换,涉及的指针处理就是定义指针类型、获取地址或值,而ctypes中都提供了相应的函数。
在上面的对应表格中,我们可以看到char * 和 void * 已经有专用类型了,直接使用即可,对于其他类型的指针,ctypes提供了两种定义方式:pointerPOINTER
查询文档:
ctypes.POINTER(type)
This factory function creates and returns a new ctypes pointer type. Pointer types are cached and reused internally, so calling this function repeatedly is cheap. type must be a ctypes type.

ctypes.pointer(obj)
This function creates a new pointer instance, pointing to obj. The returned object is of the type POINTER(type(obj)).

大意是POINTER必须传入ctypes类型,创建出新的ctypes 指针类型(pointer type),而pointer传入一个对象,创建出一个新的指针实例。可见POINTER创建出了pointer,我一般选择POINTER来使用(具体差别还没太多研究。。=_=).

传输地址,ctypes提供了byref函数:
ctypes.byref(obj[, offset])
Returns a light-weight pointer to obj, which must be an instance of a ctypes type. offset defaults to zero, and must be an integer that will be added to the internal pointer value.

The returned object can only be used as a foreign function call parameter. It behaves similar to pointer(obj), but the construction is a lot faster.

大意是返回一个指向ctypes实例对象的轻量级指针,函数中还可以通过参数(必须是int)来设置偏移地址,这个返回的对象只能用于外部函数调用的参数。

有了这几个函数,指针就能实现啦。见实例:
C端代码

void pointerTest(int * pInt, float * pFloat)
{
    *pInt = 10;
    *pFloat = 12.34;
}

python端代码

def c_pointer_test():
    library.pointerTest.argtypes = [POINTER(c_int), POINTER(c_float)]
    library.pointerTest.restype = c_void_p

    int_a = c_int(0)
    float_b = c_float(0)
    library.pointerTest(byref(int_a), byref(float_b))
    print("out_a:", int_a.value)
    print("out_b:", float_b.value)

可见我们在python中预设的值都在函数中被更改了,指针有效。
在这里插入图片描述

  • 补充样例:外部传输字符空间,在函数内部进行操作

c代码:

void mallocTest(char *pszStr)
{
    strcpy(pszStr, "Happay Children's Day!");
}

python代码:

def c_malloc_test():
    library.mallocTest.argtypes = [c_char_p]
    library.mallocTest.restype = c_void_p

    word = (c_char * 32)()
    library.mallocTest(word)
    print("out_word:", word.value)

执行成功!

  • 二级指针

在实际应用中,我处理的二级指针是在c端接口传送一个char*指针的地址作为参数,在接口中给char * 指针分配空间和赋值。
根据指针的功能,很容易实现,见实例。
c端接口代码

/**函数定义*/
void doublePointTest(int ** ppInt, char ** ppStr)
{
    printf("before int:%d\n", **ppInt);
    **ppInt = 10086;

    *ppStr = (char*)malloc(10 * sizeof(char));
    strcpy(*ppStr, "Happy  National Day!");
}
//释放函数
void freePoint(void *pt) {
    if (pt != NULL) {
        free(pt);
        pt = NULL;
    }
}

/**函数调用,运行会输出接口中复制的字符串*/
int useDoublePoint()
{
    int num_e = 10;
    int * pInt = &num_e;
    char *pStr_b = NULL;
    doublePointTest(&pInt, &pStr_b);
    printf("after int:%d\n", *pInt);
    printf("out str:%s\n", pStr_b);
    //必须释放动态内存
    freePoint(pStr_b);
}

python端代码

# 函数定义及测试
def c_double_point_and_free_test():
    library.doublePointTest.argtypes = [POINTER(POINTER(c_int)), POINTER(c_char_p)]
    library.doublePointTest.restype = c_void_p

    library.freePoint.argtypes = [c_void_p]
    library.freePoint.restype = c_void_p

    int_a = c_int(10)
    # 注意POINTER和pointer的区别,因为POINTER必须指向ctypes类型,此处只能用pointer
    int_pt = pointer(int_a)
    word_pt = c_char_p()
    library.doublePointTest(byref(int_pt), byref(word_pt))
    print("out_word:", word_pt.value)
    # contents是指针内容
    print("out_value:", int_pt.contents.value)
    library.freePoint(word_pt)

执行结果:
在这里插入图片描述

另一个c端接口,需要传入一个char* 指针的地址,出参为这个指针指向的内容和内容长度。该接口用于处理图片的rgb值,因此内容都是0-255的值,样例代码如下

......(省略上述代码)
#define LENGTH 8
void doubleUnsignedPointerTest( char ** ppChar, int *num )
{
    
    int i;
    *num = LENGTH;
    *ppChar = (char*)malloc(LENGTH*sizeof(char));
    
    for(i=0;i<LENGTH;i++)
    {
        (*ppChar)[i] = i;
    }
}

C端调用代码

	int num = 0, i;
	doubleUnsignedPointerTest(&pChar, &num);
	for(i=0;i<num;i++)
	{
	    printf("pChar[%d]:%d\n", i, pChar[i]);    
	}
	freePointer(pChar);

这时python端若使用c_char_p来构造类似上述的代码,在输出时会提示超出范围异常(out of range)。
查询了文档,原因在于:
class ctypes.c_char_p
Represents the C char * datatype when it points to a zero-terminated string…

(大意是c_char_p指针必须指向’\0’结尾的字符串。因此输出时遇到了第一个’\0’就中断了)

所以根据对应表格,使用POINTER嵌套构造了双重指针,使用后无误
python端代码

	......(省略上述代码)
    library.doubleUnendPointerTest.argtypes = [POINTER(POINTER(c_byte)), POINTER(c_int)]
    library.doubleUnendPointerTest.restype = c_void_p
    
    num = c_int(0)
    word_pt = POINTER(c_byte)()   #若使用c_char类型,在下面输出时会以字符类型输出,有乱码,因此选择c_byte
    library.doubleUnendPointerTest(byref(word_pt), byref(num))
    
    print "num:", num.value
    for i in range(8):
        print "key:", i, " value:", word_pt[i] 

执行结果:
在这里插入图片描述
3.3 结构体、共用体
结构体、共用体是c中常用类型,使用前需要先定义其成员类型,在python中也是同样的处理。查看文档:
Structures and unions must derive from the Structure and Union base classes which are defined in the ctypes module. Each subclass must define a fields attribute. fields must be a list of 2-tuples, containing a field name and a field type.
The field type must be a ctypes type like c_int, or any other derived ctypes type: structure, union, array, pointer.

大意是结构体和共用体必须继承Sturcture和Unino类,定义其成员必须使用_field_属性。该属性是一个list,其成员都是2个值的tuple,分别是每个结构体/共用体成员的类型和长度,而且定义类型必须使用ctype类型或由ctype组合而成的新类型。
以此写个样例:
c端代码

//结构体
typedef struct _rect
{
    int index;
    char info[16];
}Rect;

<1>读取结构体
C代码:

int readRect(Rect rect)
{
    printf("value=============\n");
    printf("index:%d\ninfo:%s\n", rect.index, rect.info);
    return 0;
}

python代码:


# 结构体
class Rect(Structure):
    _fields_ = [
        ('index', c_int),
        ('info', c_char * 16)
    ]
    
def c_read_rect():
    library.readRect.argtypes = [Rect]
    library.readRect.restype = c_void_p

    rect_a = Rect(10, b"Hello")
    library.readRect(rect_a)

<2>读取结构体,传参为指针
C代码:

int readRectPoint(Rect * pRect)
{
    printf("point==============\n");
    printf("index:%d\n", pRect->index);
    printf("info:%s\n", pRect->info);
    return 0;
}

python代码:

def c_read_rect_point():
    library.readRectPoint.argtypes = [POINTER(Rect)]
    library.readRectPoint.restype = c_void_p

    rect_a = Rect(10, b"Hello")
    library.readRectPoint(byref(rect_a))

<3>类似,可以传输结构体数组给动态库,实质是传输结构体数组指针,也就是首元素指针
C代码:

void readRectArray(Rect *pRectArray)
{
    int i;
    for(i=0;i<5;i++)
    {
    	printf("pRectArray.index:%d\n", pRectArray[i].index);
        printf("pRectArray.info:%s\n", pRectArray[i].info);
    }
}

python代码:

def c_read_rect_array():
    library.readRectArray.argtypes = [POINTER(Rect)]
    library.readRectArray.restype = c_void_p

    rect_array = (Rect * 5)()
    for i in range(5):
        # python2
        # rect_array[i] = Rect(i, "Hello_" + str(i))
        # python3
        rect_array[i] = Rect(i, bytes("Hello_"+str(i), encoding='utf-8') )

    # 以下两方法皆可
    # library.readRectArray(rect_array)
    library.readRectArray(byref(rect_array[0]))

执行结果:
在这里插入图片描述
<4> 从动态库中获取结构体数组内容
C代码:

/**函数定义*/
Rect * obtainRectArray(int *pArrayNum)
{
    int num = 5;
    *pArrayNum = num;
    Rect *pArray = (Rect*)malloc(num * sizeof(Rect));
    for (int i = 0; i < num; i++) {
        pArray[i].index = i;
        sprintf(pArray[i].info,"%s_%d", "Hello", i);
    }
    return pArray;
}
//必须释放内存
void freeRect(Rect *pRect)
{
    free(pRect);
}

/**c中调用方式*/
void testObtainRectArray(int *num)
{
    int num = 0;
    Rect *pstRectArray = obtainRectArray(&num);
    for (int i = 0; i < num; i++) {
        printf("index:%d\n", pstRectArray[i].index);
        printf("info:%s\n", pstRectArray[i].info);
    }
    freeRect(pstRectArray);
}

python代码:
ctypes的pointer有一个contents方法,通过该方法可以获取指针的内容。但在这个样例中,contents只能获取首元素的内容,后来才发现竟然能循环读取= =

def c_obtain_rect_array_and_free():
    library.obtainRectArray.argtypes = [POINTER(c_int)]
    # library.obtainRectArray.restype = Array()
    library.obtainRectArray.restype = POINTER(Rect)

    library.freeRect.argtypes = [POINTER(Rect)]
    library.freeRect.restype = c_void_p
    num = c_int(10)

    rect_pt = library.obtainRectArray(byref(num))
    num = num.value
    print("num:", num)
    # 这种赋值方法是真的没想到,找了好久= = 之前一直在rect_pt.contents上面绕
    # rect_pt.contents只能输出首元素的内容,如rect_pt.contents.index
    rect_array = [rect_pt[i] for i in range(num)]

    for item in rect_array:
        print("index:", item.index)
        print("info:", item.info)

    library.freeRect(rect_pt)

执行结果:
在这里插入图片描述
三、结语

通过文档,python调用C动态库还是比较容易实现的,只是第一次使用没有经验,摸索了一段时间。其实文档还有很多内容,待后续再慢慢学习吧。
附源码:https://pan.baidu.com/s/10dUqfrVQYYDDFTgNAa0Pmw
提取码: yctx

四、补充
1,动态库so中的函数,如fun_add,可能使用了c++的函数模块,直接增加extern "C"无法编译,可以再封装一层函数,如fun_c_add,该函数直接调用fun_add函数,则fun_c_add可以增加extern "C"编译通过,python也可调用;

2,特殊的函数指针,如unsigned char *,转化为python,函数的定义参数格式是POINTER(c_ubyte),较为复杂,可以用c_void_p类型代替。

  • 18
    点赞
  • 117
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值