@rn-org/react-native-thread 0.7.0 → 0.8.0
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 +69 -22
- package/ReactNativeThread.podspec +6 -0
- package/android/build.gradle +15 -2
- package/android/src/main/cpp/CMakeLists.txt +24 -0
- package/android/src/main/cpp/HermesThreadEngine.cpp +388 -0
- package/android/src/main/cpp/HermesThreadEngine.h +55 -0
- package/android/src/main/java/com/rnorg/reactnativethread/HermesThreadEngine.kt +35 -0
- package/android/src/main/java/com/rnorg/reactnativethread/ReactNativeThreadModule.kt +30 -91
- package/ios/HermesThreadEngine.cpp +263 -0
- package/ios/HermesThreadEngine.h +49 -0
- package/ios/ReactNativeThread.mm +24 -49
- package/lib/module/babel-plugin.js +100 -21
- package/lib/module/babel-plugin.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/package.json +1 -1
- package/src/babel-plugin.js +124 -29
- package/src/index.tsx +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rn-org/react-native-thread
|
|
2
2
|
|
|
3
|
-
Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses **
|
|
3
|
+
Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses **Hermes** on both iOS and Android, each on a dedicated OS-level thread. Built as a New Architecture **TurboModule**.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -41,17 +41,18 @@ Run JavaScript on real background threads in React Native — no Workers, no Wor
|
|
|
41
41
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
44
|
-
- **True background threads** — each thread runs its own
|
|
44
|
+
- **True background threads** — each thread runs its own isolated **Hermes** runtime on a dedicated OS-level thread; the main React Native runtime is never blocked. Works on both **iOS** and **Android**.
|
|
45
45
|
- **Unlimited threads** — create as many threads as you need; each is isolated.
|
|
46
46
|
- **Shared thread** (`runOnJS`) — fire-and-forget tasks on a single persistent background thread; no teardown required.
|
|
47
47
|
- **Per-thread message passing** — call `resolveThreadMessage(data)` from any thread, receive it on the main thread with `onMessage` (callback or `await`).
|
|
48
48
|
- **Parameter injection** — pass values from the main thread into the background function as a typed argument `(args) => { ... }`. Supports primitives, objects, arrays, **and functions**.
|
|
49
|
-
- **Function params** — pass functions (including imported ones) alongside plain data in the params object. The Babel plugin extracts their source at compile time
|
|
49
|
+
- **Function params** — pass functions (including imported ones) alongside plain data in the params object. The Babel plugin extracts their source at compile time and captures closed-over variables transitively.
|
|
50
50
|
- **Cross-module function params** — functions imported from other files (e.g. `import { compute } from './math'`) are automatically resolved and inlined by the Babel plugin.
|
|
51
51
|
- **Named threads** — give threads friendly names; list or destroy them by name.
|
|
52
52
|
- **Error handling** — thread exceptions are automatically caught and forwarded to the main thread. Use `onError` or `.catch()` on the `onMessage()` promise.
|
|
53
53
|
- **Full `console` support** — `console.log/info/warn/error/debug` work inside threads and appear in Logcat / Xcode logs.
|
|
54
|
-
- **
|
|
54
|
+
- **Timer support** — `setTimeout`, `setInterval`, `clearTimeout`, and `clearInterval` work inside threads.
|
|
55
|
+
- **Hermes-safe** — ships a Babel plugin that extracts function source at compile time so Hermes bytecode never breaks serialisation. Required on both iOS and Android since both platforms use Hermes.
|
|
55
56
|
- **New Architecture only** — built on the TurboModule / Codegen pipeline.
|
|
56
57
|
|
|
57
58
|
---
|
|
@@ -81,7 +82,9 @@ yarn add @rn-org/react-native-thread
|
|
|
81
82
|
cd ios && pod install
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
The `hermes-engine` pod is declared as a dependency in the library's podspec; no extra configuration needed.
|
|
86
|
+
|
|
87
|
+
**Android** — the Hermes native dependency is declared in the library's `build.gradle`; no extra steps needed.
|
|
85
88
|
|
|
86
89
|
---
|
|
87
90
|
|
|
@@ -93,8 +96,7 @@ Hermes compiles your JS to bytecode at build time. That means `fn.toString()` at
|
|
|
93
96
|
2. Detects **function-valued properties** in the params object (second argument) and inlines their source.
|
|
94
97
|
3. **Captures closed-over variables** transitively — if a function references outer `const`/`let`/`var` bindings (literals or other functions), those are bundled into a self-contained IIFE.
|
|
95
98
|
4. **Resolves cross-module imports** — functions imported from relative paths (e.g. `import { fn } from './utils'`) are read from disk and inlined.
|
|
96
|
-
5. **
|
|
97
|
-
6. **Strips TypeScript** annotations automatically when the source file is `.ts` or `.tsx`.
|
|
99
|
+
5. **Strips TypeScript** annotations automatically when the source file is `.ts` or `.tsx`.
|
|
98
100
|
|
|
99
101
|
Add the plugin to your app's `babel.config.js`:
|
|
100
102
|
|
|
@@ -109,7 +111,9 @@ module.exports = {
|
|
|
109
111
|
};
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
The plugin is a no-op on non-Hermes builds (
|
|
114
|
+
The plugin is a no-op on non-Hermes builds (V8, etc.).
|
|
115
|
+
|
|
116
|
+
> **Note:** The plugin safely handles cases where `@react-native/babel-preset` transforms `async` functions before the plugin's visitor fires. It falls back to the original source text via Babel's preserved AST positions.
|
|
113
117
|
|
|
114
118
|
---
|
|
115
119
|
|
|
@@ -149,6 +153,24 @@ thread.run(
|
|
|
149
153
|
{ x: 6, y: 7, multiply }
|
|
150
154
|
);
|
|
151
155
|
|
|
156
|
+
// Async function params work too — the Babel plugin transforms async/await
|
|
157
|
+
// to generator-based code before sending to the Hermes thread runtime:
|
|
158
|
+
const isPrime = async (num) => {
|
|
159
|
+
if (num <= 1) return 'Not Prime';
|
|
160
|
+
for (let i = 2; i < num; i++) {
|
|
161
|
+
if (num % i === 0) return 'Not Prime';
|
|
162
|
+
}
|
|
163
|
+
return 'Prime';
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
thread.run(
|
|
167
|
+
async (args) => {
|
|
168
|
+
const result = await args.isPrime(args.num);
|
|
169
|
+
resolveThreadMessage(`${args.num} is ${result}`);
|
|
170
|
+
},
|
|
171
|
+
{ num: 17, isPrime }
|
|
172
|
+
);
|
|
173
|
+
|
|
152
174
|
// Functions that close over outer variables work too:
|
|
153
175
|
const factor = 10;
|
|
154
176
|
const scale = (n) => n * factor;
|
|
@@ -404,11 +426,29 @@ console.log(data); // { status: 'done', value: 42 }
|
|
|
404
426
|
|
|
405
427
|
`console.log`, `.info`, `.warn`, `.error`, and `.debug` are all available and route to:
|
|
406
428
|
|
|
407
|
-
- **iOS** — `NSLog`, visible in Xcode / Console.app; tagged `[RNThread-<id>]`.
|
|
429
|
+
- **iOS** — `NSLog`, visible in Xcode / Console.app; tagged `[RNThread-<id>] [Hermes]`.
|
|
408
430
|
- **Android** — `android.util.Log`, visible in Logcat; tagged `RNThread-<id>`.
|
|
409
431
|
|
|
410
432
|
---
|
|
411
433
|
|
|
434
|
+
### Timers
|
|
435
|
+
|
|
436
|
+
`setTimeout`, `clearTimeout`, `setInterval`, and `clearInterval` are available inside threads.
|
|
437
|
+
|
|
438
|
+
Both iOS and Android use the same Hermes-based event loop: after the initial evaluation, the thread drains all pending timers (and microtasks) before returning. The thread blocks until all timers have fired or been cleared.
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
thread.run(() => {
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
resolveThreadMessage('delayed hello');
|
|
444
|
+
}, 2000);
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
> **Note:** The event loop blocks the thread until timers complete. Be careful with `setInterval` — the thread won't finish until the interval is cleared inside the thread.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
412
452
|
### `__params__`
|
|
413
453
|
|
|
414
454
|
```ts
|
|
@@ -469,16 +509,20 @@ The included Babel plugin runs at **compile time** — before Hermes touches the
|
|
|
469
509
|
|
|
470
510
|
#### Task function (first argument)
|
|
471
511
|
|
|
472
|
-
Arrow functions and function expressions are replaced with
|
|
512
|
+
Arrow functions and function expressions (including `async`) are replaced with a string literal wrapped as an IIFE. `async`/`await` is transformed to generator-based code because Hermes' eval-mode compiler does not support async syntax:
|
|
473
513
|
|
|
474
514
|
```js
|
|
475
515
|
// Input (your source)
|
|
476
|
-
thread.run((args) => {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
516
|
+
thread.run(async (args) => {
|
|
517
|
+
const result = await args.compute(args.num);
|
|
518
|
+
resolveThreadMessage(result);
|
|
519
|
+
}, { num: 42, compute });
|
|
520
|
+
|
|
521
|
+
// Output (what Hermes compiles) — async transformed, compute inlined
|
|
522
|
+
thread.run("((function(){ ... _asyncToGenerator ... })())(__params__)", {
|
|
523
|
+
num: 42,
|
|
524
|
+
compute: { __rnThreadFn: "(function(){ ... })()" }
|
|
525
|
+
});
|
|
482
526
|
```
|
|
483
527
|
|
|
484
528
|
#### Function params (second argument)
|
|
@@ -497,7 +541,7 @@ thread.run(fn, { multiply });
|
|
|
497
541
|
// Output — transitive closures are captured in a self-contained IIFE
|
|
498
542
|
thread.run(fn, {
|
|
499
543
|
multiply: {
|
|
500
|
-
__rnThreadFn: "(function(){ var b = 3;\nvar add =
|
|
544
|
+
__rnThreadFn: "(function(){ var b = 3;\nvar add = (x) => x + b;\nreturn function multiply(a) { return add(a) * 2; };})()"
|
|
501
545
|
}
|
|
502
546
|
});
|
|
503
547
|
```
|
|
@@ -515,7 +559,7 @@ export function checkEvenOdd(num: number): string {
|
|
|
515
559
|
// App.tsx
|
|
516
560
|
import { checkEvenOdd } from './utils';
|
|
517
561
|
thread.run(fn, { checkEvenOdd });
|
|
518
|
-
// → checkEvenOdd is inlined with TypeScript stripped
|
|
562
|
+
// → checkEvenOdd is inlined with TypeScript stripped
|
|
519
563
|
```
|
|
520
564
|
|
|
521
565
|
### Supported call sites
|
|
@@ -532,8 +576,10 @@ thread.run(fn, { checkEvenOdd });
|
|
|
532
576
|
| Value type | Captured |
|
|
533
577
|
|---|---|
|
|
534
578
|
| Inline arrow / function expression | Yes |
|
|
579
|
+
| Inline `async` arrow / function expression | Yes (transformed to generator) |
|
|
535
580
|
| Reference to local `function` declaration | Yes |
|
|
536
581
|
| Reference to local `const fn = () => ...` | Yes |
|
|
582
|
+
| Reference to local `const fn = async () => ...` | Yes (transformed to generator) |
|
|
537
583
|
| Imported function (`import { fn } from './mod'`) | Yes (relative paths only) |
|
|
538
584
|
| Closed-over `const`/`let`/`var` with literal init | Yes (transitively) |
|
|
539
585
|
| Closed-over function references | Yes (transitively) |
|
|
@@ -542,10 +588,10 @@ thread.run(fn, { checkEvenOdd });
|
|
|
542
588
|
### Limitations
|
|
543
589
|
|
|
544
590
|
- **Captured closures must be statically resolvable**: only variables initialized with literals and functions (local or imported) are captured. Runtime-computed values (e.g. `const x = fetchValue()`) must be passed explicitly as params.
|
|
591
|
+
- **`async` functions in params are supported** — the plugin transforms them to generator-based code before extraction. `await` works inside both task functions and function params.
|
|
545
592
|
- **Cross-module resolution** only follows **relative imports** (e.g. `'./utils'`). Package imports (e.g. `'lodash'`) are not resolved.
|
|
546
|
-
- Thread function bodies must be **ASCII-safe**: Rhino (Android) does not support non-ASCII identifier characters in source mode.
|
|
547
593
|
- `undefined`, `Map`, `Set`, and class instances are not serialisable as param values — use plain objects, arrays, strings, numbers, booleans, or `null`.
|
|
548
|
-
- Thread functions run in an **isolated
|
|
594
|
+
- Thread functions run in an **isolated Hermes runtime** with no access to the React tree, native modules, or the main thread's global scope.
|
|
549
595
|
|
|
550
596
|
---
|
|
551
597
|
|
|
@@ -553,12 +599,13 @@ thread.run(fn, { checkEvenOdd });
|
|
|
553
599
|
|
|
554
600
|
| Constraint | Reason |
|
|
555
601
|
|---|---|
|
|
556
|
-
| Thread functions run in isolation | They execute in a completely separate
|
|
602
|
+
| Thread functions run in isolation | They execute in a completely separate Hermes runtime (iOS and Android) |
|
|
603
|
+
| Hermes eval-mode has no `async`/`await` | The Babel plugin transforms async to generators before extraction |
|
|
557
604
|
| Function params must be statically resolvable | The Babel plugin extracts source at compile time |
|
|
558
605
|
| Non-function `params` must be JSON-serialisable | Serialised via `JSON.stringify` at runtime |
|
|
559
606
|
| `resolveThreadMessage` payload must be JSON-serialisable | Transported as a JSON string over the bridge |
|
|
560
607
|
| Cross-module resolution is relative-only | The plugin reads files from disk using the import path |
|
|
561
|
-
|
|
|
608
|
+
| No `fetch` / `XMLHttpRequest` | Thread runtimes have no network stack |
|
|
562
609
|
| New Architecture required | TurboModule / Codegen only; no bridge fallback |
|
|
563
610
|
|
|
564
611
|
---
|
|
@@ -16,5 +16,11 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
17
|
s.private_header_files = "ios/**/*.h"
|
|
18
18
|
|
|
19
|
+
s.pod_target_xcconfig = {
|
|
20
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
install_modules_dependencies(s)
|
|
24
|
+
|
|
25
|
+
s.dependency "hermes-engine"
|
|
20
26
|
end
|
package/android/build.gradle
CHANGED
|
@@ -40,10 +40,24 @@ android {
|
|
|
40
40
|
defaultConfig {
|
|
41
41
|
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
42
|
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
43
|
+
|
|
44
|
+
externalNativeBuild {
|
|
45
|
+
cmake {
|
|
46
|
+
cppFlags "-std=c++17"
|
|
47
|
+
arguments "-DANDROID_STL=c++_shared"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
externalNativeBuild {
|
|
53
|
+
cmake {
|
|
54
|
+
path "src/main/cpp/CMakeLists.txt"
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
buildFeatures {
|
|
46
59
|
buildConfig true
|
|
60
|
+
prefab true
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
buildTypes {
|
|
@@ -64,6 +78,5 @@ android {
|
|
|
64
78
|
|
|
65
79
|
dependencies {
|
|
66
80
|
implementation "com.facebook.react:react-android"
|
|
67
|
-
|
|
68
|
-
implementation "org.mozilla:rhino:1.7.15"
|
|
81
|
+
implementation "com.facebook.react:hermes-android"
|
|
69
82
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.13)
|
|
2
|
+
project(react_native_thread)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
|
|
6
|
+
find_package(fbjni REQUIRED CONFIG)
|
|
7
|
+
find_package(ReactAndroid REQUIRED CONFIG)
|
|
8
|
+
find_package(hermes-engine REQUIRED CONFIG)
|
|
9
|
+
|
|
10
|
+
add_library(react_native_thread SHARED
|
|
11
|
+
HermesThreadEngine.cpp
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
target_include_directories(react_native_thread PRIVATE
|
|
15
|
+
${CMAKE_CURRENT_SOURCE_DIR}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
target_link_libraries(react_native_thread
|
|
19
|
+
fbjni::fbjni
|
|
20
|
+
ReactAndroid::jsi
|
|
21
|
+
hermes-engine::hermesvm
|
|
22
|
+
android
|
|
23
|
+
log
|
|
24
|
+
)
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#include "HermesThreadEngine.h"
|
|
2
|
+
|
|
3
|
+
#include <android/log.h>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <thread>
|
|
6
|
+
#include <unordered_map>
|
|
7
|
+
|
|
8
|
+
#define LOG_TAG "RNThread"
|
|
9
|
+
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
|
10
|
+
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
|
11
|
+
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
12
|
+
|
|
13
|
+
using namespace facebook;
|
|
14
|
+
|
|
15
|
+
namespace rnthread {
|
|
16
|
+
|
|
17
|
+
// ──────────────────────────────────────────────────────────────────
|
|
18
|
+
// HermesThreadRuntime
|
|
19
|
+
// ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
HermesThreadRuntime::HermesThreadRuntime(
|
|
22
|
+
JNIEnv *env,
|
|
23
|
+
jobject module,
|
|
24
|
+
long threadId)
|
|
25
|
+
: runtime_(facebook::hermes::makeHermesRuntime(
|
|
26
|
+
::hermes::vm::RuntimeConfig::Builder()
|
|
27
|
+
.withES6Promise(true)
|
|
28
|
+
.withMicrotaskQueue(true)
|
|
29
|
+
.build())),
|
|
30
|
+
moduleRef_(nullptr), jvm_(nullptr) {
|
|
31
|
+
env->GetJavaVM(&jvm_);
|
|
32
|
+
moduleRef_ = env->NewWeakGlobalRef(module);
|
|
33
|
+
|
|
34
|
+
installConsole(threadId);
|
|
35
|
+
installTimers();
|
|
36
|
+
installResolveThreadMessage(env, module, threadId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
HermesThreadRuntime::~HermesThreadRuntime() {
|
|
40
|
+
runtime_.reset();
|
|
41
|
+
if (jvm_ && moduleRef_) {
|
|
42
|
+
JNIEnv *env = nullptr;
|
|
43
|
+
bool didAttach = false;
|
|
44
|
+
if (jvm_->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) ==
|
|
45
|
+
JNI_EDETACHED) {
|
|
46
|
+
jvm_->AttachCurrentThread(&env, nullptr);
|
|
47
|
+
didAttach = true;
|
|
48
|
+
}
|
|
49
|
+
if (env) {
|
|
50
|
+
env->DeleteWeakGlobalRef(moduleRef_);
|
|
51
|
+
}
|
|
52
|
+
if (didAttach) {
|
|
53
|
+
jvm_->DetachCurrentThread();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void HermesThreadRuntime::evaluate(
|
|
59
|
+
const std::string &code,
|
|
60
|
+
const std::string &sourceURL) {
|
|
61
|
+
auto buf = std::make_shared<jsi::StringBuffer>(code);
|
|
62
|
+
runtime_->evaluateJavaScript(buf, sourceURL);
|
|
63
|
+
// Run the event loop: drain microtasks and pending timers until
|
|
64
|
+
// both queues are empty.
|
|
65
|
+
drainTimerLoop();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ──────────────────────────────────────────────────────────────────
|
|
69
|
+
// Timer event loop
|
|
70
|
+
// ──────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
void HermesThreadRuntime::drainTimerLoop() {
|
|
73
|
+
auto &rt = *runtime_;
|
|
74
|
+
// Keep a JS object that maps timerId → callback so we hold references.
|
|
75
|
+
auto callbacks =
|
|
76
|
+
rt.global()
|
|
77
|
+
.getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
rt.drainMicrotasks();
|
|
81
|
+
|
|
82
|
+
if (timerQueue_.empty()) break;
|
|
83
|
+
|
|
84
|
+
auto now = std::chrono::steady_clock::now();
|
|
85
|
+
auto &next = timerQueue_.top();
|
|
86
|
+
|
|
87
|
+
if (next.fireAt > now) {
|
|
88
|
+
// Sleep until the next timer is due.
|
|
89
|
+
std::this_thread::sleep_until(next.fireAt);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pop and fire all timers that are now due.
|
|
93
|
+
bool fired = false;
|
|
94
|
+
while (!timerQueue_.empty()) {
|
|
95
|
+
now = std::chrono::steady_clock::now();
|
|
96
|
+
if (timerQueue_.top().fireAt > now) break;
|
|
97
|
+
|
|
98
|
+
auto entry = timerQueue_.top();
|
|
99
|
+
timerQueue_.pop();
|
|
100
|
+
|
|
101
|
+
// Skip cancelled timers.
|
|
102
|
+
if (cancelledTimers_.count(entry.id)) {
|
|
103
|
+
cancelledTimers_.erase(entry.id);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
auto idStr = std::to_string(entry.id);
|
|
108
|
+
auto cb = callbacks.getProperty(rt, idStr.c_str());
|
|
109
|
+
if (cb.isObject() && cb.getObject(rt).isFunction(rt)) {
|
|
110
|
+
cb.getObject(rt).asFunction(rt).call(rt);
|
|
111
|
+
fired = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (entry.repeating) {
|
|
115
|
+
// Re-enqueue for the next interval.
|
|
116
|
+
entry.fireAt = std::chrono::steady_clock::now() +
|
|
117
|
+
std::chrono::milliseconds(entry.intervalMs);
|
|
118
|
+
timerQueue_.push(entry);
|
|
119
|
+
} else {
|
|
120
|
+
// One-shot: remove the callback reference.
|
|
121
|
+
callbacks.setProperty(rt, idStr.c_str(), jsi::Value::undefined());
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (fired) {
|
|
126
|
+
// Drain microtasks generated by the callbacks.
|
|
127
|
+
rt.drainMicrotasks();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If no more timers, we're done.
|
|
131
|
+
if (timerQueue_.empty()) break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ──────────────────────────────────────────────────────────────────
|
|
136
|
+
// setTimeout / setInterval / clearTimeout / clearInterval
|
|
137
|
+
// ──────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
void HermesThreadRuntime::installTimers() {
|
|
140
|
+
auto &rt = *runtime_;
|
|
141
|
+
|
|
142
|
+
// Hidden object to hold callback references so they aren't GC'd.
|
|
143
|
+
rt.global().setProperty(
|
|
144
|
+
rt, "__rnTimerCallbacks__", jsi::Object(rt));
|
|
145
|
+
|
|
146
|
+
// Pointer to `this` captured by the lambdas — safe because the
|
|
147
|
+
// lambdas only run while `evaluate()` is executing on the owning thread.
|
|
148
|
+
auto *self = this;
|
|
149
|
+
|
|
150
|
+
// ── setTimeout(fn, ms) → id ──
|
|
151
|
+
auto setTimeoutFn = jsi::Function::createFromHostFunction(
|
|
152
|
+
rt, jsi::PropNameID::forAscii(rt, "setTimeout"), 2,
|
|
153
|
+
[self](jsi::Runtime &rt, const jsi::Value &,
|
|
154
|
+
const jsi::Value *args, size_t count) -> jsi::Value {
|
|
155
|
+
if (count < 1 || !args[0].isObject() ||
|
|
156
|
+
!args[0].getObject(rt).isFunction(rt))
|
|
157
|
+
return jsi::Value::undefined();
|
|
158
|
+
|
|
159
|
+
int ms = count >= 2 ? static_cast<int>(args[1].asNumber()) : 0;
|
|
160
|
+
int id = self->nextTimerId_++;
|
|
161
|
+
|
|
162
|
+
// Store callback in the hidden map.
|
|
163
|
+
auto cbs = rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
164
|
+
cbs.setProperty(rt, std::to_string(id).c_str(),
|
|
165
|
+
args[0].getObject(rt));
|
|
166
|
+
|
|
167
|
+
TimerEntry entry;
|
|
168
|
+
entry.id = id;
|
|
169
|
+
entry.fireAt = std::chrono::steady_clock::now() +
|
|
170
|
+
std::chrono::milliseconds(ms);
|
|
171
|
+
entry.repeating = false;
|
|
172
|
+
entry.intervalMs = 0;
|
|
173
|
+
self->timerQueue_.push(entry);
|
|
174
|
+
|
|
175
|
+
return jsi::Value(id);
|
|
176
|
+
});
|
|
177
|
+
rt.global().setProperty(rt, "setTimeout", std::move(setTimeoutFn));
|
|
178
|
+
|
|
179
|
+
// ── setInterval(fn, ms) → id ──
|
|
180
|
+
auto setIntervalFn = jsi::Function::createFromHostFunction(
|
|
181
|
+
rt, jsi::PropNameID::forAscii(rt, "setInterval"), 2,
|
|
182
|
+
[self](jsi::Runtime &rt, const jsi::Value &,
|
|
183
|
+
const jsi::Value *args, size_t count) -> jsi::Value {
|
|
184
|
+
if (count < 1 || !args[0].isObject() ||
|
|
185
|
+
!args[0].getObject(rt).isFunction(rt))
|
|
186
|
+
return jsi::Value::undefined();
|
|
187
|
+
|
|
188
|
+
int ms = count >= 2 ? static_cast<int>(args[1].asNumber()) : 0;
|
|
189
|
+
if (ms <= 0) ms = 1; // Prevent tight infinite loop.
|
|
190
|
+
int id = self->nextTimerId_++;
|
|
191
|
+
|
|
192
|
+
auto cbs = rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
193
|
+
cbs.setProperty(rt, std::to_string(id).c_str(),
|
|
194
|
+
args[0].getObject(rt));
|
|
195
|
+
|
|
196
|
+
TimerEntry entry;
|
|
197
|
+
entry.id = id;
|
|
198
|
+
entry.fireAt = std::chrono::steady_clock::now() +
|
|
199
|
+
std::chrono::milliseconds(ms);
|
|
200
|
+
entry.repeating = true;
|
|
201
|
+
entry.intervalMs = ms;
|
|
202
|
+
self->timerQueue_.push(entry);
|
|
203
|
+
|
|
204
|
+
return jsi::Value(id);
|
|
205
|
+
});
|
|
206
|
+
rt.global().setProperty(rt, "setInterval", std::move(setIntervalFn));
|
|
207
|
+
|
|
208
|
+
// ── clearTimeout(id) / clearInterval(id) ──
|
|
209
|
+
auto clearTimerFn = jsi::Function::createFromHostFunction(
|
|
210
|
+
rt, jsi::PropNameID::forAscii(rt, "clearTimeout"), 1,
|
|
211
|
+
[self](jsi::Runtime &rt, const jsi::Value &,
|
|
212
|
+
const jsi::Value *args, size_t count) -> jsi::Value {
|
|
213
|
+
if (count >= 1 && args[0].isNumber()) {
|
|
214
|
+
int id = static_cast<int>(args[0].asNumber());
|
|
215
|
+
self->cancelledTimers_.insert(id);
|
|
216
|
+
// Also remove the callback reference.
|
|
217
|
+
auto cbs =
|
|
218
|
+
rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
219
|
+
cbs.setProperty(rt, std::to_string(id).c_str(),
|
|
220
|
+
jsi::Value::undefined());
|
|
221
|
+
}
|
|
222
|
+
return jsi::Value::undefined();
|
|
223
|
+
});
|
|
224
|
+
rt.global().setProperty(rt, "clearTimeout", clearTimerFn);
|
|
225
|
+
rt.global().setProperty(rt, "clearInterval", std::move(clearTimerFn));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ──────────────────────────────────────────────────────────────────
|
|
229
|
+
// console.{log,info,warn,error,debug}
|
|
230
|
+
// ──────────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
void HermesThreadRuntime::installConsole(long threadId) {
|
|
233
|
+
auto &rt = *runtime_;
|
|
234
|
+
|
|
235
|
+
auto makeLogFn = [&rt, threadId](int prio) {
|
|
236
|
+
return jsi::Function::createFromHostFunction(
|
|
237
|
+
rt,
|
|
238
|
+
jsi::PropNameID::forAscii(rt, "log"),
|
|
239
|
+
1,
|
|
240
|
+
[threadId, prio](
|
|
241
|
+
jsi::Runtime &rt,
|
|
242
|
+
const jsi::Value &,
|
|
243
|
+
const jsi::Value *args,
|
|
244
|
+
size_t count) -> jsi::Value {
|
|
245
|
+
std::string msg;
|
|
246
|
+
for (size_t i = 0; i < count; ++i) {
|
|
247
|
+
if (i > 0) msg += ' ';
|
|
248
|
+
msg += args[i].toString(rt).utf8(rt);
|
|
249
|
+
}
|
|
250
|
+
std::string tag = "RNThread-" + std::to_string(threadId);
|
|
251
|
+
__android_log_print(prio, tag.c_str(), "%s", msg.c_str());
|
|
252
|
+
return jsi::Value::undefined();
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
auto console = jsi::Object(rt);
|
|
257
|
+
console.setProperty(rt, "log", makeLogFn(ANDROID_LOG_INFO));
|
|
258
|
+
console.setProperty(rt, "info", makeLogFn(ANDROID_LOG_INFO));
|
|
259
|
+
console.setProperty(rt, "warn", makeLogFn(ANDROID_LOG_WARN));
|
|
260
|
+
console.setProperty(rt, "error", makeLogFn(ANDROID_LOG_ERROR));
|
|
261
|
+
console.setProperty(rt, "debug", makeLogFn(ANDROID_LOG_DEBUG));
|
|
262
|
+
rt.global().setProperty(rt, "console", std::move(console));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ──────────────────────────────────────────────────────────────────
|
|
266
|
+
// resolveThreadMessage(data)
|
|
267
|
+
// ──────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
void HermesThreadRuntime::installResolveThreadMessage(
|
|
270
|
+
JNIEnv *env,
|
|
271
|
+
jobject module,
|
|
272
|
+
long threadId) {
|
|
273
|
+
auto &rt = *runtime_;
|
|
274
|
+
|
|
275
|
+
// We need a shared pointer to the weak global ref and JVM pointer
|
|
276
|
+
// so the lambda can outlive this scope.
|
|
277
|
+
JavaVM *jvm = jvm_;
|
|
278
|
+
jobject weakModule = moduleRef_;
|
|
279
|
+
|
|
280
|
+
auto fn = jsi::Function::createFromHostFunction(
|
|
281
|
+
rt,
|
|
282
|
+
jsi::PropNameID::forAscii(rt, "resolveThreadMessage"),
|
|
283
|
+
1,
|
|
284
|
+
[jvm, weakModule, threadId](
|
|
285
|
+
jsi::Runtime &rt,
|
|
286
|
+
const jsi::Value &,
|
|
287
|
+
const jsi::Value *args,
|
|
288
|
+
size_t count) -> jsi::Value {
|
|
289
|
+
// JSON.stringify(args[0])
|
|
290
|
+
auto json = rt.global().getPropertyAsObject(rt, "JSON");
|
|
291
|
+
auto stringify = json.getPropertyAsFunction(rt, "stringify");
|
|
292
|
+
std::string serialised;
|
|
293
|
+
if (count > 0) {
|
|
294
|
+
auto result = stringify.call(rt, args[0]);
|
|
295
|
+
if (result.isString()) {
|
|
296
|
+
serialised = result.getString(rt).utf8(rt);
|
|
297
|
+
} else {
|
|
298
|
+
serialised = "null";
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
serialised = "null";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Call back to Java on whatever thread we're on.
|
|
305
|
+
JNIEnv *jniEnv = nullptr;
|
|
306
|
+
bool didAttach = false;
|
|
307
|
+
if (jvm->GetEnv(
|
|
308
|
+
reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6) ==
|
|
309
|
+
JNI_EDETACHED) {
|
|
310
|
+
jvm->AttachCurrentThread(&jniEnv, nullptr);
|
|
311
|
+
didAttach = true;
|
|
312
|
+
}
|
|
313
|
+
if (!jniEnv) return jsi::Value::undefined();
|
|
314
|
+
|
|
315
|
+
// Resolve the weak ref
|
|
316
|
+
jobject strongModule = jniEnv->NewLocalRef(weakModule);
|
|
317
|
+
if (!strongModule) {
|
|
318
|
+
if (didAttach) jvm->DetachCurrentThread();
|
|
319
|
+
return jsi::Value::undefined();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
jclass cls = jniEnv->GetObjectClass(strongModule);
|
|
323
|
+
jmethodID mid = jniEnv->GetMethodID(
|
|
324
|
+
cls, "onThreadMessage", "(DLjava/lang/String;)V");
|
|
325
|
+
if (mid) {
|
|
326
|
+
jstring jData = jniEnv->NewStringUTF(serialised.c_str());
|
|
327
|
+
jniEnv->CallVoidMethod(
|
|
328
|
+
strongModule, mid, static_cast<jdouble>(threadId), jData);
|
|
329
|
+
jniEnv->DeleteLocalRef(jData);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
jniEnv->DeleteLocalRef(cls);
|
|
333
|
+
jniEnv->DeleteLocalRef(strongModule);
|
|
334
|
+
if (didAttach) jvm->DetachCurrentThread();
|
|
335
|
+
|
|
336
|
+
return jsi::Value::undefined();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
rt.global().setProperty(rt, "resolveThreadMessage", std::move(fn));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
} // namespace rnthread
|
|
343
|
+
|
|
344
|
+
// ──────────────────────────────────────────────────────────────────
|
|
345
|
+
// JNI entry points
|
|
346
|
+
// ──────────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
extern "C" {
|
|
349
|
+
|
|
350
|
+
JNIEXPORT jlong JNICALL
|
|
351
|
+
Java_com_rnorg_reactnativethread_HermesThreadEngine_nativeCreate(
|
|
352
|
+
JNIEnv *env,
|
|
353
|
+
jobject /* thiz */,
|
|
354
|
+
jobject module,
|
|
355
|
+
jlong threadId) {
|
|
356
|
+
auto *rt = new rnthread::HermesThreadRuntime(env, module, threadId);
|
|
357
|
+
return reinterpret_cast<jlong>(rt);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
JNIEXPORT void JNICALL
|
|
361
|
+
Java_com_rnorg_reactnativethread_HermesThreadEngine_nativeEvaluate(
|
|
362
|
+
JNIEnv *env,
|
|
363
|
+
jobject /* thiz */,
|
|
364
|
+
jlong ptr,
|
|
365
|
+
jstring code,
|
|
366
|
+
jstring sourceURL) {
|
|
367
|
+
auto *rt = reinterpret_cast<rnthread::HermesThreadRuntime *>(ptr);
|
|
368
|
+
const char *cCode = env->GetStringUTFChars(code, nullptr);
|
|
369
|
+
const char *cUrl = env->GetStringUTFChars(sourceURL, nullptr);
|
|
370
|
+
try {
|
|
371
|
+
rt->evaluate(std::string(cCode), std::string(cUrl));
|
|
372
|
+
} catch (const std::exception &e) {
|
|
373
|
+
LOGE("JS exception: %s", e.what());
|
|
374
|
+
}
|
|
375
|
+
env->ReleaseStringUTFChars(code, cCode);
|
|
376
|
+
env->ReleaseStringUTFChars(sourceURL, cUrl);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
JNIEXPORT void JNICALL
|
|
380
|
+
Java_com_rnorg_reactnativethread_HermesThreadEngine_nativeDestroy(
|
|
381
|
+
JNIEnv * /* env */,
|
|
382
|
+
jobject /* thiz */,
|
|
383
|
+
jlong ptr) {
|
|
384
|
+
auto *rt = reinterpret_cast<rnthread::HermesThreadRuntime *>(ptr);
|
|
385
|
+
delete rt;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
} // extern "C"
|