mirror of
https://github.com/python/cpython.git
synced 2025-10-19 16:03:42 +00:00

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.
(cherry picked from commit c933a6bb32
)
Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Éric <merwok@netwok.org>
319 lines
12 KiB
C
319 lines
12 KiB
C
#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
|
|
// don't make getuid report it correctly too we'll see some permission errors.
|
|
// Normally __syscall_getuid32 is a stub that always returns 0 but it is
|
|
// defined with weak linkage so we can override it.
|
|
EM_JS(int, __syscall_getuid32_js, (void), {
|
|
// If we're in node and we can, report the native uid
|
|
if (ENVIRONMENT_IS_NODE) {
|
|
return process.getuid();
|
|
}
|
|
// Fall back to the stub case of returning 0.
|
|
return 0;
|
|
})
|
|
|
|
int __syscall_getuid32(void) {
|
|
return __syscall_getuid32_js();
|
|
}
|
|
|
|
EM_JS(int, __syscall_umask_js, (int mask), {
|
|
// If we're in node and we can, call native process.umask()
|
|
if (ENVIRONMENT_IS_NODE) {
|
|
try {
|
|
return process.umask(mask);
|
|
} catch(e) {
|
|
// oops...
|
|
// NodeJS docs: "In Worker threads, process.umask(mask) will throw an exception."
|
|
// umask docs: "This system call always succeeds"
|
|
return 0;
|
|
}
|
|
}
|
|
// Fall back to the stub case of returning 0.
|
|
return 0;
|
|
})
|
|
|
|
int __syscall_umask(int mask) {
|
|
return __syscall_umask_js(mask);
|
|
}
|
|
|
|
#include <wasi/api.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
|
|
// Variant of EM_JS that does C preprocessor substitution on the body
|
|
#define EM_JS_MACROS(ret, func_name, args, body...) \
|
|
EM_JS(ret, func_name, args, body)
|
|
|
|
EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
|
|
// Define FS.createAsyncInputDevice(), This is quite similar to
|
|
// FS.createDevice() defined here:
|
|
// https://github.com/emscripten-core/emscripten/blob/4.0.11/src/lib/libfs.js?plain=1#L1642
|
|
// but instead of returning one byte at a time, the input() function should
|
|
// return a Uint8Array. This makes the handler code simpler, the
|
|
// `createAsyncInputDevice` simpler, and everything faster.
|
|
FS.createAsyncInputDevice = function(parent, name, input) {
|
|
parent = typeof parent == 'string' ? parent : FS.getPath(parent);
|
|
var path = PATH.join2(parent, name);
|
|
var mode = FS_getMode(true, false);
|
|
FS.createDevice.major ||= 64;
|
|
var dev = FS.makedev(FS.createDevice.major++, 0);
|
|
async function getDataBuf() {
|
|
var buf;
|
|
try {
|
|
buf = await input();
|
|
} catch (e) {
|
|
throw new FS.ErrnoError(EIO);
|
|
}
|
|
if (!buf?.byteLength) {
|
|
throw new FS.ErrnoError(EAGAIN);
|
|
}
|
|
ops._dataBuf = buf;
|
|
}
|
|
|
|
var ops = {
|
|
_dataBuf: new Uint8Array(0),
|
|
open(stream) {
|
|
stream.seekable = false;
|
|
},
|
|
async readAsync(stream, buffer, offset, length, pos /* ignored */) {
|
|
buffer = buffer.subarray(offset, offset + length);
|
|
if (!ops._dataBuf.byteLength) {
|
|
await getDataBuf();
|
|
}
|
|
var toRead = Math.min(ops._dataBuf.byteLength, buffer.byteLength);
|
|
buffer.subarray(0, toRead).set(ops._dataBuf);
|
|
buffer = buffer.subarray(toRead);
|
|
ops._dataBuf = ops._dataBuf.subarray(toRead);
|
|
if (toRead) {
|
|
stream.node.atime = Date.now();
|
|
}
|
|
return toRead;
|
|
},
|
|
};
|
|
FS.registerDevice(dev, ops);
|
|
return FS.mkdev(path, mode, dev);
|
|
};
|
|
if (!WebAssembly.promising) {
|
|
// No stack switching support =(
|
|
return;
|
|
}
|
|
const origResolveGlobalSymbol = resolveGlobalSymbol;
|
|
if (ENVIRONMENT_IS_NODE && !Module.onExit) {
|
|
Module.onExit = (code) => process.exit(code);
|
|
}
|
|
// * wrap the main symbol with WebAssembly.promising,
|
|
// * call exit_with_live_runtime() to prevent emscripten from shutting down
|
|
// the runtime before the promise resolves,
|
|
// * call onExit / process.exit ourselves, since exit_with_live_runtime()
|
|
// prevented Emscripten from calling it normally.
|
|
resolveGlobalSymbol = function (name, direct = false) {
|
|
const orig = origResolveGlobalSymbol(name, direct);
|
|
if (name === "main") {
|
|
const main = WebAssembly.promising(orig.sym);
|
|
orig.sym = (...args) => {
|
|
(async () => {
|
|
const ret = await main(...args);
|
|
Module.onExit?.(ret);
|
|
})();
|
|
_emscripten_exit_with_live_runtime();
|
|
};
|
|
}
|
|
return orig;
|
|
};
|
|
})
|
|
|
|
__attribute__((constructor)) void _emscripten_promising_main(void) {
|
|
_emscripten_promising_main_js();
|
|
}
|
|
|
|
|
|
#define IOVEC_T_BUF_OFFSET 0
|
|
#define IOVEC_T_BUF_LEN_OFFSET 4
|
|
#define IOVEC_T_SIZE 8
|
|
_Static_assert(offsetof(__wasi_iovec_t, buf) == IOVEC_T_BUF_OFFSET,
|
|
"Unexpected __wasi_iovec_t layout");
|
|
_Static_assert(offsetof(__wasi_iovec_t, buf_len) == IOVEC_T_BUF_LEN_OFFSET,
|
|
"Unexpected __wasi_iovec_t layout");
|
|
_Static_assert(sizeof(__wasi_iovec_t) == IOVEC_T_SIZE,
|
|
"Unexpected __wasi_iovec_t layout");
|
|
|
|
// If the stream has a readAsync handler, read to buffer defined in iovs, write
|
|
// number of bytes read to *nread, and return a promise that resolves to the
|
|
// errno. Otherwise, return null.
|
|
EM_JS_MACROS(__externref_t, __maybe_fd_read_async, (
|
|
__wasi_fd_t fd,
|
|
const __wasi_iovec_t *iovs,
|
|
size_t iovcnt,
|
|
__wasi_size_t *nread
|
|
), {
|
|
if (!WebAssembly.promising) {
|
|
return null;
|
|
}
|
|
var stream;
|
|
try {
|
|
stream = SYSCALLS.getStreamFromFD(fd);
|
|
} catch (e) {
|
|
// If the fd was already closed or never existed, getStreamFromFD()
|
|
// raises. We'll let fd_read_orig() handle setting errno.
|
|
return null;
|
|
}
|
|
if (!stream.stream_ops.readAsync) {
|
|
// Not an async device. Fall back to __wasi_fd_read_orig().
|
|
return null;
|
|
}
|
|
return (async () => {
|
|
// This is the same as libwasi.js fd_read() and doReadv() except we use
|
|
// readAsync and we await it.
|
|
// https://github.com/emscripten-core/emscripten/blob/4.0.11/src/lib/libwasi.js?plain=1#L331
|
|
// https://github.com/emscripten-core/emscripten/blob/4.0.11/src/lib/libwasi.js?plain=1#L197
|
|
try {
|
|
var ret = 0;
|
|
for (var i = 0; i < iovcnt; i++) {
|
|
var ptr = HEAP32[(iovs + IOVEC_T_BUF_OFFSET)/4];
|
|
var len = HEAP32[(iovs + IOVEC_T_BUF_LEN_OFFSET)/4];
|
|
iovs += IOVEC_T_SIZE;
|
|
var curr = await stream.stream_ops.readAsync(stream, HEAP8, ptr, len);
|
|
if (curr < 0) return -1;
|
|
ret += curr;
|
|
if (curr < len) break; // nothing more to read
|
|
}
|
|
HEAP32[nread/4] = ret;
|
|
return 0;
|
|
} catch (e) {
|
|
if (e.name !== 'ErrnoError') {
|
|
throw e;
|
|
}
|
|
return e["errno"];
|
|
}
|
|
})();
|
|
};
|
|
);
|
|
|
|
// Bind original fd_read syscall to __wasi_fd_read_orig().
|
|
__wasi_errno_t __wasi_fd_read_orig(__wasi_fd_t fd, const __wasi_iovec_t *iovs,
|
|
size_t iovs_len, __wasi_size_t *nread)
|
|
__attribute__((__import_module__("wasi_snapshot_preview1"),
|
|
__import_name__("fd_read"), __warn_unused_result__));
|
|
|
|
// Take a promise that resolves to __wasi_errno_t and suspend until it resolves,
|
|
// get the output.
|
|
EM_JS(int, __block_for_int, (__externref_t p), {
|
|
return p;
|
|
}
|
|
if (WebAssembly.Suspending) {
|
|
__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_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) {
|
|
__externref_t p = __maybe_fd_read_async(fd, iovs, iovs_len, nread);
|
|
if (__builtin_wasm_ref_is_null_extern(p)) {
|
|
return __wasi_fd_read_orig(fd, iovs, iovs_len, nread);
|
|
}
|
|
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);
|
|
}
|