Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions docs/LESSON.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -151,7 +162,9 @@
- **Verified Nitro C++ APIs** (compiled into the APK, use as-is):
- `#include <NitroModules/HybridObjectRegistry.hpp>`;
`auto o = HybridObjectRegistry::createHybridObject("Ecr17Transport");`
`auto t = std::static_pointer_cast<HybridEcr17TransportSpec>(o);`
`auto t = std::dynamic_pointer_cast<HybridEcr17TransportSpec>(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 <NitroModules/ArrayBuffer.hpp>`; `ArrayBuffer::copy(const std::vector<uint8_t>&)`,
`buf->data()` / `buf->size()`.
- Transport spec uses `std::shared_ptr<ArrayBuffer>` (NOT std::vector) for send/onData.
Expand Down
7 changes: 7 additions & 0 deletions package/cpp/Ecr17Client/HybridEcr17Client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_; }
Expand Down