Skip to content

Commit 1eaadc6

Browse files
clementperonclaude
andcommitted
gh-156780: Emscripten: add an embedding smoke test
Link libpython.a into a program whose main() is not Python's, without -sMAIN_MODULE or the interpreter's link flags, and run a script that imports from the stdlib, calls through the trampoline and polls. Platforms/emscripten/web_embed_test/run_test.sh builds and runs it in CI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 846d815 commit 1eaadc6

5 files changed

Lines changed: 89 additions & 1 deletion

File tree

.github/workflows/reusable-emscripten.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,5 @@ jobs:
7979
run: python3 Platforms/emscripten run --test
8080
- name: "Test Repl"
8181
run: Platforms/emscripten/browser_test/run_test.sh
82+
- name: "Test embedding"
83+
run: Platforms/emscripten/web_embed_test/run_test.sh

Makefile.pre.in

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,20 @@ web_example/python.mjs web_example/python.wasm: $(BUILDPYTHON)
11221122
cp python.mjs web_example/python.mjs
11231123
cp python.wasm web_example/python.wasm
11241124

1125+
WEB_EMBED_TEST_DIR=$(EMSCRIPTEN_DIR)/web_embed_test
1126+
1127+
# Linked like an embedder would: against libpython.a, without $(LINKFORSHARED)
1128+
# and so without -sMAIN_MODULE or the interpreter's -sEXPORTED_FUNCTIONS.
1129+
web_embed_test/main.js: $(WEB_EMBED_TEST_DIR)/main.c $(LIBRARY) $(ZIP_STDLIB)
1130+
@mkdir -p web_embed_test
1131+
$(LINKCC) $(PY_STDMODULE_CFLAGS) $(PY_CORE_EXE_LDFLAGS) -O2 -g0 -o $@ \
1132+
$(WEB_EMBED_TEST_DIR)/main.c $(LIBRARY) $(LIBS) $(MODLIBS) $(SYSLIBS) \
1133+
-sALLOW_MEMORY_GROWTH -sSTACK_SIZE=5MB \
1134+
--preload-file $(ZIP_STDLIB)@/lib/stdlib.zip
1135+
1136+
.PHONY: web_embed_test
1137+
web_embed_test: web_embed_test/main.js
1138+
11251139
.PHONY: web_example
11261140
web_example: web_example/python.mjs web_example/python.worker.mjs web_example/index.html web_example/server.py web_example/$(ZIP_STDLIB)
11271141

@@ -3288,7 +3302,7 @@ clean-retain-profile: pycremoval
32883302
find build -name '*.py[co]' -exec rm -f {} ';' || true
32893303
-rm -f pybuilddir.txt
32903304
-rm -f _bootstrap_python
3291-
-rm -rf web_example python.mjs python.wasm python*.symbols python*.map
3305+
-rm -rf web_example web_embed_test python.mjs python.wasm python*.symbols python*.map
32923306
-rm -f Programs/_testembed Programs/_freeze_module
32933307
-rm -rf Python/deepfreeze
32943308
-rm -f Python/frozen_modules/*.h
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Emscripten ``libpython.a`` can now be linked into a program statically.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/* Smoke test for libpython linked into a program whose main() is not Python's. */
2+
3+
#include <Python.h>
4+
#include <stdio.h>
5+
6+
// Imports come from the preloaded zip, len() goes through the call trampoline
7+
// and poll() through the syscall overrides.
8+
static const char *SCRIPT =
9+
"import json, select\n"
10+
"select.poll().poll(0)\n"
11+
"print(json.dumps({'embedded': len('ok')}))\n";
12+
13+
int main(void)
14+
{
15+
PyStatus status;
16+
PyConfig config;
17+
18+
PyConfig_InitIsolatedConfig(&config);
19+
config.write_bytecode = 0;
20+
config.module_search_paths_set = 1;
21+
status = PyWideStringList_Append(&config.module_search_paths,
22+
L"/lib/stdlib.zip");
23+
if (PyStatus_Exception(status)) {
24+
goto exception;
25+
}
26+
status = PyConfig_SetBytesString(&config, &config.executable, "/embed");
27+
if (PyStatus_Exception(status)) {
28+
goto exception;
29+
}
30+
status = Py_InitializeFromConfig(&config);
31+
if (PyStatus_Exception(status)) {
32+
goto exception;
33+
}
34+
PyConfig_Clear(&config);
35+
36+
if (PyRun_SimpleString(SCRIPT) != 0) {
37+
puts("web_embed_test: script failed");
38+
return 1;
39+
}
40+
if (Py_FinalizeEx() < 0) {
41+
puts("web_embed_test: Py_FinalizeEx failed");
42+
return 1;
43+
}
44+
puts("web_embed_test: ok");
45+
return 0;
46+
47+
exception:
48+
PyConfig_Clear(&config);
49+
Py_ExitStatusException(status);
50+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
# Build and run the libpython embedding smoke test.
3+
set -euo pipefail
4+
cd "$(dirname "$0")/../../.."
5+
6+
BUILD_DIR=${CROSS_BUILD_DIR:-cross-build}/wasm32-emscripten/build/python
7+
make -C "$BUILD_DIR" web_embed_test
8+
9+
# Node 24 needs JSPI enabled explicitly; it is on by default afterwards.
10+
NODE=${NODE:-node}
11+
NODE_FLAGS=
12+
if [ "$("$NODE" -p 'process.versions.node.split(".")[0]')" = 24 ]; then
13+
NODE_FLAGS=--experimental-wasm-jspi
14+
fi
15+
16+
# Run from the build directory: Emscripten resolves main.data relative to cwd.
17+
cd "$BUILD_DIR/web_embed_test"
18+
rc=0
19+
out=$("$NODE" $NODE_FLAGS main.js 2>&1) || rc=$?
20+
echo "$out"
21+
[ "$rc" -eq 0 ] && grep -q "web_embed_test: ok" <<<"$out"

0 commit comments

Comments
 (0)