README.md
1# Python WebAssembly (WASM) build
2
3**WARNING: WASM support is work-in-progress! Lots of features are not working yet.**
4
5This directory contains configuration and helpers to facilitate cross
6compilation of CPython to WebAssembly (WASM). Python supports Emscripten
7(*wasm32-emscripten*) and WASI (*wasm32-wasi*) targets. Emscripten builds
8run in modern browsers and JavaScript runtimes like *Node.js*. WASI builds
9use WASM runtimes such as *wasmtime*.
10
11Users and developers are encouraged to use the script
12`Tools/wasm/wasm_build.py`. The tool automates the build process and provides
13assistance with installation of SDKs.
14
15## wasm32-emscripten build
16
17For now the build system has two target flavors. The ``Emscripten/browser``
18target (``--with-emscripten-target=browser``) is optimized for browsers.
19It comes with a reduced and preloaded stdlib without tests and threading
20support. The ``Emscripten/node`` target has threading enabled and can
21access the file system directly.
22
23Cross compiling to the wasm32-emscripten platform needs the
24[Emscripten](https://emscripten.org/) SDK and a build Python interpreter.
25Emscripten 3.1.19 or newer are recommended. All commands below are relative
26to a repository checkout.
27
28Christian Heimes maintains a container image with Emscripten SDK, Python
29build dependencies, WASI-SDK, wasmtime, and several additional tools.
30
31From within your local CPython repo clone, run one of the following commands:
32
33```
34# Fedora, RHEL, CentOS
35podman run --rm -ti -v $(pwd):/python-wasm/cpython:Z -w /python-wasm/cpython quay.io/tiran/cpythonbuild:emsdk3
36
37# other
38docker run --rm -ti -v $(pwd):/python-wasm/cpython -w /python-wasm/cpython quay.io/tiran/cpythonbuild:emsdk3
39```
40
41### Compile a build Python interpreter
42
43From within the container, run the following command:
44
45```shell
46./Tools/wasm/wasm_build.py build
47```
48
49The command is roughly equivalent to:
50
51```shell
52mkdir -p builddir/build
53pushd builddir/build
54../../configure -C
55make -j$(nproc)
56popd
57```
58
59### Cross-compile to wasm32-emscripten for browser
60
61```shell
62./Tools/wasm/wasm_build.py emscripten-browser
63```
64
65The command is roughly equivalent to:
66
67```shell
68mkdir -p builddir/emscripten-browser
69pushd builddir/emscripten-browser
70
71CONFIG_SITE=../../Tools/wasm/config.site-wasm32-emscripten \
72 emconfigure ../../configure -C \
73 --host=wasm32-unknown-emscripten \
74 --build=$(../../config.guess) \
75 --with-emscripten-target=browser \
76 --with-build-python=$(pwd)/../build/python
77
78emmake make -j$(nproc)
79popd
80```
81
82Serve `python.html` with a local webserver and open the file in a browser.
83Python comes with a minimal web server script that sets necessary HTTP
84headers like COOP, COEP, and mimetypes. Run the script outside the container
85and from the root of the CPython checkout.
86
87```shell
88./Tools/wasm/wasm_webserver.py
89```
90
91and open http://localhost:8000/builddir/emscripten-browser/python.html . This
92directory structure enables the *C/C++ DevTools Support (DWARF)* to load C
93and header files with debug builds.
94
95
96### Cross compile to wasm32-emscripten for node
97
98```shell
99./Tools/wasm/wasm_build.py emscripten-browser-dl
100```
101
102The command is roughly equivalent to:
103
104```shell
105mkdir -p builddir/emscripten-node-dl
106pushd builddir/emscripten-node-dl
107
108CONFIG_SITE=../../Tools/wasm/config.site-wasm32-emscripten \
109 emconfigure ../../configure -C \
110 --host=wasm32-unknown-emscripten \
111 --build=$(../../config.guess) \
112 --with-emscripten-target=node \
113 --enable-wasm-dynamic-linking \
114 --with-build-python=$(pwd)/../build/python
115
116emmake make -j$(nproc)
117popd
118```
119
120```shell
121node --experimental-wasm-threads --experimental-wasm-bulk-memory --experimental-wasm-bigint builddir/emscripten-node-dl/python.js
122```
123
124(``--experimental-wasm-bigint`` is not needed with recent NodeJS versions)
125
126# wasm32-emscripten limitations and issues
127
128Emscripten before 3.1.8 has known bugs that can cause memory corruption and
129resource leaks. 3.1.8 contains several fixes for bugs in date and time
130functions.
131
132## Network stack
133
134- Python's socket module does not work with Emscripten's emulated POSIX
135 sockets yet. Network modules like ``asyncio``, ``urllib``, ``selectors``,
136 etc. are not available.
137- Only ``AF_INET`` and ``AF_INET6`` with ``SOCK_STREAM`` (TCP) or
138 ``SOCK_DGRAM`` (UDP) are available. ``AF_UNIX`` is not supported.
139- ``socketpair`` does not work.
140- Blocking sockets are not available and non-blocking sockets don't work
141 correctly, e.g. ``socket.accept`` crashes the runtime. ``gethostbyname``
142 does not resolve to a real IP address. IPv6 is not available.
143- The ``select`` module is limited. ``select.select()`` crashes the runtime
144 due to lack of exectfd support.
145
146## processes, signals
147
148- Processes are not supported. System calls like fork, popen, and subprocess
149 fail with ``ENOSYS`` or ``ENOSUP``.
150- Signal support is limited. ``signal.alarm``, ``itimer``, ``sigaction``
151 are not available or do not work correctly. ``SIGTERM`` exits the runtime.
152- Keyboard interrupt (CTRL+C) handling is not implemented yet.
153- Resource-related functions like ``os.nice`` and most functions of the
154 ``resource`` module are not available.
155
156## threading
157
158- Threading is disabled by default. The ``configure`` option
159 ``--enable-wasm-pthreads`` adds compiler flag ``-pthread`` and
160 linker flags ``-sUSE_PTHREADS -sPROXY_TO_PTHREAD``.
161- pthread support requires WASM threads and SharedArrayBuffer (bulk memory).
162 The Node.JS runtime keeps a pool of web workers around. Each web worker
163 uses several file descriptors (eventfd, epoll, pipe).
164- It's not advised to enable threading when building for browsers or with
165 dynamic linking support; there are performance and stability issues.
166
167## file system
168
169- Most user, group, and permission related function and modules are not
170 supported or don't work as expected, e.g.``pwd`` module, ``grp`` module,
171 ``os.setgroups``, ``os.chown``, and so on. ``lchown`` and `lchmod`` are
172 not available.
173- ``umask`` is a no-op.
174- hard links (``os.link``) are not supported.
175- Offset and iovec I/O functions (e.g. ``os.pread``, ``os.preadv``) are not
176 available.
177- ``os.mknod`` and ``os.mkfifo``
178 [don't work](https://github.com/emscripten-core/emscripten/issues/16158)
179 and are disabled.
180- Large file support crashes the runtime and is disabled.
181- ``mmap`` module is unstable. flush (``msync``) can crash the runtime.
182
183## Misc
184
185- Heap memory and stack size are limited. Recursion or extensive memory
186 consumption can crash Python.
187- Most stdlib modules with a dependency on external libraries are missing,
188 e.g. ``ctypes``, ``readline``, ``ssl``, and more.
189- Shared extension modules are not implemented yet. All extension modules
190 are statically linked into the main binary. The experimental configure
191 option ``--enable-wasm-dynamic-linking`` enables dynamic extensions
192 supports. It's currently known to crash in combination with threading.
193- glibc extensions for date and time formatting are not available.
194- ``locales`` module is affected by musl libc issues,
195 [bpo-46390](https://bugs.python.org/issue46390).
196- Python's object allocator ``obmalloc`` is disabled by default.
197- ``ensurepip`` is not available.
198- Some ``ctypes`` features like ``c_longlong`` and ``c_longdouble`` may need
199 NodeJS option ``--experimental-wasm-bigint``.
200
201## wasm32-emscripten in browsers
202
203- The interactive shell does not handle copy 'n paste and unicode support
204 well.
205- The bundled stdlib is limited. Network-related modules,
206 distutils, multiprocessing, dbm, tests and similar modules
207 are not shipped. All other modules are bundled as pre-compiled
208 ``pyc`` files.
209- In-memory file system (MEMFS) is not persistent and limited.
210- Test modules are disabled by default. Use ``--enable-test-modules`` build
211 test modules like ``_testcapi``.
212
213## wasm32-emscripten in node
214
215Node builds use ``NODERAWFS``.
216
217- Node RawFS allows direct access to the host file system without need to
218 perform ``FS.mount()`` call.
219
220## wasm64-emscripten
221
222- wasm64 requires recent NodeJS and ``--experimental-wasm-memory64``.
223- ``EM_JS`` functions must return ``BigInt()``.
224- ``Py_BuildValue()`` format strings must match size of types. Confusing 32
225 and 64 bits types leads to memory corruption, see
226 [gh-95876](https://github.com/python/cpython/issues/95876) and
227 [gh-95878](https://github.com/python/cpython/issues/95878).
228
229# Hosting Python WASM builds
230
231The simple REPL terminal uses SharedArrayBuffer. For security reasons
232browsers only provide the feature in secure environents with cross-origin
233isolation. The webserver must send cross-origin headers and correct MIME types
234for the JavaScript and WASM files. Otherwise the terminal will fail to load
235with an error message like ``Browsers disable shared array buffer``.
236
237## Apache HTTP .htaccess
238
239Place a ``.htaccess`` file in the same directory as ``python.wasm``.
240
241```
242# .htaccess
243Header set Cross-Origin-Opener-Policy same-origin
244Header set Cross-Origin-Embedder-Policy require-corp
245
246AddType application/javascript js
247AddType application/wasm wasm
248
249<IfModule mod_deflate.c>
250 AddOutputFilterByType DEFLATE text/html application/javascript application/wasm
251</IfModule>
252```
253
254# WASI (wasm32-wasi)
255
256WASI builds require [WASI SDK](https://github.com/WebAssembly/wasi-sdk) 16.0+.
257
258## Cross-compile to wasm32-wasi
259
260The script ``wasi-env`` sets necessary compiler and linker flags as well as
261``pkg-config`` overrides. The script assumes that WASI-SDK is installed in
262``/opt/wasi-sdk`` or ``$WASI_SDK_PATH``.
263
264```shell
265./Tools/wasm/wasm_build.py wasi
266```
267
268The command is roughly equivalent to:
269
270```shell
271mkdir -p builddir/wasi
272pushd builddir/wasi
273
274CONFIG_SITE=../../Tools/wasm/config.site-wasm32-wasi \
275 ../../Tools/wasm/wasi-env ../../configure -C \
276 --host=wasm32-unknown-wasi \
277 --build=$(../../config.guess) \
278 --with-build-python=$(pwd)/../build/python
279
280make -j$(nproc)
281popd
282```
283
284## WASI limitations and issues (WASI SDK 15.0)
285
286A lot of Emscripten limitations also apply to WASI. Noticable restrictions
287are:
288
289- Call stack size is limited. Default recursion limit and parser stack size
290 are smaller than in regular Python builds.
291- ``socket(2)`` cannot create new socket file descriptors. WASI programs can
292 call read/write/accept on a file descriptor that is passed into the process.
293- ``socket.gethostname()`` and host name resolution APIs like
294 ``socket.gethostbyname()`` are not implemented and always fail.
295- ``open(2)`` checks flags more strictly. Caller must pass either
296 ``O_RDONLY``, ``O_RDWR``, or ``O_WDONLY`` to ``os.open``. Directory file
297 descriptors must be created with flags ``O_RDONLY | O_DIRECTORY``.
298- ``chmod(2)`` is not available. It's not possible to modify file permissions,
299 yet. A future version of WASI may provide a limited ``set_permissions`` API.
300- User/group related features like ``os.chown()``, ``os.getuid``, etc. are
301 stubs or fail with ``ENOTSUP``.
302- File locking (``fcntl``) is not available.
303- ``os.pipe()``, ``os.mkfifo()``, and ``os.mknod()`` are not supported.
304- ``process_time`` does not work as expected because it's implemented using
305 wall clock.
306- ``os.umask()`` is a stub.
307- ``sys.executable`` is empty.
308- ``/dev/null`` / ``os.devnull`` may not be available.
309- ``os.utime*()`` is buggy in WASM SDK 15.0, see
310 [utimensat() with timespec=NULL sets wrong time](https://github.com/bytecodealliance/wasmtime/issues/4184)
311- ``os.symlink()`` fails with ``PermissionError`` when attempting to create a
312 symlink with an absolute path with wasmtime 0.36.0. The wasmtime runtime
313 uses ``openat2(2)`` syscall with flag ``RESOLVE_BENEATH`` to open files.
314 The flag causes the syscall to reject symlinks with absolute paths.
315- ``os.curdir`` (aka ``.``) seems to behave differently, which breaks some
316 ``importlib`` tests that add ``.`` to ``sys.path`` and indirectly
317 ``sys.path_importer_cache``.
318- WASI runtime environments may not provide a dedicated temp directory.
319
320
321# Detect WebAssembly builds
322
323## Python code
324
325```python
326import os, sys
327
328if sys.platform == "emscripten":
329 # Python on Emscripten
330if sys.platform == "wasi":
331 # Python on WASI
332
333if os.name == "posix":
334 # WASM platforms identify as POSIX-like.
335 # Windows does not provide os.uname().
336 machine = os.uname().machine
337 if machine.startswith("wasm"):
338 # WebAssembly (wasm32, wasm64 in the future)
339```
340
341```python
342>>> import os, sys
343>>> os.uname()
344posix.uname_result(
345 sysname='Emscripten',
346 nodename='emscripten',
347 release='3.1.19',
348 version='#1',
349 machine='wasm32'
350)
351>>> os.name
352'posix'
353>>> sys.platform
354'emscripten'
355>>> sys._emscripten_info
356sys._emscripten_info(
357 emscripten_version=(3, 1, 10),
358 runtime='Mozilla/5.0 (X11; Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0',
359 pthreads=False,
360 shared_memory=False
361)
362```
363
364```python
365>>> sys._emscripten_info
366sys._emscripten_info(
367 emscripten_version=(3, 1, 19),
368 runtime='Node.js v14.18.2',
369 pthreads=True,
370 shared_memory=True
371)
372```
373
374```python
375>>> import os, sys
376>>> os.uname()
377posix.uname_result(
378 sysname='wasi',
379 nodename='(none)',
380 release='0.0.0',
381 version='0.0.0',
382 machine='wasm32'
383)
384>>> os.name
385'posix'
386>>> sys.platform
387'wasi'
388```
389
390## C code
391
392Emscripten SDK and WASI SDK define several built-in macros. You can dump a
393full list of built-ins with ``emcc -dM -E - < /dev/null`` and
394``/path/to/wasi-sdk/bin/clang -dM -E - < /dev/null``.
395
396```C
397#ifdef __EMSCRIPTEN__
398 // Python on Emscripten
399#endif
400```
401
402* WebAssembly ``__wasm__`` (also ``__wasm``)
403* wasm32 ``__wasm32__`` (also ``__wasm32``)
404* wasm64 ``__wasm64__``
405* Emscripten ``__EMSCRIPTEN__`` (also ``EMSCRIPTEN``)
406* Emscripten version ``__EMSCRIPTEN_major__``, ``__EMSCRIPTEN_minor__``, ``__EMSCRIPTEN_tiny__``
407* WASI ``__wasi__``
408
409Feature detection flags:
410
411* ``__EMSCRIPTEN_PTHREADS__``
412* ``__EMSCRIPTEN_SHARED_MEMORY__``
413* ``__wasm_simd128__``
414* ``__wasm_sign_ext__``
415* ``__wasm_bulk_memory__``
416* ``__wasm_atomics__``
417* ``__wasm_mutable_globals__``
418
419## Install SDKs and dependencies manually
420
421In some cases (e.g. build bots) you may prefer to install build dependencies
422directly on the system instead of using the container image. Total disk size
423of SDKs and cached libraries is about 1.6 GB.
424
425### Install OS dependencies
426
427```shell
428# Debian/Ubuntu
429apt update
430apt install -y git make xz-utils bzip2 curl python3-minimal ccache
431```
432
433```shell
434# Fedora
435dnf install -y git make xz bzip2 which ccache
436```
437
438### Install [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html)
439
440**NOTE**: Follow the on-screen instructions how to add the SDK to ``PATH``.
441
442```shell
443git clone https://github.com/emscripten-core/emsdk.git /opt/emsdk
444/opt/emsdk/emsdk install latest
445/opt/emsdk/emsdk activate latest
446```
447
448### Optionally: enable ccache for EMSDK
449
450The ``EM_COMPILER_WRAPPER`` must be set after the EMSDK environment is
451sourced. Otherwise the source script removes the environment variable.
452
453```
454. /opt/emsdk/emsdk_env.sh
455EM_COMPILER_WRAPPER=ccache
456```
457
458### Optionally: pre-build and cache static libraries
459
460Emscripten SDK provides static builds of core libraries without PIC
461(position-independent code). Python builds with ``dlopen`` support require
462PIC. To populate the build cache, run:
463
464```shell
465. /opt/emsdk/emsdk_env.sh
466embuilder build zlib bzip2 MINIMAL_PIC
467embuilder build --pic zlib bzip2 MINIMAL_PIC
468```
469
470### Install [WASI-SDK](https://github.com/WebAssembly/wasi-sdk)
471
472**NOTE**: WASI-SDK's clang may show a warning on Fedora:
473``/lib64/libtinfo.so.6: no version information available``,
474[RHBZ#1875587](https://bugzilla.redhat.com/show_bug.cgi?id=1875587). The
475warning can be ignored.
476
477```shell
478export WASI_VERSION=16
479export WASI_VERSION_FULL=${WASI_VERSION}.0
480curl -sSf -L -O https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_VERSION}/wasi-sdk-${WASI_VERSION_FULL}-linux.tar.gz
481mkdir -p /opt/wasi-sdk
482tar --strip-components=1 -C /opt/wasi-sdk -xvf wasi-sdk-${WASI_VERSION_FULL}-linux.tar.gz
483rm -f wasi-sdk-${WASI_VERSION_FULL}-linux.tar.gz
484```
485
486### Install [wasmtime](https://github.com/bytecodealliance/wasmtime) WASI runtime
487
488wasmtime 0.38 or newer is required.
489
490```shell
491curl -sSf -L -o ~/install-wasmtime.sh https://wasmtime.dev/install.sh
492chmod +x ~/install-wasmtime.sh
493~/install-wasmtime.sh --version v0.38.0
494ln -srf -t /usr/local/bin/ ~/.wasmtime/bin/wasmtime
495```
496
497
498### WASI debugging
499
500* ``wasmtime run -g`` generates debugging symbols for gdb and lldb. The
501 feature is currently broken, see
502 https://github.com/bytecodealliance/wasmtime/issues/4669 .
503* The environment variable ``RUST_LOG=wasi_common`` enables debug and
504 trace logging.
505