mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
gh-156780: Emscripten: support building CPython with static linking (#156781)
Adds the ability to build Emscripten statically, so that it can be linked in as an embedded library. Includes a test of the embedding case. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Russell Keith-Magee <russell@keith-magee.com>
This commit is contained in:
10 files changed
+168
-49
No files matched your search
@@ -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
|
||||
+26
-6
@@ -971,9 +971,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
|
||||
@@ -1120,6 +1123,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)
|
||||
|
||||
@@ -1620,8 +1637,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
|
||||
@@ -2251,7 +2268,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)
|
||||
|
||||
|
||||
######################################################################
|
||||
@@ -2977,6 +2994,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
|
||||
@@ -3291,7 +3311,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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Emscripten ``libpython.a`` can now be linked into a program statically.
|
||||
@@ -3014,7 +3014,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 *
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -0,0 +1,14 @@
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
void _PyEmscripten_BeforeMain_js(void);
|
||||
|
||||
__attribute__((constructor)) void
|
||||
_PyEmscripten_BeforeMain(void)
|
||||
{
|
||||
_PyEmscripten_BeforeMain_js();
|
||||
}
|
||||
@@ -54,13 +54,40 @@ __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.
|
||||
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;
|
||||
(async () => {
|
||||
const ret = await main(...args);
|
||||
Module.onExit?.(ret);
|
||||
})();
|
||||
_emscripten_exit_with_live_runtime();
|
||||
};
|
||||
// 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, "emscripten_exit_with_live_runtime");
|
||||
|
||||
// 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);
|
||||
@@ -103,44 +130,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
|
||||
@@ -154,13 +151,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;
|
||||
@@ -244,7 +248,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() {
|
||||
|
||||
@@ -878,6 +878,7 @@ TRUE
|
||||
MACHDEP_OBJS
|
||||
DYNLOADFILE
|
||||
DLINCLDIR
|
||||
PLATFORM_MAIN_OBJS
|
||||
PLATFORM_OBJS
|
||||
PLATFORM_HEADERS
|
||||
DTRACE_OBJS
|
||||
@@ -20434,6 +20435,7 @@ fi
|
||||
|
||||
PLATFORM_HEADERS=
|
||||
PLATFORM_OBJS=
|
||||
PLATFORM_MAIN_OBJS=
|
||||
|
||||
case $ac_sys_system in #(
|
||||
Emscripten) :
|
||||
@@ -20444,6 +20446,7 @@ case $ac_sys_system in #(
|
||||
if test "x$enable_emscripten_syscalls" = xyes
|
||||
then :
|
||||
|
||||
as_fn_append PLATFORM_MAIN_OBJS ' Programs/emscripten_beforemain.o'
|
||||
as_fn_append PLATFORM_OBJS ' Python/emscripten_syscalls.o'
|
||||
|
||||
fi
|
||||
@@ -20460,6 +20463,7 @@ esac
|
||||
|
||||
|
||||
|
||||
|
||||
# -I${DLINCLDIR} is added to the compile rule for importdl.o
|
||||
|
||||
DLINCLDIR=.
|
||||
|
||||
@@ -5488,6 +5488,7 @@ fi
|
||||
dnl Platform-specific C and header files.
|
||||
PLATFORM_HEADERS=
|
||||
PLATFORM_OBJS=
|
||||
PLATFORM_MAIN_OBJS=
|
||||
|
||||
AS_CASE([$ac_sys_system],
|
||||
[Emscripten], [
|
||||
@@ -5495,6 +5496,7 @@ AS_CASE([$ac_sys_system],
|
||||
AS_VAR_APPEND([CFLAGS_NODIST], [" -mgc"])
|
||||
|
||||
AS_VAR_IF([enable_emscripten_syscalls], [yes], [
|
||||
AS_VAR_APPEND([PLATFORM_MAIN_OBJS], [' Programs/emscripten_beforemain.o'])
|
||||
AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_syscalls.o'])
|
||||
])
|
||||
AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h'])
|
||||
@@ -5506,6 +5508,7 @@ AS_CASE([$ac_sys_system],
|
||||
)
|
||||
AC_SUBST([PLATFORM_HEADERS])
|
||||
AC_SUBST([PLATFORM_OBJS])
|
||||
AC_SUBST([PLATFORM_MAIN_OBJS])
|
||||
|
||||
# -I${DLINCLDIR} is added to the compile rule for importdl.o
|
||||
AC_SUBST([DLINCLDIR])
|
||||
|
||||
Reference in new issue
Block a user