@fireflydb/expo-driver 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -0
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +21 -0
- package/android/src/main/cpp/firefly_jni.cpp +110 -0
- package/android/src/main/jniLibs/arm64-v8a/libfirefly.so +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libfirefly.so +0 -0
- package/android/src/main/jniLibs/x86/libfirefly.so +0 -0
- package/android/src/main/jniLibs/x86_64/libfirefly.so +0 -0
- package/android/src/main/kotlin/com/fireflydb/expo/FireflyClientModule.kt +101 -0
- package/android/src/main/kotlin/com/fireflydb/expo/FireflyJniBridge.kt +58 -0
- package/expo-module.config.json +9 -0
- package/ios/Firefly.xcframework/Info.plist +44 -0
- package/ios/Firefly.xcframework/ios-arm64/Firefly.framework/Firefly +0 -0
- package/ios/Firefly.xcframework/ios-arm64/Firefly.framework/Info.plist +30 -0
- package/ios/Firefly.xcframework/ios-arm64_x86_64-simulator/Firefly.framework/Firefly +0 -0
- package/ios/Firefly.xcframework/ios-arm64_x86_64-simulator/Firefly.framework/Info.plist +30 -0
- package/ios/FireflyClient.podspec +33 -0
- package/ios/FireflyClientModule.swift +193 -0
- package/package.json +50 -0
- package/scripts/build-libfirefly.sh +224 -0
- package/src/FireflyClientModule.ts +83 -0
- package/src/drivers/secureStorage.ts +83 -0
- package/src/drivers/sqlite.ts +322 -0
- package/src/drivers/websocket.ts +166 -0
- package/src/index.ts +120 -0
- package/src/polyfill.ts +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# @fireflydb/expo
|
|
2
|
+
|
|
3
|
+
Expo native module that bundles the Rust `libfirefly` SQLite extension and
|
|
4
|
+
wires it into [expo-sqlite](https://docs.expo.dev/versions/latest/sdk/sqlite/),
|
|
5
|
+
[expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/),
|
|
6
|
+
and React Native WebSocket. Exposes a change-listener bridge so JS gets
|
|
7
|
+
notified of CRDT-relevant local writes.
|
|
8
|
+
|
|
9
|
+
This document focuses on **how the native core is built and shipped**. For
|
|
10
|
+
SDK usage, see [`packages/core`](../core).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Layout
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
packages/expo/
|
|
18
|
+
├── cpp/ # Cross-platform C++ shared by iOS + Android
|
|
19
|
+
│ ├── firefly_abi.h # Rust C-ABI declarations (firefly_add_change_listener, …)
|
|
20
|
+
│ ├── firefly_listener.h # Listener registry public surface
|
|
21
|
+
│ └── firefly_listener.cpp # Process-wide handle→dispatch registry
|
|
22
|
+
│
|
|
23
|
+
├── ios/
|
|
24
|
+
│ ├── Firefly.xcframework/ # ← produced by scripts/build-libfirefly.sh
|
|
25
|
+
│ ├── FireflyClient.podspec # Vends the xcframework + symlinks ../cpp into the pod
|
|
26
|
+
│ └── FireflyClientModule.swift
|
|
27
|
+
│
|
|
28
|
+
├── android/
|
|
29
|
+
│ ├── build.gradle # Wires jniLibs + externalNativeBuild (CMake)
|
|
30
|
+
│ └── src/main/
|
|
31
|
+
│ ├── cpp/CMakeLists.txt # Imports prebuilt libfirefly.so, builds libfirefly_jni.so
|
|
32
|
+
│ ├── cpp/firefly_jni.cpp # JNI shim
|
|
33
|
+
│ └── jniLibs/<abi>/libfirefly.so # ← produced by scripts/build-libfirefly.sh
|
|
34
|
+
│
|
|
35
|
+
├── scripts/
|
|
36
|
+
│ └── build-libfirefly.sh # Cross-compiles core/ for iOS + Android
|
|
37
|
+
│
|
|
38
|
+
└── src/ # TypeScript surface (drivers + module bindings)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The single source of truth for the native core is the `core/` Rust crate at
|
|
42
|
+
the repo root. Everything under `ios/Firefly.xcframework/` and
|
|
43
|
+
`android/src/main/jniLibs/` is **build output** (gitignored) — never hand-edit it.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Rebuilding the core
|
|
48
|
+
|
|
49
|
+
Run from this package directory:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm build:libfirefly # both platforms
|
|
53
|
+
bash scripts/build-libfirefly.sh ios # iOS only
|
|
54
|
+
bash scripts/build-libfirefly.sh android # Android only
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The script ([scripts/build-libfirefly.sh](scripts/build-libfirefly.sh))
|
|
58
|
+
cross-compiles `core/` with `--no-default-features --features
|
|
59
|
+
loadable_extension` so the resulting binary is a SQLite loadable extension
|
|
60
|
+
(no bundled SQLite — both Expo and Android ship their own).
|
|
61
|
+
|
|
62
|
+
### Prerequisites
|
|
63
|
+
|
|
64
|
+
| Tool | Used for | Auto-installed by script? |
|
|
65
|
+
|------|----------|---------------------------|
|
|
66
|
+
| `rustup` + Rust toolchain | All targets | No (must be on PATH) |
|
|
67
|
+
| iOS rust targets (`aarch64-apple-ios`, `aarch64-apple-ios-sim`, `x86_64-apple-ios`) | iOS | Yes (`rustup target add`) |
|
|
68
|
+
| Android rust targets (`aarch64-linux-android`, `armv7-linux-androideabi`, `x86_64-linux-android`, `i686-linux-android`) | Android | Yes |
|
|
69
|
+
| `xcodebuild`, `lipo`, `install_name_tool` | iOS framework assembly | No (Xcode CLT) |
|
|
70
|
+
| `cargo-ndk` | Android cross-compile | Yes (`cargo install`) |
|
|
71
|
+
| Android NDK r24+ (`ANDROID_NDK_HOME` exported) | Android | No |
|
|
72
|
+
|
|
73
|
+
### When to rebuild
|
|
74
|
+
|
|
75
|
+
Rebuild after **any change under `core/`** that affects the C-ABI surface
|
|
76
|
+
or runtime behavior. The `.xcframework` and `.so` files are gitignored build
|
|
77
|
+
output: the `prepack` hook rebuilds them on `pnpm pack` / `pnpm publish`, so
|
|
78
|
+
the published package ships binaries and consumers never need a Rust
|
|
79
|
+
toolchain. In a fresh clone, run `pnpm build:libfirefly` once before building
|
|
80
|
+
the example app.
|
|
81
|
+
|
|
82
|
+
The shared C++ in `cpp/` (`firefly_listener.{cpp,h}`) is **not** built by
|
|
83
|
+
this script — it's compiled by the platform build systems (CocoaPods on
|
|
84
|
+
iOS, Gradle/CMake on Android) when the host app builds. Editing it requires
|
|
85
|
+
no rebuild script run; just rebuild the host app.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## iOS pipeline
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
core/ ──cargo build──▶ target/<rust-target>/release/libfirefly.dylib
|
|
93
|
+
│
|
|
94
|
+
├─ aarch64-apple-ios ─┐
|
|
95
|
+
├─ aarch64-apple-ios-sim ─┼─lipo──▶ sim-universal
|
|
96
|
+
└─ x86_64-apple-ios ─┘
|
|
97
|
+
│
|
|
98
|
+
device-arm64.dylib sim-universal.dylib
|
|
99
|
+
│ │
|
|
100
|
+
ios_make_framework ios_make_framework
|
|
101
|
+
▼ ▼
|
|
102
|
+
Firefly.framework (device) Firefly.framework (sim-universal)
|
|
103
|
+
└──── xcodebuild -create-xcframework ────┘
|
|
104
|
+
▼
|
|
105
|
+
ios/Firefly.xcframework/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
What the script does, in order:
|
|
109
|
+
|
|
110
|
+
1. **Per-slice `cargo build`** for each iOS target into the workspace
|
|
111
|
+
`target/` dir (the workspace lives at the repo root, not under `core/`).
|
|
112
|
+
2. **`lipo`** the two simulator slices (`arm64-sim` + `x86_64`) into a
|
|
113
|
+
single fat dylib. iOS device stays as a single `arm64` slice.
|
|
114
|
+
3. **`ios_make_framework`** wraps each dylib in a proper
|
|
115
|
+
`Firefly.framework` bundle: copies the dylib in as the bundle binary
|
|
116
|
+
(named `Firefly`, no extension), rewrites its `LC_ID_DYLIB` install
|
|
117
|
+
name to `@rpath/Firefly.framework/Firefly` via `install_name_tool`, and
|
|
118
|
+
writes an `Info.plist` with the right `CFBundleSupportedPlatforms` /
|
|
119
|
+
`MinimumOSVersion`.
|
|
120
|
+
4. **`xcodebuild -create-xcframework`** combines the device and
|
|
121
|
+
simulator-universal frameworks into `Firefly.xcframework`. This is the
|
|
122
|
+
canonical Apple distribution shape and the only one CocoaPods accepts
|
|
123
|
+
via `s.vendored_frameworks` for **dynamic** linking.
|
|
124
|
+
5. **`nm` verification** that `_sqlite3_firefly_init` is exported in both
|
|
125
|
+
slices — that's the entry point `expo-sqlite` calls via
|
|
126
|
+
`sqlite3_load_extension`.
|
|
127
|
+
|
|
128
|
+
`FireflyClient.podspec` then:
|
|
129
|
+
- Vendors `Firefly.xcframework` (Xcode embeds the right slice into
|
|
130
|
+
`<App>.app/Frameworks/Firefly.framework/Firefly` and re-signs at archive
|
|
131
|
+
time).
|
|
132
|
+
- Pulls in the shared C++ via `ios/_shared/*.{cpp,h}` (a committed
|
|
133
|
+
directory of per-file symlinks pointing at `../cpp/*`). This avoids
|
|
134
|
+
a separate `.mm` shim — Swift binds to the C-ABI symbols via
|
|
135
|
+
`@_silgen_name`. See the long comment in
|
|
136
|
+
[`FireflyClient.podspec`](ios/FireflyClient.podspec) for why
|
|
137
|
+
`_shared/` is committed instead of generated by `prepare_command`
|
|
138
|
+
(CocoaPods skips `prepare_command` for `:path =>` pods).
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Android pipeline
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
core/ ──cargo ndk build──▶ target/<rust-target>/release/libfirefly.so
|
|
146
|
+
│
|
|
147
|
+
├─ aarch64-linux-android ─▶ jniLibs/arm64-v8a/libfirefly.so
|
|
148
|
+
├─ armv7-linux-androideabi ─▶ jniLibs/armeabi-v7a/libfirefly.so
|
|
149
|
+
├─ x86_64-linux-android ─▶ jniLibs/x86_64/libfirefly.so
|
|
150
|
+
└─ i686-linux-android ─▶ jniLibs/x86/libfirefly.so
|
|
151
|
+
|
|
152
|
+
(later, at app build time:)
|
|
153
|
+
|
|
154
|
+
android/src/main/cpp/firefly_jni.cpp + cpp/firefly_listener.cpp
|
|
155
|
+
│
|
|
156
|
+
CMake (externalNativeBuild)
|
|
157
|
+
▼
|
|
158
|
+
jniLibs/<abi>/libfirefly_jni.so (per ABI, links against libfirefly.so)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
What the script does:
|
|
162
|
+
|
|
163
|
+
1. **`cargo ndk --target <rust-target> --platform 24`** for each ABI. The
|
|
164
|
+
`--platform 24` matches `minSdkVersion 24` in `android/build.gradle`.
|
|
165
|
+
`cargo-ndk` handles wiring the NDK toolchain into Cargo so the Rust
|
|
166
|
+
compiler emits ABI-correct shared objects.
|
|
167
|
+
2. **Copies** each `libfirefly.so` into
|
|
168
|
+
`android/src/main/jniLibs/<abi>/`.
|
|
169
|
+
|
|
170
|
+
At app build time, Gradle (`android/build.gradle`):
|
|
171
|
+
- Picks up the prebuilt `libfirefly.so` from `jniLibs/<abi>/` and packages
|
|
172
|
+
them into the APK/AAB — `applicationInfo.nativeLibraryDir` resolves them
|
|
173
|
+
at runtime.
|
|
174
|
+
- Compiles `libfirefly_jni.so` per ABI via `externalNativeBuild` /
|
|
175
|
+
CMakeLists ([android/src/main/cpp/CMakeLists.txt](android/src/main/cpp/CMakeLists.txt)).
|
|
176
|
+
CMake imports `libfirefly.so` as an `IMPORTED` target and links the JNI
|
|
177
|
+
shim against it plus the shared `cpp/firefly_listener.cpp`.
|
|
178
|
+
- `packagingOptions { doNotStrip "**/libfirefly.so" }` — the Rust release
|
|
179
|
+
profile already strips, and stripping again can drop the
|
|
180
|
+
`sqlite3_firefly_init` export.
|
|
181
|
+
|
|
182
|
+
Kotlin loads the native library with `System.loadLibrary("firefly_jni")`,
|
|
183
|
+
which transitively pulls in `libfirefly.so` because of the import
|
|
184
|
+
relationship in CMake.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Shared C++ (`cpp/`)
|
|
189
|
+
|
|
190
|
+
Both platforms include `cpp/firefly_listener.cpp` directly in their build
|
|
191
|
+
([`firefly_listener.cpp`](cpp/firefly_listener.cpp)). It's a single
|
|
192
|
+
process-wide registry mapping a caller-supplied `int64_t handle` to a
|
|
193
|
+
platform dispatcher (Swift `sendEvent` on iOS, JNI `CallStaticVoidMethod`
|
|
194
|
+
on Android). Keeping this in one place means the lifetime/race semantics
|
|
195
|
+
required by `firefly_add_change_listener`'s contract are implemented
|
|
196
|
+
once, not twice.
|
|
197
|
+
|
|
198
|
+
Linking model:
|
|
199
|
+
- **iOS:** compiled into the `FireflyClient` pod alongside the Swift
|
|
200
|
+
module, with `-fvisibility=hidden`. Swift uses `@_silgen_name` to bind
|
|
201
|
+
to `firefly_listener_register` / `firefly_listener_unregister`.
|
|
202
|
+
- **Android:** compiled into `libfirefly_jni.so` by CMake. JNI calls into
|
|
203
|
+
it directly.
|
|
204
|
+
|
|
205
|
+
The C-ABI between this layer and Rust (`firefly_add_change_listener`,
|
|
206
|
+
`firefly_remove_change_listener`) is declared in
|
|
207
|
+
[`cpp/firefly_abi.h`](cpp/firefly_abi.h).
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Troubleshooting
|
|
212
|
+
|
|
213
|
+
**`sqlite3_firefly_init` not exported.** The script verifies this on iOS
|
|
214
|
+
and aborts the build. If it ever fires, check that `core/src/lib.rs` still
|
|
215
|
+
declares the entry point with the `sqlite_extension_init` macro and that
|
|
216
|
+
release-profile `strip = true` isn't dropping the symbol on a new
|
|
217
|
+
toolchain.
|
|
218
|
+
|
|
219
|
+
**Android NDK not found.** Export `ANDROID_NDK_HOME` (e.g.
|
|
220
|
+
`export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.1.12297006`). `cargo-ndk`
|
|
221
|
+
reads it.
|
|
222
|
+
|
|
223
|
+
**`linker 'cc' not found` on Android targets.** `cargo-ndk` injects the
|
|
224
|
+
right linker via env vars only inside its child `cargo` invocation. If you
|
|
225
|
+
ran `cargo build --target aarch64-linux-android` directly (not via
|
|
226
|
+
`cargo ndk`), you'll get this. Use the script.
|
|
227
|
+
|
|
228
|
+
**Stale `target/` cache after toolchain bump.** `cargo clean` at the repo
|
|
229
|
+
root, then re-run the script. The targets are cross builds so they don't
|
|
230
|
+
share intermediate artifacts with desktop dev builds, but a corrupt cache
|
|
231
|
+
across a Rust version bump can manifest as missing symbols.
|
|
232
|
+
|
|
233
|
+
**`Undefined symbol: _firefly_listener_register` at iOS link time.** The
|
|
234
|
+
shared C++ in `cpp/` is pulled into the pod via the committed
|
|
235
|
+
`ios/_shared/` directory of per-file symlinks. Two CocoaPods quirks
|
|
236
|
+
combined to cause earlier breakage here:
|
|
237
|
+
1. CocoaPods indexes pod sources via Ruby's `Find.find`, which does NOT
|
|
238
|
+
recurse into symlinked directories — so `_shared` cannot itself be
|
|
239
|
+
a symlink. It must be a real directory containing file-level
|
|
240
|
+
symlinks (which Find DOES list).
|
|
241
|
+
2. CocoaPods does NOT run `prepare_command` for development
|
|
242
|
+
(`:path =>`) pods. Anything that expects to be set up at
|
|
243
|
+
`pod install` time silently no-ops.
|
|
244
|
+
|
|
245
|
+
So `_shared/` is checked into git; if you add a file under `cpp/`,
|
|
246
|
+
mirror it with `ln -sfn ../../cpp/<name> ios/_shared/<name>` and commit.
|
|
247
|
+
After podspec or `_shared/` changes, re-run `pod install` (or
|
|
248
|
+
`npx expo prebuild --clean` for Expo apps) so the Pods xcodeproj
|
|
249
|
+
regenerates with the new sources. The `cannot link directly with
|
|
250
|
+
'SwiftUICore'` line that often appears in the Expo CLI output is
|
|
251
|
+
unrelated noise from Xcode 26 — check
|
|
252
|
+
`apps/<app>/.expo/xcodebuild.log` for the real `Undefined symbols`
|
|
253
|
+
section.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'org.jetbrains.kotlin.android'
|
|
4
|
+
id 'expo-module-gradle-plugin'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
group = 'com.fireflydb.expo'
|
|
8
|
+
version = '0.0.0'
|
|
9
|
+
|
|
10
|
+
android {
|
|
11
|
+
namespace "com.fireflydb.expo"
|
|
12
|
+
|
|
13
|
+
defaultConfig {
|
|
14
|
+
minSdkVersion 24
|
|
15
|
+
compileSdk 34
|
|
16
|
+
versionCode 1
|
|
17
|
+
versionName "0.0.0"
|
|
18
|
+
|
|
19
|
+
externalNativeBuild {
|
|
20
|
+
cmake {
|
|
21
|
+
cppFlags '-std=c++17', '-fvisibility=hidden'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
externalNativeBuild {
|
|
27
|
+
cmake {
|
|
28
|
+
// Builds libfirefly_jni.so per ABI. CMakeLists.txt pulls in the
|
|
29
|
+
// pre-built libfirefly.so from src/main/jniLibs/${ANDROID_ABI}/.
|
|
30
|
+
path "src/main/cpp/CMakeLists.txt"
|
|
31
|
+
version "3.22.1"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
sourceSets {
|
|
36
|
+
main {
|
|
37
|
+
// jniLibs/<abi>/libfirefly.so is produced by scripts/build-libfirefly.sh.
|
|
38
|
+
// Gradle picks them up here and packages them into the host APK/AAB
|
|
39
|
+
// so applicationInfo.nativeLibraryDir resolves them at runtime.
|
|
40
|
+
// libfirefly_jni.so is produced by the externalNativeBuild above
|
|
41
|
+
// and packaged automatically.
|
|
42
|
+
jniLibs.srcDirs += ['src/main/jniLibs']
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
packagingOptions {
|
|
47
|
+
// Don't strip libfirefly.so. The Rust release profile already strips
|
|
48
|
+
// symbols for size; we keep the SQLite extension entry point exported.
|
|
49
|
+
doNotStrip "**/libfirefly.so"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.22.1)
|
|
2
|
+
project(firefly_jni)
|
|
3
|
+
|
|
4
|
+
# Pre-built libfirefly.so per ABI is already in jniLibs/${ANDROID_ABI}/.
|
|
5
|
+
# Import it so the JNI shim can resolve the firefly_* C-ABI symbols at
|
|
6
|
+
# load time, then `System.loadLibrary("firefly_jni")` in Kotlin pulls
|
|
7
|
+
# both shared objects into the process.
|
|
8
|
+
add_library(firefly SHARED IMPORTED)
|
|
9
|
+
set_target_properties(firefly PROPERTIES IMPORTED_LOCATION
|
|
10
|
+
${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libfirefly.so)
|
|
11
|
+
|
|
12
|
+
# Cross-platform listener registry shared with the iOS module. Lives at
|
|
13
|
+
# packages/expo/cpp/ — sibling of android/ and ios/. The path here
|
|
14
|
+
# climbs out of android/src/main/cpp/ to the package root.
|
|
15
|
+
set(SHARED_CPP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp)
|
|
16
|
+
|
|
17
|
+
add_library(firefly_jni SHARED
|
|
18
|
+
firefly_jni.cpp
|
|
19
|
+
${SHARED_CPP_DIR}/firefly_listener.cpp)
|
|
20
|
+
target_include_directories(firefly_jni PRIVATE ${SHARED_CPP_DIR})
|
|
21
|
+
target_link_libraries(firefly_jni firefly log)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// JNI shim that bridges the cross-platform listener registry
|
|
2
|
+
// (`firefly_listener.{h,cpp}` at packages/expo/cpp/) to the JVM. The
|
|
3
|
+
// shared registry owns the libfirefly C-ABI lifetime and the
|
|
4
|
+
// integer-handle dispatch table; this file only marshals bytes between
|
|
5
|
+
// C++ and Kotlin's `FireflyJniBridge.dispatch(handle, byte[])`.
|
|
6
|
+
|
|
7
|
+
#include <jni.h>
|
|
8
|
+
#include <android/log.h>
|
|
9
|
+
#include <cstdint>
|
|
10
|
+
|
|
11
|
+
#include "firefly_listener.h"
|
|
12
|
+
|
|
13
|
+
#define LOG_TAG "FireflyJNI"
|
|
14
|
+
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
JavaVM* g_vm = nullptr;
|
|
19
|
+
jclass g_bridgeClass = nullptr; // global ref to FireflyJniBridge
|
|
20
|
+
jmethodID g_dispatchMethod = nullptr; // dispatch(long, byte[])
|
|
21
|
+
|
|
22
|
+
JNIEnv* getEnvOrAttach(bool* attached) {
|
|
23
|
+
*attached = false;
|
|
24
|
+
if (g_vm == nullptr) return nullptr;
|
|
25
|
+
JNIEnv* env = nullptr;
|
|
26
|
+
jint res = g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
|
|
27
|
+
if (res == JNI_OK) return env;
|
|
28
|
+
if (res == JNI_EDETACHED) {
|
|
29
|
+
if (g_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
|
|
30
|
+
*attached = true;
|
|
31
|
+
return env;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return nullptr;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void detachIfNeeded(bool attached) {
|
|
38
|
+
if (attached && g_vm != nullptr) {
|
|
39
|
+
g_vm->DetachCurrentThread();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Called by the shared C++ thunk on the SQLite writer thread for every
|
|
44
|
+
// fire that maps to a JNI-registered listener. Marshals the blob into
|
|
45
|
+
// a fresh `jbyteArray` and forwards via the Kotlin static dispatcher,
|
|
46
|
+
// which routes by handle and emits the Expo event.
|
|
47
|
+
void jniDispatch(int64_t handle, const uint8_t* blob, size_t len) {
|
|
48
|
+
if (g_vm == nullptr || g_bridgeClass == nullptr || g_dispatchMethod == nullptr) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
bool attached = false;
|
|
52
|
+
JNIEnv* env = getEnvOrAttach(&attached);
|
|
53
|
+
if (env == nullptr) return;
|
|
54
|
+
|
|
55
|
+
jbyteArray arr = env->NewByteArray(static_cast<jsize>(len));
|
|
56
|
+
if (arr != nullptr) {
|
|
57
|
+
env->SetByteArrayRegion(
|
|
58
|
+
arr, 0, static_cast<jsize>(len),
|
|
59
|
+
reinterpret_cast<const jbyte*>(blob));
|
|
60
|
+
env->CallStaticVoidMethod(
|
|
61
|
+
g_bridgeClass, g_dispatchMethod, static_cast<jlong>(handle), arr);
|
|
62
|
+
if (env->ExceptionCheck()) {
|
|
63
|
+
env->ExceptionDescribe();
|
|
64
|
+
env->ExceptionClear();
|
|
65
|
+
}
|
|
66
|
+
env->DeleteLocalRef(arr);
|
|
67
|
+
}
|
|
68
|
+
detachIfNeeded(attached);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} // namespace
|
|
72
|
+
|
|
73
|
+
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
|
|
74
|
+
g_vm = vm;
|
|
75
|
+
JNIEnv* env = nullptr;
|
|
76
|
+
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
|
77
|
+
return JNI_ERR;
|
|
78
|
+
}
|
|
79
|
+
jclass local = env->FindClass("com/fireflydb/expo/FireflyJniBridge");
|
|
80
|
+
if (local == nullptr) {
|
|
81
|
+
LOGE("FireflyJniBridge class not found");
|
|
82
|
+
return JNI_ERR;
|
|
83
|
+
}
|
|
84
|
+
g_bridgeClass = static_cast<jclass>(env->NewGlobalRef(local));
|
|
85
|
+
env->DeleteLocalRef(local);
|
|
86
|
+
g_dispatchMethod = env->GetStaticMethodID(g_bridgeClass, "dispatch", "(J[B)V");
|
|
87
|
+
if (g_dispatchMethod == nullptr) {
|
|
88
|
+
LOGE("FireflyJniBridge.dispatch(J[B)V not found");
|
|
89
|
+
return JNI_ERR;
|
|
90
|
+
}
|
|
91
|
+
return JNI_VERSION_1_6;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
extern "C" JNIEXPORT jlong JNICALL
|
|
95
|
+
Java_com_fireflydb_expo_FireflyJniBridge_nativeRegister(
|
|
96
|
+
JNIEnv* env, jclass /*clazz*/, jstring path, jlong handle) {
|
|
97
|
+
if (path == nullptr) return 0;
|
|
98
|
+
const char* utf = env->GetStringUTFChars(path, nullptr);
|
|
99
|
+
if (utf == nullptr) return 0;
|
|
100
|
+
uint64_t rust_id = firefly_listener_register(
|
|
101
|
+
utf, static_cast<int64_t>(handle), &jniDispatch);
|
|
102
|
+
env->ReleaseStringUTFChars(path, utf);
|
|
103
|
+
return static_cast<jlong>(rust_id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
extern "C" JNIEXPORT void JNICALL
|
|
107
|
+
Java_com_fireflydb_expo_FireflyJniBridge_nativeUnregister(
|
|
108
|
+
JNIEnv* /*env*/, jclass /*clazz*/, jlong handle) {
|
|
109
|
+
firefly_listener_unregister(static_cast<int64_t>(handle));
|
|
110
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
package com.fireflydb.expo
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
import java.io.File
|
|
6
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
7
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
8
|
+
|
|
9
|
+
class FireflyClientModule : Module() {
|
|
10
|
+
private val nextSwiftId = AtomicLong(1)
|
|
11
|
+
// Maps the JS-visible id to the JNI dispatch handle. The shared
|
|
12
|
+
// C++ registry tracks the libfirefly rust-id internally so we no
|
|
13
|
+
// longer keep it on the Kotlin side.
|
|
14
|
+
private val listeners = ConcurrentHashMap<Long, Long>()
|
|
15
|
+
|
|
16
|
+
override fun definition() = ModuleDefinition {
|
|
17
|
+
Name("FireflyClient")
|
|
18
|
+
|
|
19
|
+
Events("FireflyChange")
|
|
20
|
+
|
|
21
|
+
Function("getLibraryPath") {
|
|
22
|
+
// Resolve libfirefly.so against the app's native library dir.
|
|
23
|
+
// Returning an absolute path (rather than the bare name "firefly")
|
|
24
|
+
// sidesteps Android linker quirks: dlopen("firefly") on bionic does
|
|
25
|
+
// not auto-prepend "lib" or scan LD_LIBRARY_PATH the way glibc does.
|
|
26
|
+
//
|
|
27
|
+
// Strip the ".so" suffix before returning. SQLite's
|
|
28
|
+
// sqlite3_load_extension auto-appends the platform extension when
|
|
29
|
+
// the path lacks one; if we returned "/.../libfirefly.so" we'd
|
|
30
|
+
// end up with "libfirefly.so.so" and dlopen would fail.
|
|
31
|
+
val nativeLibDir = appContext.reactContext?.applicationInfo?.nativeLibraryDir
|
|
32
|
+
?: throw IllegalStateException("FireflyClient: reactContext unavailable")
|
|
33
|
+
|
|
34
|
+
val absolute = File(nativeLibDir, "libfirefly.so")
|
|
35
|
+
if (!absolute.exists()) {
|
|
36
|
+
throw IllegalStateException(
|
|
37
|
+
"FireflyClient: libfirefly.so not found at ${absolute.absolutePath}. " +
|
|
38
|
+
"Did the host app rebuild after the @fireflydb/expo install?"
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
absolute.absolutePath.removeSuffix(".so")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Function("getEntryPoint") {
|
|
45
|
+
"sqlite3_firefly_init"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Register a libfirefly change listener against the SQLite database
|
|
49
|
+
// file at `path`. The native side dispatches every encoded RowState
|
|
50
|
+
// batch to JS via the "FireflyChange" event. Returns the JS-visible
|
|
51
|
+
// listener id.
|
|
52
|
+
Function("addChangeListener") { path: String ->
|
|
53
|
+
val swiftId = nextSwiftId.getAndIncrement()
|
|
54
|
+
|
|
55
|
+
// Capture the swiftId so the dispatcher knows which JS-side
|
|
56
|
+
// listener to attribute the event to.
|
|
57
|
+
val jniHandle = FireflyJniBridge.register { blob ->
|
|
58
|
+
this@FireflyClientModule.sendEvent(
|
|
59
|
+
"FireflyChange",
|
|
60
|
+
mapOf(
|
|
61
|
+
"id" to swiftId.toDouble(),
|
|
62
|
+
"blob" to blob,
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
val rustId = FireflyJniBridge.nativeRegister(path, jniHandle)
|
|
68
|
+
if (rustId == 0L) {
|
|
69
|
+
FireflyJniBridge.unregister(jniHandle)
|
|
70
|
+
throw IllegalStateException(
|
|
71
|
+
"firefly_listener_register rejected path '$path' " +
|
|
72
|
+
"(in-memory DB or invalid path?)"
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
listeners[swiftId] = jniHandle
|
|
76
|
+
// JS sees a Number; Long fits below 2^53 in any realistic session.
|
|
77
|
+
swiftId.toDouble()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Function("removeChangeListener") { handle: Double ->
|
|
81
|
+
val swiftId = handle.toLong()
|
|
82
|
+
val jniHandle = listeners.remove(swiftId) ?: return@Function
|
|
83
|
+
// Stop libfirefly dispatch first (via shared C++ registry),
|
|
84
|
+
// then drop the JVM-side lambda. Both lookups are tolerant
|
|
85
|
+
// of stale fires.
|
|
86
|
+
FireflyJniBridge.nativeUnregister(jniHandle)
|
|
87
|
+
FireflyJniBridge.unregister(jniHandle)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
OnDestroy {
|
|
91
|
+
// Best-effort cleanup if the module is being torn down (hot
|
|
92
|
+
// reload, app shutdown). Drop every still-registered listener.
|
|
93
|
+
val pending = listeners.toMap()
|
|
94
|
+
listeners.clear()
|
|
95
|
+
for ((_, jniHandle) in pending) {
|
|
96
|
+
FireflyJniBridge.nativeUnregister(jniHandle)
|
|
97
|
+
FireflyJniBridge.unregister(jniHandle)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package com.fireflydb.expo
|
|
2
|
+
|
|
3
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
4
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JVM-side counterpart to libfirefly_jni.so. Holds a static dispatch
|
|
8
|
+
* table so the JNI thread (where the cross-platform listener registry
|
|
9
|
+
* fires its dispatcher on libfirefly's writer-thread callback) has a
|
|
10
|
+
* stable, lookup-by-handle path back into Kotlin.
|
|
11
|
+
*
|
|
12
|
+
* The shared C++ registry at `packages/expo/cpp/firefly_listener.cpp`
|
|
13
|
+
* owns the libfirefly C-ABI lifetime and the integer-handle dispatch.
|
|
14
|
+
* `nativeRegister`/`nativeUnregister` delegate to that registry; the
|
|
15
|
+
* `dispatch(handle, blob)` static is the JVM half — JNI calls it on
|
|
16
|
+
* fire, we look up the handle and invoke the registered Kotlin lambda.
|
|
17
|
+
*
|
|
18
|
+
* Stale fires after `unregister` are silent no-ops via lookup miss in
|
|
19
|
+
* BOTH this Kotlin table AND the shared C++ registry, which is how we
|
|
20
|
+
* honor the libfirefly contract that user_data outlive
|
|
21
|
+
* `firefly_remove_change_listener`.
|
|
22
|
+
*/
|
|
23
|
+
internal object FireflyJniBridge {
|
|
24
|
+
init {
|
|
25
|
+
// Pulls in libfirefly_jni.so AND its link-time dependency
|
|
26
|
+
// libfirefly.so. Subsequent SQLite `loadExtension` calls on the
|
|
27
|
+
// same .so are idempotent; dlopen returns the same handle.
|
|
28
|
+
System.loadLibrary("firefly_jni")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private val listeners = ConcurrentHashMap<Long, (ByteArray) -> Unit>()
|
|
32
|
+
private val nextHandle = AtomicLong(1)
|
|
33
|
+
|
|
34
|
+
/** Register a Kotlin callback against a fresh handle. */
|
|
35
|
+
fun register(callback: (ByteArray) -> Unit): Long {
|
|
36
|
+
val handle = nextHandle.getAndIncrement()
|
|
37
|
+
listeners[handle] = callback
|
|
38
|
+
return handle
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Stop dispatching to the lambda registered under `handle`. */
|
|
42
|
+
fun unregister(handle: Long) {
|
|
43
|
+
listeners.remove(handle)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Called from `firefly_jni.cpp` on the SQLite writer thread.
|
|
48
|
+
* Lookup-and-fire is intentionally cheap; the lambda usually just
|
|
49
|
+
* forwards via `sendEvent` and returns.
|
|
50
|
+
*/
|
|
51
|
+
@JvmStatic
|
|
52
|
+
fun dispatch(handle: Long, blob: ByteArray) {
|
|
53
|
+
listeners[handle]?.invoke(blob)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
external fun nativeRegister(path: String, handle: Long): Long
|
|
57
|
+
external fun nativeUnregister(handle: Long)
|
|
58
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>AvailableLibraries</key>
|
|
6
|
+
<array>
|
|
7
|
+
<dict>
|
|
8
|
+
<key>BinaryPath</key>
|
|
9
|
+
<string>Firefly.framework/Firefly</string>
|
|
10
|
+
<key>LibraryIdentifier</key>
|
|
11
|
+
<string>ios-arm64</string>
|
|
12
|
+
<key>LibraryPath</key>
|
|
13
|
+
<string>Firefly.framework</string>
|
|
14
|
+
<key>SupportedArchitectures</key>
|
|
15
|
+
<array>
|
|
16
|
+
<string>arm64</string>
|
|
17
|
+
</array>
|
|
18
|
+
<key>SupportedPlatform</key>
|
|
19
|
+
<string>ios</string>
|
|
20
|
+
</dict>
|
|
21
|
+
<dict>
|
|
22
|
+
<key>BinaryPath</key>
|
|
23
|
+
<string>Firefly.framework/Firefly</string>
|
|
24
|
+
<key>LibraryIdentifier</key>
|
|
25
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
26
|
+
<key>LibraryPath</key>
|
|
27
|
+
<string>Firefly.framework</string>
|
|
28
|
+
<key>SupportedArchitectures</key>
|
|
29
|
+
<array>
|
|
30
|
+
<string>arm64</string>
|
|
31
|
+
<string>x86_64</string>
|
|
32
|
+
</array>
|
|
33
|
+
<key>SupportedPlatform</key>
|
|
34
|
+
<string>ios</string>
|
|
35
|
+
<key>SupportedPlatformVariant</key>
|
|
36
|
+
<string>simulator</string>
|
|
37
|
+
</dict>
|
|
38
|
+
</array>
|
|
39
|
+
<key>CFBundlePackageType</key>
|
|
40
|
+
<string>XFWK</string>
|
|
41
|
+
<key>XCFrameworkFormatVersion</key>
|
|
42
|
+
<string>1.0</string>
|
|
43
|
+
</dict>
|
|
44
|
+
</plist>
|
|
Binary file
|