PEP 776 – Emscripten Support
- Author:
- Hood Chatham <roberthoodchatham at gmail.com>
- Sponsor:
- Łukasz Langa <lukasz at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Active
- Type:
- Informational
- Created:
- 18-Mar-2025
- Python-Version:
- 3.14
- Post-History:
- 18-Mar-2025, 28-Mar-2025
- Resolution:
- 04-Apr-2026
Abstract
Emscripten is a complete open source compiler toolchain. It compiles C/C++ code into WebAssembly/JavaScript executables, for use in JavaScript runtimes, including browsers and Node.js. The Rust language also maintains an Emscripten target.
This PEP formalizes the addition of Tier 3 for Emscripten support in Python 3.14 which was approved by the Steering Council on October 25, 2024. The goals are:
- To describe the current state of the CPython Emscripten runtime
- To describe the current state of the Pyodide runtime
- To identify minor features to be upstreamed from the Pyodide runtime into the CPython Emscripten runtime
The minor features identified here are all features that could be implemented without a PEP. We discuss more significant runtime features that we would like to implement but we defer decisions on those features to subsequent PEPs.
Motivation
A web browser is a universal computing platform, available on Windows, macOS, Linux, and every smartphone.
The Pyodide project has supported Emscripten Python since 2018. Hundreds of thousands of students have learned Python through Pyodide via projects like Capytale and PyodideU. Pyodide is also increasingly being used by Python packages to provide interactive documentation. This demonstrates both the importance and the maturity of the Emscripten platform.
Emscripten and WASI are also the only supported platforms that offer any meaningful sandboxing.
Emscripten Platform Information
“Pyodide” vs “Emscripten Python”
For the sake of this document, we use the term “Emscripten Python” to refer to
the Emscripten Python maintained in the python/cpython repository, without
any downstream additions. We contrast the features present in Emscripten Python
to the features present in Pyodide.
Pyodide is maintained on GitHub and distributed via jsDelivr, npm, and GitHub releases.
Emscripten Python is not distributed, but it is possible to build by following the instructions in the devguide
Background on Emscripten
Emscripten consists of a C and C++ compiler and linker based on LLVM, together with a runtime based on a mildly patched musl libc.
Emscripten is a POSIX-based platform. It uses the WebAssembly binary format, and the WebAssembly dynamic linking section.
The emcc compiler is a wrapper around clang. The emcc linker is a
wrapper around wasm-ld (also part of the LLVM toolchain).
Emscripten support for portable C/C++ code source compatibility with Linux is fairly comprehensive, with certain expected exceptions to be spelled out. CPython already supports compilation to Emscripten, and it only requires a very modest number of modifications to the normal Linux target.
POSIX Compliance
Emscripten is a POSIX platform. However, there are POSIX APIs that exist but
always fail when called and POSIX APIs that don’t exist at all. In particular,
there are problems with networking APIs and blocking I/O, and there is no
support for fork(). See Emscripten Portability Guidelines.
Emscripten executables can be linked with threading support, but it comes with several limitations:
- Enabling threading requires websites to be served with special security headers that indicate acceptance of the possibility of Spectre-style information leakage. These headers are a usability hazard for users who are not intimately familiar with the web platform.
- If an executable is linked with both threading and a dynamic loader, Emscripten prints a warning that using dynamic loading and pthreads together is experimental. It may cause performance problems or crashes. These problems may require WebAssembly standards work to resolve.
Because of these limitations, Pyodide standardizes a no-pthreads build of Python. If there is sufficient demand, a pthreads build with no dynamic loader could be added later.
Development Tools
Emscripten development tools are equally well supported on Linux, Windows, and macOS. The upstream tools include:
- The Emscripten Software Developer Kit (emsdk) which can be used to install the Emscripten compiler toolchain (emcc).
- emcc is a C and C++ compiler, linker, and a sysroot with headers for the system libraries. The system libraries themselves are generated on the fly based on the ABI requested.
- Node.js can be used as an “emulator” to run Emscripten programs from the command line. This emulation behaves best on Linux with macOS as a runner up. Node.js is the most convenient way to test Emscripten programs.
- It is possible to run Emscripten programs inside of any web browser. Browser automation tools like Selenium, Playwright, or Puppeteer can be used to test features that are browser-only.
Pyodide’s tools:
pyodide buildcan be used to cross compile Python packages to run on Emscripten. Cross compilation works best on Linux, there is experimental support on macOS, and it is entirely unsupported on Windows.pyodide venvcan make a virtual environment that runs in Pyodide.pytest-pyodidecan test Python code against various JavaScript runtimes.
cibuildwheel supports building wheels to target Emscripten using pyodide build.
In the short term, Pyodide’s packaging tooling will stay in the Pyodide
repository. It is an open question where Pyodide’s packaging tooling should live
in the long term. Two sensible options would be for it to remain under the
pyodide organization or be moved into the pypa organization on GitHub.
Emscripten Application Lifecycle
An Emscripten “binary” consists of a pair of files, an .mjs file and a
.wasm file. The .wasm file contains all of the compiled C/C++/Rust code.
The .mjs file contains the lifecycle code to set up the runtime, locate the
.wasm file, compile it, instantiate it, call the main() function, and to
shut down the runtime on exit. It also includes an implementation for all of the
system calls, including the file system, the dynamic loader, and any logic to
expose additional functionality from the JavaScript runtime to C code.
The .mjs file exports a single bootstrapEmscriptenExecutable()
JavaScript function that bootstraps the runtime, calls the main() function,
and returns an API object that can be used to call C functions. Each time it is
called produces a complete and independent copy of the runtime with its own
separate address space.
The bootstrapEmscriptenExecutable() takes a large number of runtime
settings. The full list is described in the Emscripten documentation here. The most
important of these are as follows:
thisProgram: The value ofargv[0]. In Python, this makes its way intosys.executable.arguments: The list of string arguments to be passed tomain().preRun: A list of callbacks which are invoked after the JavaScript runtime and file system have been bootstrapped but before callingmain(). Useful to set up the file system, environment variables, and standard streams.print/printErr: Initial handlers for stdout and stderr. They are line buffered and performing aflush()of a partial line forces an extra new line. If tty-like behavior is desired, the standard stream devices should be replaced in apreRun()hook.onExit: A handler that is called when the runtime exits.instantiateWasm: A callback that is called to instantiate the WebAssembly module. Overriding the WebAssembly instantiation procedure via this function is useful when you have other custom asynchronous startup actions or downloads that can be performed in parallel to WebAssembly compilation. Implementing this callback allows performing all of these in parallel.
File System Setup
The Standard Library
In order for Python to run, it needs access to the standard library in the Emscripten file system. There are several possible approaches to this:
- The Emscripten linker has a
--preload-fileflag that will automatically handle loading files. Information about how it works is available here. This is the simplest approach, but Pyodide has moved away from it because it embeds the files into a custom archive format that cannot be processed with standard tooling. - For Node.js, use the NODEFS to mount a native directory with the files into the Emscripten file system. This is the most efficient option but is Node only. It is closely analogous to what WASI does.
- Put the standard library into a zip archive and use
ZipImporter. Using an uncompressed zip file allows the web server and client to apply better compression to the standard library itself. It also uses the more efficient native decompression algorithms of the browser rather than less efficient WebAssembly decompression. The disadvantages of this are a higher memory footprint and breakinginspect& various tests that do not expect the standard library to be packaged in this way. - Put the standard library into an uncompressed tar archive and mount it into a TARFS read only file system backed by the tar file. This has the best memory usage, runtime performance, and transfer size of the options that can be used in the browser. The disadvantage is that Emscripten does not itself include a TARFS so it requires a downstream implementation.
Pyodide uses the ZipImporter approach in every runtime. Python uses the
NODEFS approach when run with node and the ZipImporter approach for the web
example. We will continue with this approach.
The ZipImporter provides a clean resolution for a bootstrapping problem: the
Python runtime is capable of unpacking a wide variety of archive formats, but
the Python runtime is not ready to use until the standard library is already
available. Since zipimport.py is a frozen module, it avoids these problems.
All of the other approaches solve the bootstrapping problem by setting up the
standard library using JavaScript.
Third-party packages
It is also necessary to make any needed packages available in the Emscripten file system. Currently Emscripten CPython has no support for packages. Pyodide uses two different approaches for packages:
- In the browser, Pyodide downloads and unpacks wheels into the MEMFS site-packages directory. It then preloads all dynamic libraries in the wheel. The work of downloading and installing all the packages is redone every time the runtime starts.
- The Pyodide
pythonCLI entrypoint mounts all of the host file system as NODEFS directories before it bootstraps Python. This allows the normal virtual environment mechanism to work. Pyodide virtual environments contain a patched copy of pip and a custompip.confso that pip will install Pyodide wheels. On startup the PyodidepythonCLI will preload all Emscripten dynamic libraries that are in the site-packages directory.
Console and Interactive Usage
stdin defaults to always returning EOF, while stdout and stderr
default to calling console.log and console.error respectively. It is
possible to pass handlers to bootstrapEmscriptenExecutable() to configure
the standard streams, but no matter what the I/O devices have undesirable line
buffering behavior that forces a new line when flushed. To implement a well
behaved TTY in-browser, it is necessary to remove the default I/O devices and
replace them in a preRun hook.
Making stdin work correctly in the browser poses an additional challenge
because it is not allowed to block for user input in the main thread of the
browser. If Emscripten is run in a web worker and served with the shared memory
headers, it is possible to receive input using shared memory and atomics. It is
also possible for a stdin device to block in a simpler and more efficient
manner using stack switching using the experimental JavaScript Promise
Integration API.
Pyodide replaces the standard I/O devices in order to fix the line buffering
behavior. When Pyodide is run in Node.js, stdin, stdout, and stderr are
by default connected to process.stdin, process.stdout, and
process.stderr and so the standard streams work as a tty out of the box.
Pyodide also ensures that shutil.get_terminal_size returns results
consistent with process.stdout.rows and process.stdout.columns. Pyodide
currently has no support for stack switching stdin.
Currently, the Emscripten Python Node.js runner uses the default I/O that
Emscripten provides. The web example uses Atomics for stdin and has
custom stdout and stderr handlers, but they exhibit the undesirable line
buffering behavior. We will upstream the standard streams behaviors from
Pyodide.
In the long term, we hope to implement stack switching stdin devices, but
that is out of scope for this PEP.
Traps and Uncaught Exceptions
We consider the C runtime state to be corrupted if there is a WebAssembly trap, an unhandled JavaScript exception, or an uncaught WebAssembly throw instruction.
Unlike in other platforms, there is no operating system to shut down the executable when there is a trap or other unrecoverable corruption of the libc runtime. We need to provide our own code to print tracebacks, dump the memory, or do whatever else is helpful for debugging a crash. If we expose a JavaScript API, we also must ensure that it is disabled after an unrecoverable crash to prevent downstream users from observing the Python runtime in an inconsistent state.
In order to detect fatal errors, Pyodide uses the following approach: all
fallable calls from WebAssembly into JavaScript are wrapped with a JavaScript
try/catch block. Any caught JavaScript exceptions are translated into Python
exceptions. This ensures that any recoverable JavaScript error is caught before
it unwinds through any WebAssembly frames. All entrypoints to WebAssembly are
also wrapped with JavaScript try/catch blocks. Any exceptions caught there have
unwound WebAssembly frames and are thus considered to be fatal errors (though
there is a special case to handle exit()). This requires foundational
integration with the Python/JavaScript foreign function interface.
When the Pyodide runtime catches a fatal exception, it introspects the error to
determine whether it came from a trap, a logic error in a system call, a
setjmp() without a longjmp(), or a libcxxabi call to __cxa_throw()
(an uncaught C++ exception or Rust panic). We render as informative an error
message as we can. We also call _Py_DumpTraceback() so we can display a
Python traceback in addition to the JS/WebAssembly traceback. It also disables
the JavaScript API so that further attempts to call into Python result in an
error saying that the runtime has fatally failed.
Normally, WebAssembly symbols are stripped so the WebAssembly frames are not very useful. Compiling and linking with