]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-124621: Emscripten: Support pyrepl in browser (GH-136931)
authorHood Chatham <roberthoodchatham@gmail.com>
Tue, 22 Jul 2025 10:13:38 +0000 (12:13 +0200)
committerGitHub <noreply@github.com>
Tue, 22 Jul 2025 10:13:38 +0000 (12:13 +0200)
Basic support for pyrepl in Emscripten. Limitations:
* requires JSPI
* no signal handling implemented

As followup work, it would be nice to implement a webworker variant
for when JSPI is not available and proper signal handling.

Because it requires JSPI, it doesn't work in Safari. Firefox requires
setting an experimental flag. All the Chromiums have full support since
May. Until we make it work without JSPI, let's keep the original web_example
around.

Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Éric <merwok@netwok.org>
13 files changed:
Lib/_pyrepl/trace.py
Lib/test/test_fcntl.py
Makefile.pre.in
Misc/NEWS.d/next/Library/2025-07-21-16-10-24.gh-issue-124621.wyoWc1.rst [new file with mode: 0644]
Modules/clinic/posixmodule.c.h
Modules/posixmodule.c
Python/emscripten_syscalls.c
Tools/wasm/emscripten/config.site-wasm32-emscripten
Tools/wasm/emscripten/wasm_assets.py [moved from Tools/wasm/emscripten/web_example/wasm_assets.py with 98% similarity]
Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html [new file with mode: 0644]
Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs [new file with mode: 0644]
configure
configure.ac

index a8eb2433cd3ccee302d123bb86960f460dea20c3..943ee12f964b29627efc0b1a6629b9fa0504d7a6 100644 (file)
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import os
+import sys
 
 # types
 if False:
@@ -12,10 +13,22 @@ if trace_filename := os.environ.get("PYREPL_TRACE"):
     trace_file = open(trace_filename, "a")
 
 
-def trace(line: str, *k: object, **kw: object) -> None:
-    if trace_file is None:
-        return
-    if k or kw:
-        line = line.format(*k, **kw)
-    trace_file.write(line + "\n")
-    trace_file.flush()
+
+if sys.platform == "emscripten":
+    from posix import _emscripten_log
+
+    def trace(line: str, *k: object, **kw: object) -> None:
+        if "PYREPL_TRACE" not in os.environ:
+            return
+        if k or kw:
+            line = line.format(*k, **kw)
+        _emscripten_log(line)
+
+else:
+    def trace(line: str, *k: object, **kw: object) -> None:
+        if trace_file is None:
+            return
+        if k or kw:
+            line = line.format(*k, **kw)
+        trace_file.write(line + "\n")
+        trace_file.flush()
index 7140a7b4f29188d297f5b1d22e62a93090890117..222b69a6d250cd3b398d10d7e9154f9eaec42e46 100644 (file)
@@ -8,7 +8,7 @@ import struct
 import sys
 import unittest
 from test.support import (
-    cpython_only, get_pagesize, is_apple, requires_subprocess, verbose
+    cpython_only, get_pagesize, is_apple, requires_subprocess, verbose, is_emscripten
 )
 from test.support.import_helper import import_module
 from test.support.os_helper import TESTFN, unlink, make_bad_fd
@@ -211,6 +211,7 @@ class TestFcntl(unittest.TestCase):
     @unittest.skipUnless(
         hasattr(fcntl, "F_SETPIPE_SZ") and hasattr(fcntl, "F_GETPIPE_SZ"),
         "F_SETPIPE_SZ and F_GETPIPE_SZ are not available on all platforms.")
+    @unittest.skipIf(is_emscripten, "Emscripten pipefs doesn't support these")
     def test_fcntl_f_pipesize(self):
         test_pipe_r, test_pipe_w = os.pipe()
         try:
@@ -265,12 +266,14 @@ class TestFcntl(unittest.TestCase):
     @unittest.skipUnless(
         hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
         "requires F_SETOWN_EX and F_GETOWN_EX")
+    @unittest.skipIf(is_emscripten, "Emscripten doesn't actually support these")
     def test_fcntl_small_buffer(self):
         self._check_fcntl_not_mutate_len()
 
     @unittest.skipUnless(
         hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
         "requires F_SETOWN_EX and F_GETOWN_EX")
+    @unittest.skipIf(is_emscripten, "Emscripten doesn't actually support these")
     def test_fcntl_large_buffer(self):
         self._check_fcntl_not_mutate_len(2024)
 
index 959ccb891f283cb71b4a13cfcf71c94e0322ee17..fa17f5d7bfc0ac2090fcae6119e616e70717aba8 100644 (file)
@@ -804,7 +804,7 @@ build_wasm: check-clean-src $(BUILDPYTHON) platform sharedmods \
                python-config checksharedmods
 
 .PHONY: build_emscripten
-build_emscripten: build_wasm web_example
+build_emscripten: build_wasm web_example web_example_pyrepl_jspi
 
 # Check that the source is clean when building out of source.
 .PHONY: check-clean-src
@@ -1095,26 +1095,28 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
 
 # wasm32-emscripten browser web example
 
-WEBEX_DIR=$(srcdir)/Tools/wasm/emscripten/web_example/
+EMSCRIPTEN_DIR=$(srcdir)/Tools/wasm/emscripten
+WEBEX_DIR=$(EMSCRIPTEN_DIR)/web_example/
+
+ZIP_STDLIB=python$(VERSION)$(ABI_THREAD).zip
+$(ZIP_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
+           $(EMSCRIPTEN_DIR)/wasm_assets.py \
+           Makefile pybuilddir.txt Modules/Setup.local
+       $(PYTHON_FOR_BUILD) $(EMSCRIPTEN_DIR)/wasm_assets.py \
+           --buildroot . --prefix $(prefix) -o $@
+
 web_example/index.html: $(WEBEX_DIR)/index.html
        @mkdir -p web_example
        @cp $< $@
 
-web_example/python.worker.mjs: $(WEBEX_DIR)/python.worker.mjs
+web_example/server.py: $(WEBEX_DIR)/server.py
        @mkdir -p web_example
        @cp $< $@
 
-web_example/server.py: $(WEBEX_DIR)/server.py
+web_example/$(ZIP_STDLIB): $(ZIP_STDLIB)
        @mkdir -p web_example
        @cp $< $@
 
-WEB_STDLIB=web_example/python$(VERSION)$(ABI_THREAD).zip
-$(WEB_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
-           $(WEBEX_DIR)/wasm_assets.py \
-           Makefile pybuilddir.txt Modules/Setup.local
-       $(PYTHON_FOR_BUILD) $(WEBEX_DIR)/wasm_assets.py \
-           --buildroot . --prefix $(prefix) -o $@
-
 web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
        @if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
                echo "Can only build web_example when target is Emscripten" ;\
@@ -1124,7 +1126,35 @@ web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
        cp python.wasm web_example/python.wasm
 
 .PHONY: web_example
-web_example: web_example/python.mjs web_example/python.worker.mjs web_example/index.html web_example/server.py $(WEB_STDLIB)
+web_example: web_example/python.mjs web_example/index.html web_example/server.py web_example/$(ZIP_STDLIB)
+
+WEBEX2=web_example_pyrepl_jspi
+WEBEX2_DIR=$(EMSCRIPTEN_DIR)/$(WEBEX2)/
+
+$(WEBEX2)/python.mjs $(WEBEX2)/python.wasm: $(BUILDPYTHON)
+       @if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
+               echo "Can only build web_example when target is Emscripten" ;\
+               exit 1 ;\
+       fi
+       @mkdir -p $(WEBEX2)
+       @cp python.mjs $(WEBEX2)/python.mjs
+       @cp python.wasm $(WEBEX2)/python.wasm
+
+$(WEBEX2)/index.html: $(WEBEX2_DIR)/index.html
+       @mkdir -p $(WEBEX2)
+       @cp $< $@
+
+$(WEBEX2)/src.mjs: $(WEBEX2_DIR)/src.mjs
+       @mkdir -p $(WEBEX2)
+       @cp $< $@
+
+$(WEBEX2)/$(ZIP_STDLIB): $(ZIP_STDLIB)
+       @mkdir -p $(WEBEX2)
+       @cp $< $@
+
+.PHONY: web_example_pyrepl_jspi
+web_example_pyrepl_jspi: $(WEBEX2)/python.mjs $(WEBEX2)/index.html $(WEBEX2)/src.mjs $(WEBEX2)/$(ZIP_STDLIB)
+
 
 ############################################################################
 # Header files
diff --git a/Misc/NEWS.d/next/Library/2025-07-21-16-10-24.gh-issue-124621.wyoWc1.rst b/Misc/NEWS.d/next/Library/2025-07-21-16-10-24.gh-issue-124621.wyoWc1.rst
new file mode 100644 (file)
index 0000000..3404918
--- /dev/null
@@ -0,0 +1 @@
+pyrepl now works in Emscripten.
index 65f5f8c9267b6cbf28ed145a31632909da73fc43..0a281cbe6c57a2d19f57cb711fa40a446585fbcd 100644 (file)
@@ -12769,6 +12769,80 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored))
 
 #endif /* defined(__EMSCRIPTEN__) */
 
+#if defined(__EMSCRIPTEN__)
+
+PyDoc_STRVAR(os__emscripten_log__doc__,
+"_emscripten_log($module, /, arg)\n"
+"--\n"
+"\n"
+"Log something to the JS console. Emscripten only.");
+
+#define OS__EMSCRIPTEN_LOG_METHODDEF    \
+    {"_emscripten_log", _PyCFunction_CAST(os__emscripten_log), METH_FASTCALL|METH_KEYWORDS, os__emscripten_log__doc__},
+
+static PyObject *
+os__emscripten_log_impl(PyObject *module, const char *arg);
+
+static PyObject *
+os__emscripten_log(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(arg), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"arg", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "_emscripten_log",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    const char *arg;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!PyUnicode_Check(args[0])) {
+        _PyArg_BadArgument("_emscripten_log", "argument 'arg'", "str", args[0]);
+        goto exit;
+    }
+    Py_ssize_t arg_length;
+    arg = PyUnicode_AsUTF8AndSize(args[0], &arg_length);
+    if (arg == NULL) {
+        goto exit;
+    }
+    if (strlen(arg) != (size_t)arg_length) {
+        PyErr_SetString(PyExc_ValueError, "embedded null character");
+        goto exit;
+    }
+    return_value = os__emscripten_log_impl(module, arg);
+
+exit:
+    return return_value;
+}
+
+#endif /* defined(__EMSCRIPTEN__) */
+
 #ifndef OS_TTYNAME_METHODDEF
     #define OS_TTYNAME_METHODDEF
 #endif /* !defined(OS_TTYNAME_METHODDEF) */
@@ -13440,4 +13514,8 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored))
 #ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF
     #define OS__EMSCRIPTEN_DEBUGGER_METHODDEF
 #endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */
-/*[clinic end generated code: output=6cfddb3b77dc7a40 input=a9049054013a1b77]*/
+
+#ifndef OS__EMSCRIPTEN_LOG_METHODDEF
+    #define OS__EMSCRIPTEN_LOG_METHODDEF
+#endif /* !defined(OS__EMSCRIPTEN_LOG_METHODDEF) */
+/*[clinic end generated code: output=608e9bc5f631f688 input=a9049054013a1b77]*/
index 47eaf5cd428a53e04d6cea05670e928ba560ae69..77622fbc4e80655d65ef719628d9bf00feff157d 100644 (file)
@@ -16971,6 +16971,25 @@ os__emscripten_debugger_impl(PyObject *module)
     emscripten_debugger();
     Py_RETURN_NONE;
 }
+
+EM_JS(void, emscripten_log_impl_js, (const char* arg), {
+    console.warn(UTF8ToString(arg));
+});
+
+/*[clinic input]
+os._emscripten_log
+   arg: str
+
+Log something to the JS console. Emscripten only.
+[clinic start generated code]*/
+
+static PyObject *
+os__emscripten_log_impl(PyObject *module, const char *arg)
+/*[clinic end generated code: output=9749e5e293c42784 input=350aa1f70bc1e905]*/
+{
+    emscripten_log_impl_js(arg);
+    Py_RETURN_NONE;
+}
 #endif /* __EMSCRIPTEN__ */
 
 
@@ -17190,6 +17209,7 @@ static PyMethodDef posix_methods[] = {
     OS__IS_INPUTHOOK_INSTALLED_METHODDEF
     OS__CREATE_ENVIRON_METHODDEF
     OS__EMSCRIPTEN_DEBUGGER_METHODDEF
+    OS__EMSCRIPTEN_LOG_METHODDEF
     {NULL,              NULL}            /* Sentinel */
 };
 
index d3eedad30e36393e58190e0d2fc20f786b0fc65b..404d98d492a655bec0c55869b864ddefd7c1e9c4 100644 (file)
@@ -1,4 +1,5 @@
 #include "emscripten.h"
+#include "stdio.h"
 
 // If we're running in node, report the UID of the user in the native system as
 // the UID of the user. Since the nodefs will report the uid correctly, if we
@@ -40,7 +41,7 @@ int __syscall_umask(int mask) {
 
 #include <wasi/api.h>
 #include <errno.h>
-#undef errno
+#include <fcntl.h>
 
 // Variant of EM_JS that does C preprocessor substitution on the body
 #define EM_JS_MACROS(ret, func_name, args, body...)                            \
@@ -100,7 +101,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
         return;
     }
     const origResolveGlobalSymbol = resolveGlobalSymbol;
-    if (!Module.onExit && globalThis?.process?.exit) {
+    if (ENVIRONMENT_IS_NODE && !Module.onExit) {
         Module.onExit = (code) => process.exit(code);
     }
     // * wrap the main symbol with WebAssembly.promising,
@@ -115,7 +116,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
             orig.sym = (...args) => {
                 (async () => {
                     const ret = await main(...args);
-                    process?.exit?.(ret);
+                    Module.onExit?.(ret);
                 })();
                 _emscripten_exit_with_live_runtime();
             };
@@ -185,7 +186,7 @@ EM_JS_MACROS(__externref_t, __maybe_fd_read_async, (
             if (e.name !== 'ErrnoError') {
                 throw e;
             }
-            return e.errno;
+            return e["errno"];
         }
     })();
 };
@@ -199,16 +200,16 @@ __wasi_errno_t __wasi_fd_read_orig(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
 
 // Take a promise that resolves to __wasi_errno_t and suspend until it resolves,
 // get the output.
-EM_JS(__wasi_errno_t, __block_for_errno, (__externref_t p), {
+EM_JS(int, __block_for_int, (__externref_t p), {
     return p;
 }
 if (WebAssembly.Suspending) {
-    __block_for_errno = new WebAssembly.Suspending(__block_for_errno);
+    __block_for_int = new WebAssembly.Suspending(__block_for_int);
 }
 )
 
 // Replacement for fd_read syscall. Call __maybe_fd_read_async. If it returned
-// null, delegate back to __wasi_fd_read_orig. Otherwise, use __block_for_errno
+// null, delegate back to __wasi_fd_read_orig. Otherwise, use __block_for_int
 // to get the result.
 __wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
                               size_t iovs_len, __wasi_size_t *nread) {
@@ -216,6 +217,103 @@ __wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
   if (__builtin_wasm_ref_is_null_extern(p)) {
     return __wasi_fd_read_orig(fd, iovs, iovs_len, nread);
   }
-  __wasi_errno_t res = __block_for_errno(p);
-  return res;
+  return __block_for_int(p);
+}
+
+#include <poll.h>
+#define POLLFD_FD 0
+#define POLLFD_EVENTS 4
+#define POLLFD_REVENTS 6
+#define POLLFD_SIZE 8
+_Static_assert(offsetof(struct pollfd, fd) == 0, "Unepxected pollfd struct layout");
+_Static_assert(offsetof(struct pollfd, events) == 4, "Unepxected pollfd struct layout");
+_Static_assert(offsetof(struct pollfd, revents) == 6, "Unepxected pollfd struct layout");
+_Static_assert(sizeof(struct pollfd) == 8, "Unepxected pollfd struct layout");
+
+EM_JS_MACROS(__externref_t, __maybe_poll_async, (intptr_t fds, int nfds, int timeout), {
+    if (!WebAssembly.promising) {
+        return null;
+    }
+    return (async function() {
+        try {
+            var nonzero = 0;
+            var promises = [];
+            for (var i = 0; i < nfds; i++) {
+                var pollfd = fds + POLLFD_SIZE * i;
+                var fd = HEAP32[(pollfd + POLLFD_FD)/4];
+                var events = HEAP16[(pollfd + POLLFD_EVENTS)/2];
+                var mask = POLLNVAL;
+                var stream = FS.getStream(fd);
+                if (stream) {
+                    mask = POLLIN | POLLOUT;
+                    if (stream.stream_ops.pollAsync) {
+                        promises.push(stream.stream_ops.pollAsync(stream, timeout).then((mask) => {
+                            mask &= events | POLLERR | POLLHUP;
+                            HEAP16[(pollfd + POLLFD_REVENTS)/2] = mask;
+                            if (mask) {
+                                nonzero ++;
+                            }
+                        }));
+                    } else if (stream.stream_ops.poll) {
+                        var mask = stream.stream_ops.poll(stream, timeout);
+                        mask &= events | POLLERR | POLLHUP;
+                        HEAP16[(pollfd + POLLFD_REVENTS)/2] = mask;
+                        if (mask) {
+                            nonzero ++;
+                        }
+                    }
+                }
+            }
+            await Promise.all(promises);
+            return nonzero;
+        } catch(e) {
+            if (e?.name !== "ErrnoError") throw e;
+            return -e["errno"];
+        }
+    })();
+});
+
+// Bind original poll syscall to syscall_poll_orig().
+int syscall_poll_orig(intptr_t fds, int nfds, int timeout)
+    __attribute__((__import_module__("env"),
+                   __import_name__("__syscall_poll"), __warn_unused_result__));
+
+int __syscall_poll(intptr_t fds, int nfds, int timeout) {
+    __externref_t p = __maybe_poll_async(fds, nfds, timeout);
+    if (__builtin_wasm_ref_is_null_extern(p)) {
+        return syscall_poll_orig(fds, nfds, timeout);
+    }
+    return __block_for_int(p);
+}
+
+#include <sys/ioctl.h>
+
+int syscall_ioctl_orig(int fd, int request, void* varargs)
+    __attribute__((__import_module__("env"),
+                   __import_name__("__syscall_ioctl"), __warn_unused_result__));
+
+int __syscall_ioctl(int fd, int request, void* varargs) {
+    if (request == FIOCLEX || request == FIONCLEX) {
+        return 0;
+    }
+    if (request == FIONBIO) {
+        // Implement FIONBIO via fcntl.
+        // TODO: Upstream this.
+        int flags = fcntl(fd, F_GETFL, 0);
+        int nonblock = **((int**)varargs);
+        if (flags < 0) {
+            return errno;
+        }
+        if (nonblock) {
+            flags |= O_NONBLOCK;
+        } else {
+            flags &= (~O_NONBLOCK);
+        }
+        int res = fcntl(fd, F_SETFL, flags);
+        if (res < 0) {
+            return errno;
+        }
+        return res;
+    }
+    return syscall_ioctl_orig(fd, request, varargs);
 }
index 8c3a338dacb2dc970badf847869b10abeb6a3658..9f98e3f3c3bb1f7607e42b82253246b01afaaea3 100644 (file)
@@ -69,7 +69,6 @@ ac_cv_func_posix_fallocate=no
 
 # Syscalls that resulted in a segfault
 ac_cv_func_utimensat=no
-ac_cv_header_sys_ioctl_h=no
 
 # sockets are supported, but only AF_INET / AF_INET6 in non-blocking mode.
 # Disable AF_UNIX and AF_PACKET support, see socketmodule.h.
similarity index 98%
rename from Tools/wasm/emscripten/web_example/wasm_assets.py
rename to Tools/wasm/emscripten/wasm_assets.py
index deeb9229a4412bcabec6ff0c01bfdc7bfb695c78..b08e7ce1114a4a375d58b0c0f71183466464165f 100755 (executable)
@@ -18,7 +18,7 @@ import zipfile
 from typing import Dict
 
 # source directory
-SRCDIR = pathlib.Path(__file__).parents[4].absolute()
+SRCDIR = pathlib.Path(__file__).parents[3].absolute()
 SRCDIR_LIB = SRCDIR / "Lib"
 
 
@@ -84,7 +84,6 @@ OMIT_MODULE_FILES = {
     "_json": ["json/"],
     "_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"],
     "pyexpat": ["xml/", "xmlrpc/"],
-    "readline": ["rlcompleter.py"],
     "_sqlite3": ["sqlite3/"],
     "_ssl": ["ssl.py"],
     "_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"],
diff --git a/Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html b/Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html
new file mode 100644 (file)
index 0000000..1f72bd2
--- /dev/null
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+    <head>
+        <link
+            rel="stylesheet"
+            href="https://unpkg.com/xterm@4.18.0/css/xterm.css"
+        />
+        <style>
+             body {
+                 background-color: #300a24;
+             }
+
+             .xterm-dom-renderer-owner-1 .xterm-fg-3 {
+                color: #c4a000 !important;
+            }
+
+            .xterm-dom-renderer-owner-1 .xterm-fg-6 {
+                color: #2aa1b3 !important;
+            }
+
+            .xterm-dom-renderer-owner-1 .xterm-fg-12 {
+                color: #1054a6 !important;
+            }
+
+            .xterm-dom-renderer-owner-1 .xterm-fg-13 {
+                color: #a347ba !important;
+            }
+        </style>
+    </head>
+    <body>
+        <div id="terminal"></div>
+        <script type="module" src="src.mjs"></script>
+    </body>
+</html>
diff --git a/Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs b/Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs
new file mode 100644 (file)
index 0000000..5642372
--- /dev/null
@@ -0,0 +1,194 @@
+// Much of this is adapted from here:
+// https://github.com/mame/xterm-pty/blob/main/emscripten-pty.js
+// Thanks to xterm-pty for making this possible!
+
+import createEmscriptenModule from "./python.mjs";
+import { openpty } from "https://unpkg.com/xterm-pty/index.mjs";
+import "https://unpkg.com/@xterm/xterm/lib/xterm.js";
+
+var term = new Terminal();
+term.open(document.getElementById("terminal"));
+const { master, slave: PTY } = openpty();
+term.loadAddon(master);
+globalThis.PTY = PTY;
+
+async function setupStdlib(Module) {
+  const versionInt = Module.HEAPU32[Module._Py_Version >>> 2];
+  const major = (versionInt >>> 24) & 0xff;
+  const minor = (versionInt >>> 16) & 0xff;
+  // Prevent complaints about not finding exec-prefix by making a lib-dynload directory
+  Module.FS.mkdirTree(`/lib/python${major}.${minor}/lib-dynload/`);
+  const resp = await fetch(`python${major}.${minor}.zip`);
+  const stdlibBuffer = await resp.arrayBuffer();
+  Module.FS.writeFile(
+    `/lib/python${major}${minor}.zip`,
+    new Uint8Array(stdlibBuffer),
+    { canOwn: true },
+  );
+}
+
+const tty_ops = {
+  ioctl_tcgets: () => {
+    const termios = PTY.ioctl("TCGETS");
+    const data = {
+      c_iflag: termios.iflag,
+      c_oflag: termios.oflag,
+      c_cflag: termios.cflag,
+      c_lflag: termios.lflag,
+      c_cc: termios.cc,
+    };
+    return data;
+  },
+
+  ioctl_tcsets: (_tty, _optional_actions, data) => {
+    PTY.ioctl("TCSETS", {
+      iflag: data.c_iflag,
+      oflag: data.c_oflag,
+      cflag: data.c_cflag,
+      lflag: data.c_lflag,
+      cc: data.c_cc,
+    });
+    return 0;
+  },
+
+  ioctl_tiocgwinsz: () => PTY.ioctl("TIOCGWINSZ").reverse(),
+
+  get_char: () => {
+    throw new Error("Should not happen");
+  },
+  put_char: () => {
+    throw new Error("Should not happen");
+  },
+
+  fsync: () => {},
+};
+
+const POLLIN = 1;
+const POLLOUT = 4;
+
+const waitResult = {
+  READY: 0,
+  SIGNAL: 1,
+  TIMEOUT: 2,
+};
+
+function onReadable() {
+  var handle;
+  var promise = new Promise((resolve) => {
+    handle = PTY.onReadable(() => resolve(waitResult.READY));
+  });
+  return [promise, handle];
+}
+
+function onSignal() {
+  // TODO: signal handling
+  var handle = { dispose() {} };
+  var promise = new Promise((resolve) => {});
+  return [promise, handle];
+}
+
+function onTimeout(timeout) {
+  var id;
+  var promise = new Promise((resolve) => {
+    if (timeout > 0) {
+      id = setTimeout(resolve, timeout, waitResult.TIMEOUT);
+    }
+  });
+  var handle = {
+    dispose() {
+      if (id) {
+        clearTimeout(id);
+      }
+    },
+  };
+  return [promise, handle];
+}
+
+async function waitForReadable(timeout) {
+  let p1, p2, p3;
+  let h1, h2, h3;
+  try {
+    [p1, h1] = onReadable();
+    [p2, h2] = onTimeout(timeout);
+    [p3, h3] = onSignal();
+    return await Promise.race([p1, p2, p3]);
+  } finally {
+    h1.dispose();
+    h2.dispose();
+    h3.dispose();
+  }
+}
+
+const FIONREAD = 0x541b;
+
+const tty_stream_ops = {
+  async readAsync(stream, buffer, offset, length, pos /* ignored */) {
+    let readBytes = PTY.read(length);
+    if (length && !readBytes.length) {
+      const status = await waitForReadable(-1);
+      if (status === waitResult.READY) {
+        readBytes = PTY.read(length);
+      } else {
+        throw new Error("Not implemented");
+      }
+    }
+    buffer.set(readBytes, offset);
+    return readBytes.length;
+  },
+
+  write: (stream, buffer, offset, length) => {
+    // Note: default `buffer` is for some reason `HEAP8` (signed), while we want unsigned `HEAPU8`.
+    buffer = new Uint8Array(
+      buffer.buffer,
+      buffer.byteOffset,
+      buffer.byteLength,
+    );
+    const toWrite = Array.from(buffer.subarray(offset, offset + length));
+    PTY.write(toWrite);
+    return length;
+  },
+
+  async pollAsync(stream, timeout) {
+    if (!PTY.readable && timeout) {
+      await waitForReadable(timeout);
+    }
+    return (PTY.readable ? POLLIN : 0) | (PTY.writable ? POLLOUT : 0);
+  },
+  ioctl(stream, request, varargs) {
+    if (request === FIONREAD) {
+      const res = PTY.fromLdiscToUpperBuffer.length;
+      Module.HEAPU32[varargs / 4] = res;
+      return 0;
+    }
+    throw new Error("Unimplemented ioctl request");
+  },
+};
+
+async function setupStdio(Module) {
+  Object.assign(Module.TTY.default_tty_ops, tty_ops);
+  Object.assign(Module.TTY.stream_ops, tty_stream_ops);
+}
+
+const emscriptenSettings = {
+  async preRun(Module) {
+    Module.addRunDependency("pre-run");
+    Module.ENV.TERM = "xterm-256color";
+    // Uncomment next line to turn on tracing (messages go to browser console).
+    // Module.ENV.PYREPL_TRACE = "1";
+
+    // Leak module so we can try to show traceback if we crash on startup
+    globalThis.Module = Module;
+    await Promise.all([setupStdlib(Module), setupStdio(Module)]);
+    Module.removeRunDependency("pre-run");
+  },
+};
+
+try {
+  await createEmscriptenModule(emscriptenSettings);
+} catch (e) {
+  // Show JavaScript exception and traceback
+  console.warn(e);
+  // Show Python exception and traceback
+  Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
+  process.exit(1);
+}
index ef47f9b0df73a86de4789eefcb75263d74a2af53..8db2e9c46abba2f25219617f10c48188d25ea226 100755 (executable)
--- a/configure
+++ b/configure
@@ -9603,7 +9603,7 @@ fi
         as_fn_append LDFLAGS_NODIST " -sWASM_BIGINT"
 
         as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"
-    as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"
+    as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY"
     as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"
     as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB"
         as_fn_append LINKFORSHARED " -sTEXTDECODER=2"
@@ -31180,9 +31180,7 @@ case $ac_sys_system in #(
 
 
 
-    py_cv_module_fcntl=n/a
     py_cv_module_readline=n/a
-    py_cv_module_termios=n/a
     py_cv_module_=n/a
 
    ;; #(
index 23ed9cd35bc94b65967967d155b59cb2ee8c7f1d..c839dd65a5fc5a512b4afacea3c6ef4074bba422 100644 (file)
@@ -2335,7 +2335,7 @@ AS_CASE([$ac_sys_system],
 
     dnl Include file system support
     AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"])
-    AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32"])
+    AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY"])
     AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"])
     AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
     dnl Avoid bugs in JS fallback string decoding path
@@ -7768,9 +7768,7 @@ AS_CASE([$ac_sys_system],
     )
     dnl fcntl, readline, and termios are not particularly useful in browsers.
     PY_STDLIB_MOD_SET_NA(
-      [fcntl],
       [readline],
-      [termios],
     )
   ],
   [WASI], [