summaryrefslogtreecommitdiffstats
path: root/Tools/wasm
diff options
context:
space:
mode:
authorChristian Heimes <christian@python.org>2022-04-05 09:21:11 (GMT)
committerGitHub <noreply@github.com>2022-04-05 09:21:11 (GMT)
commit96e09837fb8031aebe8d823dd19ef664a34bcfad (patch)
tree28f6efee3352292d27370a2aeea27090755bed6f /Tools/wasm
parentfaa12088c179dd896fde713448a7f142f820c1aa (diff)
downloadcpython-96e09837fb8031aebe8d823dd19ef664a34bcfad.zip
cpython-96e09837fb8031aebe8d823dd19ef664a34bcfad.tar.gz
cpython-96e09837fb8031aebe8d823dd19ef664a34bcfad.tar.bz2
bpo-40280: Add limited Emscripten REPL (GH-32284)
Co-authored-by: Katie Bell <katie@katharos.id.au>
Diffstat (limited to 'Tools/wasm')
-rw-r--r--Tools/wasm/README.md59
-rw-r--r--Tools/wasm/python.html245
-rw-r--r--Tools/wasm/python.worker.js87
-rwxr-xr-xTools/wasm/wasm_webserver.py39
4 files changed, 415 insertions, 15 deletions
diff --git a/Tools/wasm/README.md b/Tools/wasm/README.md
index 6b1e7b0..40b82e8 100644
--- a/Tools/wasm/README.md
+++ b/Tools/wasm/README.md
@@ -55,9 +55,13 @@ emrun builddir/emscripten-browser/python.html
or
```shell
-python3 -m http.server
+./Tools/wasm/wasm_webserver.py
```
+and open http://localhost:8000/builddir/emscripten-browser/python.html . This
+directory structure enables the *C/C++ DevTools Support (DWARF)* to load C
+and header files with debug builds.
+
### Cross compile to wasm32-emscripten for node
```
@@ -79,17 +83,17 @@ popd
node --experimental-wasm-threads --experimental-wasm-bulk-memory builddir/emscripten-node/python.js
```
-## wasm32-emscripten limitations and issues
+# wasm32-emscripten limitations and issues
-- Heap and stack are limited.
-- Most stdlib modules with a dependency on external libraries are missing:
- ``ctypes``, ``readline``, ``sqlite3``, ``ssl``, and more.
-- Shared extension modules are not implemented yet. All extension modules
- are statically linked into the main binary.
- The experimental configure option ``--enable-wasm-dynamic-linking`` enables
- dynamic extensions.
-- Processes are not supported. System calls like fork, popen, and subprocess
- fail with ``ENOSYS`` or ``ENOSUP``.
+Emscripten before 3.1.8 has known bugs that can cause memory corruption and
+resource leaks. 3.1.8 contains several fixes for bugs in date and time
+functions.
+
+## Network stack
+
+- Python's socket module does not work with Emscripten's emulated POSIX
+ sockets yet. Network modules like ``asyncio``, ``urllib``, ``selectors``,
+ etc. are not available.
- Only ``AF_INET`` and ``AF_INET6`` with ``SOCK_STREAM`` (TCP) or
``SOCK_DGRAM`` (UDP) are available. ``AF_UNIX`` is not supported.
- ``socketpair`` does not work.
@@ -98,8 +102,21 @@ node --experimental-wasm-threads --experimental-wasm-bulk-memory builddir/emscri
does not resolve to a real IP address. IPv6 is not available.
- The ``select`` module is limited. ``select.select()`` crashes the runtime
due to lack of exectfd support.
+
+## processes, threads, signals
+
+- Processes are not supported. System calls like fork, popen, and subprocess
+ fail with ``ENOSYS`` or ``ENOSUP``.
- Signal support is limited. ``signal.alarm``, ``itimer``, ``sigaction``
are not available or do not work correctly. ``SIGTERM`` exits the runtime.
+- Keyboard interrupt (CTRL+C) handling is not implemented yet.
+- Browser builds cannot start new threads. Node's web workers consume
+ extra file descriptors.
+- Resource-related functions like ``os.nice`` and most functions of the
+ ``resource`` module are not available.
+
+## file system
+
- Most user, group, and permission related function and modules are not
supported or don't work as expected, e.g.``pwd`` module, ``grp`` module,
``os.setgroups``, ``os.chown``, and so on. ``lchown`` and `lchmod`` are
@@ -113,23 +130,35 @@ node --experimental-wasm-threads --experimental-wasm-bulk-memory builddir/emscri
and are disabled.
- Large file support crashes the runtime and is disabled.
- ``mmap`` module is unstable. flush (``msync``) can crash the runtime.
-- Resource-related functions like ``os.nice`` and most functions of the
- ``resource`` module are not available.
+
+## Misc
+
+- Heap memory and stack size are limited. Recursion or extensive memory
+ consumption can crash Python.
+- Most stdlib modules with a dependency on external libraries are missing,
+ e.g. ``ctypes``, ``readline``, ``sqlite3``, ``ssl``, and more.
+- Shared extension modules are not implemented yet. All extension modules
+ are statically linked into the main binary.
+ The experimental configure option ``--enable-wasm-dynamic-linking`` enables
+ dynamic extensions.
- glibc extensions for date and time formatting are not available.
- ``locales`` module is affected by musl libc issues,
[bpo-46390](https://bugs.python.org/issue46390).
- Python's object allocator ``obmalloc`` is disabled by default.
- ``ensurepip`` is not available.
-### wasm32-emscripten in browsers
+## wasm32-emscripten in browsers
+- The interactive shell does not handle copy 'n paste and unicode support
+ well.
- The bundled stdlib is limited. Network-related modules,
distutils, multiprocessing, dbm, tests and similar modules
are not shipped. All other modules are bundled as pre-compiled
``pyc`` files.
- Threading is not supported.
+- In-memory file system (MEMFS) is not persistent and limited.
-### wasm32-emscripten in node
+## wasm32-emscripten in node
Node builds use ``NODERAWFS``, ``USE_PTHREADS`` and ``PROXY_TO_PTHREAD``
linker options.
diff --git a/Tools/wasm/python.html b/Tools/wasm/python.html
new file mode 100644
index 0000000..c8d1748
--- /dev/null
+++ b/Tools/wasm/python.html
@@ -0,0 +1,245 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="author" content="Katie Bell">
+ <meta name="description" content="Simple REPL for Python WASM">
+ <title>wasm-python terminal</title>
+ <link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css" crossorigin/>
+ <style>
+ body {
+ font-family: arial;
+ max-width: 800px;
+ margin: 0 auto
+ }
+ #code {
+ width: 100%;
+ height: 180px;
+ }
+ #info {
+ padding-top: 20px;
+ }
+ .button-container {
+ display: flex;
+ justify-content: end;
+ height: 50px;
+ align-items: center;
+ gap: 10px;
+ }
+ button {
+ padding: 6px 18px;
+ }
+ </style>
+ <script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin></script>
+ <script type="module">
+class WorkerManager {
+ constructor(workerURL, standardIO, readyCallBack) {
+ this.workerURL = workerURL
+ this.worker = null
+ this.standardIO = standardIO
+ this.readyCallBack = readyCallBack
+
+ this.initialiseWorker()
+ }
+
+ async initialiseWorker() {
+ if (!this.worker) {
+ this.worker = new Worker(this.workerURL)
+ this.worker.addEventListener('message', this.handleMessageFromWorker)
+ }
+ }
+
+ async run(options) {
+ this.worker.postMessage({
+ type: 'run',
+ args: options.args || [],
+ files: options.files || {}
+ })
+ }
+
+ handleStdinData(inputValue) {
+ if (this.stdinbuffer && this.stdinbufferInt) {
+ let startingIndex = 1
+ if (this.stdinbufferInt[0] > 0) {
+ startingIndex = this.stdinbufferInt[0]
+ }
+ const data = new TextEncoder().encode(inputValue)
+ data.forEach((value, index) => {
+ this.stdinbufferInt[startingIndex + index] = value
+ })
+
+ this.stdinbufferInt[0] = startingIndex + data.length - 1
+ Atomics.notify(this.stdinbufferInt, 0, 1)
+ }
+ }
+
+ handleMessageFromWorker = (event) => {
+ const type = event.data.type
+ if (type === 'ready') {
+ this.readyCallBack()
+ } else if (type === 'stdout') {
+ this.standardIO.stdout(event.data.stdout)
+ } else if (type === 'stderr') {
+ this.standardIO.stderr(event.data.stderr)
+ } else if (type === 'stdin') {
+ // Leave it to the terminal to decide whether to chunk it into lines
+ // or send characters depending on the use case.
+ this.stdinbuffer = event.data.buffer
+ this.stdinbufferInt = new Int32Array(this.stdinbuffer)
+ this.standardIO.stdin().then((inputValue) => {
+ this.handleStdinData(inputValue)
+ })
+ } else if (type === 'finished') {
+ this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`)
+ }
+ }
+}
+
+class WasmTerminal {
+
+ constructor() {
+ this.input = ''
+ this.resolveInput = null
+ this.activeInput = false
+ this.inputStartCursor = null
+
+ this.xterm = new Terminal(
+ { scrollback: 10000, fontSize: 14, theme: { background: '#1a1c1f' }, cols: 100}
+ );
+
+ this.xterm.onKey((keyEvent) => {
+ // Fix for iOS Keyboard Jumping on space
+ if (keyEvent.key === " ") {
+ keyEvent.domEvent.preventDefault();
+ }
+ });
+
+ this.xterm.onData(this.handleTermData)
+ }
+
+ open(container) {
+ this.xterm.open(container);
+ }
+
+ handleReadComplete(lastChar) {
+ this.resolveInput(this.input + lastChar)
+ this.activeInput = false
+ }
+
+ handleTermData = (data) => {
+ if (!this.activeInput) {
+ return
+ }
+ const ord = data.charCodeAt(0);
+ let ofs;
+
+ // TODO: Handle ANSI escape sequences
+ if (ord === 0x1b) {
+ // Handle special characters
+ } else if (ord < 32 || ord === 0x7f) {
+ switch (data) {
+ case "\r": // ENTER
+ case "\x0a": // CTRL+J
+ case "\x0d": // CTRL+M
+ this.xterm.write('\r\n');
+ this.handleReadComplete('\n');
+ break;
+ case "\x7F": // BACKSPACE
+ case "\x08": // CTRL+H
+ case "\x04": // CTRL+D
+ this.handleCursorErase(true);
+ break;
+ }
+ } else {
+ this.handleCursorInsert(data);
+ }
+ }
+
+ handleCursorInsert(data) {
+ this.input += data;
+ this.xterm.write(data)
+ }
+
+ handleCursorErase() {
+ // Don't delete past the start of input
+ if (this.xterm.buffer.active.cursorX <= this.inputStartCursor) {
+ return
+ }
+ this.input = this.input.slice(0, -1)
+ this.xterm.write('\x1B[D')
+ this.xterm.write('\x1B[P')
+ }
+
+ prompt = async () => {
+ this.activeInput = true
+ // Hack to allow stdout/stderr to finish before we figure out where input starts
+ setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1)
+ return new Promise((resolve, reject) => {
+ this.resolveInput = (value) => {
+ this.input = ''
+ resolve(value)
+ }
+ })
+ }
+
+ clear() {
+ this.xterm.clear();
+ }
+
+ print(message) {
+ const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n");
+ this.xterm.write(normInput);
+ }
+}
+
+const replButton = document.getElementById('repl')
+const clearButton = document.getElementById('clear')
+
+window.onload = () => {
+ const terminal = new WasmTerminal()
+ terminal.open(document.getElementById('terminal'))
+
+ const stdio = {
+ stdout: (s) => { terminal.print(s) },
+ stderr: (s) => { terminal.print(s) },
+ stdin: async () => {
+ return await terminal.prompt()
+ }
+ }
+
+ replButton.addEventListener('click', (e) => {
+ // Need to use "-i -" to force interactive mode.
+ // Looks like isatty always returns false in emscripten
+ pythonWorkerManager.run({args: ['-i', '-'], files: {}})
+ })
+
+ clearButton.addEventListener('click', (e) => {
+ terminal.clear()
+ })
+
+ const readyCallback = () => {
+ replButton.removeAttribute('disabled')
+ clearButton.removeAttribute('disabled')
+ }
+
+ const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback)
+}
+ </script>
+</head>
+<body>
+ <h1>Simple REPL for Python WASM</h1>
+ <div id="terminal"></div>
+ <div class="button-container">
+ <button id="repl" disabled>Start REPL</button>
+ <button id="clear" disabled>Clear</button>
+ </div>
+ <div id="info">
+ The simple REPL provides a limited Python experience in the browser.
+ <a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">
+ Tools/wasm/README.md</a> contains a list of known limitations and
+ issues. Networking, subprocesses, and threading are not available.
+ </div>
+</body>
+</html>
diff --git a/Tools/wasm/python.worker.js b/Tools/wasm/python.worker.js
new file mode 100644
index 0000000..c3a8bdf
--- /dev/null
+++ b/Tools/wasm/python.worker.js
@@ -0,0 +1,87 @@
+class StdinBuffer {
+ constructor() {
+ this.sab = new SharedArrayBuffer(128 * Int32Array.BYTES_PER_ELEMENT)
+ this.buffer = new Int32Array(this.sab)
+ this.readIndex = 1;
+ this.numberOfCharacters = 0;
+ this.sentNull = true
+ }
+
+ prompt() {
+ this.readIndex = 1
+ Atomics.store(this.buffer, 0, -1)
+ postMessage({
+ type: 'stdin',
+ buffer: this.sab
+ })
+ Atomics.wait(this.buffer, 0, -1)
+ this.numberOfCharacters = this.buffer[0]
+ }
+
+ stdin = () => {
+ if (this.numberOfCharacters + 1 === this.readIndex) {
+ if (!this.sentNull) {
+ // Must return null once to indicate we're done for now.
+ this.sentNull = true
+ return null
+ }
+ this.sentNull = false
+ this.prompt()
+ }
+ const char = this.buffer[this.readIndex]
+ this.readIndex += 1
+ // How do I send an EOF??
+ return char
+ }
+}
+
+const stdoutBufSize = 128;
+const stdoutBuf = new Int32Array()
+let index = 0;
+
+const stdout = (charCode) => {
+ if (charCode) {
+ postMessage({
+ type: 'stdout',
+ stdout: String.fromCharCode(charCode),
+ })
+ } else {
+ console.log(typeof charCode, charCode)
+ }
+}
+
+const stderr = (charCode) => {
+ if (charCode) {
+ postMessage({
+ type: 'stderr',
+ stderr: String.fromCharCode(charCode),
+ })
+ } else {
+ console.log(typeof charCode, charCode)
+ }
+}
+
+const stdinBuffer = new StdinBuffer()
+
+var Module = {
+ noInitialRun: true,
+ stdin: stdinBuffer.stdin,
+ stdout: stdout,
+ stderr: stderr,
+ onRuntimeInitialized: () => {
+ postMessage({type: 'ready', stdinBuffer: stdinBuffer.sab})
+ }
+}
+
+onmessage = (event) => {
+ if (event.data.type === 'run') {
+ // TODO: Set up files from event.data.files
+ const ret = callMain(event.data.args)
+ postMessage({
+ type: 'finished',
+ returnCode: ret
+ })
+ }
+}
+
+importScripts('python.js')
diff --git a/Tools/wasm/wasm_webserver.py b/Tools/wasm/wasm_webserver.py
new file mode 100755
index 0000000..ef642bf
--- /dev/null
+++ b/Tools/wasm/wasm_webserver.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+import argparse
+from http import server
+
+parser = argparse.ArgumentParser(
+ description="Start a local webserver with a Python terminal."
+)
+parser.add_argument(
+ "--port", type=int, default=8000, help="port for the http server to listen on"
+)
+parser.add_argument(
+ "--bind", type=str, default="127.0.0.1", help="Bind address (empty for all)"
+)
+
+
+class MyHTTPRequestHandler(server.SimpleHTTPRequestHandler):
+ def end_headers(self):
+ self.send_my_headers()
+ super().end_headers()
+
+ def send_my_headers(self):
+ self.send_header("Cross-Origin-Opener-Policy", "same-origin")
+ self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
+
+
+def main():
+ args = parser.parse_args()
+ if not args.bind:
+ args.bind = None
+
+ server.test(
+ HandlerClass=MyHTTPRequestHandler,
+ protocol="HTTP/1.1",
+ port=args.port,
+ bind=args.bind,
+ )
+
+if __name__ == "__main__":
+ main()