Как скомпилировать файл init.py с помощью Cython в окне - PullRequest
3 голосов
/ 11 ноября 2019

Когда я компилирую файл __init__.py для арбитража с помощью команды setup.py build_ext --inplace, в нем возникает неразрешимая ошибка внешнего символа. Я увидел описание в вопросе этого друга и столкнулся с тем же вопросом, что и мой, но не смог получить доступ к ссылке в его ответе ...

Локальная среда:

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

Ответы [ 2 ]

2 голосов
/ 13 ноября 2019

Возможно, это поведение можно рассматривать как небольшую ошибку в distutils -пакете. Тем не менее, это также показывает, что цитонизация / компиляция __init__.py не очень популярна и использует некоторые недокументированные детали реализации, которые могут измениться в будущем, поэтому было бы разумнее воздержаться от вмешательства с __init__.py.

* 1005. * Но если вы должны ...

Когда пакет импортируется явно, например,

import ctest

или неявно, например,

import ctest.something

FileFinder увидит, что пакет, а не модуль импортирован, и попытается загрузить 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-функции, которую Python-интерпретатор будет вызывать при загрузке расширения __init__.so, равно PyInit_ctest в вашем случае (а не PyInit___init__, как можно подумать).

Выше объясняетсяпочему все это работает на Linux из коробки? А как насчет Windows?

Загрузчик может использовать только символы из so / dll, которые не скрыты. По умолчанию все символы, созданные с помощью gcc, являются видимыми, но не для VisualStudio в Windows, где все символы скрыты по умолчанию (см., Например, SO-post ).

Однако init-функция C-расширения должна быть видимой (и только init-функцией), чтобы ее можно было вызывать с помощью загрузчика - решение состоит в том, чтобы экспортировать этот символ (т.е. PyInit_ctest) при компоновке, в вашем случае этонеправильный вариант /EXPORT:PyInit___init__ для компоновщика.

Проблема может быть найдена в distutils или, более точно, в build_ext -классе :

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)

Таким образом, нет необходимости отмечать символ как видимый в командной строке. Что еще хуже, это является причиной предупреждения LNK4197 :

init .obj: предупреждение LNK4197: экспорт 'PyInit_ctest' указан несколько раз;с использованием первой спецификации

в качестве PyInit_test помечается как __declspec(dllexport) и одновременно экспортируется через опцию /EXPORT:.

/EXPORT: -опция будетпропущенный 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!

1 голос
/ 11 ноября 2019

Это очень предварительный ответ, потому что у меня нет простого способа проверить его на 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 bug tracker (возможно, Cython) с этими подробностями, и, надеюсь, это можно исправить.

...