创建一个C文件,并在Python中调用该C文件中的函数
目录
- 首先创建sample.c和sample.h文件,并确保没有错误
- 编译成动态链接库文件(Linux:.so, Windows:.dll)
- 测试库文件
- Python访问C代码
- 对c库文件进行封装
- 对Python包装模块进行测试
- 结语
首先创建sample.c和sample.h文件,并确保没有错误
//sample.c
// /* sample.c */_method;
#include <math.h>
#include "sample.h"
/* Compute the greatest common divisor */
int gcd(int x, int y)
{
int g = y;
while (x > 0)
{
g = x;
x = y % x;
y = g;
}
return g;
}
/* Test if (x0,y0) is in the Mandelbrot set or not */
int in_mandel(double x0, double y0, int n)
{
double x=0,y=0,xtemp;
while (n > 0)
{
xtemp = x*x - y*y + x0;
y = 2*x*y + y0;
x = xtemp;
n -= 1;
if (x*x + y*y > 4) return 0;
}
return 1;
}
/* Divide two numbers */
int divide(int a, int b, int *remainder)
{
int quot = a / b;
*remainder = a % b;
return quot;
}
/* Average values in an array */
double avg(double *a, int n)
{
int i;
double total = 0.0;
for (i = 0; i < n; i++)
{
total += a[i];
}
return total / n;
}
/* Function involving a C data structure */
double distance(Point *p1, Point *p2)
{
return hypot(p1->x - p2->x, p1->y - p2->y);//hypot标准库文件math.h里的函数
}
//sample.h
#pragma once //只被包含一次
int gcd(int x, int y);
int in_mandel(double x0, double y0, int n);
int divide(int a, int b, int *remainder);
double avg(double *a, int n);
/* A C data structure */
typedef struct Point{ //结构体放入头文件
double x,y;
} Point;
double distance(Point *p1, Point *p2);
编译成动态链接库文件(Linux:.so, Windows:.dll)
C文件生成可执行的文件需要经过预处理(处理头文件和宏等),编译(生成汇编程序),汇编(生成二进制程序),链接(生成可执行的程序)
gcc sample.c -shared -fPIC -o libsample.so
-shared选项说明编译成的文件为动态链接库,不使用该选项相当于可执行文件
-fPIC 表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的。
所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
-o filename 表示输出到文件 filename
另外,动态链接库中可以包含其他静态链接库和动态链接库,所以#include<math.h>是合法的。
然后这里有个小问题,就是上面的那个动态链接库其实用到了math.h,我们应该将math.h对应的库文件libm.so
也链接进来,
奇怪的是没有链接也没有报错,如果有c文件用到该libsample.so
链接库,就会出现错误,
.更奇怪的是用python的外部库ctypes访问的时候,竟然不报错
猜测是ctypes实现了.so文件对标准库的使用
没有测试,该命令应该解决这个问题:gcc sample.c -lm -shared -fPIC -o libsample.so
, -l前缀的作用后面会说到。
假设我们没有链接库文件libm.so
,在用到的时候链接也是可以的。
测试库文件
为了测试我们的库文件,我们写一个test_sample.c文件去进行测试:
//<pre name="code" class="cpp">
#include "sample.h"
#include<stdio.h>
int main()
{
int x = 5, y = 7 , z = 8;
z = gcd(x, y);
printf("%d\n", z);
return 0;
}
gcc test_sample.c -L. -Wl,-rpath,libsample_path -lsample -lm -o sample
-lsample(-l) 代表链接的文件名,gcc会自动为其前面添加lib,在其后边添加.so 即libsample.so, libm.so(math.h对应的头文件)
-L. 表示链接的文件在当前目录(.)下,这里指定了链接时的路径,如果这个路径下没有这个库文件,是会报错的,因为找不到动态链接库文件,
但是对于标准库文件,gcc都会去指定的路径去找
-Wl,-rpath,libsample_path:指定程序运行时加载的共享库搜索目录,libsample_path就是Libsample.so所在的目录,如果不加这个参数和路径的话,
执行sample时,会去环境变量LD_LIBRARY_PATH
所指定的目录去搜索libsample.so,这显然是找不到的
注意和-L 的区别
下面这句话和上一句一个意思,但是包含了链接程序,附一下:
-Wl,表示后面的参数将传给link程序ld(因为gcc可能会自动调用ld,ld猜测为链接程序)。这里通过gcc 的参数"-Wl,-rpath,"指定执行时链接库的搜索路径
接下来执行命令
./sample
就会得到正确的输出
Python访问C代码
对c库文件进行封装
cookbook 首先,需要创建一个Python模块来对C文件进行包装,一个Python模块就是一个.py文件,
如sample.py ,访问该模块时,只需要 import sample即可
要访问C代码,需要使用Python外部库 ctypes
下面的代码假定libsample.so和sample.py放在了一个文件夹,当然也可以不在,用下面的变量取代sample.py的变量的值即可
_file = ""
_path = "libsample.so的绝对路径"
#sample.py
import ctypes
import os #操作系统有关的接口库,这里用到的是文件目录操作
# Try to locate the .so file in the same directory as this file
_file = 'libsample.so'
#_path即为libsample.so的绝对路径,
#os.path.realpath(__file__)为sample.py的绝对路径
#os.path.split(os.path.realpath(__file__)) 切割路径,得到一个元组(tuple)
#os.path.split(os.path.realpath(__file__))[:-1] + (_file,) [:-1]去除最后一个元素,即sample.py, 并加上'libsample.so'在最后
#*()的作用是将元组中的元素解压成多个独立的元素
#os.path.join 将这些独立的元素合成一个字符串路径
_path = os.path.join( *(os.path.split(os.path.realpath(__file__))[:-1] + (_file,) ) )
#从指定的文件路径加载一个c库
_mod = ctypes.cdll.LoadLibrary(_path)
if __name__ == 'main': #查看其类型
print(_mod)
print(type(_mod ) )
gcd = _mod.gcd #这样就可以访问gcd函数,只是用gcd包装了起来,所以需要我们了解c库的底层细节(函数的实现),而外部只需要sample.gcd就可以访问c库中的函数了
if __name__ == 'main':
print(type(gcd) )
#ctypes.c_int,ctypes.c_double等等都是ctypes中定义的和C语言数据类型对应的类
gcd.argtypes = (ctypes.c_int, ctypes.c_int) #函数参数类型,gcd.argtypes本身是元组类型
gcd.restype = ctypes.c_int #放回值类型
#后面的包装也都是按照这种方式,只是因函数实现的不同而略显不同
#int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int
#int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
#包装了一下,返回值改成了元组
def divide(x, y):
rem = ctypes.c_int()
quot = _divide(x, y, rem)
return quot,rem.value
#void avg(double *, int n)
#Define a special type for the 'double *' argument
#数组在Python中怎么表示?list tuple都可以是数组
#我们定义一个类DoubleArrayType,来表示Python的数组类型
class DoubleArrayType:
def from_param(self, param):
typename = type(param).__name__ #获取类型的字符串表示
if hasattr(self, 'from_' + typename): #如果这个类本身定义了这个函数,能将某种类型封装成数组
return getattr(self, 'from_' + typename)(param) #调用该函数
elif isinstance(param, ctypes.Array): #如果本身就是ctypes库中的数组类型
return param
else:
raise TypeError("Can't convert %s" % typename)
#Cast from array.array objects
#array是Python的一个表示数组的模块,有typecode表示数组类型,'d'表示double, 因为我们定义的类就是double的数组,所以要返回错误信息
def from_array(self, param):
if param.typecode != 'd':
raise TypeError('must be an array of doubles')
ptr, _ = param.buffer_info()
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
#Cast from lists/tuples
def from_list(self, param):
val = ((ctypes.c_double)*len(param))(*param)
return val
from_tuple = from_list
# Cast from a numpy array
def from_ndarray(self, param):
return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
DoubleArray = DoubleArrayType()
_avg = _mod.avg
_avg.argtypes = (DoubleArray, ctypes.c_int)
_avg.restype = ctypes.c_double
def avg(values):
return _avg(values, len(values))
# struct Point { },结构体也是通过定义一个类来实现,父类是ctypes.Structure
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_double),
('y', ctypes.c_double)]
# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
distance.restype = ctypes.c_double
对Python包装模块进行测试
我们可以在sample.py所在目录下的终端下执行python3
来进入python交互式解释器,然后import sample
,最后通过sample.gcd就能访问c函数了
如果想在任意python文件中使用访问sample模块,需要将sample.py放进第三方模块所在的文件目录,一般解释器会去该目录去寻找第三方模块
import sys #python解释器及其所依赖环境有关的模块
import pprint #多行输出,避免扎堆
pprint.pprint(sys.path)
可以查看该文件目录
也可以告诉Python解释器,除了去常规的路径寻找导入的Module外,还可以去你指定的路径寻找Module。
import sys
sys.path.append('sample.py所在的绝对路径')
结语
希望通过Python对C库的访问这个学习,能对C语言,Python,以及深度学习编译器TVM的理解产生帮助。