Search code examples
pythonc++cythonmingw-w64gmp

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


When I try to compile a Cython project with submodules using the gmp library and including C ++ files, I get an error:

ImportError: DLL load failed while importing ...

In particular, I want to wrap in Cython a class written in C++, which use GMP, by using Mingw64 as compiler. I use Python3.8 and Mingw64 has been is installed by means of MSYS2.


In the following, I provide you with a minimal reproducible example (as asked by @ead).

The directory tree is as follows:

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

  test    
    submodule
      cy_test.pxd
      cy_test.pyx

where, in project/include, I putted the class written in C++ I want to wrap.

project/include/test.h is

#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

whereas project/include/test.cpp is

#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 is:

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 +

Instead, project/test/submodule/cy_test.pyx is:

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

The file project/pytest.py is simply an import of cy_test:

from test.submodule import cy_test

Finally, project/setup.py is:

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),
    )

I compile setup.py as python setup.py build_ext --inplace --compiler=mingw32 in order to use MinGW and g++.exe as, by default on Windows, the compiler is taken from Visual Studio, which gives me other errors like undefined reference to '__gmpz_init'.

The compilation ends without errors, but when I run the pytest.py script I get the following error:

ImportError: DLL load failed while importing cy_test

I have already dumped cy_test.cp38-win_amd64.pyd as in link and I have got this:

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

How can I solve? I have already seen on cython-github-issues that, by including the following two lines in pytest.py

import os
os.add_dll_directory(mingw64_bin_path)

the above error disappear and the submodule is imported correctly, but I would like a solution that avoids including the Mingw path outside the setup (assuming it exists).


Solution

  • I just accidentally found the solution to the above problem. The problem is the package setuptools (which in my case is the version 60.9.1)! Indeed, by executing python setup.py build_ext --inplace --compiler=mingw32, the latter will call the class Mingw32CCompiler into setuptools/_distutils/cygwinccompiler.py which contains these two lines:

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

    These lines will produce the compiling command g++ -shared ... -lucrt -lvcruntime140, that is, the pyd file, generated by this latter command, would be a shared library which needs a lot of dll dependencies in order to be executed. In order to avoid these dependencies, it is mandatory to comment the line self.dll_libraries = get_msvcr(), which as a consequence removes -lucrt -lvcruntime140 from the compilation command. Furthermore, one has to modify the setup.py by substituting EXTRA_LINK_ARGS = [] with EXTRA_LINK_ARGS = ["-static"] in order to get the following compiling command: g++ -shared ... -static, which builds the pyd file as a static library that have no dll dependencies. Indeed, after the above modifications, by dumping cy_test.cp38-win_amd64.pyd we get:

    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
    

    I wonder why MSVCR it is not optional for the MinGW compiler in setuptools...