Fix KiCad WASM build and runtime errors

Build improvements:
- Change -O0 to -O1 to fix "local count too large" asyncify error
- Add wasm-emscripten-finalize to host (Docker OOMs on large WASM)
- Use -gseparate-dwarf for smaller main binary with debug info
- Build native protoc for code generation
- Add more functions to asyncify removelist

Runtime fixes:
- Add inject-dyncall-shims.sh to fix "dynCall_* is not defined" in Emscripten 4.x
- Update wxwidgets with browserInfo.name fix

New stubs and bindings:
- Add Embind bindings for JavaScript interop
- Add stubs for scripting, API plugin, PCB frame, navlib

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2025-12-22 16:30:53 +01:00
commit 9c271996c2
17 changed files with 699 additions and 13 deletions

View file

@ -83,7 +83,7 @@ PCRE2_INCLUDE="$BUILD_DIR/3rdparty/pcre/src"
# Configure debug/release flags based on DEBUG_BUILD environment variable
if [ "${DEBUG_BUILD:-1}" = "1" ]; then
WX_DEBUG_FLAGS="-g -O0"
WX_DEBUG_FLAGS="-g -O1"
WX_CONFIGURE_DEBUG="--enable-debug"
echo "Building wxWidgets in DEBUG mode"
else

View file

@ -39,6 +39,13 @@ ASYNCIFY_REMOVE=$(cat << 'REMOVELIST'
COLOR_SETTINGS::COLOR_SETTINGS(wxString const&, bool)
BuildBitmapInfo(std::__2::unordered_map<BITMAPS, std::__2::vector<BITMAP_INFO, std::__2::allocator<BITMAP_INFO>>, std::__2::hash<BITMAPS>, std::__2::equal_to<BITMAPS>, std::__2::allocator<std::__2::pair<BITMAPS const, std::__2::vector<BITMAP_INFO, std::__2::allocator<BITMAP_INFO>>>>>&)
match
DIALOG_PAD_PROPERTIES_BASE::DIALOG_PAD_PROPERTIES_BASE(wxWindow*, int, wxString const&, wxPoint const&, wxSize const&, long)
buildKicadAboutBanner(EDA_BASE_FRAME*, ABOUT_APP_INFO&)
IGESToBRep_CurveAndSurface::TransferGeometry(opencascade::handle<IGESData_IGESEntity> const&, Message_ProgressRange const&)
StepAP214_Protocol::StepAP214_Protocol()
BRepCheck_ParallelAnalyzer::operator()(int) const
ShapeFix_Wire::FixGap3d(int, bool)
ShapeFix_Wire::FixGap2d(int, bool)
REMOVELIST
)

View file

@ -0,0 +1,46 @@
#!/bin/bash
# Apply wasm-emscripten-finalize transformation on host
# Uses the same Binaryen v121 as wasm-opt to ensure version consistency
#
# Usage: ./scripts/common/apply-finalize.sh <input.wasm> <output.wasm>
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# Get wasm-emscripten-finalize from same Binaryen install as wasm-opt
BINARYEN_DIR="${PROJECT_ROOT}/tools/binaryen-121"
FINALIZE="${BINARYEN_DIR}/bin/wasm-emscripten-finalize"
# Ensure Binaryen is downloaded
if [ ! -x "${FINALIZE}" ]; then
echo "Downloading Binaryen v121..."
"${SCRIPT_DIR}/get-wasm-opt.sh" > /dev/null
fi
INPUT_WASM="${1:-output/pcbnew.wasm}"
OUTPUT_WASM="${2:-${INPUT_WASM}}"
if [ ! -f "${INPUT_WASM}" ]; then
echo "ERROR: Input file not found: ${INPUT_WASM}"
exit 1
fi
echo "Applying wasm-emscripten-finalize..."
echo " Input: ${INPUT_WASM}"
echo " Output: ${OUTPUT_WASM}"
echo " Tool: ${FINALIZE}"
# Run finalize with the same flags Emscripten would use
# NOTE: --dwarf removed because debug info is in separate .debug.wasm file
"${FINALIZE}" \
-g \
--bigint \
--no-legalize-javascript-ffi \
--detect-features \
"${INPUT_WASM}" \
-o "${OUTPUT_WASM}"
echo "Finalize complete: ${OUTPUT_WASM}"
ls -lh "${OUTPUT_WASM}"

View file

@ -40,7 +40,7 @@ DEBUG_BUILD="${DEBUG_BUILD:-1}"
if [ "$DEBUG_BUILD" = "1" ]; then
export BUILD_TYPE="Debug"
export DEBUG_CFLAGS="-g -O0"
export DEBUG_CFLAGS="-g -O1"
export DEBUG_LDFLAGS="-g -gsource-map"
else
export BUILD_TYPE="Release"

View file

@ -0,0 +1,114 @@
#!/bin/bash
# Inject dynCall_* shims into Emscripten-generated JS
#
# Emscripten 4.x no longer generates signature-specific dynCall_* functions,
# but the invoke_* wrappers (for C++ exception handling) still call them.
# This script auto-generates shims using getWasmTableEntry() which IS defined.
#
# Usage: inject-dyncall-shims.sh <pcbnew.js>
set -e
JS_FILE="$1"
if [ -z "$JS_FILE" ] || [ ! -f "$JS_FILE" ]; then
echo "Error: JS file not found: $JS_FILE"
echo "Usage: $0 <path/to/pcbnew.js>"
exit 1
fi
echo "Extracting dynCall signatures from $JS_FILE..."
# Extract all unique dynCall_* signatures from the file
# Matches patterns like: dynCall_i, dynCall_ii, dynCall_viijj, etc.
SIGNATURES=$(grep -oE 'dynCall_[a-zA-Z0-9]+' "$JS_FILE" | sort -u | sed 's/dynCall_//')
if [ -z "$SIGNATURES" ]; then
echo "No dynCall signatures found - nothing to inject"
exit 0
fi
SIG_COUNT=$(echo "$SIGNATURES" | wc -l | tr -d ' ')
echo "Found $SIG_COUNT unique signatures"
# Generate shim code
SHIM_FILE=$(mktemp)
cat > "$SHIM_FILE" << 'HEADER'
// === dynCall shims for Emscripten exception handling ===
// Auto-generated: maps dynCall_SIG() calls to getWasmTableEntry()
// This fixes "dynCall_* is not defined" errors in Emscripten 4.x
HEADER
for sig in $SIGNATURES; do
# Count args: signature length - 1 (first char is return type)
argcount=$((${#sig} - 1))
# Generate argument list: index, a0, a1, a2, ...
args="index"
call_args=""
for ((i=0; i<argcount; i++)); do
args="$args, a$i"
if [ $i -gt 0 ]; then
call_args="$call_args, "
fi
call_args="${call_args}a$i"
done
echo "var dynCall_$sig = ($args) => getWasmTableEntry(index)($call_args);" >> "$SHIM_FILE"
done
echo "" >> "$SHIM_FILE"
echo "// === End dynCall shims ===" >> "$SHIM_FILE"
# Find the insertion point: after getWasmTableEntry definition
# The pattern is:
# var getWasmTableEntry = funcPtr => {
# ...
# };
# We insert after the closing `};`
# Find line number of getWasmTableEntry definition
GWTL_LINE=$(grep -n '^var getWasmTableEntry = funcPtr => {' "$JS_FILE" | head -1 | cut -d: -f1)
if [ -z "$GWTL_LINE" ]; then
echo "Warning: Could not find getWasmTableEntry definition"
echo "Trying alternate pattern..."
GWTL_LINE=$(grep -n 'var getWasmTableEntry' "$JS_FILE" | head -1 | cut -d: -f1)
fi
if [ -z "$GWTL_LINE" ]; then
echo "Error: Could not find getWasmTableEntry in $JS_FILE"
echo "The shims need to be inserted after getWasmTableEntry is defined"
rm "$SHIM_FILE"
exit 1
fi
# Find the closing `};` after getWasmTableEntry (within next 10 lines)
INSERT_LINE=""
for ((i=GWTL_LINE; i<=GWTL_LINE+10; i++)); do
LINE_CONTENT=$(sed -n "${i}p" "$JS_FILE")
if [[ "$LINE_CONTENT" == "};" ]]; then
INSERT_LINE=$i
break
fi
done
if [ -z "$INSERT_LINE" ]; then
echo "Warning: Could not find closing }; for getWasmTableEntry"
echo "Inserting after line $GWTL_LINE"
INSERT_LINE=$GWTL_LINE
fi
echo "Injecting shims after line $INSERT_LINE..."
# Create output file with shims inserted
head -n "$INSERT_LINE" "$JS_FILE" > "${JS_FILE}.tmp"
cat "$SHIM_FILE" >> "${JS_FILE}.tmp"
tail -n +$((INSERT_LINE + 1)) "$JS_FILE" >> "${JS_FILE}.tmp"
# Replace original file
mv "${JS_FILE}.tmp" "$JS_FILE"
rm "$SHIM_FILE"
echo "Successfully injected $SIG_COUNT dynCall shims into $JS_FILE"

View file

@ -49,8 +49,37 @@ if [ ! -d "${PROTOBUF_DIR}" ]; then
rm "protobuf-${PROTOBUF_VERSION}.tar.gz"
fi
log_info "Building Protobuf ${PROTOBUF_VERSION} for WASM..."
log_info "Building Protobuf ${PROTOBUF_VERSION}..."
# Step 1: Build native protoc for the host (needed for code generation)
# This is required because the system protoc may be a different version
PROTOBUF_HOST_BUILD="${BUILD_ROOT}/deps/protobuf-host"
PROTOC_NATIVE="${PROTOBUF_HOST_BUILD}/protoc"
if [ ! -f "${PROTOC_NATIVE}" ]; then
log_info "Building native protoc for host..."
mkdir -p "${PROTOBUF_HOST_BUILD}"
cd "${PROTOBUF_HOST_BUILD}"
# Build native (not cross-compiled) protoc
cmake "${PROTOBUF_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-Dprotobuf_BUILD_TESTS=OFF \
-Dprotobuf_BUILD_EXAMPLES=OFF \
-Dprotobuf_BUILD_SHARED_LIBS=OFF \
-Dprotobuf_WITH_ZLIB=OFF
make -j${JOBS} protoc
log_info "Native protoc built: ${PROTOC_NATIVE}"
fi
# Install native protoc to sysroot/bin for CMake to find
mkdir -p "${SYSROOT}/bin"
cp "${PROTOC_NATIVE}" "${SYSROOT}/bin/protoc"
log_info "Native protoc installed to ${SYSROOT}/bin/protoc"
# Step 2: Build WASM library
log_info "Building Protobuf library for WASM..."
mkdir -p "${PROTOBUF_BUILD}"
cd "${PROTOBUF_BUILD}"

View file

@ -127,11 +127,11 @@ log_info "Building KiCad PCBnew ${KICAD_VERSION} for WASM..."
if [ "${DEBUG_BUILD:-0}" = "1" ] || [ $DEBUG -eq 1 ]; then
BUILD_TYPE="Debug"
EXTRA_FLAGS="-g -O1 -fexceptions -matomics -mbulk-memory"
# -O0 at link time skips wasm-opt (which can OOM on large WASM with debug symbols)
# NOTE: -gsource-map removed because wasm-emscripten-finalize OOMs on large WASM
# with source maps enabled. Keep -g for debug symbols in the WASM.
LINKER_DEBUG_FLAGS="-O0 -g -fexceptions"
log_info "Building KiCad in DEBUG mode (debug symbols, no source maps due to memory)"
# -gseparate-dwarf puts debug info in a separate .debug.wasm file
# This keeps the main WASM small (~200MB) while preserving full debug info
# DevTools loads the debug file on-demand when debugging
LINKER_DEBUG_FLAGS="-O1 -g -gseparate-dwarf -fexceptions"
log_info "Building KiCad in DEBUG mode (separate DWARF for smaller main binary)"
else
BUILD_TYPE="Release"
EXTRA_FLAGS="-O2 -fexceptions -matomics -mbulk-memory"
@ -187,6 +187,18 @@ cp "${STUBS_DIR}/wasm-opt-stub.sh" "${EMSDK_WASM_OPT}"
chmod +x "${EMSDK_WASM_OPT}"
log_info "wasm-opt stub installed (asyncify will run on host)"
# Step 6.3: Replace wasm-emscripten-finalize with stub (same pattern as wasm-opt)
# This tool also OOMs on large WASM with debug symbols, so we run it on the host
EMSDK_FINALIZE="/emsdk/upstream/bin/wasm-emscripten-finalize"
if [ -f "${EMSDK_FINALIZE}" ] && [ ! -f "${EMSDK_FINALIZE}.real" ]; then
log_info "Backing up real wasm-emscripten-finalize..."
mv "${EMSDK_FINALIZE}" "${EMSDK_FINALIZE}.real"
fi
# Always copy the latest stub (in case it was updated)
cp "${STUBS_DIR}/wasm-emscripten-finalize-stub.sh" "${EMSDK_FINALIZE}"
chmod +x "${EMSDK_FINALIZE}"
log_info "wasm-emscripten-finalize stub installed (finalize will run on host)"
# Step 6.5: Verify WASM support is in KiCad fork
# The kicad submodule should already have WASM port detection and kiplatform support
KICAD_CMAKE="${KICAD_DIR}/CMakeLists.txt"
@ -214,7 +226,7 @@ emcmake cmake "${KICAD_DIR}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -DKICAD_USE_PLATFORM_WASM=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread -sUSE_ZLIB=1 -I${SYSROOT}/include -I${STUBS_DIR}" \
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8'] -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a ${STUBS_BUILD}/libglu_stub.a ${STUBS_BUILD}/libpcbnew_scripting_stub.a ${STUBS_BUILD}/libnng_stub.a" \
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread -sUSE_ZLIB=1 -sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall'] --bind -L${SYSROOT}/lib ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a ${STUBS_BUILD}/libglu_stub.a ${STUBS_BUILD}/libpcbnew_scripting_stub.a ${STUBS_BUILD}/libnng_stub.a ${STUBS_BUILD}/pcbnew_embind.o" \
-DCMAKE_PREFIX_PATH="${SYSROOT};${WX_BUILD}" \
-DwxWidgets_CONFIG_EXECUTABLE="${WX_BUILD}/wx-config" \
\
@ -252,7 +264,29 @@ emcmake cmake "${KICAD_DIR}" \
-DODBC_LIBRARIES:STRING="" \
\
-DBUILD_GITHUB_PLUGIN=OFF \
-DKICAD_PCM=OFF
-DKICAD_PCM=OFF \
\
-DHAVE_STRCASECMP=1 \
-DHAVE_STRNCASECMP=1
# Step 7.1: Compile Embind bindings (after CMake so config.h exists)
# Exposes KiCad objects to JavaScript for future Pyodide integration
EMBIND_SRC="${PROJECT_ROOT}/wasm/bindings/pcbnew_embind.cpp"
if [ -f "$EMBIND_SRC" ]; then
log_info "Compiling Embind bindings..."
# Use the same includes and flags that KiCad uses
KICAD_INCLUDES="-I${KICAD_BUILD} -I${KICAD_DIR}/include -I${KICAD_DIR}/pcbnew -I${KICAD_DIR}/common"
KICAD_INCLUDES+=" -I${KICAD_DIR}/libs/core/include -I${KICAD_DIR}/libs/kimath/include -I${KICAD_DIR}/libs/kiplatform/include"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/clipper2/Clipper2Lib/include"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nlohmann_json"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/dynamic_bitset"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/nanodbc"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty/picosha2"
KICAD_INCLUDES+=" -I${KICAD_DIR}/thirdparty"
KICAD_INCLUDES+=" -I${SYSROOT}/include"
# KiCad requires C++20 for concepts
em++ -std=c++20 -c ${EXTRA_FLAGS} ${WX_CXXFLAGS} ${KICAD_INCLUDES} "$EMBIND_SRC" -o "${STUBS_BUILD}/pcbnew_embind.o"
fi
# Step 8: Build pcbnew target
log_info "Building pcbnew..."