From eda124b15fff4662bb1a33a2d580f071ec4e3a81 Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 18 Jul 2026 10:11:06 +0000 Subject: [PATCH] Trim compiled unit export surface --- docs/wasm-runtime-architecture.md | 9 ++++ scripts/compile_wasm_unit | 5 +- scripts/run_cli_tests.sh | 1 + scripts/test_unit_export_surface.sh | 79 +++++++++++++++++++++++++++++ src/lib/compiler.cpp | 2 +- src/lib/compiler.h | 22 +++++--- 6 files changed, 107 insertions(+), 11 deletions(-) create mode 100755 scripts/test_unit_export_surface.sh diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 27368f0..e831b55 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -131,6 +131,15 @@ __uce_[_] | `exists` | probe only — resolves the unit, loads nothing | `sanitize_symbol_suffix()` keeps `[A-Za-z0-9_]` (mirrors `ascii_safe_name`). +Unit code is hidden by default so the linker can discard loaded helpers that the +unit does not use. Handler macros and the explicit `EXPORT` directive are the +only application symbols promoted to default visibility; generated request +binding and constructor exports remain part of the runtime ABI. Debug sections +are removed after linking while the wasm name section is retained for runtime +traces. This keeps each side module's code and public ABI local to its actual +handler responsibility instead of duplicating every helper from a loaded app +library. + `wasm_resolve_target(unit, handler)` (`src/wasm/core.cpp`) resolves the source path and looks up the export's funcref slot; `exists` lets callers probe a unit without instantiating it. diff --git a/scripts/compile_wasm_unit b/scripts/compile_wasm_unit index 0f16d02..b04de17 100755 --- a/scripts/compile_wasm_unit +++ b/scripts/compile_wasm_unit @@ -20,7 +20,7 @@ PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1} PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch} COMMON_FLAGS=( --target=wasm32-wasip1 - -fPIC -fvisibility=default -fvisibility-inlines-hidden + -fPIC -fvisibility=hidden -fvisibility-inlines-hidden -O1 -g -std=c++20 # The server captures this script's output and treats any non-empty result as # a compile failure (then drops the .wasm), so a successful build must be @@ -80,6 +80,7 @@ fi -c "$DEST_DIR/$PP_FN" -o "$OBJ_FN" "$SDK/bin/wasm-ld" -shared --experimental-pic \ + --gc-sections \ --unresolved-symbols=import-dynamic \ --Bsymbolic \ "$OBJ_FN" -o "$WASM_TMP" \ @@ -92,7 +93,7 @@ fi --export-if-defined=__uce_once \ --export-if-defined=__uce_init -"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$WASM_TMP" +"$SDK/bin/llvm-objcopy" --strip-debug --add-section=uce.abi="$ABI_TMP" "$WASM_TMP" python3 scripts/check_unit_wasm.py "$WASM_TMP" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm" mv "$WASM_TMP" "$DEST_DIR/$WASM_FN" diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index e9e176e..6903cc6 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -76,5 +76,6 @@ if [[ "$action" == "run" ]]; then scripts/test_log_timeliness.sh scripts/test_raw_http_request_log.sh scripts/test_component_resolution_ttl.sh + scripts/test_unit_export_surface.sh scripts/test_socket_activation.sh fi diff --git a/scripts/test_unit_export_surface.sh b/scripts/test_unit_export_surface.sh new file mode 100755 index 0000000..9c4d590 --- /dev/null +++ b/scripts/test_unit_export_surface.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="unit-export-surface-test-$$" +site_directory="${UCE_TEST_SITE_DIRECTORY:-site}" +bin_directory="${UCE_TEST_BIN_DIRECTORY:-/tmp/uce/work}" +if [[ -r /etc/uce/settings.cfg ]]; then + if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" ]]; then + configured_site_directory=$(awk -F= '/^[[:space:]]*HTTP_DOCUMENT_ROOT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + site_directory="${configured_site_directory:-$site_directory}" + fi + if [[ -z "${UCE_TEST_BIN_DIRECTORY:-}" ]]; then + configured_bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + bin_directory="${configured_bin_directory:-$bin_directory}" + fi +fi +source_dir="$site_directory/$test_name" +artifact_dir="" + +cleanup() { + rm -rf "$source_dir" + if [[ -n "$artifact_dir" ]]; then + rm -rf "$artifact_dir" + fi +} +trap cleanup EXIT +mkdir -p "$source_dir" +absolute_source_dir=$(realpath "$source_dir") +artifact_dir="$bin_directory$absolute_source_dir" + +printf '%s\n' \ + 'String visibility_used() { return("private-used"); }' \ + 'String visibility_unused() { return("uce-private-unused-marker-8f61d2"); }' \ + 'EXPORT String visibility_shared() { return("shared"); }' \ + 'CLI(Request& context) { print(visibility_shared(), ":", visibility_used(), ":", component("named:NAMED", context)); }' \ + >"$source_dir/entry.uce" +printf '%s\n' \ + 'String named_used() { return("private-named-used"); }' \ + 'String named_unused() { return("uce-named-unused-marker-4ae973"); }' \ + 'EXPORT String named_shared() { return("named-export"); }' \ + 'COMPONENT:NAMED(Request& context) { print(named_shared(), ":", named_used()); }' \ + >"$source_dir/named.uce" + +output=$(scripts/uce-cli "/$test_name/entry.uce") +if [[ "$output" != "shared:private-used:named-export:private-named-used" ]]; then + echo "unit export surface runtime failed: $output" >&2 + exit 1 +fi + +entry_wasm="$artifact_dir/entry.uce.wasm" +named_wasm="$artifact_dir/named.uce.wasm" +python3 - "$entry_wasm" "$named_wasm" <<'PY' +import sys +from pathlib import Path + +from scripts.check_unit_wasm import collect + +cases = [ + (Path(sys.argv[1]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_cli", "visibility_shared"}, b"uce-private-unused-marker-8f61d2"), + (Path(sys.argv[2]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_component_NAMED", "named_shared"}, b"uce-named-unused-marker-4ae973"), +] +for path, allowed, unused_marker in cases: + data = path.read_bytes() + _, imports, exports = collect(path) + names = {name for name, _ in exports} + unexpected = names - allowed + missing = allowed - names + if unexpected or missing: + raise SystemExit(f"{path}: unexpected exports={sorted(unexpected)} missing={sorted(missing)} all={sorted(names)}") + if unused_marker in data: + raise SystemExit(f"{path}: retained unused private code marker") + if len(imports) >= 40: + raise SystemExit(f"{path}: retained {len(imports)} imports (expected fewer than 40)") + if path.stat().st_size >= 1024 * 1024: + raise SystemExit(f"{path}: artifact is {path.stat().st_size} bytes (expected under 1 MiB)") +PY + +echo "unit export surface passed" diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index b43fff2..ad4ea01 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -16,7 +16,7 @@ namespace { -const u64 UCE_UNIT_ABI_VERSION = 8; +const u64 UCE_UNIT_ABI_VERSION = 9; struct SharedUnitFilesystemState { diff --git a/src/lib/compiler.h b/src/lib/compiler.h index 84115d1..9f4bd10 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -1,13 +1,19 @@ #pragma once -#define RENDER(X) extern "C" void __uce_render(X) -#define COMPONENT(X) extern "C" void __uce_component(X) -#define ONCE(X) extern "C" void __uce_once(X) -#define INIT(X) extern "C" void __uce_init(X) -#define WS(X) extern "C" void __uce_websocket(X) -#define CLI(X) extern "C" void __uce_cli(X) -#define SERVE_HTTP(X) extern "C" void __uce_serve_http(X) -#define EXPORT extern "C" +#if defined(__UCE_WASM_UNIT__) +#define UCE_UNIT_EXPORT __attribute__((visibility("default"))) +#else +#define UCE_UNIT_EXPORT +#endif + +#define RENDER(X) extern "C" UCE_UNIT_EXPORT void __uce_render(X) +#define COMPONENT(X) extern "C" UCE_UNIT_EXPORT void __uce_component(X) +#define ONCE(X) extern "C" UCE_UNIT_EXPORT void __uce_once(X) +#define INIT(X) extern "C" UCE_UNIT_EXPORT void __uce_init(X) +#define WS(X) extern "C" UCE_UNIT_EXPORT void __uce_websocket(X) +#define CLI(X) extern "C" UCE_UNIT_EXPORT void __uce_cli(X) +#define SERVE_HTTP(X) extern "C" UCE_UNIT_EXPORT void __uce_serve_http(X) +#define EXPORT extern "C" UCE_UNIT_EXPORT String preprocess_shared_unit(Request* context, SharedUnit* su); String compiler_generated_cpp_path(Request* context, String source_file);