A portable app runtime. Apps are written once in Embedded Swift, built
as a wasm32 module and optional native static libraries, and run inside a
native host on iOS, macOS, Android and Linux. Guest and host talk only
through a small dynamic-runtime C ABI, rt.h,
modeled on the Objective-C runtime and reduced to what Objective-C, JNI and C
all support. Platforms implement portable contracts declared as Swift
protocols.
- Design: Documentation/DynamicRuntime.md
- Phase one status, decisions and findings: Documentation/Implementation.md
This repository holds the shared, platform-independent code (the ABI, the guest library and macros, the runtime core, the provider contracts, the guest build scripts), the wasm import shim, and the Linux bundle launcher and sandbox. Platform hosts and UI renderers (iOS, Android, Linux) are written separately.
The package supports macOS 12 and iOS 15, the oldest deployment targets
current Xcode accepts. The WasmKit engine needs macOS 15 / iOS 18, so it is
its own package, Packages/RuntimeWasm, which
depends on this one by path. Hosts that ship only native slices or wasm2c
don't depend on WasmKit.
| Target | Role | Embedded |
|---|---|---|
CRuntime |
rt.h, plus rt_guest.h (ABI version, section names, wasm allocator) |
C, freestanding |
RuntimeABI |
Wire types, signatures, status codes, contract descriptors, rt.contracts encoding |
yes |
DynamicRuntime |
Guest library: rt_* wrappers, proxies, marshaling, rt_dispatch / rt_finalize (the GuestExports trait), List, macros |
yes |
DynamicRuntimeMacros |
@DynamicRuntime / @DynamicCallback: proxies, dispatch adapters, metadata records |
build machine |
RuntimeCore |
Runtime core: handle table, registry, Swift-registry backend, validation | yes |
RuntimeNative |
rt_* as C symbols for native slices, and a direct binding for host Swift code |
yes |
RuntimeWasm |
WasmKit import shim: runs App.wasm against RuntimeCore. A separate package in Packages/RuntimeWasm |
host, macOS 15 / iOS 18 |
DynamicBluetooth, DynamicLocation, DynamicNetworking, DynamicFormatting |
Provider contracts (declarations) | yes |
AppRuntime, bundle-runtime, CBundleRuntime |
Linux bundle model, launcher and sandbox | Linux |
import DynamicRuntime
@DynamicRuntime("org.pureswift.bluetooth.Central", requires: .bluetooth)
public protocol Central: AnyObject {
static func make(_ delegate: CentralDelegate) -> Self
func state() -> Int32
func scan(services: [String], allowDuplicates: Bool)
}The macro generates CentralProxy (calls through rt_call), the descriptor
and an rt.contracts record, plus adapters that export a Swift
implementation to the runtime. Guest code uses CentralProxy.make(delegate);
a Swift host registers CentralProxy.register(MyCentral.self, in: registrar).
An app exports one entry point and no main. Runtime.main runs the async
work it starts before returning to the host:
@_expose(wasm, "rt_main")
@c(rt_main)
public func appMain() -> rt_status {
Runtime.main { App.start() }
}Async code runs on the guest executor: tasks run before each call from the
host returns, and RuntimeClock.sleep(nanoseconds:) stands in for
Task.sleep, which Embedded Swift doesn't have.
Every host also provides the built-in Permissions, Bundle (AppBundle in
Swift) and Application contracts: one way to ask for any capability, the
app's metadata and localized strings, and its lifecycle and incoming URLs.
let greeting = try AppBundleProxy.localizedString("greeting", table: nil) ?? "Hello"
if try PermissionsProxy.status(.location) == PermissionStatus.notDetermined.rawValue {
try PermissionsProxy.request(.location, delegate: permissionsDelegate)
}
let application = try ApplicationProxy.make(lifecycleDelegate)Each provider contract also has async wrappers for guest code, such as
AsyncCentral, AsyncLocationManager and HTTPSession.data(for:). Event
sequences are EventStreams, which stop the native work when released:
let central = AsyncCentral()
guard await central.requestAuthorization() == .allowed else { return }
for try await result in central.scan(services: [.bit16(0x180D)]) {
let peripheral = try await central.connect(result.peripheral)
// …
}Guests write SwiftUI. import SwiftUI and import Observation give guest
builds the Embedded frontend and the Observation stopgap. The frontend does
no layout or rendering: it sends the host's renderer a render IR tree, and
then only what changes (Documentation/RenderIR.md).
@Observable
final class Store {
var name: String = "World"
}
struct ContentView: View {
@Bindable var store: Store
var body: some View {
VStack {
Text("Hello, \(store.name)!")
TextField("Name", text: $store.name)
}
}
}
struct ExampleApp: App {
let store = Store()
var body: some Scene {
WindowGroup { ContentView(store: store) }
}
}
@_expose(wasm, "rt_main")
@c(rt_main)
public func appMain() -> rt_status {
Runtime.main { ExampleApp.run() }
}examples/ExampleUI is a fuller app. No player renders the IR yet.
The toolchain is pinned in .swift-version (Swift 6.4.2). Install it and
the matching wasm Swift SDK:
swiftly install 6.4.2
swift sdk install https://download.swift.org/swift-6.4.2-release/wasm-sdk/swift-6.4.2-RELEASE/swift-6.4.2-RELEASE_wasm.artifactbundle.tar.gz --checksum 83baca83553f8c443cda7b84800912e6e709c3acd2e7ebf277794ca5836c0da5swift build
swift testThe WasmKit engine's tests run from its own package:
cd Packages/RuntimeWasm
swift testScripts/guest.py compiles a package's guest library product for each
target, seals every native slice (§7.5), links App.wasm as a WASI reactor
and assembles the universal bundle a host consumes. It builds no host code.
Scripts/build-wasm.sh ExampleApp # wasm32
Scripts/build-ios.sh ExampleApp # iOS 6 armv7, iOS 13 arm64
Scripts/build-macos.sh ExampleApp # macOS 10.15, arm64 + x86_64
Scripts/build-tvos.sh ExampleApp # tvOS 13
Scripts/build-watchos.sh ExampleApp # watchOS 6 (arm64)
Scripts/build-visionos.sh ExampleApp # visionOS 1
Scripts/build-linux.sh ExampleApp # armv7, arm64, x86_64
Scripts/build-android.sh ExampleApp # armeabi-v7a, arm64-v8a, x86_64An app's manifest is Swift, like Package.swift
(examples/ExampleAppManifest/App.swift):
import AppManifest
let app = App(
id: "org.pureswift.example",
name: .localized("app.name"),
version: "1.0", build: 1,
languages: ["en", "es"],
product: "ExampleApp",
capabilities: [.bluetooth(usage: .localized("perm.bluetooth"))],
platforms: [.iOS("15.0"), .macOS("12.0"), .android(minSDK: 26, targetSDK: 35), .linux]
)Scripts/build-bundle.sh --manifest examples/ExampleAppManifest/App.swiftguest.py compiles the manifest with the AppManifest library and runs it
sandboxed. It checks the manifest against the app's contracts, its
resources/<language>.lproj/Manifest.strings and the slices' minimum OS
versions. It writes the portable Info.plist keys, and embeds the manifest
in every slice's rt.manifest section. Without a manifest, flags describe
the app:
Scripts/build-bundle.sh --product ExampleApp --id org.pureswift.example \
--name Example --version 1.0 --build 1 --capability bluetoothThe scripts' own tests:
python3 -m unittest discover -s Scripts -p 'test_*.py'Scripts/guest.py targets lists every target ID. Output goes to
.build/guest/<product>/.
The toolchain has no Embedded concurrency runtime for the arm64 ELF slices'
bare-metal triple, so linux-arm64 and android-arm64-v8a use the one for
aarch64-unknown-linux-gnu. arm64 Linux toolchains include it. Elsewhere,
copy it from the swift Docker image once per Swift version:
Scripts/fetch-embedded-linux.shWithout it, those two slices are built without concurrency.
RuntimeWasmTests (in Packages/RuntimeWasm) runs the example app's
App.wasm when it has been built (Scripts/build-wasm.sh ExampleApp), the
same app DynamicRuntimeTests runs as a native slice.
A host links the app's libapp.a next to its own Swift code. The slice
defines rt_dispatch and rt_finalize, so the host depends on AppRuntime
without the GuestExports trait, and binds its own DynamicRuntime to the
runtime directly:
.package(url: "https://github.com/PureSwift/AppRuntime.git", from: "…", traits: [])let native = NativeRuntime.shared
native.bindDirectly()
MyContractProxy.register(MyImplementation.self, in: native)
native.start(rt_main, dispatch: rt_dispatch, finalize: rt_finalize)The app's calls arrive through the rt_* C symbols; the host's proxies and
exports never use them. Packages/LinkedSlice tests this with the example
app's macOS slice:
Scripts/guest.py slice --product ExampleApp --target macos
cd Packages/LinkedSlice
swift testA host with no slice for its platform translates App.wasm to C with
WABT's wasm2c (brew install wabt). Scripts/wasm2c-shim.py adds the
import shim and a minimal WASI, and seals the result into a libapp.a that
a host links exactly like a slice:
Scripts/build-wasm.sh ExampleApp
Scripts/wasm2c-shim.py build --wasm .build/guest/ExampleApp/wasm32/App.wasm \
--output .build/guest/ExampleApp/wasm2c
cd Packages/LinkedSlice
APPRUNTIME_LINKED_ENGINE=wasm2c swift testAPPRUNTIME_LINKED_PRODUCT=ExampleUI runs the example UI app instead,
either way.
bundle-runtime resolves a bundle, selects an architecture and launches it
inside a namespace sandbox. See Sandbox. The
bundle format it implements predates the
universal bundle of the new design (§7.5) and will move to it.
bundle-runtime com.example.myapp # by identifier
bundle-runtime /path/to/bundle.app # by pathUse CMake to cross-compile and install the launcher and libAppRuntime.so
for a target system (the Swift language requires the Ninja generator):
cmake -B build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/linux.toolchain.cmake \
-DAPPRUNTIME_ARCH=arm64 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usrcmake --build build && DESTDIR=/path/to/rootfs cmake --install build| Option | Default | Meaning |
|---|---|---|
APPRUNTIME_ARCH |
arm64 |
Target: arm64, x86_64, armv7, i386 |
APPRUNTIME_SYSROOT |
Swift SDK sysroot for the arch | Override the target sysroot |
APPRUNTIME_TOOLCHAIN_BIN |
~/.swiftly/bin |
Directory holding clang/swiftc |
APPRUNTIME_BUILD_LAUNCHER |
ON |
Build bundle-runtime (Linux only) |
BUILD_SHARED_LIBS |
ON |
Build the library shared rather than static |
examples/ also holds build scripts that package Celeste Classic
(ccleste) and Nanosaur 2 as port bundles.