Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions Examples/embedded-esp32/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
build/
build.*/
managed_components/
dependencies.lock
sdkconfig
sdkconfig.*
!sdkconfig.defaults
build-*.log
5 changes: 5 additions & 0 deletions Examples/embedded-esp32/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
47 changes: 47 additions & 0 deletions Examples/embedded-esp32/README.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions Examples/embedded-esp32/components/cwasmkit/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
27 changes: 27 additions & 0 deletions Examples/embedded-esp32/components/wasmkit/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
"$<$<COMPILE_LANGUAGE:Swift>: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)
1 change: 1 addition & 0 deletions Examples/embedded-esp32/components/wasmkit/Empty.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Intentionally empty bridging header.
2 changes: 2 additions & 0 deletions Examples/embedded-esp32/components/wasmkit/idf_component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
espressif/idf_swift: "^1.0.0"
25 changes: 25 additions & 0 deletions Examples/embedded-esp32/components/wasmparser/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-I ${CMAKE_BINARY_DIR}/swift-modules -package-name wasmkit>")
add_dependencies(${COMPONENT_LIB} __idf_wasmtypes)
1 change: 1 addition & 0 deletions Examples/embedded-esp32/components/wasmparser/Empty.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Intentionally empty bridging header.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
espressif/idf_swift: "^1.0.0"
24 changes: 24 additions & 0 deletions Examples/embedded-esp32/components/wasmtypes/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-package-name wasmkit>")
set_target_properties(${COMPONENT_LIB} PROPERTIES
Swift_MODULE_NAME WasmTypes
Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift-modules
)
1 change: 1 addition & 0 deletions Examples/embedded-esp32/components/wasmtypes/Empty.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Intentionally empty bridging header.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
espressif/idf_swift: "^1.0.0"
2 changes: 2 additions & 0 deletions Examples/embedded-esp32/main/BridgingHeader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#pragma once
#include <stdio.h>
30 changes: 30 additions & 0 deletions Examples/embedded-esp32/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
"$<$<COMPILE_LANGUAGE:Swift>: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")
84 changes: 84 additions & 0 deletions Examples/embedded-esp32/main/Main.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
2 changes: 2 additions & 0 deletions Examples/embedded-esp32/main/idf_component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
espressif/idf_swift: "^1.0.0"
6 changes: 6 additions & 0 deletions Examples/embedded-esp32/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -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
Loading