在使用cython --embed时,静态链接python37.dll和vcruntime140.dll。

3
假设我正在对这个test.py进行"Cython化"处理:
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

正如在最少的文件集合来分发嵌入Cython编译代码并使其在任何计算机上工作中提到的那样,需要分发python37.dllvcruntime140.dll以及Lib\的内容(可以是Lib\或打包为python37.zip),以及test.exe文件。

问题:如何修改cl.exe ...命令,要求编译器将python37.dllvcruntime140.dll静态链接到test.exe文件内?

(这样就不再需要单独运送python37.dllvcruntime140.dll)


请参阅静态构建Python及其链接。 - dxiv
@dxiv 谢谢。这是否意味着需要完全重建Python?我可以想象这需要很长时间?(遗憾的是,此页面上的最后一个链接是404)。一个详细的Windows教程会很有趣(使用cl.exe而不是gcc等)。 - Basj
我的理解是“是”,尽管我没有亲自尝试过。损坏链接的缓存副本在此处。 - dxiv
谢谢@ead,我将首先从vcruntime开始。您如何使用cl.exe从命令行执行?只需添加/MT吗?这似乎太简单了,难以置信 :) 因为我不使用MSVC++ IDE,所以需要从cl.exe中执行。 - Basj
@ead ODR 是什么?只是出于好奇,您如何通过 cl.exe 进行相当于在 /C++ > 代码生成 > 运行时库 中添加 /MT 的操作? - Basj
显示剩余2条评论
2个回答

5

提示:这里可能有比下面介绍的更好/更稳健/更简单的选择。

这两种方法的主要区别在于:在这种方法中,所有的C扩展都必须被编入最终的可执行文件中,而在另一种方法中,C扩展是单独编译的,或者可以在分发时添加额外的C扩展。


在Linux上创建静态链接的嵌入式Python可执行文件相对容易(例如参见这篇SO-post),但在Windows上则更为复杂。而且你可能不想这么做。
此外,结果可能并非如人们所期望的那样:由于dll与Linux共享对象相比存在的限制,编译/链接时内置的python版本将无法使用/加载其他任何c扩展,除了自带的一个(注意:这并不完全正确,this answer中提供了一种解决方法)。
我也不建议从vcruntime-dll切换到其静态版本——只有当 所有内容 (exe、c扩展、其他依赖于vcruntime的dll)都静态链接成一个巨大的可执行文件时才有意义。
第一个绊脚石是:虽然在Linux上python发行版通常已经有一个静态的Python库,但Windows发行版只有dll,不能静态链接。
因此,在Windows上需要构建静态库。一个很好的起点是这个链接
在下载了正确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.dll复制到static_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文件夹中打开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")添加到预处理器定义中,否则链接器将查找错误的符号。
  • 现在需要显式地将Python DLL带来的Windows依赖项(即version.lib等)传递给链接器(可以在pythoncore项目的链接器命令行中查找)。
  • 库路径显示为静态文件夹,即"/LIBPATH:<path_to_code>\cpython\PCbuild\static_amd64"
  • 根据您的确切工具链,可能会出现其他较小的问题(不同的优化级别、链接时代码生成、禁用整个程序优化等)。

现在,我们可以从static_amd64中删除python38.dllhello_prog.exe仍然可以正常工作。

在Linux上,这将是“任务完成”,在Windows上我们才刚刚开始...
确保`cpython`文件夹中有一个正确的`DLLs`文件夹,其中包含所有正确的pyd文件,否则请创建并从`PCbuild/amd64`文件夹复制所有pyd文件。
让我们使我们的pyx文件变得更加复杂:
import _decimal
print("I'm standalone")

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

将其进行Cython编译和构建后,运行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,否则由于Py_NO_ENABLE_SHAREDbecomes invisible。结果不依赖于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,我们只有一种出路:它必须直接链接到我们的hello_word可执行文件中。因此,让我们将_decimal项目更改为静态库,并在static_amd64文件夹中重新构建它。从“DLLs”文件夹中删除pyd,并将/WHOLEARCHIVE:_decimal.lib添加到链接器命令行(整个存档,否则链接器会将_decimal.lib丢弃,因为它的符号没有被引用),导致出现以下错误:

ModuleNotFoundError: No module named '_decimal'

这是预期的-我们需要告诉解释器,模块_decimal已经支持,不应在Python路径上搜索。

这个问题的常规解决方案是在Py_Initialize之前使用PyImport_AppendInittab,这意味着我们需要更改由Cython生成的c文件(可能会有解决方法,但由于多阶段初始化,这并不容易。因此,嵌入Python的更明智的方法可能是这里这里介绍的方法,其中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();

现在构建所有内容会生成一个可执行文件,该文件会打印输出。
I'm standalone

这次它没有欺骗我们!

现在我们需要为所有其他内置扩展重复上述步骤。


以上意味着静态构建的Python解释器存在一些限制:所有内置模块都需要嵌入可执行文件中,我们无法在后续使用类似numpy/scipy这样的库扩展解释器(但可以直接在编译/链接时完成)。
摆脱vcruntime-dll更容易:必须使用/MT选项而不是MD选项完成上述所有步骤。然而,由于使用了其他使用dll版本编译的dll(例如_ctypes需要ffi-dll),可能会出现一些问题(因此我们再次违反ODR),所以我不建议这样做。

如果你试着用"类似"于Linux的方式去做呢?将静态Python库链接到你的可执行文件中,这样该可执行文件就会导出所有Python函数。当然,扩展也必须链接到你的可执行文件而不是Python DLL,因此你只能将它用于这个可执行文件。 - ssbssa
@ssbssa,问题在于MSVC没有-Xlinker -export-dynamic选项,这就是Python exe只是dll的包装器的原因,您需要将c扩展链接到它上面-无法链接到exe。如果使用gcc-ports编译,则可能会起作用:我没有尝试过。 - ead
是的,你可以。而且我认为MSVC甚至会在可执行文件具有任何导出函数时自动创建导入库。 - ssbssa
@ssvssa,你能提供一个链接吗?我从来没有见过有人这样做。 - ead
我不确定你想要哪些链接。类似于这个或者这个?肯定有人已经做过了(包括我在内)。 - ssbssa
@ssbssa 当它工作时,它很棒,但这绝不是最标准的做法(通常的方法是创建一个dll和一个exe包装器,就像cpython一样)。 - ead

2
这是一种稍微不同的方法(根据@ssbssa的评论建议):主要区别在于,使用此版本可以稍后添加更多的C扩展,并且不必将它们反向编译到生成的可执行文件中。
直到创建链接到静态Python库的hello_prog.exe之前的第一步与上面的答案相同。 命令为:
...
link hello.obj ... /OUT:hello_prog.exe ...

创建的不仅是exe本身,还包括lib文件hello_prob.lib。该lib文件可用于链接,因为exe本身从Python库中导出了许多符号。当在Linux上链接嵌入式Python可执行文件时,其行为类似于-Xlinker -export-dynamic行为。

现在,当构建C扩展(例如_decimal)时,我们需要将hello_prog.lib添加为链接依赖项(即在属性/链接器/输入->附加依赖项中)。

当我们查看_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扩展,必须提供/存储hello_prob.lib文件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接