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

CSS*_*782 5 c python dll cython python-c-api

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

使用文档中的此页面作为指南,我可以毫无问题地编译和运行下面的代码。

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

// 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)
)
Run Code Online (Sandbox Code Playgroud)

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

>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
Run Code Online (Sandbox Code Playgroud)

但是,当我修改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;
}
Run Code Online (Sandbox Code Playgroud)

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

>>> 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.
Run Code Online (Sandbox Code Playgroud)

通过阅读这篇 SO 文章,我发现这个错误意味着我没有正确指向或可能首先创建 zstd.lib 发挥其魔力所需的 DLL 文件。它是否正确?如果是这样,我该怎么做?如果没有,问题出在哪里?

ead*_*ead 8

我们将 cython 扩展链接到 windows-dll,这意味着:

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

通常,Windows 不会查找"path/to/zstd/lib". 所以我们得到了一条有点神秘的错误消息:

ImportError: DLL 加载失败: 找不到指定的模块。

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

虽然 linux 有- 选项用于可以传递的-rpath链接器"path/to/zstd/lib"(可以使用runtime_library_dirs- 参数添加到Extension),但 Windows 上没有这样的选项。

适用于 Windows 的 dll-search-algorithmus可在此处找到。简而言之,搜索 dll(可能按照此处介绍的另一种顺序)

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

然而,从Python3.8开始,上述默认算法不再用于CPython:当前工作目录和PATH-变量在调用过程中不再使用,但os.add_dll_directory可以用来添加解析依赖项时使用的路径。

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

  • (最简单的?)将其复制zstd.dll到已编译的扩展中
  • 用于os.add_dll_directory将位置添加到 Python>=3.8 的搜索中
  • 将 zstd-path 添加到 -variable PATH,例如set PATH="path/to/zstd/lib";%PATH%(对于 Python<3.8)

另一种选择有点棘手:鉴于

如果具有相同模块名称的 DLL 已加载到内存中,则系统在解析加载的 DLL 之前仅检查重定向和清单,无论它位于哪个目录。系统不会搜索 DLL。

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

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

import hello_wrapper  # works now!
Run Code Online (Sandbox Code Playgroud)

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

  • *.h-、*.lib- 和*.dll-files 放入“package_data”中(无论如何它似乎都会自动发生)
  • 可以在链接器中找到正确的相对路径library_path(或以编程方式设置绝对路径)。setup.py*.lib
  • dll 将放在*.pyd安装中编译的文件旁边。

一个示例可能是以下或多或少的minimal setup.py,其中所有内容(pyx文件、h文件、lib文件、dll文件)都放入包/文件夹中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"},
)
Run Code Online (Sandbox Code Playgroud)

现在它可以安装python setup.py install或用于创建例如源发行版,python setup.py sdist然后可以通过该发行版安装pip