From aaca02ad810aa09e8ce19b9a9e6520abd40f3709 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 20 Jul 2025 14:20:40 +0200 Subject: [PATCH] [cpp] nostdlib build to be used with WASM and other environments that do not want stdlibc++ linked in. --- spine-cpp/CMakeLists.txt | 35 +++++++++++++ spine-cpp/build.sh | 37 ++++++++++++-- spine-cpp/src/nostdlib.cpp | 54 ++++++++++++++++++++ spine-cpp/src/spine/Extension.cpp | 2 +- spine-cpp/test-nostdlib.sh | 82 +++++++++++++++++++++++++++++++ 5 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 spine-cpp/src/nostdlib.cpp create mode 100755 spine-cpp/test-nostdlib.sh diff --git a/spine-cpp/CMakeLists.txt b/spine-cpp/CMakeLists.txt index 8c8d0d74d..3c28d9be5 100644 --- a/spine-cpp/CMakeLists.txt +++ b/spine-cpp/CMakeLists.txt @@ -3,6 +3,8 @@ project(spine-cpp) include(${CMAKE_CURRENT_LIST_DIR}/../flags.cmake) +option(SPINE_NO_FILE_IO "Disable file I/O operations" OFF) + include_directories(include) file(GLOB INCLUDES "include/**/*.h") file(GLOB SOURCES "src/**/*.cpp") @@ -10,6 +12,15 @@ file(GLOB SOURCES "src/**/*.cpp") add_library(spine-cpp STATIC ${SOURCES} ${INCLUDES}) target_include_directories(spine-cpp PUBLIC include) +if(SPINE_NO_FILE_IO) + target_compile_definitions(spine-cpp PRIVATE SPINE_NO_FILE_IO) +endif() + +# nostdcpp variant (no C++ standard library) +file(GLOB NOSTDCPP_SOURCES ${SOURCES} "src/nostdlib.cpp") +add_library(spine-cpp-nostdcpp STATIC ${NOSTDCPP_SOURCES} ${INCLUDES}) +target_include_directories(spine-cpp-nostdcpp PUBLIC include) + # Install target install(TARGETS spine-cpp EXPORT spine-cpp_TARGETS DESTINATION dist/lib) install(FILES ${INCLUDES} DESTINATION dist/include) @@ -24,4 +35,28 @@ export( if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) add_executable(headless-test ${CMAKE_CURRENT_SOURCE_DIR}/tests/HeadlessTest.cpp) target_link_libraries(headless-test spine-cpp) + + if(SPINE_NO_FILE_IO) + target_compile_definitions(headless-test PRIVATE SPINE_NO_FILE_IO) + endif() + + # nostdcpp test executable (no C++ stdlib) + add_executable(headless-test-nostdcpp ${CMAKE_CURRENT_SOURCE_DIR}/tests/HeadlessTest.cpp) + + if(MSVC) + # On Windows/MSVC, disable default libraries but keep C runtime + target_link_libraries(headless-test-nostdcpp spine-cpp-nostdcpp) + target_link_options(headless-test-nostdcpp PRIVATE /NODEFAULTLIB) + target_link_libraries(headless-test-nostdcpp msvcrt kernel32) + else() + # Unix/Linux: avoid linking libstdc++ automatically + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_link_libraries(headless-test-nostdcpp spine-cpp-nostdcpp) + target_link_options(headless-test-nostdcpp PRIVATE -nostdlib++ -lc) + else() + # GCC: use -nodefaultlibs and link minimal libraries including libgcc for operator new/delete + target_link_options(headless-test-nostdcpp PRIVATE -nodefaultlibs) + target_link_libraries(headless-test-nostdcpp spine-cpp-nostdcpp -lm -lc -lgcc) + endif() + endif() endif() \ No newline at end of file diff --git a/spine-cpp/build.sh b/spine-cpp/build.sh index 1fd7eb5fc..4ce5b0f51 100755 --- a/spine-cpp/build.sh +++ b/spine-cpp/build.sh @@ -3,11 +3,38 @@ set -e cd "$(dirname "$0")" -# Clean only if explicitly requested -if [ "$1" = "clean" ]; then +# Parse arguments +BUILD_TYPE="debug" +NOFILEIO="" +CLEAN="" + +for arg in "$@"; do + case $arg in + clean) + CLEAN="true" + ;; + release) + BUILD_TYPE="release" + ;; + debug) + BUILD_TYPE="debug" + ;; + nofileio) + NOFILEIO="-DSPINE_NO_FILE_IO=ON" + ;; + *) + echo "Unknown argument: $arg" + echo "Usage: $0 [clean] [release|debug] [nofileio]" + exit 1 + ;; + esac +done + +# Clean if requested +if [ "$CLEAN" = "true" ]; then rm -rf build fi -# Always build -cmake --preset=debug . -cmake --build --preset=debug \ No newline at end of file +# Configure and build +cmake --preset=$BUILD_TYPE $NOFILEIO . +cmake --build --preset=$BUILD_TYPE \ No newline at end of file diff --git a/spine-cpp/src/nostdlib.cpp b/spine-cpp/src/nostdlib.cpp new file mode 100644 index 000000000..0b0e2a0de --- /dev/null +++ b/spine-cpp/src/nostdlib.cpp @@ -0,0 +1,54 @@ +/* + * Minimal runtime stubs for nostdlib spine-cpp build + * + * Provides minimal implementations of C++ runtime symbols required + * for spine-cpp to work without the standard library. + */ + +#include + +// ============================================================================ +// C++ Runtime Stubs +// ============================================================================ + +// Memory operators - basic malloc/free wrappers +// Note: These require a C library that provides malloc/free +extern "C" { + void* malloc(size_t size); + void free(void* ptr); +} + +// Memory operators - use malloc/free but with careful implementation +// Make them weak so system can override if needed +__attribute__((weak)) void* operator new(size_t size) { + return malloc(size); +} + +__attribute__((weak)) void operator delete(void* ptr) { + if (ptr) free(ptr); +} + +// Static initialization guards (single-threaded stubs) +// Make them weak so system can override if needed +extern "C" __attribute__((weak)) int __cxa_guard_acquire(char* guard) { + return *guard == 0 ? (*guard = 1, 1) : 0; +} + +extern "C" __attribute__((weak)) void __cxa_guard_release(char* guard) { + // No-op for single-threaded + (void)guard; +} + +// Pure virtual function handler +extern "C" __attribute__((weak)) void __cxa_pure_virtual() { + // In a real implementation, this would abort or throw + // For minimal stub, we'll just return (undefined behavior but won't crash) + // This should never be called in a correctly written program +} + +// Stack protection (for GCC -fstack-protector) +// Make it weak so libc can override it in static builds +extern "C" __attribute__((weak)) char __stack_chk_guard = 0; +extern "C" __attribute__((weak)) void __stack_chk_fail() { + // Could call abort() or be no-op for minimal runtime +} \ No newline at end of file diff --git a/spine-cpp/src/spine/Extension.cpp b/spine-cpp/src/spine/Extension.cpp index e905cf735..8369aae43 100644 --- a/spine-cpp/src/spine/Extension.cpp +++ b/spine-cpp/src/spine/Extension.cpp @@ -101,7 +101,7 @@ void DefaultSpineExtension::_free(void *mem, const char *file, int line) { } char *DefaultSpineExtension::_readFile(const String &path, int *length) { -#ifndef __EMSCRIPTEN__ +#if !defined(__EMSCRIPTEN__) && !defined(SPINE_NO_FILE_IO) char *data; FILE *file = fopen(path.buffer(), "rb"); if (!file) return 0; diff --git a/spine-cpp/test-nostdlib.sh b/spine-cpp/test-nostdlib.sh new file mode 100755 index 000000000..a06e610dd --- /dev/null +++ b/spine-cpp/test-nostdlib.sh @@ -0,0 +1,82 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")" + +# Build or reuse Docker image +IMAGE_NAME="spine-cpp-nostdcpp-test" +if ! docker image inspect $IMAGE_NAME >/dev/null 2>&1; then + echo "Building Docker image (one-time setup)..." + docker build -t $IMAGE_NAME - <<'EOF' +FROM ubuntu:22.04 +RUN apt-get update >/dev/null 2>&1 && \ + apt-get install -y build-essential cmake ninja-build git file >/dev/null 2>&1 && \ + rm -rf /var/lib/apt/lists/* +EOF +fi + +echo "Running spine-cpp nostdcpp test..." + +# Run Docker container with spine-runtimes directory mounted +docker run --rm \ + -v "$(pwd)/..:/workspace/spine-runtimes" \ + -w /workspace/spine-runtimes/spine-cpp \ + $IMAGE_NAME \ + bash -c " + + # Build everything first + echo '=== Building all variants ===' + ./build.sh clean release >/dev/null 2>&1 + + # Try to build static regular executable + echo 'Building static regular executable...' + g++ -static -o build/headless-test-static build/CMakeFiles/headless-test.dir/tests/HeadlessTest.cpp.o build/libspine-cpp.a >/dev/null 2>&1 || echo 'Static regular build failed' + + # Try to build static nostdcpp executable (multiple approaches) + echo 'Building static nostdcpp executable...' + + # Approach 1: Try with -static-libgcc and -static-libstdc++ but no libstdc++ + if g++ -static -static-libgcc -Wl,--exclude-libs,libstdc++.a -o build/headless-test-nostdcpp-static build/CMakeFiles/headless-test-nostdcpp.dir/tests/HeadlessTest.cpp.o build/libspine-cpp-nostdcpp.a -lm -lc 2>/dev/null; then + echo 'SUCCESS: Static nostdcpp built (approach 1)' + # Approach 2: Try minimal static linking + elif g++ -static -o build/headless-test-nostdcpp-static-minimal build/CMakeFiles/headless-test-nostdcpp.dir/tests/HeadlessTest.cpp.o build/libspine-cpp-nostdcpp.a 2>/dev/null; then + echo 'SUCCESS: Static nostdcpp built (approach 2 - minimal)' + else + echo 'All static nostdcpp approaches failed - static linking may not be practical on this system' + fi + + echo '' + echo '=== FINAL RESULTS ===' + echo '' + echo 'File sizes:' + for exe in build/headless-test*; do + if [ -f \"\$exe\" ]; then + ls -lah \"\$exe\" | awk '{printf \"%-30s %s\\n\", \$9, \$5}' + fi + done + + echo '' + echo 'Dependencies:' + for exe in build/headless-test*; do + if [ -f \"\$exe\" ]; then + echo \"\$(basename \$exe):\" + ldd \"\$exe\" 2>/dev/null || echo \" (statically linked)\" + echo '' + fi + done + + echo 'Functional test:' + if [ -f build/headless-test-nostdcpp ]; then + echo 'Testing headless-test-nostdcpp with spineboy...' + if OUTPUT=\$(./build/headless-test-nostdcpp ../examples/spineboy/export/spineboy-pro.skel ../examples/spineboy/export/spineboy-pma.atlas idle 2>&1); then + echo \"\$OUTPUT\" | head -10 + echo '... (output truncated)' + echo 'SUCCESS: nostdcpp executable works!' + else + echo 'FAILED: nostdcpp executable failed to run' + echo \"Error: \$OUTPUT\" + fi + else + echo 'nostdcpp executable not found' + fi + " \ No newline at end of file