mirror of
https://github.com/python/cpython.git
synced 2025-11-11 19:12:05 +00:00
gh-124621: Emscripten: Support pyrepl in browser (GH-136931)
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>
This commit is contained in:
parent
22c8658906
commit
c933a6bb32
13 changed files with 505 additions and 40 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
# types
|
# types
|
||||||
if False:
|
if False:
|
||||||
|
|
@ -12,10 +13,22 @@
|
||||||
trace_file = open(trace_filename, "a")
|
trace_file = open(trace_filename, "a")
|
||||||
|
|
||||||
|
|
||||||
def trace(line: str, *k: object, **kw: object) -> None:
|
|
||||||
if trace_file is None:
|
if sys.platform == "emscripten":
|
||||||
return
|
from posix import _emscripten_log
|
||||||
if k or kw:
|
|
||||||
line = line.format(*k, **kw)
|
def trace(line: str, *k: object, **kw: object) -> None:
|
||||||
trace_file.write(line + "\n")
|
if "PYREPL_TRACE" not in os.environ:
|
||||||
trace_file.flush()
|
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()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from test.support import (
|
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.import_helper import import_module
|
||||||
from test.support.os_helper import TESTFN, unlink, make_bad_fd
|
from test.support.os_helper import TESTFN, unlink, make_bad_fd
|
||||||
|
|
@ -211,6 +211,7 @@ def test_fcntl_f_getpath(self):
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
hasattr(fcntl, "F_SETPIPE_SZ") and hasattr(fcntl, "F_GETPIPE_SZ"),
|
hasattr(fcntl, "F_SETPIPE_SZ") and hasattr(fcntl, "F_GETPIPE_SZ"),
|
||||||
"F_SETPIPE_SZ and F_GETPIPE_SZ are not available on all platforms.")
|
"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):
|
def test_fcntl_f_pipesize(self):
|
||||||
test_pipe_r, test_pipe_w = os.pipe()
|
test_pipe_r, test_pipe_w = os.pipe()
|
||||||
try:
|
try:
|
||||||
|
|
@ -265,12 +266,14 @@ def _check_fcntl_not_mutate_len(self, nbytes=None):
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
|
hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
|
||||||
"requires F_SETOWN_EX and 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):
|
def test_fcntl_small_buffer(self):
|
||||||
self._check_fcntl_not_mutate_len()
|
self._check_fcntl_not_mutate_len()
|
||||||
|
|
||||||
@unittest.skipUnless(
|
@unittest.skipUnless(
|
||||||
hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
|
hasattr(fcntl, "F_SETOWN_EX") and hasattr(fcntl, "F_GETOWN_EX"),
|
||||||
"requires F_SETOWN_EX and 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):
|
def test_fcntl_large_buffer(self):
|
||||||
self._check_fcntl_not_mutate_len(2024)
|
self._check_fcntl_not_mutate_len(2024)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -804,7 +804,7 @@ build_wasm: check-clean-src $(BUILDPYTHON) platform sharedmods \
|
||||||
python-config checksharedmods
|
python-config checksharedmods
|
||||||
|
|
||||||
.PHONY: build_emscripten
|
.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.
|
# Check that the source is clean when building out of source.
|
||||||
.PHONY: check-clean-src
|
.PHONY: check-clean-src
|
||||||
|
|
@ -1095,12 +1095,17 @@ $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS)
|
||||||
|
|
||||||
# wasm32-emscripten browser web example
|
# wasm32-emscripten browser web example
|
||||||
|
|
||||||
WEBEX_DIR=$(srcdir)/Tools/wasm/emscripten/web_example/
|
EMSCRIPTEN_DIR=$(srcdir)/Tools/wasm/emscripten
|
||||||
web_example/index.html: $(WEBEX_DIR)/index.html
|
WEBEX_DIR=$(EMSCRIPTEN_DIR)/web_example/
|
||||||
@mkdir -p web_example
|
|
||||||
@cp $< $@
|
|
||||||
|
|
||||||
web_example/python.worker.mjs: $(WEBEX_DIR)/python.worker.mjs
|
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
|
@mkdir -p web_example
|
||||||
@cp $< $@
|
@cp $< $@
|
||||||
|
|
||||||
|
|
@ -1108,12 +1113,9 @@ web_example/server.py: $(WEBEX_DIR)/server.py
|
||||||
@mkdir -p web_example
|
@mkdir -p web_example
|
||||||
@cp $< $@
|
@cp $< $@
|
||||||
|
|
||||||
WEB_STDLIB=web_example/python$(VERSION)$(ABI_THREAD).zip
|
web_example/$(ZIP_STDLIB): $(ZIP_STDLIB)
|
||||||
$(WEB_STDLIB): $(srcdir)/Lib/*.py $(srcdir)/Lib/*/*.py \
|
@mkdir -p web_example
|
||||||
$(WEBEX_DIR)/wasm_assets.py \
|
@cp $< $@
|
||||||
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)
|
web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
|
||||||
@if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
|
@if test $(HOST_GNU_TYPE) != 'wasm32-unknown-emscripten' ; then \
|
||||||
|
|
@ -1124,7 +1126,35 @@ web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
|
||||||
cp python.wasm web_example/python.wasm
|
cp python.wasm web_example/python.wasm
|
||||||
|
|
||||||
.PHONY: web_example
|
.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
|
# Header files
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
pyrepl now works in Emscripten.
|
||||||
80
Modules/clinic/posixmodule.c.h
generated
80
Modules/clinic/posixmodule.c.h
generated
|
|
@ -12769,6 +12769,80 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored))
|
||||||
|
|
||||||
#endif /* defined(__EMSCRIPTEN__) */
|
#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
|
#ifndef OS_TTYNAME_METHODDEF
|
||||||
#define OS_TTYNAME_METHODDEF
|
#define OS_TTYNAME_METHODDEF
|
||||||
#endif /* !defined(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
|
#ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF
|
||||||
#define OS__EMSCRIPTEN_DEBUGGER_METHODDEF
|
#define OS__EMSCRIPTEN_DEBUGGER_METHODDEF
|
||||||
#endif /* !defined(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]*/
|
||||||
|
|
|
||||||
|
|
@ -16971,6 +16971,25 @@ os__emscripten_debugger_impl(PyObject *module)
|
||||||
emscripten_debugger();
|
emscripten_debugger();
|
||||||
Py_RETURN_NONE;
|
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__ */
|
#endif /* __EMSCRIPTEN__ */
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17190,6 +17209,7 @@ static PyMethodDef posix_methods[] = {
|
||||||
OS__IS_INPUTHOOK_INSTALLED_METHODDEF
|
OS__IS_INPUTHOOK_INSTALLED_METHODDEF
|
||||||
OS__CREATE_ENVIRON_METHODDEF
|
OS__CREATE_ENVIRON_METHODDEF
|
||||||
OS__EMSCRIPTEN_DEBUGGER_METHODDEF
|
OS__EMSCRIPTEN_DEBUGGER_METHODDEF
|
||||||
|
OS__EMSCRIPTEN_LOG_METHODDEF
|
||||||
{NULL, NULL} /* Sentinel */
|
{NULL, NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "emscripten.h"
|
#include "emscripten.h"
|
||||||
|
#include "stdio.h"
|
||||||
|
|
||||||
// If we're running in node, report the UID of the user in the native system as
|
// 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
|
// 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 <wasi/api.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#undef errno
|
#include <fcntl.h>
|
||||||
|
|
||||||
// Variant of EM_JS that does C preprocessor substitution on the body
|
// Variant of EM_JS that does C preprocessor substitution on the body
|
||||||
#define EM_JS_MACROS(ret, func_name, args, body...) \
|
#define EM_JS_MACROS(ret, func_name, args, body...) \
|
||||||
|
|
@ -100,7 +101,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const origResolveGlobalSymbol = resolveGlobalSymbol;
|
const origResolveGlobalSymbol = resolveGlobalSymbol;
|
||||||
if (!Module.onExit && globalThis?.process?.exit) {
|
if (ENVIRONMENT_IS_NODE && !Module.onExit) {
|
||||||
Module.onExit = (code) => process.exit(code);
|
Module.onExit = (code) => process.exit(code);
|
||||||
}
|
}
|
||||||
// * wrap the main symbol with WebAssembly.promising,
|
// * wrap the main symbol with WebAssembly.promising,
|
||||||
|
|
@ -115,7 +116,7 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
|
||||||
orig.sym = (...args) => {
|
orig.sym = (...args) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const ret = await main(...args);
|
const ret = await main(...args);
|
||||||
process?.exit?.(ret);
|
Module.onExit?.(ret);
|
||||||
})();
|
})();
|
||||||
_emscripten_exit_with_live_runtime();
|
_emscripten_exit_with_live_runtime();
|
||||||
};
|
};
|
||||||
|
|
@ -185,7 +186,7 @@ EM_JS_MACROS(__externref_t, __maybe_fd_read_async, (
|
||||||
if (e.name !== 'ErrnoError') {
|
if (e.name !== 'ErrnoError') {
|
||||||
throw e;
|
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,
|
// Take a promise that resolves to __wasi_errno_t and suspend until it resolves,
|
||||||
// get the output.
|
// 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;
|
return p;
|
||||||
}
|
}
|
||||||
if (WebAssembly.Suspending) {
|
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
|
// 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.
|
// to get the result.
|
||||||
__wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
|
__wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
|
||||||
size_t iovs_len, __wasi_size_t *nread) {
|
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)) {
|
if (__builtin_wasm_ref_is_null_extern(p)) {
|
||||||
return __wasi_fd_read_orig(fd, iovs, iovs_len, nread);
|
return __wasi_fd_read_orig(fd, iovs, iovs_len, nread);
|
||||||
}
|
}
|
||||||
__wasi_errno_t res = __block_for_errno(p);
|
return __block_for_int(p);
|
||||||
return res;
|
}
|
||||||
|
|
||||||
|
#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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,6 @@ ac_cv_func_posix_fallocate=no
|
||||||
|
|
||||||
# Syscalls that resulted in a segfault
|
# Syscalls that resulted in a segfault
|
||||||
ac_cv_func_utimensat=no
|
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.
|
# sockets are supported, but only AF_INET / AF_INET6 in non-blocking mode.
|
||||||
# Disable AF_UNIX and AF_PACKET support, see socketmodule.h.
|
# Disable AF_UNIX and AF_PACKET support, see socketmodule.h.
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
# source directory
|
# source directory
|
||||||
SRCDIR = pathlib.Path(__file__).parents[4].absolute()
|
SRCDIR = pathlib.Path(__file__).parents[3].absolute()
|
||||||
SRCDIR_LIB = SRCDIR / "Lib"
|
SRCDIR_LIB = SRCDIR / "Lib"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,7 +84,6 @@
|
||||||
"_json": ["json/"],
|
"_json": ["json/"],
|
||||||
"_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"],
|
"_multiprocessing": ["concurrent/futures/process.py", "multiprocessing/"],
|
||||||
"pyexpat": ["xml/", "xmlrpc/"],
|
"pyexpat": ["xml/", "xmlrpc/"],
|
||||||
"readline": ["rlcompleter.py"],
|
|
||||||
"_sqlite3": ["sqlite3/"],
|
"_sqlite3": ["sqlite3/"],
|
||||||
"_ssl": ["ssl.py"],
|
"_ssl": ["ssl.py"],
|
||||||
"_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"],
|
"_tkinter": ["idlelib/", "tkinter/", "turtle.py", "turtledemo/"],
|
||||||
34
Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html
Normal file
34
Tools/wasm/emscripten/web_example_pyrepl_jspi/index.html
Normal file
|
|
@ -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>
|
||||||
194
Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs
Normal file
194
Tools/wasm/emscripten/web_example_pyrepl_jspi/src.mjs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
4
configure
generated
vendored
4
configure
generated
vendored
|
|
@ -9603,7 +9603,7 @@ fi
|
||||||
as_fn_append LDFLAGS_NODIST " -sWASM_BIGINT"
|
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 " -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 " -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 " -sSTACK_SIZE=5MB"
|
||||||
as_fn_append LINKFORSHARED " -sTEXTDECODER=2"
|
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_readline=n/a
|
||||||
py_cv_module_termios=n/a
|
|
||||||
py_cv_module_=n/a
|
py_cv_module_=n/a
|
||||||
|
|
||||||
;; #(
|
;; #(
|
||||||
|
|
|
||||||
|
|
@ -2335,7 +2335,7 @@ AS_CASE([$ac_sys_system],
|
||||||
|
|
||||||
dnl Include file system support
|
dnl Include file system support
|
||||||
AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"])
|
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], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,__PyEM_EMSCRIPTEN_COUNT_ARGS_OFFSET,_PyGILState_GetThisThreadState,__Py_DumpTraceback"])
|
||||||
AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
|
AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
|
||||||
dnl Avoid bugs in JS fallback string decoding path
|
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.
|
dnl fcntl, readline, and termios are not particularly useful in browsers.
|
||||||
PY_STDLIB_MOD_SET_NA(
|
PY_STDLIB_MOD_SET_NA(
|
||||||
[fcntl],
|
|
||||||
[readline],
|
[readline],
|
||||||
[termios],
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[WASI], [
|
[WASI], [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue