好的,各位观众,各位朋友,欢迎来到今天的“Python FFI 跨语言调用深度”讲座!我是你们的老朋友,今天咱们就来聊聊这个有点神秘,但又非常实用的技术——Python FFI(Foreign Function Interface)。
开场白:Python 的野心与局限
话说 Python 这门语言,优点那真是数不胜数:简单易学、代码可读性强、库多到眼花缭乱。简直就是编程界的“瑞士军刀”,啥都能干。但是,但是!它也有自己的局限性。
- 性能问题: Python 毕竟是解释型语言,运行速度相对较慢,尤其是在处理计算密集型任务时,就有点力不从心了。
- 底层控制: Python 对于底层硬件的控制能力比较弱,想直接操作内存、寄存器啥的,那是相当困难。
- 已有代码的复用: 很多时候,我们可能需要用到一些已经用 C/C++ 等语言编写好的库,不想重写一遍,怎么办?
这时候,FFI 就闪亮登场了!它就像一座桥梁,连接了 Python 和其他语言的世界,让 Python 可以调用其他语言编写的代码,从而弥补自身的不足。
什么是 FFI?
FFI,顾名思义,就是“外部函数接口”。它允许一种编程语言调用另一种编程语言编写的函数。简单来说,就是让 Python 可以调用 C/C++、Fortran 等语言的代码。
为什么要用 FFI?
- 提升性能: 将性能瓶颈部分用 C/C++ 等语言编写,然后通过 FFI 调用,可以显著提升程序运行速度。
- 利用现有资源: 可以直接使用已经存在的 C/C++ 库,避免重复造轮子。
- 底层控制: 可以通过 C/C++ 代码实现对底层硬件的控制,实现 Python 无法完成的任务。
Python 中 FFI 的实现方式
Python 中实现 FFI 的方式有很多,比较常见的有:
ctypes
: Python 内置的库,使用简单,无需编译,但功能相对有限。cffi
: 比ctypes
更强大,支持更复杂的 C 代码,但需要编译。Cython
: 一种介于 Python 和 C 之间的语言,可以编写 C 扩展,性能很高,但学习曲线较陡峭。
今天,咱们重点讲讲 ctypes
和 cffi
。
ctypes
:简单易用的 FFI
ctypes
是 Python 内置的库,使用起来非常方便。它允许你直接加载动态链接库(如 .dll
、.so
)中的函数,并像调用 Python 函数一样调用它们。
ctypes
的基本用法
-
加载动态链接库:
import ctypes # 加载 C 动态链接库 (Windows) # my_lib = ctypes.cdll.LoadLibrary("my_lib.dll") # 加载 C 动态链接库 (Linux/macOS) my_lib = ctypes.CDLL("./my_lib.so") # 假设 my_lib.so 在当前目录下
-
定义函数原型:
在使用 C 函数之前,需要告诉
ctypes
函数的参数类型和返回值类型。# 假设 C 函数原型是:int add(int a, int b); my_lib.add.argtypes = [ctypes.c_int, ctypes.c_int] my_lib.add.restype = ctypes.c_int
这里,
argtypes
指定参数类型,restype
指定返回值类型。ctypes
提供了一系列与 C 语言类型对应的 Python 类型,如下表所示:C Type ctypes
TypePython Type char
c_char
1-character string int
c_int
integer long
c_long
integer float
c_float
float double
c_double
float char *
c_char_p
string void *
c_void_p
integer -
调用 C 函数:
result = my_lib.add(10, 20) print(result) # 输出 30
一个简单的 ctypes
例子
首先,我们创建一个简单的 C 文件 my_lib.c
:
// my_lib.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
void print_message(const char* message) {
printf("C says: %sn", message);
}
然后,将它编译成动态链接库:
- Linux/macOS:
gcc -shared -o my_lib.so my_lib.c
- Windows:
gcc -shared -o my_lib.dll my_lib.c
(你需要安装 MinGW 或类似的工具)
接下来,编写 Python 代码:
import ctypes
# 加载动态链接库
my_lib = ctypes.CDLL("./my_lib.so") # 或者 "my_lib.dll"
# 定义 add 函数原型
my_lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
my_lib.add.restype = ctypes.c_int
# 定义 print_message 函数原型
my_lib.print_message.argtypes = [ctypes.c_char_p]
my_lib.print_message.restype = None # void 返回值
# 调用 C 函数
result = my_lib.add(10, 20)
print(f"Python says: The result of add(10, 20) is {result}")
message = "Hello from Python!"
my_lib.print_message(message.encode('utf-8')) # C expects bytes, not str
ctypes
的优点和缺点
- 优点:
- 简单易用,无需编译。
- Python 内置,无需安装额外库。
- 缺点:
- 性能相对较低。
- 类型检查较弱,容易出错。
- 对于复杂的 C 代码,处理起来比较麻烦。
cffi
:更强大的 FFI
cffi
是一个比 ctypes
更强大的 FFI 库。它可以处理更复杂的 C 代码,并且性能也更好。
cffi
的基本用法
cffi
的使用分为两个步骤:
-
定义 C 接口:
使用
cffi.FFI
对象定义 C 接口。你可以直接在 Python 代码中编写 C 声明,也可以从 C 头文件中读取。 -
编译和加载库:
cffi
会将你定义的 C 接口编译成一个 Python 模块,然后你可以像使用普通 Python 模块一样使用它。
一个简单的 cffi
例子
首先,安装 cffi
:
pip install cffi
然后,编写 Python 代码:
from cffi import FFI
# 创建 FFI 对象
ffi = FFI()
# 定义 C 接口
ffi.cdef("""
int add(int a, int b);
void print_message(const char* message);
""")
# 加载动态链接库
lib = ffi.dlopen("./my_lib.so") # 或者 "my_lib.dll"
# 调用 C 函数
result = lib.add(10, 20)
print(f"Python says: The result of add(10, 20) is {result}")
message = "Hello from Python!"
lib.print_message(ffi.new("char[]", message.encode('utf-8')))
在这个例子中,ffi.cdef()
函数用于定义 C 接口。你可以直接在字符串中编写 C 声明,也可以从 C 头文件中读取。ffi.dlopen()
函数用于加载动态链接库。
使用 C 头文件
通常,我们更倾向于从 C 头文件中读取 C 接口,这样可以避免手动编写 C 声明,并且可以保证 C 接口的正确性。
假设我们有一个 C 头文件 my_lib.h
:
// my_lib.h
#ifndef MY_LIB_H
#define MY_LIB_H
int add(int a, int b);
void print_message(const char* message);
#endif
然后,我们可以使用 cffi
从头文件中读取 C 接口:
from cffi import FFI
ffi = FFI()
# 从 C 头文件中读取 C 接口
with open("my_lib.h", "r") as f:
ffi.cdef(f.read())
lib = ffi.dlopen("./my_lib.so") # 或者 "my_lib.dll"
result = lib.add(10, 20)
print(f"Python says: The result of add(10, 20) is {result}")
message = "Hello from Python!"
lib.print_message(ffi.new("char[]", message.encode('utf-8')))
cffi
的优点和缺点
- 优点:
- 性能较高。
- 支持更复杂的 C 代码。
- 可以从 C 头文件中读取 C 接口。
- 缺点:
- 需要编译。
- 使用起来比
ctypes
稍微复杂一些。
cffi
的两种使用模式:ABI 和 API
cffi
提供了两种使用模式:ABI (Application Binary Interface) 和 API (Application Programming Interface)。
-
ABI 模式:
ABI 模式类似于
ctypes
,直接调用动态链接库中的函数。它不需要编译 C 代码,但性能相对较低,并且依赖于平台的 ABI 兼容性。 -
API 模式:
API 模式需要编译 C 代码。
cffi
会生成一个包含 C 代码的 Python 模块,然后你可以像使用普通 Python 模块一样使用它。API 模式性能更高,并且可以处理更复杂的 C 代码。
cffi
的 API 模式示例
from cffi import FFI
ffi = FFI()
# 定义 C 接口和实现
ffi.cdef("""
int add(int a, int b);
""")
ffi.set_source("_my_lib", # module name
"""
int add(int a, int b) {
return a + b;
}
"""
)
# 编译 C 代码
ffi.compile()
# 导入编译后的模块
import _my_lib
# 调用 C 函数
result = _my_lib.lib.add(10, 20)
print(f"Python says: The result of add(10, 20) is {result}")
在这个例子中,ffi.set_source()
函数用于定义 C 接口和实现。ffi.compile()
函数用于编译 C 代码。编译后,会生成一个名为 _my_lib
的 Python 模块,你可以像使用普通 Python 模块一样使用它。
高级话题:内存管理
在使用 FFI 时,内存管理是一个非常重要的问题。C 语言需要手动管理内存,而 Python 使用垃圾回收机制。因此,在使用 FFI 时,需要注意内存的分配和释放,避免内存泄漏或其他问题。
-
C 分配的内存:
如果 C 代码分配了内存,你需要确保在 Python 中释放这些内存。
ctypes
和cffi
都提供了释放内存的函数。 -
Python 对象传递给 C:
如果将 Python 对象传递给 C 代码,你需要确保 C 代码不会持有对这些对象的引用,否则可能会导致循环引用,从而阻止垃圾回收。
FFI 的应用场景
- 科学计算: 利用 C/C++ 编写高性能的数值计算库,然后通过 FFI 在 Python 中调用。
- 游戏开发: 使用 C/C++ 编写游戏引擎,然后通过 FFI 在 Python 中编写游戏逻辑。
- 嵌入式系统: 使用 C/C++ 编写底层驱动程序,然后通过 FFI 在 Python 中编写应用程序。
- 遗留系统集成: 将现有的 C/C++ 代码集成到 Python 项目中。
总结
FFI 是一种非常强大的技术,它可以让 Python 调用其他语言编写的代码,从而弥补自身的不足。ctypes
和 cffi
是 Python 中常用的 FFI 库。ctypes
简单易用,但功能相对有限。cffi
更强大,支持更复杂的 C 代码,并且性能也更好。在使用 FFI 时,需要注意内存管理,避免内存泄漏或其他问题。
最后的温馨提示
FFI 虽然强大,但也不是万能的。在使用 FFI 之前,请仔细评估是否真的需要使用 FFI。如果 Python 代码本身能够满足需求,尽量不要使用 FFI,因为 FFI 会增加代码的复杂性,并且可能会引入一些难以调试的问题。
好了,今天的讲座就到这里。希望大家有所收获!谢谢大家!