如何使用 Mingw 在 Windows 上使用 Cython、C++ 和 gmp 构建包?

How to build a package with Cython, C++ and gmp on Windows with Mingw?

当我尝试使用 gmp 库和包含 C++ 文件的子模块编译 Cython 项目时,出现错误:

ImportError: DLL load failed while importing ...

特别是,我想通过使用 Mingw64 作为编译器,在 Cython 中包装一个用 C++ 编写的 class,它使用 GMP。 我使用 Python3.8 并且 Mingw64 已通过 MSYS2.

安装

在下文中,我为您提供了一个最小的可重现示例(正如@ead 所要求的)。

目录树如下:

project
  setup.py
  pytest.py
 
  include
    test.h
    test.cpp

  test    
    submodule
      cy_test.pxd
      cy_test.pyx

其中,在 project/include 中,我放入了我要包装的用 C++ 编写的 class。

project/include/test.h

#ifndef TEST_LIB
#define TEST_LIB

#include <gmpxx.h>

using namespace std;

class Test
{
    private:
        mpz_t n;
    public:
        Test();
        Test(const char* expr);
        virtual ~Test()
        {
            mpz_clear(this->n);
        }
};

#endif // TEST_LIB

project/include/test.cpp

#ifndef TEST_IMPL
#define TEST_IMPL

#include <iostream>
#include <cstdarg>
#include <test.h>

Test::Test()
{
    mpz_init_set_ui(this->n, 0);
}

Test::Test(const char* expr)
{
    mpz_init_set_str(this->n, expr, 10);
}

#endif // TEST_IMPL

project/test/submodule/cy_test.pxd 是:

cdef extern from "test.cpp":
    pass
# Declare the class with cdef
cdef extern from "test.h":
    cdef cppclass Test:
        Test() except +
        Test(char* n) except +

相反,project/test/submodule/cy_test.pyx 是:

from test.submodule.cy_test cimport Test

cdef class PyTest:
    cdef Test* n    
    def __cinit__(self, object n):
        cdef char* string = n
        self.n = new Test(string)
    
    def __dealloc__(self):
        del self.n

文件 project/pytest.py 只是 cy_test:

的导入
from test.submodule import cy_test

最后,project/setup.py是:

import os
import sys
from setuptools import find_packages
from distutils.core import setup
from distutils.command.clean import clean
from distutils.sysconfig import get_python_inc
from distutils.command.build_ext import build_ext
from Cython.Build import cythonize
from Cython.Distutils import Extension


os.environ["CC"] = "g++"
os.environ["CXX"] = "g++"


MINGW_DIR = "C:/msys64/mingw64"

CWD = os.getcwd()
BASE_DIRS = [os.path.join(CWD, "include"), os.path.join(CWD, "test"), get_python_inc(), "C:/Program Files/Python38"]
INCLUDE_DIRS = [os.path.join(MINGW_DIR, "include")] + BASE_DIRS
LIB_DIRS = [os.path.join(MINGW_DIR, "lib")]
EXTRA_ARGS = ["-O3", "-std=c++17"]
EXTRA_LINK_ARGS = []
LIBRARIES = ["gmp", "gmpxx", "mpc", "mpfr"]
EXTRA_LIBRARIES = []

ext = [
    Extension(
        name="test.submodule.cy_test", 
        sources=["./test/submodule/cy_test.pyx"],
        language="c++",
        include_dirs=INCLUDE_DIRS,
        library_dirs=LIB_DIRS,
        libraries=LIBRARIES,
        extra_link_args=EXTRA_LINK_ARGS,
        extra_compile_args=EXTRA_ARGS,
        extra_objects=EXTRA_LIBRARIES,
        cython_cplus=True,
        cython_c_in_temp=True)
]

setup(
    name="test",
    packages=find_packages(),
    package_dir={
        "test": "test", 
        "test/submodule": "test/submodule"},
    include_package_data=True,
    package_data={
        'test': ['*.pyx', '*.pxd', '*.h', '*.c', '*.cpp', '*.dll'],
        'test/submodule': ['*.pyx', '*.pxd', '*.h', '*.c', '*.cpp']
    },
    cmdclass={'clean': clean, 'build_ext': build_ext},
    include_dirs=BASE_DIRS,
    ext_modules=cythonize(ext,
        compiler_directives={
            'language_level': "3str",
            "c_string_type": "str",
            "c_string_encoding": "utf-8"},
        force=True,
        cache=False,
        quiet=False),
    )

我将 setup.py 编译为 python setup.py build_ext --inplace --compiler=mingw32 以便使用 MinGW 和 g++.exe 作为,默认情况下 Windows,编译器取自 Visual Studio ,这给了我其他错误,例如 undefined reference to '__gmpz_init'.

编译结束没有错误,但是当我 运行 pytest.py 脚本时,我得到以下错误:

ImportError: DLL load failed while importing cy_test

我已经像 link 那样转储了 cy_test.cp38-win_amd64.pyd 我得到了这个:

Dump of file cy_test.cp38-win_amd64.pyd

File Type: DLL

  Image has the following dependencies:

    KERNEL32.dll
    msvcrt.dll
    api-ms-win-crt-environment-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-private-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-time-l1-1-0.dll
    libgcc_s_seh-1.dll
    libstdc++-6.dll
    libgmp-10.dll
    python38.dll

  Summary

        1000 .CRT
        1000 .bss
        1000 .data
        1000 .edata
        2000 .idata
        1000 .pdata
        2000 .rdata
        1000 .reloc
        4000 .text
        1000 .tls
        1000 .xdata

我该如何解决?我已经在 cython-github-issues 上看到,通过在 pytest.py

中包含以下两行
import os
os.add_dll_directory(mingw64_bin_path)

上述错误消失并且 submodule 被正确导入,但我想要一个避免在设置之外包含 Mingw 路径的解决方案(假设它存在)。

我只是无意中找到了上述问题的解决方法。问题是包 setuptools(在我的例子中是版本 60.9.1)!实际上,通过执行 python setup.py build_ext --inplace --compiler=mingw32,后者会将 class Mingw32CCompiler 调用为包含以下两行的 setuptools/_distutils/cygwinccompiler.py

...
shared_option = "-shared"
...
self.dll_libraries = get_msvcr()

这些行将产生编译命令g++ -shared ... -lucrt -lvcruntime140,即后一个命令生成的pyd文件将是一个共享库,需要大量的dll依赖才能被执行。为了避免这些依赖性,必须注释行 self.dll_libraries = get_msvcr(),结果从编译命令中删除了 -lucrt -lvcruntime140。此外,必须通过将 EXTRA_LINK_ARGS = [] 替换为 EXTRA_LINK_ARGS = ["-static"] 来修改 setup.py 以获得以下编译命令:g++ -shared ... -static,它将 pyd 文件构建为一个没有 dll 依赖关系的静态库。确实,经过上述修改,通过转储 cy_test.cp38-win_amd64.pyd 我们得到:

Dump of file cy_test.cp38-win_amd64.pyd

File Type: DLL

  Summary

        1000 .CRT
        1000 .bss
        4000 .data
        1000 .edata
        2000 .idata
        C000 .pdata
       16000 .rdata
        2000 .reloc
       E8000 .text
        1000 .tls
       10000 .xdata

我想知道为什么 MSVCR 对于 setuptools 中的 MinGW 编译器不是可选的...