调用 C 函数

注意

此页面使用两种不同的语法变体

  • Cython 特定的 cdef 语法,旨在使类型声明简洁,并易于从 C/C++ 的角度阅读。

  • 纯 Python 语法,允许在 纯 Python 代码 中进行静态 Cython 类型声明,遵循 PEP-484 类型提示和 PEP 526 变量注释。

    要在 Python 语法中使用 C 数据类型,您需要在要编译的 Python 模块中导入特殊的 cython 模块,例如

    import cython
    

    如果您使用纯 Python 语法,我们强烈建议您使用最新的 Cython 3 版本,因为与 0.29.x 版本相比,这里已经进行了重大改进。

本教程简要介绍了从 Cython 代码调用 C 库函数所需的知识。有关使用外部 C 库、包装它们和处理错误的更长、更全面的教程,请参阅 使用 C 库

为简单起见,让我们从标准 C 库中的一个函数开始。这不会给您的代码添加任何依赖项,并且它还有一个额外的好处,即 Cython 已经为您定义了许多这样的函数。因此,您只需 cimport 并使用它们即可。

例如,假设您需要一种低级方法来从 char* 值解析数字。您可以使用 atoi() 函数,如 stdlib.h 头文件所定义。这可以通过以下方式完成

atoi.py
from cython.cimports.libc.stdlib import atoi

@cython.cfunc
def parse_charptr_to_py_int(s: cython.p_char):
    assert s is not cython.NULL, "byte string value is NULL"
    return atoi(s)  # note: atoi() has no error detection!

您可以在 Cython 的源代码包 Cython/Includes/ 中找到这些标准 cimport 文件的完整列表。它们存储在 .pxd 文件中,这是提供可重用 Cython 声明的标准方式,这些声明可以在模块之间共享(请参阅 在 Cython 模块之间共享声明)。

Cython 还有一套完整的 CPython C-API 声明。例如,要在 C 编译时测试您的代码正在编译的 CPython 版本,您可以执行以下操作

py_version_hex.py
from cython.cimports.cpython.version import PY_VERSION_HEX

# Python version >= 3.2 final ?
print(PY_VERSION_HEX >= 0x030200F0)

Cython 还提供了 C 数学库的声明

libc_sin.py
from cython.cimports.libc.math import sin

@cython.cfunc
def f(x: cython.double) -> cython.double:
    return sin(x * x)

动态链接

libc 数学库很特殊,因为它不是在某些类 Unix 系统(如 Linux)上默认链接的。除了 cimport 声明之外,您还必须配置构建系统以链接到共享库 m。对于 setuptools,将其添加到 Extension() 设置的 libraries 参数中就足够了

from setuptools import Extension, setup
from Cython.Build import cythonize

ext_modules = [
    Extension("demo",
              sources=["demo.pyx"],
              libraries=["m"]  # Unix-like specific
              )
]

setup(name="Demos",
      ext_modules=cythonize(ext_modules))

外部声明

如果您想访问 Cython 未提供现成声明的 C 代码,则必须自己声明它们。例如,上面的 sin() 函数定义如下

cdef extern from "math.h":
    double sin(double x)

这声明了 sin() 函数,使其可用于 Cython 代码,并指示 Cython 生成包含 math.h 头文件的 C 代码。C 编译器将在编译时看到 math.h 中的原始声明,但 Cython 不会解析“math.h”并需要单独的定义。

就像来自数学库的 sin() 函数一样,只要 Cython 生成的模块与共享或静态库正确链接,就可以声明并调用任何 C 库。

请注意,您可以通过将外部 C 函数声明为 cpdef 从您的 Cython 模块轻松导出它。这将为它生成一个 Python 包装器并将其添加到模块字典中。以下是一个 Cython 模块,它为 Python 代码提供了对 C sin() 函数的直接访问。

"""
>>> sin(0)
0.0
"""

cdef extern from "math.h":
    cpdef double sin(double x)

当此声明出现在属于 Cython 模块的 .pxd 文件中时,您将获得相同的结果(即具有相同名称,请参阅 在 Cython 模块之间共享声明)。这允许在其他 Cython 模块中重用 C 声明,同时在此特定模块中仍然提供自动生成的 Python 包装器。

注意

外部声明必须放在纯 Python 模式下的 .pxd 文件中。

命名参数

C 和 Cython 都支持没有参数名称的签名声明,如下所示

cdef extern from "string.h":
    char* strstr(const char*, const char*)

但是,这会阻止 Cython 代码使用关键字参数调用它。因此,最好改为这样编写声明

cdef extern from "string.h":
    char* strstr(const char *haystack, const char *needle)

现在您可以清楚地表明在您的调用中两个参数中的哪一个做什么,从而避免任何歧义,并且通常使您的代码更具可读性。

keyword_args_call.py
from cython.cimports.strstr import strstr

def main():
    data: cython.p_char = "hfvcakdfagbcffvschvxcdfgccbcfhvgcsnfxjh"

    pos = strstr(needle='akd', haystack=data)
    print(pos is not cython.NULL)
strstr.pxd
cdef extern from "string.h":
    char* strstr(const char *haystack, const char *needle)

请注意,以后更改现有参数名称是向后不兼容的 API 修改,就像 Python 代码一样。因此,如果您为外部 C 或 C++ 函数提供自己的声明,通常值得付出额外的努力来很好地选择其参数的名称。