提示:这里可能有比下面介绍的更好/更稳健/更简单的选择。
这两种方法的主要区别在于:在这种方法中,所有的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.dll
,hello_prog.exe
仍然可以正常工作。
在Linux上,这将是“任务完成”,在Windows上我们才刚刚开始...
确保`cpython`文件夹中有一个正确的`DLLs`文件夹,其中包含所有正确的pyd文件,否则请创建并从`PCbuild/amd64`文件夹复制所有pyd文件。
让我们使我们的pyx文件变得更加复杂:
import _decimal
print("I'm standalone")
_decimal
是decimal
模块的快速实现,它是一个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_SHARED
它
becomes 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文件应如下所示:
extern PyObject* PyInit__decimal();
...
int main(int argc, char** argv) {
...
if (argc && argv)
Py_SetProgramName(argv[0]);
PyImport_AppendInittab("_decimal", PyInit__decimal);
Py_Initialize();
现在构建所有内容会生成一个可执行文件,该文件会打印输出。
I'm standalone
这次它没有欺骗我们!
现在我们需要为所有其他内置扩展重复上述步骤。
以上意味着静态构建的Python解释器存在一些限制:所有内置模块都需要嵌入可执行文件中,我们无法在后续使用类似numpy/scipy这样的库扩展解释器(但可以直接在编译/链接时完成)。
摆脱vcruntime-dll更容易:必须使用
/MT
选项而不是
MD
选项完成上述所有步骤
。然而,由于使用了其他使用dll版本编译的dll(例如
_ctypes
需要
ffi
-dll),可能会出现一些问题(因此我们再次违反ODR),所以我不建议这样做。
cl.exe
而不是gcc等)。 - Basj