如何在 Windows 上使用 cython 编译 __init__.py 文件

ppp*_*pig 6 python windows cython

当我使用命令在 Windows 上编译任意 __init__.py 文件时setup.py build_ext --inplace,它出现无法解析的外部符号错误(即“LINK:错误 LNK2001:无法解析的外部符号 PyInit___init__”)。

当地环境:

python3.7,
Cython 0.29.14,
window10 x64,
Microsoft Visual Studio 2017,
Run Code Online (Sandbox Code Playgroud)

ctest/__init__.py

# cython: language_level=3
print('__init__')

Run Code Online (Sandbox Code Playgroud)

安装程序.py

python3.7,
Cython 0.29.14,
window10 x64,
Microsoft Visual Studio 2017,
Run Code Online (Sandbox Code Playgroud)

终端打印的信息:

Compiling ctest/__init__.py because it changed.
[1/1] Cythonizing ctest/__init__.py
running build_ext
building 'ctest.__init__' extension
creating build
creating build\temp.win-amd64-3.7
creating build\temp.win-amd64-3.7\Release
creating build\temp.win-amd64-3.7\Release\ctest
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\bin\HostX86\x64\cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -Id:\py37\include -Id:\py37\incl
ude "-IC:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\include" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.6.1\include\um" "-IC:\Pro
gram Files (x86)\Windows Kits\10\include\10.0.18362.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.18362.0\shared" "-IC:\Program Files (x86)\Windows Kits\10\includ
e\10.0.18362.0\um" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.18362.0\winrt" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.18362.0\cppwinrt" /Tcctest/__init__
.c /Fobuild\temp.win-amd64-3.7\Release\ctest/__init__.obj
__init__.c
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\bin\HostX86\x64\link.exe /nologo /INCREMENTAL:NO /LTCG /DLL /MANIFEST:EMBED,ID=2 /MANIFESTU
AC:NO /LIBPATH:d:\py37\Libs /LIBPATH:D:\ENVS\cpytrantest\libs /LIBPATH:D:\ENVS\cpytrantest\PCbuild\amd64 "/LIBPATH:C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC
\Tools\MSVC\14.16.27023\lib\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\NETFXSDK\4.6.1\lib\um\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\ucrt\x6
4" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\um\x64" /EXPORT:PyInit___init__ build\temp.win-amd64-3.7\Release\ctest/__init__.obj /OUT:C:\Users\76923\Deskto
p\cpythonrecord\ctest\__init__.cp37-win_amd64.pyd /IMPLIB:build\temp.win-amd64-3.7\Release\ctest\__init__.cp37-win_amd64.lib
LINK : error LNK2001: An unresolvable external symbol PyInit___init__
build\temp.win-amd64-3.7\Release\ctest\__init__.cp37-win_amd64.lib : fatal error LNK1120: An external command that cannot be parsed
error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Tools\\MSVC\\14.16.27023\\bin\\HostX86\\x64\\link.exe' failed with exit status 1120
Run Code Online (Sandbox Code Playgroud)

ead*_*ead 7

也许这种行为可能被视为distutils-package 中的一个小错误(正如 @DavidW 指出的,存在这个未解决的问题:https ://bugs.python.org/issue35893 )。然而,它也表明,cythonizing/compiling__init__.py并不是很流行,并且使用了一些未记录的实现细节,这些细节将来可能会发生变化,因此最好不要干预__init__.py.

但如果你必须...


当显式导入包时,例如

import ctest
Run Code Online (Sandbox Code Playgroud)

或隐式地,例如

import ctest.something
Run Code Online (Sandbox Code Playgroud)

FileFinder看到导入了一个包,而不是一个模块,并将尝试加载 (它很可能不存在):ctest/__init__.pyctest.py

    # Check if the module is the name of a directory (and thus a package).
    if cache_module in cache:
        base_path = _path_join(self.path, tail_module)
        for suffix, loader_class in self._loaders:
            init_filename = '__init__' + suffix
            full_path = _path_join(base_path, init_filename)
            if _path_isfile(full_path):
                return self._get_spec(loader_class, fullname, full_path, [base_path], target)
Run Code Online (Sandbox Code Playgroud)

用于suffix, loader_class加载__init__.so__init__.py__init__.pyc 按此顺序(另请参阅此SO-post)。这意味着,__init__.so将被加载,而不是__init__.py我们设法创建一个。

在执行时__init__.py,该属性__name__是包的名称,即ctest在您的情况下,而不是__init__像人们想象的那样。因此,Python 解释器在加载扩展时将调用的 init 函数的名称__init__.so就是PyInit_ctest您的情况(而不是PyInit___init__像人们想象的那样)。

上面解释了为什么它可以在 Linux 上开箱即用。那么 Windows 呢?

加载程序只能使用 so/dll 中未隐藏的符号。默认情况下,使用 gcc 构建的所有符号都是可见的,但对于 Windows 上的 VisualStudio 来说则不然 - 默认情况下所有符号都是隐藏的(例如参见此SO-post)。

但是,C 扩展的 init 函数必须是可见的(并且只有 init 函数),以便可以在加载程序的帮助下调用它 - 解决方案是PyInit_ctest在链接时导出此符号(即 ),在您的情况下它是/EXPORT:PyInit___init__链接器的错误选项。

该问题可以在 distutils 中找到,或者更精确地在build_ext-class中找到:

def get_export_symbols(self, ext):
    """Return the list of symbols that a shared extension has to
    export.  This either uses 'ext.export_symbols' or, if it's not
    provided, "PyInit_" + module_name.  Only relevant on Windows, where
    the .pyd file (DLL) must export the module "PyInit_" function.
    """
    initfunc_name = "PyInit_" + ext.name.split('.')[-1]
    if initfunc_name not in ext.export_symbols:
        ext.export_symbols.append(initfunc_name)
    return ext.export_symbols
Run Code Online (Sandbox Code Playgroud)

在这里,可悲的ext.name__init__

从这里开始,一种可能的解决方案很简单:覆盖get_export_symbols,即将以下内容添加到您的setup.py文件中(继续阅读以获得更简单的版本):

...
from distutils.command.build_ext import build_ext
def get_export_symbols_fixed(self, ext):
    names = ext.name.split('.')
    if names[-1] != "__init__":
        initfunc_name = "PyInit_" + names[-1]
    else:
        # take name of the package if it is an __init__-file
        initfunc_name = "PyInit_" + names[-2]
    if initfunc_name not in ext.export_symbols:
        ext.export_symbols.append(initfunc_name)
    return ext.export_symbols

# replace wrong version with the fixed:
build_ext.get_export_symbols = get_export_symbols_fixed
...
Run Code Online (Sandbox Code Playgroud)

现在调用python setup.py build_ext -i应该足够了(因为__init__.so将被加载而不是__init__.py)。


然而,正如 @DawidW 指出的那样,Cython 使用宏PyMODINIT_FUNC,它定义为

import ctest
Run Code Online (Sandbox Code Playgroud)

在 Windows 上被Py_EXPORTED_SYMBOL标记为可见/导出:

import ctest.something
Run Code Online (Sandbox Code Playgroud)

因此,无需将符号标记为在命令行中可见。更糟糕的是,这就是警告 LNK4197的原因:

__init__.obj :警告 LNK4197:多次指定导出“PyInit_ctest”;使用第一个规范

asPyInit_test被标记为并同时__declspec(dllexport)通过选项导出。/EXPORT:

/EXPORT:-option 将被 distutils 跳过,如果export_symbols为空,我们可以使用更简单的版本command.build_ext

...
from distutils.command.build_ext import build_ext
def get_export_symbols_fixed(self, ext):
    pass  # return [] also does the job!

# replace wrong version with the fixed:
build_ext.get_export_symbols = get_export_symbols_fixed
...
Run Code Online (Sandbox Code Playgroud)

这甚至比第一个版本更好,因为它还修复了警告 LNK4197!