如何使用正确的 dll 文件在 Cython C 扩展中启用第 3 方 C 库?

How do I use the correct dll files to enable 3rd party C libraries in a Cython C extension?

我有一个涉及使用 zstd 解压缩数据的 C 函数。我正在尝试使用 Cython 调用该函数。

Using this page 以文档为指导,我可以编译 运行 下面的代码没有问题。

(我实际上并没有在这里使用 zstd 库)

// hello.c
#include <stdio.h>
#include <zstd.h>

int hello() {
   printf("Hello, World!\n");
   void *next_in = malloc(0);
   void *next_out = malloc(0);
   return 0;
}

# Hello.pyx

cdef extern from "hello.c":
  int hello()

cpdef int callHello():
  hello()

# hello_wrapper.setup.py

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

ext_modules = [
    Extension(
        "hello_wrapper",
        ["hello_wrapper.pyx"],
        libraries=["zstd"],
        library_dirs=["path/to/zstd/lib"],
        include_dirs=['path/to/zstd/include'],
    )
]

setup(
    ext_modules = cythonize(ext_modules, gdb_debug=True)
)

使用如下命令我得到了预期的输出:

>py hello_wrapper.setup.py build_ext --inplace
>py
Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:19) [MSC v.1925 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello_wrapper
>>> hello_wrapper.callHello()
Hello, World!
0

然而,当我修改 hello.c 以实际使用 zstd 库时:

// hello.c
#include <stdio.h>
#include <zstd.h>

int hello() {
   printf("Hello, World!\n");
   void *next_in = malloc(0);
   void *next_out = malloc(0);
   size_t const dSize = ZSTD_decompress(next_out, 0, next_in, 0); //the added line
   return 0;
}

虽然 hello_wrapper.setup.py 编译正常,但当我进入导入语句时,出现以下错误:

>>> import hello_wrapper
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: DLL load failed while importing hello_wrapper: The specified module could not be found.

通过阅读 ,我了解到这个错误意味着我没有正确指向或可能首先创建了 zstd.lib 发挥其魔力所需的 DLL 文件。这个对吗?如果是这样,我该怎么做?如果不是,是什么问题?

我们 link 我们的 cython 扩展针对 windows-dll,这意味着:

  • *.lib-文件(即 zstd.lib)在编译期间 "path/to/zstd/lib" 中需要
  • *.dll-文件(即 zstd.dll)需要在导入模块时 Windows 可以找到的地方。

通常,Windows 不会在 "path/to/zstd/lib" 中查找。所以我们得到了一个有点神秘的错误信息:

ImportError: DLL load failed: The specified module could not be found.

这并不意味着模块有问题 - 它只是恰好依赖于无法找到的 dll。

虽然 linux 有 可以传递 "path/to/zstd/lib"(它可以用 runtime_library_dirs 参数添加到 Extension),但有Windows.

上没有这样的选项

Windows can be found here 的 dll 搜索算法。简而言之,搜索 dll(可能以此处显示的另一种顺序)

  • 加载模块的目录。
  • 当前工作目录。
  • 系统目录(例如C:\Windows\System32
  • windows-目录(例如C:\Windows
  • PATH-变量中列出的目录
  • 其他

但是,since Python3.8 the above default algorithm isn't used for CPython: The current working directory and the PATH-variable are no longer used during the call, but there is os.add_dll_directory 可用于添加解析依赖项时将使用的路径。

将 dll 放入 system- 或 windows- 目录听起来不太吸引人,这给我们留下了以下选择:

  • (最简单?)复制编译扩展
  • 旁边的zstd.dll
  • 使用 os.add_dll_directory 将位置添加到 Python>=3.8
  • 的搜索中
  • 将 zstd 路径添加到 PATH 变量,例如set PATH="path/to/zstd/lib";%PATH%(Python<3.8)

另一个选项有点棘手:Given that

If a DLL with the same module name is already loaded in memory, the system checks only for redirection and a manifest before resolving to the loaded DLL, no matter which directory it is in. The system does not search for the DLL.

我们可以使用ctypes来“预加载”正确的dll,在导入包装器模块时将使用(无需在光盘上搜索它),即:

import ctypes; 
ctypes.CDLL("path/to/zstd/lib/zstd.dll"); # we preload with the full path

import hello_wrapper  # works now!

如果扩展是在同一系统上构建和使用的(例如通过 build_ext --inplace),则以上内容适用。 installation/distribution有点麻烦(这个涵盖了),一个想法是:

  • *.h-、*.lib- 和 *.dll- 文件放入 'package_data'(无论如何它似乎自动发生)
  • 可以在 setup.py 中设置正确的相对 library_path(或以编程方式设置绝对路径),因此 *.lib 会被 linker 找到。
  • dll 将放在编译后的 *.pyd-文件旁边。

一个例子可能是以下或多或少最小的 setup.py,其中所有内容(pyx 文件、h 文件、lib 文件、dll 文件)都放入 package/folder src/zstd:

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

ext_modules = [
    Extension(
        "zstd.zstdwrapper",
        ["src/zstd/zstdwrapper.pyx"],
        libraries=["zstd"],
        library_dirs=["src/zstd"],
        include_dirs=[], # set automatically to src/zstd during the build
    )
]

print(find_packages(where='src'))

setup(
    name = 'zstdwrapper',
    ext_modules = cythonize(ext_modules),
    packages = find_packages(where='src'),
    package_dir = {"": "src"},
)

现在可以使用 python setup.py install 安装或用于创建例如通过 python setup.py sdist 的源代码分发,然后可以通过 pip.

安装