diff --git a/AGENTS.md b/AGENTS.md index e19cb1a..9630efd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,24 @@ still has open, valid reviewer comments. via `HybridObjectRegistry::createHybridObject(name)`. Kotlin HybridObjects get an auto-generated JNI bridge (no manual JNI); use `Promise.parallel{}` for blocking work, `ArrayBuffer.copy(ByteArray)/toByteArray()`. +- **Nitro Android native integration (JNI) — gotchas only reproducible by RUNNING + the app, not by the build** (full detail in docs/LESSON.md): + 1. The C++ impl file MUST be named after `implementationClassName` from + `nitro.json` (`HybridEcr17Client.{hpp,cpp}`) and its dir be in CMake + `include_directories` — the generated `*OnLoad.cpp` does a flat + `#include "HybridEcr17Client.hpp"`. + 2. Downcast `createHybridObject` results with `dynamic_pointer_cast` (HybridObject + is a *virtual* base); null-check. + 3. Autolinking + `.so` load needs `package/react-native.config.js` (declares + android/ios) so RN registers `Ecr17Package`, whose `companion init` runs + `System.loadLibrary`. + 4. Commands run on Nitro worker threads → attach to the JVM with fbjni + `ThreadScope` (`#ifdef __ANDROID__`) before any C++→Kotlin call, else "Unable + to retrieve jni environment". + 5. `createHybridObject` (JNI `FindClass`) must run on the **JS thread** (do it in + `configure()`), because attached worker threads use the system class loader → + `ClassNotFoundException`. fbjni caches the jclass so later worker-thread method + calls work. - Don't reuse a Nitro-generated struct name in our namespace (clash) — e.g. our parser DCC struct is `DccInfo`, not `CurrencyExchange`. - ECR17: status code is lowercase `'s'`; payment `'P'` = 167 bytes; progress diff --git a/docs/LESSON.md b/docs/LESSON.md index d351f7e..3bf1359 100644 --- a/docs/LESSON.md +++ b/docs/LESSON.md @@ -43,6 +43,16 @@ guarded by `#ifdef __ANDROID__` (no-op on iOS). We attach in `ensureConnected`, `runTransaction`, `runAckOnly` (the worker-thread paths that hit the transport). Only reproducible by RUNNING the app — not by the build. +- **`ClassNotFoundException` for the Kotlin HybridObject when created from a + worker thread.** After attaching a Nitro worker thread with `ThreadScope`, + `createHybridObject` does a JNI `FindClass` that resolves against the *thread's* + class loader — an attached worker thread gets the SYSTEM class loader (the error + shows `DexPathList[... /system/lib64 ...]`), which can't see app classes. Fix: + perform `ensureInit()` (the `createHybridObject`) on the **JS thread** (in + `configure()`), which has the app class loader; fbjni caches the resolved + `jclass` globally, so later method calls from worker threads work (they only need + a `JNIEnv`, supplied by the `ThreadScope` guards). Only reproducible by RUNNING + the app. (Alternative: `ThreadScope::WithClassLoader`.) - **Emit `DISCONNECTED` on a failed connect**, else listeners stay stuck on `CONNECTING`. `connect()` delegates to `ensureConnected()` which emits CONNECTING→(CONNECTED | DISCONNECTED on throw). @@ -77,7 +87,8 @@ - iOS `package/Ecr17.podspec` globs `cpp/**/*.{hpp,cpp}` — new C++ auto-included. - C++20 on both; Android NDK provides POSIX sockets in libc (no extra link lib). - Include convention: cross-unit includes are subdir-qualified from the `../cpp` - root, e.g. `#include "Lcr/Lcr.hpp"`, `#include "Ecr17Client/Ecr17Client.hpp"`. + root, e.g. `#include "Lcr/Lcr.hpp"`, `#include "Ecr17Client/HybridEcr17Client.hpp"` + (the client impl file is named after its Nitro class — see Build wiring). ## ECR17 protocol facts (from docs/) - Status command code is lowercase `'s'` (0x73). Payment `'P'` request = 167 bytes. @@ -151,7 +162,9 @@ - **Verified Nitro C++ APIs** (compiled into the APK, use as-is): - `#include `; `auto o = HybridObjectRegistry::createHybridObject("Ecr17Transport");` - `auto t = std::static_pointer_cast(o);` + `auto t = std::dynamic_pointer_cast(o);` (NOT + static_pointer_cast — HybridObject is a virtual base; null-check `t`). Call + `createHybridObject` on the JS thread (app class loader) — see Runtime (Android). - `#include `; `ArrayBuffer::copy(const std::vector&)`, `buf->data()` / `buf->size()`. - Transport spec uses `std::shared_ptr` (NOT std::vector) for send/onData. diff --git a/package/cpp/Ecr17Client/HybridEcr17Client.cpp b/package/cpp/Ecr17Client/HybridEcr17Client.cpp index 8f867bb..74ab716 100644 --- a/package/cpp/Ecr17Client/HybridEcr17Client.cpp +++ b/package/cpp/Ecr17Client/HybridEcr17Client.cpp @@ -227,6 +227,13 @@ void HybridEcr17Client::configure(const Ecr17Config& config) { session_.reset(); adapter_.reset(); transport_.reset(); + // Create the transport HybridObject NOW, on this (JS) thread. createHybridObject + // does a JNI FindClass for the Kotlin transport, which resolves only against the + // app class loader — and the JS thread has it. Doing it lazily on a Nitro worker + // thread (attached via ThreadScope) would use the system class loader and throw + // ClassNotFoundException. fbjni caches the resolved jclass globally, so later + // method calls from worker threads work (they only need a JNIEnv, see the guards). + ensureInit(); } Ecr17Config HybridEcr17Client::configuration() { return config_; }