【问题标题】:How to compile __init__.py file using cython on Windows如何在 Windows 上使用 cython 编译 __init__.py 文件
【发布时间】:2020-11-20 21:07:55
【问题描述】:

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

当地环境:

python3.7,
Cython 0.29.14,
window10 x64,
Microsoft Visual Studio 2017,

ctest/__init__.py

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

setup.py

from distutils.core import setup
from Cython.Build import cythonize


def compile_code(name, filename):
    setup(
        name=name,
        ext_modules=cythonize(filename),
    )


if __name__ == '__main__':
    compile_code('a', 'ctest/__init__.py')

终端打印的信息:

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

【问题讨论】:

  • 我宁愿不 cythonize/编译 init.py。它似乎可以在 Linux 上运行,但在 Windows 上存在问题。
  • @ead 感谢您的回复。我在 Linux 上执行了相同的命令,它的工作与我希望的一样,但我现在需要在窗口上运行它......
  • 这可能很有趣:stackoverflow.com/a/32067984/5769463 暗示它有点意外 - 但应该依赖它吗?但是,__init__.py 并不是真正的模块,那么为什么不让它不编译呢?首先编译它有什么好处?
  • 我记得看到如果您更改符号可见性(或类似的东西,我不记得细节),您可以使其在 Windows 上工作。不过,这似乎毫无意义
  • @ead 再次感谢。这怎么可能不是一个好的解决方案。_init_.py 文件中只有一点代码,不编译也没关系.也许是因为它在Linux上工作,但在Windows上是一个问题,所以我正在寻找一个解决方案。

标签: python windows cython


【解决方案1】:

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

但如果你必须...


显式导入包时,例如

import ctest

或隐含的,例如

import ctest.something

FileFinder 将看到导入的是包而不是模块,并且 will try to load ctest/__init__.py 而不是 ctest.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)

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

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

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

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

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

问题可以在 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

在这里,遗憾的是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
...

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


但是,正如@DawidW 所指出的,Cython 使用宏PyMODINIT_FUNC,其定义为

#define PyMODINIT_FUNC Py_EXPORTED_SYMBOL PyObject*

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

#define Py_EXPORTED_SYMBOL __declspec(dllexport)

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

__init__.obj : 警告 LNK4197: export 'PyInit_ctest' 指定了多次; 使用第一个规范

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

/EXPORT:-option 将be skipped by 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
...

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

【讨论】:

  • @ead 这似乎应该由PyMODINIT_FUNC 处理。不添加initfunc_name 的更简单的版本也可以吗?
  • @DavidW 我认为你是对的。我现在无法检查,但如果我没记错的话,这就是人们从 Windows 上的链接器收到警告 LNK4197 的原因:符号 PyInit_XXX 曾经被声明为 __declspec(dllexport) 和一次通过 /EXPORT:。一个就够了。
  • @ead 我猜它可能是为那些编写自己的 C API 模块而忽略 PyMODINIT_FUNC 的人提供的备份,但无论如何......并不重要。
  • 这是bugs.python.org/issue35893的错误报告链接
【解决方案2】:

这是一个非常试探性的答案,因为我没有简单的方法在 Windows 上对其进行测试,所以如果有错误,请告诉我,我会删除它。

你可以尝试运行(在推荐行上):

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 /MANIFESTUAC: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\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.18362.0\um\x64" /EXPORT:PyInit_ctest build\temp.win-amd64-3.7\Release\ctest/__init__.obj /OUT:C:\Users\76923\Desktop\cpythonrecord\ctest\__init__.cp37-win_amd64.pyd /IMPLIB:build\temp.win-md64-3.7\Release\ctest\__init__.cp37-win_amd64.lib

我所做的只是使用 distutils 生成的编译命令并将/EXPORT:PyInit___init__ 替换为/EXPORT:PyInit_ctest/EXPORT 是 Windows 特定的编译器选项,不会在 Linux 上添加。看起来 distutils 或 Cython 将名称 PyInit___init__ 传递给 MSVC,但如果我查看实际生成的 C 文件,则名称似乎是 PyInit_ctest,因此是未定义的符号。

如果该解决方法(独立于 distutils 进行编译)有效,那么您应该向 distutils 或 Cython 错误跟踪器(可能是 Cython)报告错误并提供这些详细信息,并希望它可以修复。

【讨论】:

  • 这个答案很可能只会让您了解链接问题中的问题,在这种情况下,我不知道是否值得保留
  • 不知道什么时候应该执行,什么时候直接在终端执行还是执行setup.py build_ext --inplace命令后(会生成build\temp.win-amd64-3.7\Release\ctest\__init__.obj,__init__.c和无效文件@987654330 @),它打印LINK : fatal error LNK1181: Unable to open input file “build\temp.win-amd64-3.7\Release\ctest\\__init__.obj”,对
  • @pppig 恐怕我不知道。我没有在 Windows 上安装 Python 和 MSVC,所以我不能轻易测试任何东西,这个答案有点猜测。我的感觉是,这是正确的方向,但我无能为力。我要做的是将答案转换为“社区 wiki”,以便其他人更容易对其进行编辑和提出建议——如果你幸运的话,其他人会有一些想法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-10
  • 1970-01-01
相关资源
最近更新 更多