Skip to content

Commit f960de4

Browse files
authored
Merge pull request #16 from MillerTechnologyPeru/feature/wasm-parser-plugins
WASM parser plugins: runtime, host engine and Embedded Swift SDK
2 parents ba6b7bd + d43573e commit f960de4

48 files changed

Lines changed: 3938 additions & 36 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,6 @@ Android/app/src/main/swift/.swiftpm
8181
# VS Code
8282
.vscode
8383

84+
85+
# Plugin SDK build artifacts (the installed module lives under Sources/.../Plugins)
86+
PluginSDK/Examples/*/*.wasm

Darwin/BluetoothExplorer.xcconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*] = YES
2828
INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault
2929
INFOPLIST_KEY_UISupportedInterfaceOrientations[sdk=iphone*] = UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown
3030

31-
IPHONEOS_DEPLOYMENT_TARGET = 17.0
32-
MACOSX_DEPLOYMENT_TARGET = 14.0
31+
IPHONEOS_DEPLOYMENT_TARGET = 18.0
32+
MACOSX_DEPLOYMENT_TARGET = 15.0
3333
SUPPORTS_MACCATALYST = NO
3434

3535
// iPhone + iPad

Documentation/PluginABI.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# BLE Parser Plugin ABI v1 (normative)
2+
3+
A plugin is a WebAssembly module that decodes a BLE advertisement field or GATT attribute value into
4+
structured fields. The host is `BluetoothExplorerPluginEngine`, running the module under the WasmKit
5+
interpreter.
6+
7+
This document is normative. The host implementation lives in
8+
`Sources/BluetoothExplorerPluginEngine/PluginABI.swift`; plugins are authored in Embedded Swift
9+
against `PluginSDK/BLEPluginSDK`, with a complete example in `PluginSDK/Examples/BatteryLevel`.
10+
11+
## Module requirements
12+
13+
- **Target.** `wasm32-unknown-wasip1` built as a **reactor** (`-mexec-model=reactor`) — what the
14+
Embedded Swift SDK emits. Import-free core modules are also accepted.
15+
- **Imports.** Only `wasi_snapshot_preview1` may be imported; any other import module is rejected at
16+
load. The host satisfies those imports with a minimal shim rather than a real WASI
17+
implementation: `random_get` fills guest memory with real random bytes (the Embedded Swift runtime
18+
requires it), and every other WASI function is stubbed to return success with zeroed results. A
19+
plugin therefore has no filesystem, network, environment or clock access, and cannot call back
20+
into the host.
21+
- **No JIT / no threads required.** Pure computation only.
22+
23+
## Required exports
24+
25+
| Export | Signature | Purpose |
26+
|---|---|---|
27+
| `bleplug_abi_1` | `() -> ()` | Version marker. Never called; its presence declares ABI major 1. |
28+
| `bleplug_alloc` | `(size: i32) -> i32` | Return a pointer to `size` writable bytes in linear memory, or `0` on failure. The guest owns all allocation. |
29+
| `memory` | (exported linear memory) | The host reads and writes here. |
30+
31+
## Capability exports (optional; presence = capability)
32+
33+
| Export | Signature | Invoked for |
34+
|---|---|---|
35+
| `bleplug_parse_manufacturer` | `(ptr: i32, len: i32) -> i64` | Manufacturer-specific advertisement data |
36+
| `bleplug_parse_service_data` | `(ptr: i32, len: i32) -> i64` | Service data advertisement fields |
37+
| `bleplug_parse_characteristic` | `(ptr: i32, len: i32) -> i64` | GATT characteristic values |
38+
| `bleplug_parse_descriptor` | `(ptr: i32, len: i32) -> i64` | GATT descriptor values |
39+
40+
Each declared match category in the manifest must have its corresponding export, or the plugin is
41+
rejected at load.
42+
43+
## Optional exports
44+
45+
| Export | Signature | Purpose |
46+
|---|---|---|
47+
| `bleplug_free` | `(ptr: i32, size: i32) -> ()` | Called on the input and output buffers after the host copies them out. |
48+
| `bleplug_reset` | `() -> ()` | Called after each parse to reset a per-call arena. |
49+
| `_initialize` | `() -> ()` | wasip1 reactor init; called once after instantiation. |
50+
51+
## Call sequence
52+
53+
For each parse, the host:
54+
55+
1. Encodes the input envelope (below) into `n` bytes.
56+
2. Calls `bleplug_alloc(n)``ptr` (non-zero).
57+
3. Writes the envelope into `memory` at `ptr`.
58+
4. Calls the capability export `(ptr, n)` → packed `i64`.
59+
5. Interprets the result: `0` means "not mine / no result" (not an error). Otherwise the value is
60+
`(result_ptr << 32) | result_len`, and the host reads `result_len` bytes from `memory` at
61+
`result_ptr`.
62+
6. Calls `bleplug_free` and `bleplug_reset` if exported.
63+
64+
The parse function must be a **pure function of the envelope**: the host may cache results for
65+
identical inputs and may recycle the instance between calls.
66+
67+
## Input envelope
68+
69+
Little-endian scalars; the UUID is 16 RFC-4122 **big-endian** bytes.
70+
71+
```
72+
offset size field
73+
0 1 envelope_version (= 1)
74+
1 1 kind (1=manufacturer, 2=serviceData, 3=characteristic, 4=descriptor)
75+
2 2 company_id (u16; 0xFFFF unless kind == 1)
76+
4 16 uuid (zeroed when kind == 1; 16/32-bit UUIDs promoted via the base UUID)
77+
20 4 payload_len (u32)
78+
24 n payload (manufacturer additionalData with company id stripped, or
79+
service-data value, or characteristic/descriptor value)
80+
```
81+
82+
## Output (CBOR)
83+
84+
Definite-length, integer-keyed CBOR:
85+
86+
```
87+
{
88+
0: <summary text>? ; optional one-line title
89+
1: [ ; fields
90+
{ 0: <key text>, ; stable machine key
91+
1: <label text>, ; human label
92+
2: <value>, ; tstr | uint | negint | float64 | bool | bstr | tag37 bstr(uuid)
93+
3: <unit text>? } ; optional unit
94+
, ...
95+
]
96+
}
97+
```
98+
99+
- Byte fields use a CBOR byte string; UUID fields use tag 37 over a 16-byte string.
100+
- The host decoder is strict: max depth 8, max 64 items per collection, max 1 KiB per string, and
101+
the total output must not exceed the manifest's `maxOutputBytes`.
102+
103+
## Manifest sidecar (`<name>.bleplugin.json`)
104+
105+
```json
106+
{
107+
"manifestVersion": 1,
108+
"id": "org.example.plugin.foo",
109+
"name": "Foo",
110+
"version": "1.0.0",
111+
"abi": 1,
112+
"module": "foo.wasm",
113+
"sha256": "<lowercase hex of foo.wasm>",
114+
"matches": {
115+
"companyIdentifiers": [76],
116+
"serviceDataUUIDs": ["FEAA"],
117+
"characteristicUUIDs": ["2A19"],
118+
"descriptorUUIDs": []
119+
},
120+
"limits": { "maxMemoryPages": 16, "maxOutputBytes": 16384 }
121+
}
122+
```
123+
124+
Routing is driven entirely by `matches` — the host never executes a plugin for data it did not
125+
declare. `sha256` is mandatory for user-imported plugins and verified before load.
126+
127+
## Resource limits & failure isolation
128+
129+
- Guest linear memory is capped at `maxMemoryPages` (default 16, ceiling 64) × 64 KiB.
130+
- Each parse call has a wall-clock deadline (default 50 ms). The one-time cost of parsing,
131+
instantiating and `_initialize`-ing a module is bounded separately by a generous warmup deadline
132+
(default 5 s), and WasmKit compiles eagerly, so translation never lands on the per-call deadline.
133+
On timeout the plugin is **quarantined** (WasmKit has no execution interruption; the wedged call
134+
is abandoned).
135+
- A trap, allocation failure, out-of-bounds result, oversized output, or malformed CBOR is a
136+
failure; three consecutive failures quarantine the plugin. Failures never propagate — the UI
137+
falls back to the raw hex view.

0 commit comments

Comments
 (0)