使用 cython --embed 时静态 link python37.dll 和 vcruntime140.dll

Statically link python37.dll and vcruntime140.dll when using cython --embed

假设我正在“cythonizing”这个 test.py:

import json
print(json.dumps({'key': 'hello world'}))

与:

cython test.py --embed
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x64
cl test.c /I C:\Python37\include /link C:\Python37\libs\python37.lib

中所述,需要分发python37.dllvcruntime140.dll以及Lib\的内容(作为Lib\或打包到python37.zip) 以及 test.exe 文件。

问题:如何修改cl.exe ...命令让编译器在[=21=里面静态地linkpython37.dllvcruntime140.dll ] 文件?

(因此不再需要单独运送 python37.dllvcruntime140.dll

备注: 可能是 better/saner/easier 选项,而不是下面显示的选项。

这两种方法的主要区别:在这种方法中,所有 C 扩展都必须返回到生成的可执行文件中,而在替代方法中,C 扩展是单独编译的,或者也可以添加额外的 C 扩展稍后进行分发。


虽然创建静态 linked 嵌入-Python-可执行文件在 Linux 上相对容易(例如参见 [​​=67=]),但在 Linux 上要复杂得多Windows。你可能不想这样做。

此外,结果可能不是人们所期望的:由于 dll 与 Linux' 共享对象相比的局限性,生成的静态 python-version 将无法 use/load 任何其他 c 扩展,作为在 compile/link 时间内支持的扩展(注意:这不完全正确, 中提供了一种解决方法)。

我也不建议从 vcruntime-dll 切换到它的静态版本 - 只有当 everything (exe, c-extensions, other dll which depend在 vcruntime 上)被静态 link 编辑成一个巨大的可执行文件。

第一个绊脚石:Linux python 发行版通常有一个静态 Python-library 已经发布,Windows 发行版只有 dll,不能静态 linked in.

因此需要在Windows上构建一个静态库。一个好的起点是 link.

下载正确 Python 版本 (git clone --depth=1 --branch v3.8.0 https://github.com/python/cpython.git) 的源代码后,您可以转到 cpython\PCBuild 并按照文档中的说明构建 cpython(可能有所不同从一个版本到另一个版本)。

在我的例子中是

cd cpython/PCbuild
.\build.bat -e -p x64 

不,我们有一个正常运行的 Python3.8 安装,可以在 cpython/PCbuild/amd64 中找到。创建文件夹 cpython/PCbuild/static_amd64 并添加以下 pyx 文件:

#hello.pyx
print("I'm standalone")

暂时复制python38.dllstatic_amd64

现在让我们使用嵌入式 python 解释器构建我们的程序:

cython --embed -3 hello.pyx
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x64
cl /c hello.c /Fohello.obj  /nologo /Ox /W3 /GL /DNDEBUG /MD -I<path_to_code>\cpython\include -I<path_to_code>\cpython\PC
link hello.obj python38.lib  /OUT:hello_prog.exe /nologo "/LIBPATH:<path_to_code>\cpython\PCbuild\amd64"

开始后,hello_prog.exe 欺骗了我们,因为它并不是真正独立的。好消息是:它找到了 Python 所需要的安装,例如 .

现在让我们创建一个静态 python38 库。为此,我们在 cpython/PCbuild-folder 中打开 pcbuild.sln 并更改 pythoncore-项目的设置以在 PCbuild\amd64_static- 文件夹中生成静态库。重建它。

现在我们可以构建嵌入式-python-exe:

cl /c hello.c /Fohello.obj /D "Py_NO_ENABLE_SHARED" /nologo /Ox /W3 /GL /DNDEBUG /MD -I<path_to_code>\cpython\include -I<path_to_code>\cpython\PC
link hello.obj python38.lib "version.lib" "shlwapi.lib" "ws2_32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" /OUT:hello_prog.exe /nologo "/LIBPATH:<path_to_code>\cpython\PCbuild\static_amd64"

与针对 dll 的构建相比,我们必须更改以下内容:

  • Py_NO_ENABLE_SHARED(即 /D "Py_NO_ENABLE_SHARED")被添加到预处理器定义中,否则 linker 将查找错误的符号。
  • 由 python-dll 带来的 Windows 依赖项(即 version.lib 等)现在需要显式传递给 linker (这可以在 python 核心项目的 linker 命令行中查找。
  • lib 路径显示到静态文件夹,即 "/LIBPATH:<path_to_code>\cpython\PCbuild\static_amd64" 现在。
  • 根据您的具体工具链,可能还有其他小问题(不同的优化级别、link-时间代码生成、禁用整个程序优化等)。

我们现在可以从 static_amd64 中删除 python38.dllhello_prog.exe 仍然有效。

在 Linux,这将是“任务完成”,在 Windows,我们才刚刚开始...

确保 cpython-文件夹有一个包含所有正确 pyd 文件的 DLLs-文件夹,否则从 PCbuild/amd64-文件夹创建并复制所有 pyd-文件。

让我们的 pyx 文件稍微复杂一点:

import _decimal
print("I'm standalone")

_decimaldecimal-模块的快速实现,它是一个 C 扩展,可以在 DLL- 文件夹中找到。

cythonizing 和构建后,运行 hello_prog.exe 导致以下错误消息:

import _decimal
ImportError: DLL load failed while importing _decimal: The specified module could not be found.

问题容易发现:

dumpbin /DEPENDENTS ../amd64/_decimal.pyd
...
python38.dll
... 

我们安装的扩展仍然依赖于python-dll。让我们根据静态库重建它们——我们需要将库路径从 amd64 更改为 static_amd64,以添加预处理器定义 Py_NO_ENABLE_SHARED 和所有缺失的 windows-库(即“ "version.lib"& Co.) 并将 /EXPORT:PyInit__decimal 添加到 link 选项,否则,由于 Py_NO_ENABLE_SHARED。结果与 python-dll! 我们将其复制到 DLLs 文件夹并...

hello_prog.exe
# crash/stopped worked

这是怎么回事?我们违反了一个定义规则 (ODR),最终得到两个 Python-解释器:一个来自 hello_prog.exe,已初始化,另一个来自 _decimal.pyd,未初始化。 _decimal.pyd 与其未初始化的解释器“对话”,并且发生了不好的事情。

与 Linux 的区别在于共享对象和 dll 之间的区别:共享对象可以使用 exe 中的符号(如果 exe 是使用正确的选项构建的)dll 不能,因此必须依赖在 dll 上(我们不想要)或需要有自己的版本。

为了避免违反 ODR,我们只有一个出路:它必须直接 linked 到我们的 hello_word- 可执行文件中。因此,让我们将 _decimal 的项目更改为静态库并在 static_amd64 文件夹中重建它。从“DLLs”文件夹中删除 pyd 并将 /WHOLEARCHIVE:_decimal.lib 添加到 linker 命令行(整个存档,否则 linker 将丢弃 _decimal.lib 作为 none 的符号在某处被引用),导致可执行文件出现以下错误:

ModuleNotFoundError: No module named '_decimal'

这是预料之中的——我们需要告诉解释器,模块 _decimal 已返回,不应在 python 路径上搜索。

此问题的通常解决方案是使用 PyImport_AppendInittab just before Py_Initialize, that means we need to change the c-file generated by cython (there might be , but due to it is not that easy. So probably a saner way to embed Python is the one presented or ,因为 main 不是由 Cython 编写的)。 c 文件应如下所示:

//decalare init-functions
extern  PyObject* PyInit__decimal();
...
int main(int argc, char** argv) {
...
    if (argc && argv)
        Py_SetProgramName(argv[0]);
    PyImport_AppendInittab("_decimal", PyInit__decimal); //  HERE WE GO
                                                         //  BEFORE Py_Initialize
    
    Py_Initialize();

构建所有内容现在会生成一个打印

的exe
I'm standalone

而且这次没有骗我们!

现在我们必须为我们需要的所有其他内置扩展重复最后的步骤。


以上意味着静态构建的 python-interpreter 有一些限制:所有内置模块都需要备份到可执行文件中,我们不能使用像 [=164= 这样的库扩展解释器](但可以直接在compile/link时进行)。


摆脱 vcruntime-dll 更容易:必须完成上述所有步骤 。但是,由于使用其他 dll(例如 _ctypes 需要 ffi-dll)可能会出现一些问题,这些 dll 是使用 dll 版本构建的(因此我们再次违反了 ODR)- 所以我不会推荐它。

这是一种略有不同的方法(如@ssbssa 的评论所建议):主要区别在于使用此版本后,可以进一步添加 C-extensions 而不必将它们返回到生成的可执行文件。

创建与静态 python-library 链接的 hello_prog.exe 之前的第一步与 中的相同。命令

...
link hello.obj ... /OUT:hello_prog.exe ...

不仅创建 exe 本身,还创建 lib-file hello_prob.lib。这个lib-file可以用来做联动,因为exe本身就从Python-library导出了很多符号。该行为类似于 -Xlinker -export-dynamic 在 Linux.

上链接嵌入式 python 可执行文件时的行为

现在,当构建 C-extensions 时(例如 _decimal 我们需要添加 hello_prog.lib 作为链接依赖项(即在 Properties/Linker/Input->Additional Dependencies 中)。

当我们查看 _decimal.pyd 的 运行 时间依赖性时,我们看到:

dumpbin /DEPENDENTS _decimal.pyd
...
Dump of file _decimal.pyd

File Type: DLL

  Image has the following dependencies:

    hello_prog.exe
    ...

当生成的 pyds 以这种方式链接并且可以被嵌入式解释器找到时,我们 运行 hello_prog 并查看:

I'm standalone

这意味着一切正常。

要进一步 C-extension 构建,hello_prob.lib-文件必须在 provided/stored 某处。