本文档由小小明个人学习整理

文章链接:https://blog.csdn.net/as604049322/article/details/112058313

pdf下载地址:https://download.csdn.net/download/as604049322/13999212

python调用go语言

​ Python是一个生产力很高的语言,能够以最高的效率完成最多的事,但是Python的性能,是我们一直诟病的一个问题,尤其是一个大锁GIL。当然现在大部分程序都是(IO)网络密集型程序,Python足以胜任,但是如果说我们已经存在的项目或者想要开发的项目中,存在有计算密集型的程序场景,该如何提升性能呢?

​ 一般是可以用C\C++重写Python计算密集的地方,来提高性能,但是C\C++是有一些学习成本的,指针和自己释放内存都有一定门槛。Go就很方便了,自动垃圾自动回收,还有天生高并发等优势。

​ python的ctypes模块提供了和C语言兼容的数据类型和函数来加载so/dll动态链接库文件,而GO语言本身就可以编译出符合c语言规范的dll或so动态链接库,基于这两项特性,于是我们可以顺利的使用python来调用go语言。

Golang环境配置

Go官方镜像站点:https://golang.google.cn/dl/

选择默认的最高版本就好,Go代码向下兼容版本之间的差异并无所谓

查看是否安装成功

>go version
go version go1.15.2 windows/amd64

注:由于已经是1.11+版本,我们以后使用go mod进行管理依赖,不需要配置GOPATH等奇怪的东西。

配置GOPROXY(代理)

可能我们需要借用Go下载一些包什么的,但是默认官网源GOPROXY=https://proxy.golang.org,direct,在国内访问不到

输入go env查看Go配置:

>go env
...
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=D:\Go
...

改成国内镜像站点:

go env -w GOPROXY=https://goproxy.cn,direct

再次查看Go配置:

>go env
...
set GOPROXY=https://goproxy.cn,direct
set GOROOT=D:\Go
...

go语言跨平台编译

跨平台编译,也叫交叉编译,我可以在win平台上,编译成linux平台可执行的文件。

这也是Go备受青睐的原因,像java,python,php等语言,我们开发一般是在win平台上开发,部署的时候在linux上部署,在处理第三方依赖是比较麻烦,不仅开发累,运维也累,虽然现在有docker解决了这个痛点,但是应该还是没原生来的舒服。

如果使用Go的话,不管是什么第三方依赖,最终只会打包成一个可执行文件,直接部署即刻,并且是高并发方式,心再大一点,连Nginx都不用,但是一点不用担心并发问题。

示例

Windows下编译linux平台可执行程序:

cmd下依次执行以下命令:

SET CGO_ENABLED=0  // 禁用CGO
SET GOOS=linux  // 目标平台是linux
SET GOARCH=amd64  // 目标处理器架构是amd64

然后执行go build,得到的就是能够在linux上的可执行文件。

将这个文件上传到linux服务器上,即使Go环境都没有,都可以执行成功。

Windows下编译Mac平台64位可执行程序:

SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build

Mac 下编译 Linux 和 Windows平台 64位 可执行程序:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

Linux 下编译 Mac 和 Windows 平台64位可执行程序:

CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

python与go性能对比

为了更好的体现出来优化之后的效果,我们大概对比一下两个语言在计算密集情况下的差距。

测试:分别计算一个亿(100000000)的累加模拟大量计算。

Python代码:

import time

def run(n):
    sum = 0
    for i in range(n):
        sum += i
    print(sum)


if __name__ == '__main__':
    startTime = time.time()
    run(100000000)
    endTime = time.time()
    print("耗时:", endTime - startTime)

耗时5s左右:

image-20200926224048065

Go代码:

package main

import (
  "fmt"
  "time"
)

func run(n int) {
  sum := 0
  for i := 0; i < n; i++ {
    sum += i
  }
  fmt.Println(sum)
}
func main() {
  var startTime = time.Now()
  run(100000000)
  fmt.Println("耗时:", time.Since(startTime))
}

耗时50ms左右:

image-20200926224400517

Go代码编译为Python可调用的.so文件

安装64位gcc工具MinGW

去https://sourceforge.net/projects/mingw-w64/下载后,一步步安装

已经将离线包上传到了百度云:

https://pan.baidu.com/s/1ZmjQUf5QcBbeHCi7mIrYxg 提取码: edc5

Windows适应于x86_64-8.1.0-release-win32-seh-rt_v6-rev0,直接解压并将MinGW的bin目录加入环境变量中后即可使用。

查看gcc版本:

>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=D:/develop/mingw64/bin/../libexec/gcc/x86_64-w64-mingw32/8.1.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../../../src/gcc-8.1.0/configure --host=x86_64-w64-mingw32 --build=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --prefix=/mingw64 --with-sysroot=/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64 --enable-shared --enable-static --disable-multilib --enable-languages=c,c++,fortran,lto --enable-libstdcxx-time=yes --enable-threads=win32 --enable-libgomp --enable-libatomic --enable-lto --enable-graphite --enable-checking=release --enable-fully-dynamic-string --enable-version-specific-runtime-libs --disable-libstdcxx-pch --disable-libstdcxx-debug --enable-bootstrap --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-gnu-as --with-gnu-ld --with-arch=nocona --with-tune=core2 --with-libiconv --with-system-zlib --with-gmp=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-mpfr=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-mpc=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-isl=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-pkgversion='x86_64-win32-seh-rev0, Built by MinGW-W64 project' --with-bugurl=https://sourceforge.net/projects/mingw-w64 CFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/include' CXXFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/include' CPPFLAGS=' -I/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/include' LDFLAGS='-pipe -fno-ident -L/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/lib -L/c/mingw810/prerequisites/x86_64-zlib-static/lib -L/c/mingw810/prerequisites/x86_64-w64-mingw32-static/lib '
Thread model: win32
gcc version 8.1.0 (x86_64-win32-seh-rev0, Built by MinGW-W64 project)

需要被编译.so文件的go代码有些要求,例如必须导入C:

package main

import (
  "C" //C必须导入
)

//export run
func run(n int) int{
  // 必须通过export 函数名格式的注释申明该函数可以被外部接口
  sum := 0
  for i := 0; i < n; i++ {
    sum += i
  }
  fmt.Println("我是Go代码,我跑完了,我的结果是:",sum)
  return sum
}

func main() {
  //main函数中什么都不要写,和包名main要对应
}

编译为.so文件供Python调用:

go build -buildmode=c-shared -o s1.so s1.go

格式:go build -buildmode=c-shared -o 输出的.so文件 go源文件

会生成.h文件和.so文件,.so文件供Python调用,如下图所示:

image-20200927002106002

Ptyhon调用so文件

将上述生成的.so文件复制到Python项目的同一级目录。

编写s1.py,依然是计算一个亿,关键部分由Go生成的.so执行:

from ctypes import *
import time

if __name__ == '__main__':
    startTime = time.time()

    s = CDLL("s1.so")  # 加载s1.so文件
    result = s.run(100000000)  # 调用Go生成的.so文件里面的run函数
    print("result:", result)

    endTime = time.time()
    print("耗时:", endTime - startTime)

共耗时:0.04s左右:

image-20200927003245302

可以看到,虽然速度很快,但是Python在调用Go生成的.so文件之后,拿到的返回值竟然是错的,但是在Go中打印的确实对的!

但是计算一些的比较小的数,以10023为例,结果是正确的:

image-20200927003538178

.h文件探究

上面的问题是因为默认返回值类型存储范围有限导致的,下面将具体分析go编译生成的c中间文件一探究竟。

打开.h文件,翻到末尾:

image-20200927005445020

找到extern开头的声明:

extern GoInt run(GoInt n);

这是前面go源码中声明的run方法被转换为c语言代码,表示参数和返回值类型在c语言中都是GoInt类型。

翻到类型定义的位置:

image-20200927010139762

可以看到,GoInt其实就是GoInt64,GoInt64的类型是long long类型。

Python使用ctypes模块调用.so文件时有一个对应表:

参考:https://docs.python.org/zh-tw/3.7/library/ctypes.html

ctypes 类型C 类型Python 类型
c_bool_Boolbool (1)
c_charchar单字符字节对象
c_wcharwchar_t单字符字符串
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 结尾)字节串对象或 None
c_wchar_pwchar_t * (以 NUL 结尾)字符串或 None
c_void_pvoid *int 或 None

根据上述表格可以发现,在C中的long long类型对应的ctype类型是c_longlong,在python中的类型是int。

python的默认数值处理类型是Long(8字节),go语言编译的run方法,未申明的情况下返回值类型却是Int(4字节),所以当计算结果超过Int的可存储范围时就会出现问题。

Int的取值范围为:-2^31 — 2^31-1,即-2147483648 — 2147483647

现在根据实际的ctype在python中申明run的实际返回值类型即可:

from ctypes import *
import time

if __name__ == '__main__':
    beginTime = time.time()
    s = CDLL("s1.so")  # 加载s1.so文件
    # 根据查表,C中的long long,对应的ctypes 是 c_longlong
    s.run.restype = c_longlong  # 声明.so的run函数返回值类型,固定格式
    result = s.run(100000000)  # 调用Go生成的.so文件里面的run函数

    print(result)
    endTime = time.time()
    print("耗时:", endTime - beginTime)

image-20200927013751047

现在结果就没有问题了。

处理返回值为字符串的情况

s2.go的代码:

package main

import (
	"C" //C必须导入
)

//export speak
func speak(n int) string {
	return "996好累呀,难得休息一天,好好休息 "
}
func main() {
	//main函数中什么都不要写,和包名main要对应
}

查看s2.h:

typedef struct { const char *p; ptrdiff_t n; } _GoString_;
typedef _GoString_ GoString;
...
extern GoString speak(GoInt n);
...

上面表示GoString是_GoString_类型,而_GoString_是char *和ptrdiff_t的结构体

在c语言规范中,ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。ptrdiff_t定义在stddef.h(cstddef)这个文件内。ptrdiff_t通常被定义为long int类型,可以被定义为long long类型。

查表可知,在python中应申明c_char_p和c_longlong的结构体:

class GoString(Structure):
    # typedef struct { const char *p; ptrdiff_t n; } _GoString_;
    # ptrdiff_t == long long
    _fields_ = [("p", c_char_p), ("n", c_longlong)]

s3.py完整代码:

from ctypes import *
import time


class GoString(Structure):
    # typedef struct { const char *p; ptrdiff_t n; } _GoString_;
    _fields_ = [("p", c_char_p), ("n", c_longlong)]


if __name__ == '__main__':
    beginTime = time.time()
    s = CDLL("s2.so")  # 加载s1.so文件

    s.speak.restype = GoString
    speakStr = s.speak(5)
    # 返回的是字节类型,需要转字符串,返回的内容在.p中,.n是切的长度
    speakStr = speakStr.p[:speakStr.n].decode("utf-8")
    print("speak:", speakStr)

    endTime = time.time()
    print("耗时:", endTime - beginTime)

image-20200927024920472

但是上面的这种代码只支持返回的字符串为常量,一旦我将go代码修改为以下内容再重复以上步骤时:

s2.go代码:

package main

import (
	"C" //C必须导入
	"strconv"
)

//export speak
func speak(n int) string {
	s := "996好累呀,难得休息一天,好好休息 " + strconv.Itoa(n)
	return s
}

func main() {
	//main函数中什么都不要写,和包名main要对应
}

重复以上步骤,运行s3.py,出现如下错误:

image-20200927031344190

这是因为此时的string不是c语言的对象,而是go语言的对象,修改为如下代码即可:

s2.go代码:

package main

import (
	"C" //C必须导入
	"strconv"
)

//export speak
func speak(n int) *C.char {
	s := "996好累呀,难得休息一天,好好休息 " + strconv.Itoa(n)
	return C.CString(s)
}

func main() {
	//main函数中什么都不要写,和包名main要对应
}

以上代码申明返回c语言的字符串类型,查看.h文件可以看到:

extern char* speak(GoInt n);

那么s3.py代码只需修改为:

from ctypes import *
import time

if __name__ == '__main__':
    beginTime = time.time()
    s = CDLL("s3.so")  # 加载s1.so文件

    s.speak.restype = c_char_p
    speakStr = s.speak(7).decode("utf-8")
    print("speak:", speakStr)

    endTime = time.time()
    print("耗时:", endTime - beginTime)

image-20200927031932312

顺利运行。

使用ctypes访问C代码

基本示例

实现两数求和的C代码add.c文件:

#include <stdio.h>

int add_int(int, int);
float add_float(float, float);

int add_int(int num1, int num2){
    return num1 + num2;
}

float add_float(float num1, float num2){
    return num1 + num2;
}

将C文件编译为.so文件:

#For Linux or windows
gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c

#For Mac
gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c

在Python代码中来调用它:

from ctypes import *

adder = CDLL('adder.so')

res_int = adder.add_int(4, 5)
print("Sum of 4 and 5 = " + str(res_int))

add_float = adder.add_float
add_float.restype = c_float
a = c_float(5.5)
b = c_float(4.1)
print("Sum of 5.5 and 4.1 = ", str(add_float(a, b)))

输出:

Sum of 4 and 5 = 9
Sum of 5.5 and 4.1 =  9.600000381469727

ctypes接口允许我们在调用C函数时参数使用原生Python中默认的字符串型和整型,而对于其他类似布尔型和浮点型这样的类型,必须要使用正确的ctype类型才可以。如向adder.add_float()函数传参时, 要先将Python中的float类型转化为c_float类型,然后才能传送给C函数。

复杂示例

编码c代码,sample.c文件的内容为:

#include <math.h>

int gcd(int x, int y) {
    int g = y;
    while (x > 0) {
        g = x;
        x = y % x;
        y = g;
    }
    return g;
}

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;
}

int divide(int a, int b, int *remainder) {
    int quot = a / b;
    *remainder = a % b;
    return quot;
}

double avg(double *a, int n) {
    int i;
    double total = 0.0;
    for (i = 0; i < n; i++) {
        total += a[i];
    }
    return total / n;
}

typedef struct Point {
    double x, y;
} Point;

double distance(Point *p1, Point *p2) {
    return hypot(p1->x - p2->x, p1->y - p2->y);
}

命令行执行以下代码,编译c:

gcc -shared -o sample.so sample.c

在 sample.so 所在文件相同的目录编写python代码,sample.py文件

import ctypes

_mod = ctypes.cdll.LoadLibrary('sample.so')

# int gcd(int, int)
gcd = _mod.gcd
gcd.argtypes = (ctypes.c_int, ctypes.c_int)
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 *a, int n)
# 定义 'double *'参数的类型
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):
            return param
        else:
            raise TypeError("Can't convert %s" % typename)

    # Cast from array.array objects
    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))


_avg = _mod.avg
_avg.argtypes = (DoubleArrayType(), ctypes.c_int)
_avg.restype = ctypes.c_double


def avg(values):
    return _avg(values, len(values))


# struct Point { }
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

然后就可以加载并使用里面定义的C函数了,编写test.py

import sample

print("sample.gcd(35, 42):", sample.gcd(35, 42))
print("sample.in_mandel(0, 0, 500):", sample.in_mandel(0, 0, 500))
print("sample.in_mandel(2.0, 1.0, 500):", sample.in_mandel(2.0, 1.0, 500))
print("sample.divide(42, 8):", sample.divide(42, 8))
print("sample.avg([1, 2, 3]):", sample.avg([1, 2, 3]))

p1 = sample.Point(1, 2)
p2 = sample.Point(4, 5)
print("sample.distance(p1, p2):", sample.avg([1, 2, 3]))

执行结果:

sample.gcd(35, 42): 7
sample.in_mandel(0, 0, 500): 1
sample.in_mandel(2.0, 1.0, 500): 0
sample.divide(42, 8): (5, 2)
sample.avg([1, 2, 3]): 2.0
sample.distance(p1, p2): 2.0

复杂示例解析

加载c函数库

如果C函数库被安装为一个标准库,那么可以使用 ctypes.util.find_library() 函数来查找它所在的位置:

>>> from ctypes.util import find_library
>>> find_library('m')
'libm.so.6'
>>> find_library('pthread')
'libpthread.so.0'
>>> find_library('sample')

如果是非标准库,则需要知道C函数库的位置,然后使用 ctypes.cdll.LoadLibrary() 来加载它:

_mod = ctypes.cdll.LoadLibrary(_path) #_path是C函数库的位置,全路径和相对路径都可以

指定参数和返回值的类型

函数库被加载后,需要提取特定的符号指定它们的类型。例如:

# 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

这段代码中,函数的.argtypes 属性是一个元组,包含了某个函数的输入参数,而 .restype 是函数的返回类型。

ctypes 定义的c_double, c_int, c_short, c_float等代表了对应的C数据类型。

为了让Python能够传递正确的参数类型并且正确的转换数据,这些类型签名的绑定是很重要的一步。如果省略这个类型签名的步骤,可能导致代码不能正常运行,甚至整个解释器进程挂掉。

指针参数需要以ctypes对象形式传入

原生的C代码的类型有时跟Python不能明确的对应上来,例如:

# c代码中的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
# python代码中的调用
x = 0
divide(10, 3, x)

这种写法违反了Python对于整数的不可更改原则,并且可能会导致整个解释器陷入一个黑洞中。

对于涉及到指针的参数,通常需要先构建一个相应的ctypes对象再作为参数传入:

x = ctypes.c_int()
divide(10, 3, x)

ctypes.c_int 实例是作为指针被传进去的,跟普通Python整数不同的是,c_int 对象是可以被修改的。

.value属性可被用来获取或更改这个值:

x.value

对于这种不像Python的C调用,通常可以写一个包装函数:

# 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

参数包含数组

对于avg()函数,double avg(double *a, int n),C代码期望接受到一个double类型的数组指针和一个数组的长度值。

在Python中数组有多种形式,包括列表、元组、array 模块的数组、 numpy 数组等。

DoubleArrayType 演示了怎样处理这种情况。

方法 from_param() 接受一个单个参数然后将其向下转换为一个合适的ctypes对象:

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):
        return param
    else:
        raise TypeError("Can't convert %s" % typename)

参数的类型名被提取出来并被用于分发到一个更具体的方法中去。

例如,如果参数是一个列表,那么 typename 就是 list ,然后 from_list 方法就会被调用。

def from_list(self, param):
    val = ((ctypes.c_double) * len(param))(*param)
    return val

演示通过交互式命令行将list列表转换为 ctypes 数组:

>>> import ctypes
>>> nums = [1, 2, 3]
>>> a = (ctypes.c_double * len(nums))(*nums)
>>> a
<__main__.c_double_Array_3 object at 0x10069cd40>
>>> a[0]
1.0
>>> a[1]
2.0
>>> a[2]
3.0

如果参数是一个numpy数组,那么 typename 就是 ndarray,然后 from_ndarray方法就会被调用:

def from_ndarray(self, param):
	return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

如果参数是一个数组对象,那么 typename 就是 array,然后 from_array方法就会被调用:

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))

对于数组对象,buffer_info()方法可以获取到数组对应的内存地址和长度,ctypes.cast()可以将内存地址转换为ctypes 指针对象:

>>> import array
>>> a = array.array('d',[1,2,3])
>>> a
array('d', [1.0, 2.0, 3.0])
>>> ptr,length = a.buffer_info()
>>> ptr
4298687200
>>> length
3
>>> ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
<__main__.LP_c_double object at 0x10069cd40>

通过定义 DoubleArrayType类并在 avg() 类型签名中使用它,那么这个函数就能接受多个不同的类数组输入了:

import sample
sample.avg([1,2,3])
2.0
sample.avg((1,2,3))
2.0
import array
sample.avg(array.array('d',[1,2,3]))
2.0
import numpy
sample.avg(numpy.array([1.0,2.0,3.0]))
2.0

参数包含结构体

对于结构体,只需要简单的定义一个类,包含相应的字段和类型即可:

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

一旦类被定义后,就可以在类型签名中或者是需要实例化结构体的代码中使用它。例如:

>>> p1 = sample.Point(1,2)
>>> p2 = sample.Point(4,5)
>>> p1.x
1.0
>>> p1.y
2.0
>>> sample.distance(p1,p2)
4.242640687119285

将函数指针转换为可调用对象

获取C函数的内存地址(经测试,在linux上支持,windows上不支持):

import ctypes
lib = ctypes.cdll.LoadLibrary(None)
# 获取C语言math库的sin()函数的地址
addr = ctypes.cast(lib.sin, ctypes.c_void_p).value
print(addr)

上述代码在linux下得到整数140266666308000,而在Windows下会报错TypeError: LoadLibrary() argument 1 must be str, not None

有了函数的内存地址,就可以将它转换成一个Python可调用对象:

# 将函数地址转换成一个Python的可调用对象,参数为函数的返回值类型和参数类型
functype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double)
sin = functype(addr)
print(sin)

CFUNCTYPE() 的第一个参数是返回类型,接下来的参数是参数类型,生成的对象被当做普通的可通过 ctypes 访问的函数来使用。

打印:<CFunctionType object at 0x7f9261becb38>

调用测试:

>>> import math
>>> math.pi
3.141592653589793
>>> sin(math.pi)
1.2246467991473532e-16
>>> sin(math.pi/2)
1.0
>>> sin(math.pi/6)
0.49999999999999994
>>> sin(2)
0.9092974268256817
>>> sin(0)
0.0

这里面涉及的技术被广泛使用于各种高级代码生成技术,比如即时编译,在LLVM函数库中可以看到。

下面简单演示下 llvmpy 扩展,构建一个小的聚集函数,获取它的函数指针,然后转换为一个Python可调用对象,并执行函数:

>>> from llvm.core import Module, Function, Type, Builder
>>> mod = Module.new('example')
>>> f = Function.new(mod,Type.function(Type.double(), [Type.double(), Type.double()], False), 'foo')
>>> block = f.append_basic_block('entry')
>>> builder = Builder.new(block)
>>> x2 = builder.fmul(f.args[0],f.args[0])
>>> y2 = builder.fmul(f.args[1],f.args[1])
>>> r = builder.fadd(x2,y2)
>>> builder.ret(r)
<llvm.core.Instruction object at 0x10078e990>
>>> from llvm.ee import ExecutionEngine
>>> engine = ExecutionEngine.new(mod)
>>> ptr = engine.get_pointer_to_function(f)
>>> ptr
4325863440
>>> foo = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double, ctypes.c_double)(ptr)
>>> foo(2,3)
13.0
>>> foo(4,5)
41.0
>>> foo(1,2)
5.0

注意:这是在直接跟机器级别的内存地址和本地机器码打交道,而不是Python函数。

处理参数包含字符串的情况

测试程序str1.c

#include <stdio.h>

void print_chars(char *s) {
    printf("%s\n",s);
    while (*s) {
        printf("%2x ", (unsigned char) *s);
        s++;
    }
    printf("\n");
}

int main() {
    print_chars("Hello");
}

执行结果:

> gcc str1.c&a.exe
Hello
48 65 6c 6c 6f

编译c程序为so文件:

gcc -shared -o str1.so str1.c

用python调用:

import ctypes

_mod = ctypes.cdll.LoadLibrary('str1.so')
# void print_chars(char *s)
print_chars = _mod.print_chars
print_chars.argtypes = (ctypes.c_char_p,)

print_chars(b'Hello')
print_chars(b'Hello\x00World')

打印结果:

Hello
48 65 6c 6c 6f 
Hello
48 65 6c 6c 6f 

不能直接传入python的字符串类型,例如:print_chars('Hello World')

否则会报错:ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type

如果需要传递字符串而不是字节,可以先编码成 UTF-8 转成字节:

>>> print_chars('Hello World'.encode('utf-8'))
Hello World
48 65 6c 6c 6f 20 57 6f 72 6c 64 

本文转载:CSDN博客