PyOxidizer Support for Windows
January 06, 2019 at 10:00 AM | categories: Python, PyOxidizer, RustA few weeks ago I introduced PyOxidizer, a project that aims to make it easier to produce completely self-contained executables embedding a Python interpreter (using Rust). A few days later I observed some PyOxidizer performance benefits.
After a few more hacking sessions, I'm very pleased to report that PyOxidizer is now working on Windows!
I am able to produce a standalone Windows .exe
containing a fully
featured CPython interpreter, all its library dependencies (OpenSSL, SQLite,
liblzma, etc), and a copy of the Python standard library (both source and
bytecode data). The binary weighs in at around 25 MB. (It could be smaller
if we didn't embed .py
source files or stripped some dependencies.)
The only DLL dependencies of the exe are vcruntime140.dll
and various
system DLLs that are always present on Windows.
Like I did for Linux and macOS, I produced a Python script that performs
~500 import
statements for the near entirety of the Python standard library.
I then ran this script with both the official 64-bit Python distribution
and an executable produced with PyOxidizer:
# Official CPython 3.7.2 Windows distribution.
$ time python.exe < import_stdlib.py
real 0m0.475s
# PyOxidizer with non-PGO CPython 3.7.2
$ time target/release/pyapp.exe < import_stdlib.py
real 0m0.347s
Compared to the official CPython distribution, a PyOxidizer executable can import almost the entirety of the Python standard library ~125ms faster - or ~73% of original. In terms of the percentage of speedup, the gains are similar to Linux and macOS. However, there is substantial new process overhead on Windows compared to POSIX architectures. On the same machine, a hello world Python process will execute in ~10ms on Linux and ~40ms on Windows. If we remove the startup overhead, importing the Python standard library runs at ~70% of its original time, making the relative speedup on par with that seen on macOS + APFS.
Windows support is a major milestone for PyOxidizer. And it was the
hardest platform to make work. CPython's build system on Windows uses
Visual Studio project files. And coercing the build system to produce
static libraries was a real pain. Lots of CPython's build tooling assumes
Python is built in a very specific manner and multiple changes I made
completely break those assumptions. On top of that, it's very easy to
encounter problems with symbol name mismatch due to the use of
__declspec(dllexport)
and __declspec(dllimport)
. I spent
several hours going down a rabbit hole learning how Rust generates symbols
on Windows for extern {}
items. Unfortunately, we currently have
to use a Rust Nightly feature (the static-nobundle
linkage kind)
to get things to work. But I think there are options to remove that
requirement.
Up to this point, my work on PyOxidizer has focused on prototyping the concept. With Windows out of the way and PyOxidizer working on Linux, macOS, and Windows, I have achieved confidence that my vision of a single executable embedding a full-featured Python interpreter is technically viable on major desktop platforms! (BSD people, I care about you too. The solution for Linux should be portable to BSD.) This means I can start focusing on features, usability, and optimization. In other words, I can start building a tool that others will want to use.
As always, you can follow my work on this blog and by following the python-build-standalone and PyOxidizer projects on GitHub.