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