0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Building binary wheel for Python 2.7/3.4 on Windows WITH PYBIND11

Last updated at Posted at 2022-03-20

Building binary wheel for Python 2.7/3.4 on Windows WITH PYBIND11

Background

On C, ABI is preserved. This means that you can link gcc/clang .o with msvc .o files.

(Note that this does not apply to C++.)

It means that (also because Python itself does not use C++) you can build binary wheel for Python 2.7/3.4 on Windows with Pybind11.

Building module with msvc-unsupported features on Python 3.5+

Let's start from Python 3.5+ but you need msvc-unsupported features.

In my example python-slz, https://github.com/wtarreau/libslz/blob/v1.2.0/src/slz.c requires inline assembler. So it has to be compiled using gcc/clang.

Declare ext_module

At very first, you have to declare ext_module as usual Extension task. For this phase, there are a lot of documentation floating on the Internet.

ext_modules = [
    Pybind11Extension(
        name="slz",
        sources=["src/pyslz.cpp"],
        extra_objects=[],
        extra_compile_args=['-O2'],
        extra_link_args=['-s'],
    ),
]

Declare build_ext extending class

You then declare a build_ext extending class (build_ext_hook) and pass cmdclass={"build_ext": build_ext_hook} to setup().

class build_ext_hook(build_ext, object):
    def build_extension(self, ext):
        build_ext.build_extension(self, ext)

Add platform condition

Now let's implement the build_extension (Note that src/pyslz.cpp is already set in ext.sources).

We first introduce Windows conditional, which can be achieved by platform.system().

class build_ext_hook(build_ext, object):
    def build_extension(self, ext):
        if platform.system() == 'Windows':
            pass
        else:
            ext.sources.append('src/libslz/src/slz.c')
        build_ext.build_extension(self, ext)

Add gcc subprocess

As we know src/libslz/src/slz.c is not compilable by MSVC, we need to compile it on gcc and pass .o to extra_objects.
Also, gcc adds ___chkstk_ms function dependency. It can be resolved by assembling https://github.com/llvm-mirror/compiler-rt/blob/master/lib/builtins/x86_64/chkstk.S 1.

class build_ext_hook(build_ext, object):
    def build_extension(self, ext):
        if platform.system() == 'Windows':
            # -DPRECOMPUTE_TABLES=1 should be added because otherwise __attribute__((constructor)) code is inserted
            subprocess.check_call(['gcc', '-c', '-DPRECOMPUTE_TABLES=1', '-o', 'slz.o', '-O2', 'src/libslz/src/slz.c'])
            subprocess.check_call(['gcc', '-c', '-o', 'chkstk.o', 'src/chkstk.S'])
            ext.extra_objects.extend(['slz.o', 'chkstk.o'])
        else:
            ext.sources.append('src/libslz/src/slz.c')
        build_ext.build_extension(self, ext)

Determine 32bit or 64bit

Finally we want to compile both 32bit and 64bit, which can be checked by sys.maxsize:

class build_ext_hook(build_ext, object):
    def build_extension(self, ext):
        if platform.system() == 'Windows':
            if sys.maxsize < 1<<32:
                msiz = '-m32'
            else:
                msiz = '-m64'
            subprocess.check_call(['gcc', msiz, '-c', '-DPRECOMPUTE_TABLES=1', '-o', 'slz.o', '-O2', 'src/libslz/src/slz.c'])
            subprocess.check_call(['gcc', msiz, '-c', '-o', 'chkstk.o', 'src/chkstk.S'])
            ext.extra_objects.extend(['slz.o', 'chkstk.o'])
        else:
            ext.sources.append('src/libslz/src/slz.c')
        build_ext.build_extension(self, ext)

Now this module can be compiled on Windows although some part is incompatible with MSVC.

(This state is the same as https://github.com/cielavenir/python-slz/tree/48c7c420e8620db27e141d6ca91a076035e8fdf5)

Building module on Python 2.7/3.4

According to https://wiki.python.org/moin/WindowsCompilers, MSVC 2015+ is used only on Python 3.5+.

So for Python 2.7/3.4 we need to go a little further because src/pyslz.cpp needs to be compiled by MSVC 2015+ but it is not used by setuptools. Actually we compile it using clang.

Switch to clang

For some compatibility, you need to use clang, not gcc (see appendix below for detail).

class build_ext_hook(build_ext, object):
    def build_extension(self, ext):
        if platform.system() == 'Windows':
            if sys.maxsize < 1<<32:
                msiz = '-m32'
            else:
                msiz = '-m64'
            subprocess.check_call(['clang', msiz, '-c', '-DPRECOMPUTE_TABLES=1', '-o', 'slz.o', '-O2', 'src/libslz/src/slz.c'])
            subprocess.check_call(['clang', msiz, '-c', '-o', 'chkstk.o', 'src/chkstk.S'])
            ext.extra_objects.extend(['slz.o', 'chkstk.o'])
        else:
            ext.sources.append('src/libslz/src/slz.c')
        build_ext.build_extension(self, ext)

Add python binding compilation

Now you need to move sources=["src/pyslz.cpp"] logic inside build_ext_hook.

Also there are a few points:

  1. Need to pass -DHAVE_UINTPTR_T=1 as it is not written in pyconfig.h.
  2. Need to add include paths: sysconfig.get_paths()['include'] sysconfig.get_paths()['platinclude'] pybind11.get_include(), which is mentioned in https://github.com/pybind/pybind11/blob/v2.9.1/pybind11/__main__.py#L11 .
  3. 64bit build requires -DMS_WIN64=1 cf https://stackoverflow.com/a/9673051
  4. Need to #define _hypot hypot in pybind11 library cf https://stackoverflow.com/a/65694764
class build_ext_hook(build_ext, object):
    def build_extension(self, ext):
        if platform.system() == 'Windows':
            if sys.maxsize < 1<<32:
                msiz = '-m32'
                plat = 'win32'
                win64flags = []
            else:
                msiz = '-m64'
                plat = 'win-amd64'
                win64flags = ['-DMS_WIN64=1']
            subprocess.check_call(['clang', msiz, '-c', '-DPRECOMPUTE_TABLES=1', '-o', 'slz.o', '-O2', 'src/libslz/src/slz.c'])
            subprocess.check_call(['clang', msiz, '-c', '-o', 'chkstk.o', 'src/chkstk.S'])
            if sys.version_info < (3,5):
                import sysconfig
                import pybind11
                subprocess.check_call(['clang++', msiz, '-c', '-o', 'pyslz.o', '-O2',
                    '-DHAVE_UINTPTR_T=1',
                    '-I', sysconfig.get_paths()['include'],
                    '-I', sysconfig.get_paths()['platinclude'],
                    '-I', pybind11.get_include(),
                    'src/pyslz.cpp']+win64flags)
                ext.extra_objects.append('pyslz.o')
            else:
                ext.sources.append('src/pyslz.cpp')
            ext.extra_objects.extend(['slz.o', 'chkstk.o'])
        else:
            ext.sources.extend(['src/pyslz.cpp', 'src/libslz/src/slz.c'])
        build_ext.build_extension(self, ext)

Linking

You noticed that you have to link pyslz.o using clang as C++ runtime and compiler_rt (runtime like libgcc) must be linked and specifying them is difficult on MSVC.

  1. the path to put pyd can be computed as 'build/lib.%s-%d.%d/%s.pyd'%(plat, sys.hexversion // 16777216, sys.hexversion // 65536 % 256, ext.name.replace('.', '/')).
  2. pythonNN.lib import library path can be computed as below:

Also remember that after linking you need to return without delegating to setuptools's build_ext.

                    libname = 'python%d%d.lib'%(sys.hexversion // 16777216, sys.hexversion // 65536 % 256)
                    # https://stackoverflow.com/a/48360354/2641271
                    d = Distribution()
                    b = d.get_command_class('build_ext')(d)
                    b.finalize_options()
                    libpath = next(join(dir, libname) for dir in b.library_dirs if isfile(join(dir, libname)))
class build_ext_hook(build_ext, object):
    def build_extension(self, ext):
        if platform.system() == 'Windows':
            if sys.maxsize < 1<<32:
                msiz = '-m32'
                plat = 'win32'
                win64flags = []
            else:
                msiz = '-m64'
                plat = 'win-amd64'
                win64flags = ['-DMS_WIN64=1']
            subprocess.check_call(['clang', msiz, '-c', '-DPRECOMPUTE_TABLES=1', '-o', 'slz.o', '-O2', 'src/libslz/src/slz.c'])
            subprocess.check_call(['clang', msiz, '-c', '-o', 'chkstk.o', 'src/chkstk.S'])
            if sys.version_info < (3,5):
                import sysconfig
                import pybind11
                subprocess.check_call(['clang++', msiz, '-c', '-o', 'pyslz.o', '-O2',
                    '-DHAVE_UINTPTR_T=1',
                    '-I', sysconfig.get_paths()['include'],
                    '-I', sysconfig.get_paths()['platinclude'],
                    '-I', pybind11.get_include(),
                    'src/pyslz.cpp']+win64flags)
                ext.extra_objects.append('pyslz.o')
                if True:
                    ext.extra_objects.extend(['slz.o'])
                    pydpath = 'build/lib.%s-%d.%d/%s.pyd'%(plat, sys.hexversion // 16777216, sys.hexversion // 65536 % 256, ext.name.replace('.', '/'))
                    subprocess.check_call(['mkdir', '-p', dirname(pydpath)])
                    libname = 'python%d%d.lib'%(sys.hexversion // 16777216, sys.hexversion // 65536 % 256)
                    # https://stackoverflow.com/a/48360354/2641271
                    d = Distribution()
                    b = d.get_command_class('build_ext')(d)
                    b.finalize_options()
                    libpath = next(join(dir, libname) for dir in b.library_dirs if isfile(join(dir, libname)))
                    subprocess.check_call([
                        'clang++', msiz, '-shared', '-o', pydpath,
                    ]+ext.extra_objects+[libpath])
                    return
            else:
                ext.sources.append('src/pyslz.cpp')
            ext.extra_objects.extend(['slz.o', 'chkstk.o'])
        else:
            ext.sources.extend(['src/pyslz.cpp', 'src/libslz/src/slz.c'])
        build_ext.build_extension(self, ext)

Now you got Python 2.7 wheel for Windows even though Python 2.7's MSVC version is incompatible with Pybind11 :tada:

Appendix

Can we keep gcc?

Technically yes.

  • For 32bit, you need to download Install mingw-w64-i686-gcc using this yaml. Unlike clang, gcc -m32 will work only for compilation, not linking.
    - name: "Install mingw-w64-i686-gcc"
      if: ${{ matrix.os == 'windows-latest' && matrix.architecture == 'x86' }}
      uses: msys2/setup-msys2@v2
      with:
        msystem: MINGW32
        path-type: inherit
        install: mingw-w64-i686-gcc
  • You need to add link option:
'-static-libstdc++', '-static-libgcc', '-Wl,-Bstatic,--whole-archive', '-lwinpthread', '-Wl,--no-whole-archive,-Bdynamic'

However this will result in very huge binary.

  1. For __divdi3 etc, check https://github.com/glitchub/arith64/blob/master/arith64.c as well.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?