Python调用C的基础学习(传递数字、字符串、数组(一维、二维)、结构体)

一:Python调用windows下DLL

注:我使用的环境:IDLE (Python 3.6 32-bit)  ;vs2010;Windows

1、如何使用vs2010生成dll

参见1~4步:https://jingyan.baidu.com/article/5bbb5a1bd4a7bd13eaa17968.html

注:生成的dll文件在建立的项目debug文件夹下,如下图所示:

Python调用C的基础学习(传递数字、字符串、数组(一维、二维)、结构体)

需注意vs和Python的位数相同(如:同为32位)

 

在python中某些时候需要C做效率上的补充,在实际应用中,需要做部分数据的交互。使用python中的ctypes模块可以很方便的调用windows的dll(也包括linux下的so等文件),下面将详细的讲解这个模块(以windows平台为例子)。

ctypes官方文档

2、加载DLL

  • 访问dll,首先需引入ctypes库(from ctypes import *  )

DLL有stdcall调用约定和cdecl调用约定声明的导出函数,在使用python加载时使用的加载函数是不同的

调用约定的介绍:详见:带你玩转Visual Studio——调用约定__cdecl、__stdcall和__fastcall

1)stdcall调用约定:两种加载方式

Objdll = ctypes.windll.LoadLibrary("dllpath")  
Objdll = ctypes.WinDLL("dllpath") 

2)cdecl调用约定:也有两种加载方式

Objdll = ctypes.cdll.LoadLibrary("dllpath")  
Objdll = ctypes.CDLL("dllpath")  
//其实windll和cdll分别是WinDLL类和CDll类的对象。

3、调用dll中的方法

在2中加载dll的时候会返回一个DLL对象(假设名字叫Objdll),利用该对象就可以调用dll中的方法。 
e.g.如果dll中有个方法名字叫Add(注意如果经过stdcall声明的方法,如果不是用def文件声明的导出函数或者extern “C” 声明的话,编译器会对函数名进行修改,这个要注意,我想你们懂的。) 
调用:nRet = Objdll.Add(12, 15) 即完成一次调用。

看起来调用似乎很简单,不要只看表象,呵呵,这是因为Add这个函数太简单了,现在假设函数需要你传入一个int类型的指针(int*),可以通过库中的byref关键字来实现,假设现在调用的函数的第三个参数是个int类型的指针。

intPara = c_int(9)  
dll.sub(23, 102, byref(intPara))  
print intPara.value  
  • 函数参数申明,通过设置函数的argtypes属性
  • 函数返回类型,函数默认返回c_int类型,如果需要返回其他类型,需要设置函数的restype属性

4、C基本类型和ctypes中实现的类型映射表

ctypes数据类型 C数据类型
c_char char
c_short short
c_int int
c_long long
c_ulong unsign long
c_float float
c_double double
c_void_p void

对应的指针类型是在后面加上”_p”,如int*是c_int_p等等。 
在python中要实现c语言中的结构,需要用到类。

5. 指针与引用

常用的通过调用ctypes类型的指针函数来创建指针实例:

from ctype import *

i = c_int(1)
pi = POINTER(i)

对指针实例赋值只会改变其指向的内存地址,而不是改变内存的内容,与其他语言类似,如需要可改变内容的字符串,可须使用create_string_buffer()

>>> p = create_string_buffer("Hello", 10)  # create a 10 byte buffer
>>> print sizeof(p), repr(p.raw)
10 'Hello/x00/x00/x00/x00/x00'

不带参数的调用指针类型创建一个NULL指针, NULL指针有一个False布尔值

>>> null_ptr = POINTER(c_int)() 
>>> print bool(null_ptr) 
False 


指针实例有一个contents属性,返回这个指针所指向的对象
另外,byref()是用来传递引用参数,pointer()作为传参通常会创建一个实际的指针对象,当不需要实际指针对象时,则可使用byref()
 

二:进行Python调用C的代码实现

注:下面是C中的头文件和声明

#include <stdio.h>
#include <string.h>
#include <stdlib.h >

#define DLLEXPORT __declspec(dllexport)

Python头文件部分和导入dll

from ctypes import *
func = CDLL("dll/first_dll.dll")

 

1、传递并返回数字

C部分:

//传递与返回数值
DLLEXPORT int add(int num1, int num2){
	//printf("hhh\n");
    return num1 + num2;
}

Python部分:

print ("传递并返回数字")
x=4
y=5
res_int = func.add(x,y)
print(x,' + ',y,' = ',res_int)

2、传递并返回字符串

使用ctypesj进行python调用c/c++时,当把string类型str直接传进c/c++的函数时,发现strlen(str)永远是1。例如当输入为"abcdefg"时,强行输出传入的char* s后面的几位s[0]、s[1]、s[2]、s[3]……时,发现输出是a b c d e f g,每个char输出后都会有个空格,然后我再输出(int)s[0]…..发现那些空格都是’\0’。因此strlen(str)是1原因很明显是因为其读到了’\0’就停下来了。而python的string的一个字符明显是占了2byte的,当字符为英文字母时,后1byte为0,前1byte储存ascII码,这估计是为了支持中文这些复杂的文字,而char*是按照1byte为单位进行读取和写入的,所以就有了上面的现象。

故可以把str类型的参数转换成bytes类型进行传递

 

C部分:

DLLEXPORT char* strTest(char* pVal){
	return pVal;
}

Python部分:

print ("传递并返回字符串")
#字符串转化为byte传送  方法一
#val = bytes('qqqqqq',encoding='utf-8')
#方法二
str_test="need_bytes"  
val=str_test.encode()  
func.strTest.restype = c_char_p
print(func.strTest(val)) 

3、传递一维数组

注:可以将数组指针传递给dll,但无法返回数组指针,python中没有对应的数组指针类型

如果需要返回数组,需借助结构体(详见结构体部分)

C部分:

DLLEXPORT int szTest(int a[], int nLen){	
	//int i; printf("一维数组:");
	//for(i = 0; i<nLen; i++)  printf("%d ", a[i]);  printf("\n");
	return a[nLen-1]+5;
}

Python部分:

print ("传递一维数组")
INPUT = c_int * 10
data = INPUT()

for i in range(10):
        data[i] = i

result=func.szTest(data,len(data))
print('data[9] ',data[9],'   返回值result: data[9]+5  ',result)

4、传递二维数组

C部分:

DLLEXPORT int szSecTest(int a[2][3], int iLen,int jLen){
	int i,j;
	
	for(i = 0; i<iLen; i++){
		for(j=0;j<jLen;j++)
			printf("%d ", a[i][j]);
		printf("\n");
	}	
	return a[1][2]+5;
}

注:目前使用Python调用C的dll时,C中的printf并不能显示出来(我未找到解决办法,不知道是否有办法),故若想知道传递的数组是否正确可以,把其输出到文件file中(fprintf)

Python部分:

print ("传递二维数组")
SZ1=c_int *3
SZ2=SZ1*2
data=SZ2()
#data=[[i+j for i in range(2)] for j in range(2)]  #使用它时,相当于list数组,会报错ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1
for i in range(2):
     for j in range(3):
             data[i][j]=i+j
#print(data)
result=func.szSecTest(data,2,3)             
print('data[1][2] ',data[1][2],'   返回值result: data[1][2]+5  ',result) 

5、传递并返回结构体

C部分:

typedef struct StructPointerTest{  
    char name[20];  
    int age;
	int arr[3];
	int arrTwo[2][3];
}StructPointerTest, *StructPointer; 

 // 传递结构体并返回结构体指针(把数组存在结构体中返回)
DLLEXPORT StructPointer testStruct(StructPointerTest input){     
	int i,j;
    StructPointer p = (StructPointer)malloc(sizeof(StructPointerTest)); 
    strcpy(p->name, strcat(input.name,"add"));  
    p->age =input.age + 5;  
	for(i=0;i<3;i++)
		p->arr[i] =input.arr[i]+1;
	for(i=0;i<2;i++)
		for(j=0;j<3;j++)
			p->arrTwo[i][j] =input.arrTwo[i][j]+2;
    return p;   
} 

Python部分:

class StructPointer(Structure):
        _fields_ = [("name",c_char * 20),
                    ("age",c_int),
                    ("arr", c_int * 3),
                    ("arrTwo", (c_int * 3)*2)]

sp=StructPointer()
sp.name=bytes('joe_',encoding='utf-8')
sp.age=23
for i in range(3):
        sp.arr[i]=i
for i in range(2):
     for j in range(3):
          sp.arrTwo[i][j]=i+j   
func.testStruct.restype = POINTER(StructPointer)
p = func.testStruct(sp)
print ('*' * 20)
print ("传递并返回结构体:\n 传递值:name:joe_   age:23   arr:0 1 2  arrTwo:0 1 2;1 2 3")
print("返回值为:name+add:",p.contents.name,'   age+5:',p.contents.age,
      '   arr+1:',p.contents.arr[0],p.contents.arr[1],p.contents.arr[2],
      '   arrTwo+2:',p.contents.arrTwo[0][0],p.contents.arrTwo[0][1],p.contents.arrTwo[0][2],
      p.contents.arrTwo[1][0],p.contents.arrTwo[1][1],p.contents.arrTwo[1][2])

 

以上的运行效果:

Python调用C的基础学习(传递数字、字符串、数组(一维、二维)、结构体)

 

 

参考以下文章:

Python的学习(三十二)---- ctypes库的使用整理

python调用c/c++时string的传入与返回深入分析

ctypes 加载的so库中函数参数的字符串传递问题(str与bytes转换)

python ctypes库3_如何传递并返回一个数组