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