Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/reusable-emscripten.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ jobs:
run: python3 Platforms/emscripten run --test
- name: "Test Repl"
run: Platforms/emscripten/browser_test/run_test.sh
- name: "Test embedding"
run: Platforms/emscripten/web_embed_test/run_test.sh
32 changes: 26 additions & 6 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -970,9 +970,12 @@ clinic: check-clean-src
clinic-tests: check-clean-src $(srcdir)/Lib/test/clinic.test.c
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/clinic/clinic.py -f $(srcdir)/Lib/test/clinic.test.c

# Objects linked into the interpreter only, not into libpython.
PLATFORM_MAIN_OBJS= @PLATFORM_MAIN_OBJS@

# Build the interpreter
$(BUILDPYTHON): Programs/python.o $(LINK_PYTHON_DEPS)
$(LINKCC) $(PY_CORE_EXE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(LINK_PYTHON_OBJS) $(LIBS) $(MODLIBS) $(SYSLIBS)
$(BUILDPYTHON): Programs/python.o $(PLATFORM_MAIN_OBJS) $(LINK_PYTHON_DEPS)
$(LINKCC) $(PY_CORE_EXE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(PLATFORM_MAIN_OBJS) $(LINK_PYTHON_OBJS) $(LIBS) $(MODLIBS) $(SYSLIBS)

platform: $(PYTHON_FOR_BUILD_DEPS) pybuilddir.txt
$(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print("%s-%d.%d" % (get_platform(), *sys.version_info[:2]))' >platform
Expand Down Expand Up @@ -1119,6 +1122,20 @@ web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
cp python.mjs web_example/python.mjs
cp python.wasm web_example/python.wasm

WEB_EMBED_TEST_DIR=$(EMSCRIPTEN_DIR)/web_embed_test

# Linked like an embedder would: against libpython.a, without $(LINKFORSHARED)
# and so without -sMAIN_MODULE or the interpreter's -sEXPORTED_FUNCTIONS.
web_embed_test/main.js: $(WEB_EMBED_TEST_DIR)/main.c $(LIBRARY) $(ZIP_STDLIB)
@mkdir -p web_embed_test
$(LINKCC) $(PY_STDMODULE_CFLAGS) $(PY_CORE_EXE_LDFLAGS) -O2 -g0 -o $@ \
$(WEB_EMBED_TEST_DIR)/main.c $(LIBRARY) $(LIBS) $(MODLIBS) $(SYSLIBS) \
-sALLOW_MEMORY_GROWTH -sSTACK_SIZE=5MB \
--preload-file $(ZIP_STDLIB)@/lib/stdlib.zip

.PHONY: web_embed_test
web_embed_test: web_embed_test/main.js

.PHONY: web_example
web_example: web_example/python.mjs web_example/python.worker.mjs web_example/index.html web_example/server.py web_example/$(ZIP_STDLIB)

Expand Down Expand Up @@ -1614,8 +1631,8 @@ regen-re: $(BUILDPYTHON)
# using Tools/build/generate_re_casefix.py
$(RUNSHARED) ./$(BUILDPYTHON) $(srcdir)/Tools/build/generate_re_casefix.py $(srcdir)/Lib/re/_casefix.py

Programs/_testembed: Programs/_testembed.o $(LINK_PYTHON_DEPS)
$(LINKCC) $(PY_CORE_EXE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/_testembed.o $(LINK_PYTHON_OBJS) $(LIBS) $(MODLIBS) $(SYSLIBS)
Programs/_testembed: Programs/_testembed.o $(PLATFORM_MAIN_OBJS) $(LINK_PYTHON_DEPS)
$(LINKCC) $(PY_CORE_EXE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/_testembed.o $(PLATFORM_MAIN_OBJS) $(LINK_PYTHON_OBJS) $(LIBS) $(MODLIBS) $(SYSLIBS)

############################################################################
# "Bootstrap Python" used to run Programs/_freeze_module.py
Expand Down Expand Up @@ -2235,7 +2252,7 @@ regen-slots: Python/slots.toml
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/build/generate_slots.py \
--generate-all

$(LIBRARY_OBJS) $(MODOBJS) Programs/python.o: $(PYTHON_HEADERS)
$(LIBRARY_OBJS) $(MODOBJS) Programs/python.o $(PLATFORM_MAIN_OBJS): $(PYTHON_HEADERS)


######################################################################
Expand Down Expand Up @@ -2961,6 +2978,9 @@ libainstall: all scripts
fi; \
fi; \
$(INSTALL_DATA) Programs/python.o $(DESTDIR)$(LIBPL)/python.o; \
for i in $(PLATFORM_MAIN_OBJS); do \
$(INSTALL_DATA) $$i $(DESTDIR)$(LIBPL); \
done; \
fi
$(INSTALL_DATA) Modules/config.c $(DESTDIR)$(LIBPL)/config.c
$(INSTALL_DATA) $(srcdir)/Modules/config.c.in $(DESTDIR)$(LIBPL)/config.c.in
Expand Down Expand Up @@ -3282,7 +3302,7 @@ clean-retain-profile: pycremoval
find build -name '*.py[co]' -exec rm -f {} ';' || true
-rm -f pybuilddir.txt
-rm -f _bootstrap_python
-rm -rf web_example python.mjs python.wasm python*.symbols python*.map
-rm -rf web_example web_embed_test python.mjs python.wasm python*.symbols python*.map
-rm -f Programs/_testembed Programs/_freeze_module
-rm -rf Python/deepfreeze
-rm -f Python/frozen_modules/*.h
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Emscripten ``libpython.a`` can now be linked into a program statically.
2 changes: 1 addition & 1 deletion Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2961,7 +2961,7 @@ EM_JS(int, emscripten_set_up_async_input_device_js, (void), {
await sleep(5);
return bufs[(idx ++) % 3];
});
return !!WebAssembly.promising;
return !!Module.Py_EmscriptenStackSwitching;
});

static PyObject *
Expand Down
50 changes: 50 additions & 0 deletions Platforms/emscripten/web_embed_test/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* Smoke test for libpython linked into a program whose main() is not Python's. */

#include <Python.h>
#include <stdio.h>

// Imports come from the preloaded zip, len() goes through the call trampoline
// and poll() through the syscall overrides.
static const char *SCRIPT =
"import json, select\n"
"select.poll().poll(0)\n"
"print(json.dumps({'embedded': len('ok')}))\n";

int main(void)
{
PyStatus status;
PyConfig config;

PyConfig_InitIsolatedConfig(&config);
config.write_bytecode = 0;
config.module_search_paths_set = 1;
status = PyWideStringList_Append(&config.module_search_paths,
L"/lib/stdlib.zip");
if (PyStatus_Exception(status)) {
goto exception;
}
status = PyConfig_SetBytesString(&config, &config.executable, "/embed");
if (PyStatus_Exception(status)) {
goto exception;
}
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
goto exception;
}
PyConfig_Clear(&config);

if (PyRun_SimpleString(SCRIPT) != 0) {
puts("web_embed_test: script failed");
return 1;
}
if (Py_FinalizeEx() < 0) {
puts("web_embed_test: Py_FinalizeEx failed");
return 1;
}
puts("web_embed_test: ok");
return 0;

exception:
PyConfig_Clear(&config);
Py_ExitStatusException(status);
}
21 changes: 21 additions & 0 deletions Platforms/emscripten/web_embed_test/run_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
# Build and run the libpython embedding smoke test.
set -euo pipefail
cd "$(dirname "$0")/../../.."

BUILD_DIR=${CROSS_BUILD_DIR:-cross-build}/wasm32-emscripten/build/python
make -C "$BUILD_DIR" web_embed_test

# Node 24 needs JSPI enabled explicitly; it is on by default afterwards.
NODE=${NODE:-node}
NODE_FLAGS=
if [ "$("$NODE" -p 'process.versions.node.split(".")[0]')" = 24 ]; then
NODE_FLAGS=--experimental-wasm-jspi
fi

# Run from the build directory: Emscripten resolves main.data relative to cwd.
cd "$BUILD_DIR/web_embed_test"
rc=0
out=$("$NODE" $NODE_FLAGS main.js 2>&1) || rc=$?
echo "$out"
[ "$rc" -eq 0 ] && grep -q "web_embed_test: ok" <<<"$out"
41 changes: 41 additions & 0 deletions Programs/emscripten_beforemain.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* Emscripten setup for when Python is the program. Linked into the
* interpreter only, never into libpython.
*
* Runs main() under WebAssembly.promising so that libpython can suspend the
* wasm stack in a syscall (see Python/emscripten_syscalls.c).
*/

#include <emscripten.h>

EM_JS(void, _PyEmscripten_BeforeMain_js, (void), {
if (!WebAssembly.promising) {
// No stack switching support =(
return;
}
if (ENVIRONMENT_IS_NODE && !Module.onExit) {
Module.onExit = (code) => process.exit(code);
}
// promising() needs the raw export; _main may be a JS wrapper around it.
const main = WebAssembly.promising(wasmExports.__main_argc_argv);
_main = (...args) => {
Module.Py_EmscriptenStackSwitching = true;
// Exit the way callMain() would have, once main() is actually done.
main(...args).then((ret) => exitJS(ret, true)).catch(handleException);
// Unwind to callMain() without letting it exit: main() is still
// running on the promising stack.
throw "unwind";
};
// callMain() takes the entry point from _main, or from wasmImports.main
// when linked with -sMAIN_MODULE.
if ("main" in wasmImports) {
wasmImports.main = _main;
}
})

EM_JS_DEPS(_PyEmscripten_BeforeMain, "$exitJS,$handleException");

__attribute__((constructor)) void
_PyEmscripten_BeforeMain(void)
{
_PyEmscripten_BeforeMain_js();
}
61 changes: 19 additions & 42 deletions Python/emscripten_syscalls.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ __attribute__((constructor)) void __syscall_init_umask(void) {
#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.
// 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.
EM_JS_MACROS(void, __fs_init_async_input_device_js, (void), {
FS.createAsyncInputDevice = function(parent, name, input) {
parent = typeof parent == 'string' ? parent : FS.getPath(parent);
var path = PATH.join2(parent, name);
Expand Down Expand Up @@ -103,44 +103,14 @@ EM_JS_MACROS(void, _emscripten_promising_main_js, (void), {
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;
};
})

EM_JS_DEPS(_emscripten_promising_main,
"$FS,$PATH,$FS_getMode,$resolveGlobalSymbol,"
"emscripten_exit_with_live_runtime");
EM_JS_DEPS(__fs_init_async_input_device, "$FS,$PATH,$FS_getMode");

__attribute__((constructor)) void _emscripten_promising_main(void) {
_emscripten_promising_main_js();
__attribute__((constructor)) void __fs_init_async_input_device(void) {
__fs_init_async_input_device_js();
}


#define IOVEC_T_BUF_OFFSET 0
#define IOVEC_T_BUF_LEN_OFFSET 4
#define IOVEC_T_SIZE 8
Expand All @@ -154,13 +124,20 @@ _Static_assert(sizeof(__wasi_iovec_t) == IOVEC_T_SIZE,
// 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.
//
// Reading from an async input device and poll() suspend the wasm stack
// instead of blocking when main() runs under WebAssembly.promising, which
// Programs/emscripten_beforemain.c arranges for the interpreter. An embedder
// running its own promising entry point opts in with
// Module.Py_EmscriptenStackSwitching = true;
// Otherwise these calls keep their synchronous behavior.
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) {
if (!Module.Py_EmscriptenStackSwitching) {
return null;
}
var stream;
Expand Down Expand Up @@ -244,7 +221,7 @@ _Static_assert(offsetof(struct pollfd, revents) == 6, "Unepxected pollfd struct
_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) {
if (!Module.Py_EmscriptenStackSwitching) {
return null;
}
return (async function() {
Expand Down
21 changes: 18 additions & 3 deletions Python/emscripten_trampoline.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ typedef PyObject* (*TrampolineFunc)(int* success,
PyObject* args,
PyObject* kw);

// Lets JS reach _PyRuntime without it being in -sEXPORTED_FUNCTIONS.
EMSCRIPTEN_KEEPALIVE _PyRuntimeState *const _PyEM_runtime = &_PyRuntime;

// Its table slot is taken over by the wasm-gc trampoline, so the table
// never grows.
static PyObject*
trampoline_placeholder(int* success, PyCFunctionWithKeywords func,
PyObject* self, PyObject* args, PyObject* kw)
{
Py_FatalError("Emscripten trampoline slot was not set up");
}
EMSCRIPTEN_KEEPALIVE const TrampolineFunc _PyEM_trampoline_slot = trampoline_placeholder;

/**
* Backwards compatible trampoline works with all JS runtimes
*/
Expand Down Expand Up @@ -79,7 +92,9 @@ function getPyEMTrampolinePtr() {
const trampolineInstance = new WebAssembly.Instance(trampolineModule, {
env: { __indirect_function_table: wasmTable, memory: wasmMemory },
});
return addFunction(trampolineInstance.exports.trampoline_call);
const slot = HEAPU32[__PyEM_trampoline_slot / 4];
wasmTable.set(slot, trampolineInstance.exports.trampoline_call);
return slot;
}
// We have to be careful to work correctly with memory snapshots -- the value of
// _PyRuntimeState.emscripten_trampoline needs to reflect whether wasm-gc is
Expand All @@ -90,12 +105,12 @@ function getPyEMTrampolinePtr() {
addOnPreRun(function setEmscriptenTrampoline() {
const ptr = getPyEMTrampolinePtr();
const offset = HEAP32[__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET / 4];
HEAP32[(__PyRuntime + offset) / 4] = ptr;
HEAP32[(HEAPU32[__PyEM_runtime / 4] + offset) / 4] = ptr;
});
);

EM_JS_DEPS(_PyEM_TrampolineCall,
"$wasmTable,$wasmMemory,$addFunction,$addOnPreRun");
"$wasmTable,$wasmMemory,$addOnPreRun");

PyObject*
_PyEM_TrampolineCall(PyCFunctionWithKeywords func,
Expand Down
6 changes: 5 additions & 1 deletion configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading