Skip to content

Commit 4ca4b81

Browse files
committed
Make Embedded Swift the supported plugin toolchain
1 parent 480c7c2 commit 4ca4b81

1 file changed

Lines changed: 52 additions & 54 deletions

File tree

Documentation/WasmPluginPlan.md

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ native Swift).
77

88
---
99

10-
> **Implementation status (M0–M2 + app integration done).** The runtime, host engine, native
11-
> parsers, ABI, a bundled reference plugin, tests, and full Store/UI integration are implemented and
12-
> building. See `Sources/BluetoothExplorerPluginEngine/`, `Documentation/PluginABI.md`, and the
13-
> M0-driven pin change below. Remaining: guest SDK repo (Rust PDK + `bleplug` CLI), user-import UX
14-
> (M4), and the Android build verification (still a gate — see §9).
10+
> **Implementation status.** The runtime, host engine, native parsers, ABI, the Embedded Swift guest
11+
> SDK, a bundled Embedded-Swift reference plugin, tests, and full Store/UI integration are
12+
> implemented and building. See `Sources/BluetoothExplorerPluginEngine/`, `PluginSDK/`, and
13+
> `Documentation/PluginABI.md`. Remaining: user-import UX (M4) and Android build verification
14+
> (still a gate — see §9).
1515
1616
## 1. Runtime decision: WasmKit 0.3.x (app floor raised to iOS 18 / macOS 15)
1717

@@ -78,12 +78,9 @@ Extism (no Swift host SDK; wasmtime-based — copy its manifest/ABI ideas only).
7878
│ PluginEngine actor → WasmKit (per-plugin Store/Instance, │
7979
│ ResourceLimiter, deadline watchdog, quarantine) │
8080
└─────────────────────────────────────────────────────────────────┘
81-
ble-plugin-sdk (separate repo)
82-
spec/ABI.md + conformance vectors (normative, frozen first)
83-
rust/ble-plugin-pdk (reference PDK, wasm32-unknown-unknown)
84-
swift/BLEPluginSDK (Embedded Swift, wasip1 reactor, template)
85-
bleplug CLI (new / run / pack / conformance)
86-
examples/ (ibeacon, heart-rate, battery-level)
81+
PluginSDK/ (in-repo, never part of the app build)
82+
BLEPluginSDK/ Embedded Swift guest SDK (envelope, PayloadReader, Fields/CBOR)
83+
Examples/ plugin projects; `make install` builds + installs the .wasm
8784
```
8885

8986
Key layering choice: the `ParserPlugin` protocol + registry ship **before** any wasm code,
@@ -92,16 +89,19 @@ with the existing native decoders (`AppleBeacon` iBeacon parser, the
9289
gets the decoded-fields UI and routing in a fully shippable milestone with zero wasm risk;
9390
WasmKit plugins then join the same registry.
9491

95-
## 4. Normative ABI v1 — freeze first, in `spec/ABI.md`
92+
## 4. Normative ABI v1 — see `Documentation/PluginABI.md`
9693

97-
The three drafts produced during design diverged on envelope encoding, UUID byte order, and
98-
export names. **One byte-level spec with test vectors must be written and frozen before any
99-
host or SDK code.** The decisions:
94+
The design drafts diverged on envelope encoding, UUID byte order, and export names, so one
95+
byte-level spec was frozen before the host and SDK were written. It lives in
96+
`Documentation/PluginABI.md`; the summary below is informative.
10097

101-
**Module forms accepted:** import-free core module (`wasm32-unknown-unknown`) — canonical;
102-
or `wasm32-unknown-wasip1` **reactor** (for Embedded Swift; host links WASI with *no*
103-
preopens/env/args — allocator plumbing only; host calls `_initialize` once). Any other
104-
import ⇒ load rejection. No host-callback imports in v1 (one-directional trust boundary).
98+
**Module forms accepted:** `wasm32-unknown-wasip1` **reactor** (what the Embedded Swift SDK emits;
99+
the host calls `_initialize` once), and import-free core modules. Imports are permitted **only** from
100+
`wasi_snapshot_preview1`; any other import module is rejected at load. Those WASI imports are
101+
satisfied by a minimal in-house shim (§5.4) rather than a full WASI implementation: `random_get`
102+
returns real randomness because the Embedded Swift runtime requires it, and everything else is
103+
stubbed to success. Plugins get no filesystem, network, environment or clock access, and there are
104+
no host-callback imports (the trust boundary stays one-directional).
105105

106106
**Exports:**
107107

@@ -122,7 +122,7 @@ Return `u64 = (result_ptr << 32) | result_len`; `0` = "not mine / no parse" (not
122122
trap = plugin failure. Only i32/i64 cross the boundary.
123123

124124
**Input envelope** — fixed little-endian binary header + payload (trivially readable from
125-
`no_std` Rust and Embedded Swift, no decoder dependency):
125+
Embedded Swift without a decoder dependency):
126126

127127
```
128128
offset size field
@@ -333,37 +333,35 @@ Precondition: give `AttributeValue` a proper unique `id` (UUID) — it is curren
333333
imports + memory caps + deadline + quarantine. No signing in v1; decide the authenticity
334334
story before any non-local distribution (and see §2 — no marketplace, ever).
335335

336-
## 6. Guest SDKs (`ble-plugin-sdk`, separate repo)
337-
338-
- **`spec/ABI.md` + `spec/vectors/*.json`** — the normative spec (§4) and conformance
339-
vectors (hex input → expected fields). Everything else conforms to this.
340-
- **Rust reference PDK** (`ble-plugin-pdk`, `no_std` + alloc, `wasm32-unknown-unknown`):
341-
author writes one pure function over `AdvertisementInput`/`Fields`; a `ble_plugin!` macro
342-
emits the exports, allocator, and CBOR (minicbor). `opt-level="z"` + lto + `panic="abort"`
343-
+ `wasm-opt -Oz` → 1–30 KB. This is the always-working path.
344-
- **Embedded Swift SDK** (`BLEPluginSDK`, wasip1 reactor, `swift-6.3_wasm-embedded` SDK,
345-
pinned): same author experience (`PayloadReader`, `Fields` builder, hand-rolled ~100-line
346-
CBOR emitter — Embedded-safe: no existentials, no Codable, no Foundation). Because
347-
`@_expose(wasm)` symbols are dropped from dependency modules (swiftlang/swift #77812), the
348-
export shims live in the **author's module via template**, not in the SDK library.
349-
Flagship but experimental — docs must not promise parity with Rust. Sizes: tens–hundreds
350-
of KB. AssemblyScript documented; TinyGo tolerated; full non-embedded Swift explicitly
351-
unsupported (9–50 MB binaries).
352-
- **`bleplug` CLI**: `new` (scaffold), `run <plugin.wasm> --adv --company 004C --hex …`
353-
(instant feedback under real WasmKit, no app/device), `pack` (validate exports vs
354-
manifest, sha256, emit pair), `conformance` (run vectors).
355-
- **Two-tier author testing**: the parse function is a plain function — unit-test it
356-
natively with LLDB; only the thin shim needs wasm-level conformance runs.
357-
- **Reference plugins** (examples + bundled in the app):
358-
1. `battery-level.wasm` (Rust, ~1 KB) — 0x2A19; the "hello world"; also exercises
359-
coexistence with the native decoder for the same UUID.
360-
2. `ibeacon.wasm` (Rust) — conformance twin of the native `AppleBeacon` parser; tests
361-
diff their outputs byte-for-byte on the `MockAdvertisement` fixtures.
362-
3. `heart-rate.wasm` (Embedded Swift) — 0x2A37 flags/8-16-bit BPM/RR intervals; fills a
363-
real gap (`GATTHeartRateMeasurement` doesn't exist in BluetoothGATT) and exercises
364-
notifications live with any HR strap.
365-
4. (Post-v1) Eddystone 0xFEAA — exercises the service-data route.
366-
Built artifacts checked into the app repo; SDK-repo CI rebuilds and diffs hashes.
336+
## 6. Guest SDK — Embedded Swift (`PluginSDK/`, implemented)
337+
338+
Plugins are authored in **Embedded Swift** and compiled to wasm32 with the
339+
`swift-<version>_wasm-embedded` Swift SDK. This is the only supported guest toolchain: it keeps
340+
plugin authoring in the same language as the app, and the SDK is a normal SwiftPM package.
341+
342+
- **`PluginSDK/BLEPluginSDK`** — the guest SDK, Embedded-safe (no Foundation, no existentials, no
343+
`Codable`): `ParseInput`/`UUIDBytes` envelope decoding, an allocation-free `PayloadReader`
344+
cursor, a `Fields` builder that emits the CBOR result incrementally, and `PluginRuntime` for
345+
guest allocation and the packed-`u64` return convention. Keys/labels/units are `StaticString`,
346+
so they cost no allocation.
347+
- **Author experience** — one pure function, `(ParseInput) -> Fields?`. Returning `nil` means "not
348+
mine" and is not an error. `Exports.swift` is copied boilerplate declaring the ABI exports; it
349+
lives in the author's module because `@_expose(wasm)` symbols are dropped when they come from a
350+
dependency module (swiftlang/swift#77812).
351+
- **Build**`make install` in an example: `swift build --swift-sdk …_wasm-embedded`, then
352+
`wasm-opt -Oz`, then copy into `Sources/BluetoothExplorerPluginEngine/Plugins/` and refresh the
353+
manifest `sha256`. Expect ~90–110 KB per plugin.
354+
- **Verified end to end**: multi-module Embedded Swift (SDK as a separate package) links cleanly for
355+
wasm32; the resulting module exports exactly the ABI and imports only
356+
`wasi_snapshot_preview1.random_get`, which the host satisfies (§5.4).
357+
- **Bundled reference plugin**: `battery-level.wasm` (GATT Battery Level `0x2A19`), authored in
358+
Embedded Swift, tested for byte-parity against the native parser across values.
359+
- **Testing** — because the parse function is a plain Swift function over plain structs, authors can
360+
unit-test it natively before it ever becomes wasm; the app's test suite additionally runs the
361+
built module under the real interpreter.
362+
363+
Full non-embedded Swift is not supported for plugins (9–50 MB binaries). Other guest languages are
364+
out of scope: the ABI is language-agnostic, but only the Embedded Swift path is maintained here.
367365

368366
## 7. Testing
369367

@@ -405,12 +403,12 @@ MockCentral tests. App behaves identically-or-better with zero wasm.
405403

406404
**M2 — Wasm execution (2–3 weeks, shippable).**
407405
`PluginEngine` actor, ABI v1 host side, MicroCBOR, resource limiter, deadline/quarantine,
408-
memoization + CPU budget + kill switch. Bundled battery + iBeacon (Rust) plugins.
406+
memoization + CPU budget + kill switch. Bundled Embedded Swift battery-level plugin.
409407
`PluginManager` bundled discovery + enable/disable. Basic `PluginsView` (toggles).
410408
Golden-file tests both platforms.
411409

412410
**M3 — SDK + author tooling (2 weeks, parallelizable with M2 tail).**
413-
`ble-plugin-sdk` repo: Rust PDK + macro, `bleplug` CLI, conformance harness, Embedded Swift
411+
`PluginSDK/`: Embedded Swift guest SDK, example plugins, build/install tooling, Embedded Swift
414412
template + pinned toolchain docs, heart-rate reference plugin, docs site page.
415413

416414
**M4 — User-imported plugins + hardening (1.5–2 weeks).**
@@ -431,7 +429,7 @@ Total ≈ 8–11 weeks. M1 and M2 are each independently shippable.
431429
| No fuel/interruption in WasmKit — runaway plugin = abandoned spinning thread | dedicated thread per plugin, quarantine on first timeout, memory caps, CPU budget + kill switch; WAMR if containment becomes hard requirement |
432430
| App Store review of user-imported plugins (2.5.2 / 3.3.1(B)) | bundled-first rollout; import framed as same-purpose interpreted content; no marketplace ever |
433431
| `.copy` binary resources through skipstone into the APK unproven | M0 item 5; base64-embed escape hatch |
434-
| Embedded Swift toolchain churn (#77812 export-dropping, duplicate-symbol issues) | shims-in-template design, pinned SDK versions, Rust as reference PDK |
432+
| Embedded Swift toolchain churn (#77812 export-dropping, duplicate-symbol issues) | export shims live in the author's module by design; pin the wasm-embedded SDK version |
435433
| Dense-environment scan load (dozens of devices × ~10 adv/s) at interpreter speed | synchronous fingerprint dedupe before Task spawn, manifest-only routing, memoization, CPU budget; profile on low-end Android |
436434
| Hand-rolled CBOR in three places (host Swift, Embedded Swift, minicbor) | shared conformance vectors exercised on all three |
437435

0 commit comments

Comments
 (0)