创建一个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的理解产生帮助。