From faf2a8540a524aefdfe71863583ee2e7d3bb10b8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 29 Jul 2026 16:55:16 +0100 Subject: [PATCH] Add ESP32 example with a portable smoke test Builds WasmKit for ESP32-C6 via ESP-IDF v5.5 + Embedded Swift (one IDF component per SwiftPM module) and runs a wasm module on emulated ESP32-C3 in QEMU via Examples/embedded-esp32/smoke-test.sh. A new build-embedded CI job compile-checks the engine for riscv32-none-none-eabi with Embedded Swift (Utilities/build-embedded.sh), so embedded compatibility is maintained without ESP-IDF on the runners. The Embedded Swift support in the engine was originally developed by Joannis Orlandos in #357. Co-authored-by: Joannis Orlandos --- .github/workflows/main.yml | 29 ++++ Examples/embedded-esp32/.gitignore | 8 + Examples/embedded-esp32/CMakeLists.txt | 5 + Examples/embedded-esp32/README.md | 47 ++++++ .../components/cwasmkit/CMakeLists.txt | 12 ++ .../components/wasmkit/CMakeLists.txt | 27 ++++ .../embedded-esp32/components/wasmkit/Empty.h | 1 + .../components/wasmkit/idf_component.yml | 2 + .../components/wasmparser/CMakeLists.txt | 25 +++ .../components/wasmparser/Empty.h | 1 + .../components/wasmparser/idf_component.yml | 2 + .../components/wasmtypes/CMakeLists.txt | 24 +++ .../components/wasmtypes/Empty.h | 1 + .../components/wasmtypes/idf_component.yml | 2 + Examples/embedded-esp32/main/BridgingHeader.h | 2 + Examples/embedded-esp32/main/CMakeLists.txt | 30 ++++ Examples/embedded-esp32/main/Main.swift | 84 ++++++++++ .../embedded-esp32/main/idf_component.yml | 2 + Examples/embedded-esp32/sdkconfig.defaults | 6 + Examples/embedded-esp32/smoke-test.sh | 153 ++++++++++++++++++ Utilities/build-embedded.sh | 40 +++++ 21 files changed, 503 insertions(+) create mode 100644 Examples/embedded-esp32/.gitignore create mode 100644 Examples/embedded-esp32/CMakeLists.txt create mode 100644 Examples/embedded-esp32/README.md create mode 100644 Examples/embedded-esp32/components/cwasmkit/CMakeLists.txt create mode 100644 Examples/embedded-esp32/components/wasmkit/CMakeLists.txt create mode 100644 Examples/embedded-esp32/components/wasmkit/Empty.h create mode 100644 Examples/embedded-esp32/components/wasmkit/idf_component.yml create mode 100644 Examples/embedded-esp32/components/wasmparser/CMakeLists.txt create mode 100644 Examples/embedded-esp32/components/wasmparser/Empty.h create mode 100644 Examples/embedded-esp32/components/wasmparser/idf_component.yml create mode 100644 Examples/embedded-esp32/components/wasmtypes/CMakeLists.txt create mode 100644 Examples/embedded-esp32/components/wasmtypes/Empty.h create mode 100644 Examples/embedded-esp32/components/wasmtypes/idf_component.yml create mode 100644 Examples/embedded-esp32/main/BridgingHeader.h create mode 100644 Examples/embedded-esp32/main/CMakeLists.txt create mode 100644 Examples/embedded-esp32/main/Main.swift create mode 100644 Examples/embedded-esp32/main/idf_component.yml create mode 100644 Examples/embedded-esp32/sdkconfig.defaults create mode 100755 Examples/embedded-esp32/smoke-test.sh create mode 100755 Utilities/build-embedded.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 81ec9fd6e..8f60a7622 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -297,6 +297,35 @@ jobs: - run: cmake --build ./build - run: ./build/bin/wasmkit --version + build-embedded: + runs-on: ubuntu-24.04 + container: + image: swiftlang/swift:nightly-main-noble + steps: + - uses: actions/checkout@v6 + - run: ./Utilities/build-embedded.sh + + smoke-esp32: + runs-on: ubuntu-24.04 + container: + image: espressif/idf:v5.5 + steps: + - uses: actions/checkout@v6 + - name: Install Swift development snapshot + run: | + apt-get update && apt-get install -y libcurl4 libedit2 libxml2 libsqlite3-0 libz3-4 zlib1g libncurses6 + dir=$(curl -sSL https://download.swift.org/development/ubuntu2204/latest-build.yml | sed -n 's/^dir: //p') + download=$(curl -sSL https://download.swift.org/development/ubuntu2204/latest-build.yml | sed -n 's/^download: //p') + mkdir -p /opt/swift + curl -sSL "https://download.swift.org/development/ubuntu2204/$dir/$download" \ + | tar xz -C /opt/swift --strip-components 1 + - name: Install QEMU + run: | + apt-get install -y libpixman-1-0 libglib2.0-0 libgcrypt20 libslirp0 + python $IDF_PATH/tools/idf_tools.py install qemu-riscv32 + - name: Run ESP32 smoke test in QEMU + run: SWIFT_TOOLCHAIN=/opt/swift ./Examples/embedded-esp32/smoke-test.sh --qemu + build-wasi: runs-on: ubuntu-24.04 container: diff --git a/Examples/embedded-esp32/.gitignore b/Examples/embedded-esp32/.gitignore new file mode 100644 index 000000000..97cd1d83c --- /dev/null +++ b/Examples/embedded-esp32/.gitignore @@ -0,0 +1,8 @@ +build/ +build.*/ +managed_components/ +dependencies.lock +sdkconfig +sdkconfig.* +!sdkconfig.defaults +build-*.log diff --git a/Examples/embedded-esp32/CMakeLists.txt b/Examples/embedded-esp32/CMakeLists.txt new file mode 100644 index 000000000..72d9ee8d1 --- /dev/null +++ b/Examples/embedded-esp32/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.29) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(wasmkit-esp32c6) diff --git a/Examples/embedded-esp32/README.md b/Examples/embedded-esp32/README.md new file mode 100644 index 000000000..3371ac1c7 --- /dev/null +++ b/Examples/embedded-esp32/README.md @@ -0,0 +1,47 @@ +# WasmKit on ESP32 (Embedded Swift + ESP-IDF) + +Runs a WebAssembly module on an ESP32-C6 (RISC-V). The project compiles the +WasmKit interpreter with Embedded Swift under ESP-IDF. The demo calls a wasm +function, lets the guest call back into a Swift host function, and round-trips +a host-thrown error with its identity. + +Each SwiftPM module becomes an ESP-IDF component (`wasmtypes` → `wasmparser` → +`wasmkit`, plus the C shims in `cwasmkit`), compiled through +[`espressif/idf_swift`](https://components.espressif.com/components/espressif/idf_swift). +`main/Main.swift` parses a bundled `add.wasm`, instantiates it, and calls the +exported `add` function. + +## Requirements + +- ESP-IDF v5.5+ with tools installed for `esp32c6` (`./install.sh esp32c6`) +- A Swift development snapshot toolchain that ships the Embedded + `riscv32-none-none-eabi` standard library +- For the QEMU test: Espressif's QEMU (`idf_tools.py install qemu-riscv32`) + +## Building and testing + +```sh +./smoke-test.sh # build for esp32c6 +./smoke-test.sh --qemu # also boot an esp32c3 build in QEMU and check output +``` + +The script locates ESP-IDF (`$IDF_PATH` or `~/esp/esp-idf*`) and a Swift +toolchain (`$SWIFT_TOOLCHAIN` or the newest snapshot in +`~/Library/Developer/Toolchains`). It also patches the local ESP-IDF +linker-script template once to keep `.got`/`.got.plt` in flash: Embedded Swift +emits a GOT-indirect reference to the Unicode data table symbols, and stock +ESP-IDF discards those sections. + +To flash real hardware after a build: + +```sh +idf.py -B build.c6 -D SDKCONFIG=sdkconfig.c6 -p /dev/cu.usbmodem* flash monitor +``` + +## Notes + +- The default 512 KiB WasmKit value stack does not fit in on-chip SRAM, so + `Main.swift` sets `EngineConfiguration.stackSize` to 64 KiB. +- The QEMU run uses the ESP32-C3 machine because Espressif's QEMU has no + ESP32-C6 model; both chips are RV32IMC-class cores running the same code + paths. diff --git a/Examples/embedded-esp32/components/cwasmkit/CMakeLists.txt b/Examples/embedded-esp32/components/cwasmkit/CMakeLists.txt new file mode 100644 index 000000000..b76ae2eaa --- /dev/null +++ b/Examples/embedded-esp32/components/cwasmkit/CMakeLists.txt @@ -0,0 +1,12 @@ +# C support code for WasmKit (_CWasmKit SwiftPM target). +if(DEFINED ENV{WASMKIT_ROOT}) + set(WASMKIT_ROOT $ENV{WASMKIT_ROOT}) +else() + get_filename_component(WASMKIT_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../.." ABSOLUTE) +endif() + +idf_component_register( + SRCS ${WASMKIT_ROOT}/Sources/_CWasmKit/_CWasmKit.c + ${WASMKIT_ROOT}/Sources/_CWasmKit/TrapGuard.c + INCLUDE_DIRS ${WASMKIT_ROOT}/Sources/_CWasmKit/include +) diff --git a/Examples/embedded-esp32/components/wasmkit/CMakeLists.txt b/Examples/embedded-esp32/components/wasmkit/CMakeLists.txt new file mode 100644 index 000000000..0586cb414 --- /dev/null +++ b/Examples/embedded-esp32/components/wasmkit/CMakeLists.txt @@ -0,0 +1,27 @@ +if(DEFINED ENV{WASMKIT_ROOT}) + set(WASMKIT_ROOT $ENV{WASMKIT_ROOT}) +else() + get_filename_component(WASMKIT_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../.." ABSOLUTE) +endif() +if(WIN32) + set(devnull NUL) +else() + set(devnull /dev/null) +endif() +idf_component_register(SRCS ${devnull} PRIV_REQUIRES idf_swift wasmtypes wasmparser cwasmkit) + +file(GLOB_RECURSE wasmkit_srcs ${WASMKIT_ROOT}/Sources/WasmKit/*.swift) +idf_component_register_swift( + ${COMPONENT_LIB} + BRIDGING_HEADER ${CMAKE_CURRENT_LIST_DIR}/Empty.h + SRCS ${wasmkit_srcs} +) +set_target_properties(${COMPONENT_LIB} PROPERTIES + Swift_MODULE_NAME WasmKit + Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift-modules +) +# -I for the WasmTypes/WasmParser swiftmodules; -Xcc -I so the Clang importer +# finds _CWasmKit's module.modulemap. +target_compile_options(${COMPONENT_LIB} PRIVATE + "$<$:SHELL:-I ${CMAKE_BINARY_DIR}/swift-modules -Xcc -I${WASMKIT_ROOT}/Sources/_CWasmKit/include -package-name wasmkit>") +add_dependencies(${COMPONENT_LIB} __idf_wasmtypes __idf_wasmparser) diff --git a/Examples/embedded-esp32/components/wasmkit/Empty.h b/Examples/embedded-esp32/components/wasmkit/Empty.h new file mode 100644 index 000000000..40cd95a81 --- /dev/null +++ b/Examples/embedded-esp32/components/wasmkit/Empty.h @@ -0,0 +1 @@ +// Intentionally empty bridging header. diff --git a/Examples/embedded-esp32/components/wasmkit/idf_component.yml b/Examples/embedded-esp32/components/wasmkit/idf_component.yml new file mode 100644 index 000000000..dbb1cb378 --- /dev/null +++ b/Examples/embedded-esp32/components/wasmkit/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/idf_swift: "^1.0.0" diff --git a/Examples/embedded-esp32/components/wasmparser/CMakeLists.txt b/Examples/embedded-esp32/components/wasmparser/CMakeLists.txt new file mode 100644 index 000000000..7777cbd7a --- /dev/null +++ b/Examples/embedded-esp32/components/wasmparser/CMakeLists.txt @@ -0,0 +1,25 @@ +if(DEFINED ENV{WASMKIT_ROOT}) + set(WASMKIT_ROOT $ENV{WASMKIT_ROOT}) +else() + get_filename_component(WASMKIT_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../.." ABSOLUTE) +endif() +if(WIN32) + set(devnull NUL) +else() + set(devnull /dev/null) +endif() +idf_component_register(SRCS ${devnull} PRIV_REQUIRES idf_swift wasmtypes) + +file(GLOB_RECURSE wasmparser_srcs ${WASMKIT_ROOT}/Sources/WasmParser/*.swift) +idf_component_register_swift( + ${COMPONENT_LIB} + BRIDGING_HEADER ${CMAKE_CURRENT_LIST_DIR}/Empty.h + SRCS ${wasmparser_srcs} +) +set_target_properties(${COMPONENT_LIB} PROPERTIES + Swift_MODULE_NAME WasmParser + Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift-modules +) +target_compile_options(${COMPONENT_LIB} PRIVATE + "$<$:SHELL:-I ${CMAKE_BINARY_DIR}/swift-modules -package-name wasmkit>") +add_dependencies(${COMPONENT_LIB} __idf_wasmtypes) diff --git a/Examples/embedded-esp32/components/wasmparser/Empty.h b/Examples/embedded-esp32/components/wasmparser/Empty.h new file mode 100644 index 000000000..40cd95a81 --- /dev/null +++ b/Examples/embedded-esp32/components/wasmparser/Empty.h @@ -0,0 +1 @@ +// Intentionally empty bridging header. diff --git a/Examples/embedded-esp32/components/wasmparser/idf_component.yml b/Examples/embedded-esp32/components/wasmparser/idf_component.yml new file mode 100644 index 000000000..dbb1cb378 --- /dev/null +++ b/Examples/embedded-esp32/components/wasmparser/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/idf_swift: "^1.0.0" diff --git a/Examples/embedded-esp32/components/wasmtypes/CMakeLists.txt b/Examples/embedded-esp32/components/wasmtypes/CMakeLists.txt new file mode 100644 index 000000000..09d8a5ddd --- /dev/null +++ b/Examples/embedded-esp32/components/wasmtypes/CMakeLists.txt @@ -0,0 +1,24 @@ +if(DEFINED ENV{WASMKIT_ROOT}) + set(WASMKIT_ROOT $ENV{WASMKIT_ROOT}) +else() + get_filename_component(WASMKIT_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../.." ABSOLUTE) +endif() +if(WIN32) + set(devnull NUL) +else() + set(devnull /dev/null) +endif() +idf_component_register(SRCS ${devnull} PRIV_REQUIRES idf_swift) + +file(GLOB_RECURSE wasmtypes_srcs ${WASMKIT_ROOT}/Sources/WasmTypes/*.swift) +idf_component_register_swift( + ${COMPONENT_LIB} + BRIDGING_HEADER ${CMAKE_CURRENT_LIST_DIR}/Empty.h + SRCS ${wasmtypes_srcs} +) +target_compile_options(${COMPONENT_LIB} PRIVATE + "$<$:SHELL:-package-name wasmkit>") +set_target_properties(${COMPONENT_LIB} PROPERTIES + Swift_MODULE_NAME WasmTypes + Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift-modules +) diff --git a/Examples/embedded-esp32/components/wasmtypes/Empty.h b/Examples/embedded-esp32/components/wasmtypes/Empty.h new file mode 100644 index 000000000..40cd95a81 --- /dev/null +++ b/Examples/embedded-esp32/components/wasmtypes/Empty.h @@ -0,0 +1 @@ +// Intentionally empty bridging header. diff --git a/Examples/embedded-esp32/components/wasmtypes/idf_component.yml b/Examples/embedded-esp32/components/wasmtypes/idf_component.yml new file mode 100644 index 000000000..dbb1cb378 --- /dev/null +++ b/Examples/embedded-esp32/components/wasmtypes/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/idf_swift: "^1.0.0" diff --git a/Examples/embedded-esp32/main/BridgingHeader.h b/Examples/embedded-esp32/main/BridgingHeader.h new file mode 100644 index 000000000..90de25c32 --- /dev/null +++ b/Examples/embedded-esp32/main/BridgingHeader.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/Examples/embedded-esp32/main/CMakeLists.txt b/Examples/embedded-esp32/main/CMakeLists.txt new file mode 100644 index 000000000..ca1e6dbdc --- /dev/null +++ b/Examples/embedded-esp32/main/CMakeLists.txt @@ -0,0 +1,30 @@ +if(WIN32) + set(devnull NUL) +else() + set(devnull /dev/null) +endif() +idf_component_register( + SRCS ${devnull} + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES idf_swift wasmkit +) + +idf_component_register_swift( + ${COMPONENT_LIB} + BRIDGING_HEADER ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h + SRCS Main.swift +) +target_compile_options(${COMPONENT_LIB} PRIVATE + "$<$:SHELL:-I ${CMAKE_BINARY_DIR}/swift-modules>") +add_dependencies(${COMPONENT_LIB} __idf_wasmkit) + +# String hashing/comparison in embedded Swift needs the Unicode data tables. +if(DEFINED ENV{SWIFT_EMBEDDED_LIB_DIR}) + set(swift_embedded_lib_dir $ENV{SWIFT_EMBEDDED_LIB_DIR}) +else() + get_filename_component(swift_bin_dir ${CMAKE_Swift_COMPILER} DIRECTORY) + get_filename_component(swift_embedded_lib_dir + "${swift_bin_dir}/../lib/swift/embedded/riscv32-none-none-eabi" ABSOLUTE) +endif() +target_link_libraries(${COMPONENT_LIB} INTERFACE + "${swift_embedded_lib_dir}/libswiftUnicodeDataTables.a") diff --git a/Examples/embedded-esp32/main/Main.swift b/Examples/embedded-esp32/main/Main.swift new file mode 100644 index 000000000..889ae15d7 --- /dev/null +++ b/Examples/embedded-esp32/main/Main.swift @@ -0,0 +1,84 @@ +import WasmKit + +// (module +// (import "host" "mul" (func $mul (param i32 i32) (result i32))) +// (import "host" "boom" (func $boom)) +// (func (export "add") (param i32 i32) (result i32) +// local.get 0 +// local.get 1 +// i32.add) +// (func (export "mul3") (param i32) (result i32) +// local.get 0 +// i32.const 3 +// call $mul) +// (func (export "callBoom") +// call $boom)) +let demoWasm: [UInt8] = [ + 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0F, 0x03, 0x60, + 0x02, 0x7F, 0x7F, 0x01, 0x7F, 0x60, 0x00, 0x00, 0x60, 0x01, 0x7F, 0x01, + 0x7F, 0x02, 0x18, 0x02, 0x04, 0x68, 0x6F, 0x73, 0x74, 0x03, 0x6D, 0x75, + 0x6C, 0x00, 0x00, 0x04, 0x68, 0x6F, 0x73, 0x74, 0x04, 0x62, 0x6F, 0x6F, + 0x6D, 0x00, 0x01, 0x03, 0x04, 0x03, 0x00, 0x02, 0x01, 0x07, 0x19, 0x03, + 0x03, 0x61, 0x64, 0x64, 0x00, 0x02, 0x04, 0x6D, 0x75, 0x6C, 0x33, 0x00, + 0x03, 0x08, 0x63, 0x61, 0x6C, 0x6C, 0x42, 0x6F, 0x6F, 0x6D, 0x00, 0x04, + 0x0A, 0x17, 0x03, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6A, 0x0B, 0x08, + 0x00, 0x20, 0x00, 0x41, 0x03, 0x10, 0x00, 0x0B, 0x04, 0x00, 0x10, 0x01, + 0x0B, +] + +/// An engine-unknown error type thrown by a host function; WasmKit must +/// rethrow it with its identity intact. +struct DemoHostError: Error { + let code: UInt32 +} + +@_cdecl("app_main") +func app_main() { + print("WasmKit on ESP32-C6") + do { + let module = try parseWasm(bytes: demoWasm) + // The default 512 KiB VM stack does not fit in on-chip SRAM. + var configuration = EngineConfiguration() + configuration.stackSize = 64 * 1024 + let engine = Engine(configuration: configuration) + let store = Store(engine: engine) + + var imports = Imports() + imports.define( + module: "host", name: "mul", + Function(store: store, parameters: [.i32, .i32], results: [.i32]) { _, arguments in + [.i32(arguments[0].i32 &* arguments[1].i32)] + }) + imports.define( + module: "host", name: "boom", + Function(store: store, parameters: []) { _, _ in + throw DemoHostError(code: 42) + }) + + let instance = try module.instantiate(store: store, imports: imports) + + guard let add = instance.exports[function: "add"], + let mul3 = instance.exports[function: "mul3"], + let callBoom = instance.exports[function: "callBoom"] + else { + print("missing export") + return + } + + if case .i32(let value) = try add([.i32(2), .i32(3)])[0] { + print("2 + 3 = \(value)") + } + // Guest -> host call. + if case .i32(let value) = try mul3([.i32(7)])[0] { + print("7 * 3 = \(value)") + } + // Host error identity round-trips through the interpreter. + do { + _ = try callBoom() + } catch let error as DemoHostError { + print("host error \(error.code)") + } + } catch { + print("wasm error") + } +} diff --git a/Examples/embedded-esp32/main/idf_component.yml b/Examples/embedded-esp32/main/idf_component.yml new file mode 100644 index 000000000..dbb1cb378 --- /dev/null +++ b/Examples/embedded-esp32/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/idf_swift: "^1.0.0" diff --git a/Examples/embedded-esp32/sdkconfig.defaults b/Examples/embedded-esp32/sdkconfig.defaults new file mode 100644 index 000000000..d43202bb7 --- /dev/null +++ b/Examples/embedded-esp32/sdkconfig.defaults @@ -0,0 +1,6 @@ +CONFIG_IDF_TARGET="esp32c6" +# WasmKit's interpreter needs a much deeper stack than the default 3.5 KB. +CONFIG_ESP_MAIN_TASK_STACK_SIZE=65536 +# WasmKit is a large binary; use the single large app partition layout. +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y diff --git a/Examples/embedded-esp32/smoke-test.sh b/Examples/embedded-esp32/smoke-test.sh new file mode 100755 index 000000000..d47b37662 --- /dev/null +++ b/Examples/embedded-esp32/smoke-test.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# Smoke test for WasmKit on ESP32 (Embedded Swift + ESP-IDF). +# +# Builds this example for ESP32-C6 and, with --qemu, also builds it for +# ESP32-C3 and boots it in Espressif's QEMU, checking that the guest wasm +# module actually executes ("2 + 3 = 5" on the serial console; QEMU has no +# ESP32-C6 machine, hence the C3 run). +# +# Requirements: +# - ESP-IDF v5.5+ installed and its tools provisioned (`install.sh esp32c6`). +# Discovered via $IDF_PATH, or ~/esp/esp-idf*. +# - A Swift toolchain with the Embedded riscv32-none-none-eabi stdlib +# (development snapshots from swift.org). Discovered via $SWIFT_TOOLCHAIN +# (path to a .xctoolchain), or the newest match under +# {~,}/Library/Developer/Toolchains. +# - For --qemu: qemu-system-riscv32 from Espressif +# (`idf_tools.py install qemu-riscv32`) or on PATH. +# +# Usage: +# ./smoke-test.sh # build for esp32c6 +# ./smoke-test.sh --qemu # additionally run the esp32c3 build in QEMU + +set -euo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +run_qemu=false +[ "${1:-}" = "--qemu" ] && run_qemu=true + +log() { echo "==> $*"; } +die() { echo "error: $*" >&2; exit 1; } + +# --- Locate ESP-IDF ----------------------------------------------------------- +if [ -z "${IDF_PATH:-}" ]; then + for d in "$HOME"/esp/esp-idf*; do + [ -f "$d/export.sh" ] && IDF_PATH="$d" + done +fi +[ -n "${IDF_PATH:-}" ] && [ -f "$IDF_PATH/export.sh" ] || die "ESP-IDF not found; set IDF_PATH" +log "Using ESP-IDF: $IDF_PATH" + +# --- Locate an Embedded Swift toolchain with riscv32 stdlib ------------------- +embedded_stdlib="usr/lib/swift/embedded/riscv32-none-none-eabi" +if [ -z "${SWIFT_TOOLCHAIN:-}" ]; then + best_version="" + for tc in "$HOME"/Library/Developer/Toolchains/swift-*.xctoolchain \ + /Library/Developer/Toolchains/swift-*.xctoolchain; do + [ -d "$tc/$embedded_stdlib" ] || continue + [ "$(basename "$tc")" = "swift-latest.xctoolchain" ] && continue + # Pick the newest compiler; Embedded Swift restrictions relax over time + # (e.g. untyped throws needs a 6.5+ snapshot). + version="$("$tc/usr/bin/swiftc" --version 2>/dev/null | head -1 | sed -n 's/.*Swift version \([0-9.]*\).*/\1/p')" + if [ -z "$best_version" ] || [ "$(printf '%s\n%s\n' "$best_version" "$version" | sort -V | tail -1)" = "$version" ]; then + best_version="$version" + SWIFT_TOOLCHAIN="$tc" + fi + done +fi +[ -n "${SWIFT_TOOLCHAIN:-}" ] && [ -d "$SWIFT_TOOLCHAIN/$embedded_stdlib" ] \ + || die "no Swift toolchain with the Embedded riscv32-none-none-eabi stdlib found; set SWIFT_TOOLCHAIN" +log "Using Swift toolchain: $SWIFT_TOOLCHAIN" + +# --- ESP-IDF linker-script patch for Embedded Swift's GOT --------------------- +# Embedded Swift emits a GOT-indirect reference to the Unicode data table +# symbols; stock ESP-IDF discards .got/.got.plt and the link fails with +# "discarded output section: `.got.plt'". Keep the (read-only) GOT in flash. +patch_ld_template() { + local template="$IDF_PATH/components/esp_system/ld/$1/sections.ld.in" + [ -f "$template" ] || return 0 + if ! grep -q 'Embedded Swift may emit GOT-indirect references' "$template"; then + log "Patching $template to keep .got sections" + python3 - "$template" <<'EOF' +import sys +path = sys.argv[1] +s = open(path).read() +anchor = " _flash_rodata_start = ABSOLUTE(.);\n" +insert = anchor + """ + /* Embedded Swift may emit GOT-indirect references (e.g. to Unicode data + * table symbols). The GOT is read-only here; keep it instead of relying + * on the generic discard below. */ + *(.got) + *(.got.plt) +""" +assert anchor in s, f"anchor not found in {path}" +open(path, "w").write(s.replace(anchor, insert, 1)) +EOF + fi +} +patch_ld_template esp32c6 +patch_ld_template esp32c3 + +# --- Environment --------------------------------------------------------------- +export WASMKIT_ROOT="${WASMKIT_ROOT:-$(cd "$here/../.." && pwd)}" +log "Using WasmKit: $WASMKIT_ROOT" +# shellcheck disable=SC1091 +source "$IDF_PATH/export.sh" > /dev/null +export PATH="$SWIFT_TOOLCHAIN/usr/bin:$PATH" +export SWIFT_EMBEDDED_LIB_DIR="$SWIFT_TOOLCHAIN/$embedded_stdlib" + +cd "$here" + +# --- Build for ESP32-C6 --------------------------------------------------------- +log "Building for esp32c6" +idf.py -B build.c6 -D SDKCONFIG=sdkconfig.c6 set-target esp32c6 > build-c6.log 2>&1 \ + || { tail -30 build-c6.log; die "esp32c6 set-target failed (see build-c6.log)"; } +idf.py -B build.c6 -D SDKCONFIG=sdkconfig.c6 build >> build-c6.log 2>&1 \ + || { tail -30 build-c6.log; die "esp32c6 build failed (see build-c6.log)"; } +log "esp32c6 build OK: $(ls -lh build.c6/*.bin | awk '{print $9, "("$5")"}')" + +$run_qemu || { log "Done. Pass --qemu to also run the esp32c3 build in QEMU."; exit 0; } + +# --- Build for ESP32-C3 and run in QEMU ------------------------------------------ +qemu_bin="$(command -v qemu-system-riscv32 || true)" +if [ -z "$qemu_bin" ]; then + qemu_bin="$(find "${IDF_TOOLS_PATH:-$HOME/.espressif}/tools/qemu-riscv32" -name qemu-system-riscv32 -type f 2>/dev/null | head -1)" +fi +[ -n "$qemu_bin" ] || die "qemu-system-riscv32 not found; run 'idf_tools.py install qemu-riscv32'" +log "Using QEMU: $qemu_bin" + +log "Building for esp32c3 (QEMU)" +idf.py -B build.c3 -D SDKCONFIG=sdkconfig.c3 set-target esp32c3 > build-c3.log 2>&1 \ + || { tail -30 build-c3.log; die "esp32c3 set-target failed (see build-c3.log)"; } +idf.py -B build.c3 -D SDKCONFIG=sdkconfig.c3 build >> build-c3.log 2>&1 \ + || { tail -30 build-c3.log; die "esp32c3 build failed (see build-c3.log)"; } + +log "Merging flash image" +(cd build.c3 && esptool.py --chip esp32c3 merge_bin -o flash.bin --fill-flash-size 4MB @flash_args > /dev/null) + +log "Booting in QEMU (up to 60s)" +qemu_out="$here/build.c3/qemu-out.txt" +"$qemu_bin" -M esp32c3 -drive file="$here/build.c3/flash.bin,if=mtd,format=raw" \ + -nographic -serial file:"$qemu_out" -monitor null > /dev/null 2>&1 & +qemu_pid=$! +trap 'kill $qemu_pid 2>/dev/null || true' EXIT + +result=1 +for _ in $(seq 1 60); do + if grep -q 'host error 42' "$qemu_out" 2>/dev/null; then result=0; break; fi + if grep -qE 'Guru Meditation|abort\(\) was called' "$qemu_out" 2>/dev/null; then break; fi + kill -0 $qemu_pid 2>/dev/null || break + sleep 1 +done +kill $qemu_pid 2>/dev/null || true + +echo "--- serial output ---" +sed -n '/Calling app_main/,$p' "$qemu_out" | head -20 +echo "----------------------" +if [ $result -eq 0 ]; then + grep -q '2 + 3 = 5' "$qemu_out" && grep -q '7 \* 3 = 21' "$qemu_out" \ + || die "smoke test FAILED: incomplete output (see $qemu_out)" + log "SMOKE TEST PASSED: wasm + host functions executed on emulated ESP32" +else + die "smoke test FAILED: expected '2 + 3 = 5' in serial output (see $qemu_out)" +fi diff --git a/Utilities/build-embedded.sh b/Utilities/build-embedded.sh new file mode 100755 index 000000000..709640abe --- /dev/null +++ b/Utilities/build-embedded.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Compile-checks the WasmKit engine for a bare-metal Embedded Swift target. +# +# This is the cheap, toolchain-only half of the ESP32 support: it catches +# Embedded Swift regressions (unsupported language features, OS dependencies +# leaking into shared code) without requiring ESP-IDF or QEMU. The full +# on-target build lives in Examples/embedded-esp32/smoke-test.sh. +set -euo pipefail + +TARGET="${EMBEDDED_TARGET:-riscv32-none-none-eabi}" +SWIFTC="${SWIFTC:-swiftc}" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${ROOT}/.build/embedded-check" +mkdir -p "$BUILD" + +common=( + -target "$TARGET" + -enable-experimental-feature Embedded + -wmo -parse-as-library -Osize + -package-name wasmkit + -I "$BUILD" +) + +compile_module() { + local name="$1" + shift + echo "Compiling $name for $TARGET" + # shellcheck disable=SC2046 + "$SWIFTC" "${common[@]}" "$@" \ + $(find "$ROOT/Sources/$name" -name '*.swift') \ + -module-name "$name" \ + -emit-module -emit-module-path "$BUILD/$name.swiftmodule" \ + -c -o "$BUILD/$name.o" +} + +compile_module WasmTypes +compile_module WasmParser +compile_module WasmKit -Xcc "-I$ROOT/Sources/_CWasmKit/include" + +echo "OK: WasmKit compiles for $TARGET with Embedded Swift"