@rn-org/react-native-thread 0.7.1 → 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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <jni.h>
|
|
4
|
+
#include <jsi/jsi.h>
|
|
5
|
+
#include <hermes/hermes.h>
|
|
6
|
+
#include <chrono>
|
|
7
|
+
#include <memory>
|
|
8
|
+
#include <mutex>
|
|
9
|
+
#include <queue>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <unordered_set>
|
|
12
|
+
|
|
13
|
+
namespace rnthread {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Holds a standalone Hermes runtime for a single background thread.
|
|
17
|
+
* All evaluateJavaScript / global-injection calls must happen on the
|
|
18
|
+
* same thread that created the runtime (enforced by the caller in Kotlin).
|
|
19
|
+
*/
|
|
20
|
+
class HermesThreadRuntime {
|
|
21
|
+
public:
|
|
22
|
+
HermesThreadRuntime(JNIEnv *env, jobject module, long threadId);
|
|
23
|
+
~HermesThreadRuntime();
|
|
24
|
+
|
|
25
|
+
void evaluate(const std::string &code, const std::string &sourceURL);
|
|
26
|
+
|
|
27
|
+
private:
|
|
28
|
+
void installConsole(long threadId);
|
|
29
|
+
void installResolveThreadMessage(JNIEnv *env, jobject module, long threadId);
|
|
30
|
+
void installTimers();
|
|
31
|
+
void drainTimerLoop();
|
|
32
|
+
|
|
33
|
+
std::unique_ptr<facebook::hermes::HermesRuntime> runtime_;
|
|
34
|
+
jobject moduleRef_;
|
|
35
|
+
JavaVM *jvm_;
|
|
36
|
+
|
|
37
|
+
// ── Timer state ──
|
|
38
|
+
struct TimerEntry {
|
|
39
|
+
int id;
|
|
40
|
+
std::chrono::steady_clock::time_point fireAt;
|
|
41
|
+
bool repeating;
|
|
42
|
+
int intervalMs;
|
|
43
|
+
|
|
44
|
+
bool operator>(const TimerEntry &o) const { return fireAt > o.fireAt; }
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Min-heap ordered by fire time.
|
|
48
|
+
std::priority_queue<TimerEntry, std::vector<TimerEntry>,
|
|
49
|
+
std::greater<TimerEntry>>
|
|
50
|
+
timerQueue_;
|
|
51
|
+
std::unordered_set<int> cancelledTimers_;
|
|
52
|
+
int nextTimerId_ = 1;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
} // namespace rnthread
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.rnorg.reactnativethread
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin JNI wrapper around the C++ HermesThreadRuntime.
|
|
5
|
+
* Each instance owns a standalone Hermes runtime.
|
|
6
|
+
*/
|
|
7
|
+
class HermesThreadEngine {
|
|
8
|
+
companion object {
|
|
9
|
+
init {
|
|
10
|
+
System.loadLibrary("react_native_thread")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a new Hermes runtime. Must be called on the thread that
|
|
16
|
+
* will subsequently call [evaluate].
|
|
17
|
+
*
|
|
18
|
+
* @param module The ReactNativeThreadModule instance (so native
|
|
19
|
+
* code can call back onThreadMessage).
|
|
20
|
+
* @param threadId The numeric thread ID.
|
|
21
|
+
* @return A native pointer (opaque handle) to the C++ runtime.
|
|
22
|
+
*/
|
|
23
|
+
external fun nativeCreate(module: Any, threadId: Long): Long
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Evaluate a JS code string on the runtime identified by [ptr].
|
|
27
|
+
* Must be called on the same thread that created the runtime.
|
|
28
|
+
*/
|
|
29
|
+
external fun nativeEvaluate(ptr: Long, code: String, sourceURL: String)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Destroy the native runtime and free its memory.
|
|
33
|
+
*/
|
|
34
|
+
external fun nativeDestroy(ptr: Long)
|
|
35
|
+
}
|
|
@@ -4,12 +4,6 @@ import android.util.Log
|
|
|
4
4
|
import com.facebook.react.bridge.Arguments
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
6
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
7
|
-
import org.mozilla.javascript.BaseFunction
|
|
8
|
-
import org.mozilla.javascript.Context as RhinoContext
|
|
9
|
-
import org.mozilla.javascript.NativeObject
|
|
10
|
-
import org.mozilla.javascript.ScriptRuntime
|
|
11
|
-
import org.mozilla.javascript.Scriptable
|
|
12
|
-
import org.mozilla.javascript.ScriptableObject
|
|
13
7
|
import java.util.concurrent.CompletableFuture
|
|
14
8
|
import java.util.concurrent.ConcurrentHashMap
|
|
15
9
|
import java.util.concurrent.ExecutorService
|
|
@@ -21,9 +15,10 @@ class ReactNativeThreadModule(reactContext: ReactApplicationContext) :
|
|
|
21
15
|
|
|
22
16
|
private data class ThreadEntry(
|
|
23
17
|
val executor: ExecutorService,
|
|
24
|
-
val
|
|
18
|
+
val runtimePtr: Long
|
|
25
19
|
)
|
|
26
20
|
|
|
21
|
+
private val engine = HermesThreadEngine()
|
|
27
22
|
private val threads = ConcurrentHashMap<Long, ThreadEntry>()
|
|
28
23
|
private val nextId = AtomicLong(1L)
|
|
29
24
|
|
|
@@ -36,24 +31,19 @@ class ReactNativeThreadModule(reactContext: ReactApplicationContext) :
|
|
|
36
31
|
Thread(r, "RNThread-$id")
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
// Create the Hermes runtime on the thread that will own it.
|
|
35
|
+
val ptrFuture = CompletableFuture<Long>()
|
|
40
36
|
executor.execute {
|
|
41
|
-
val cx = RhinoContext.enter()
|
|
42
37
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
injectConsole(scope, id)
|
|
46
|
-
injectResolveThreadMessage(scope, id)
|
|
47
|
-
scopeFuture.complete(scope)
|
|
38
|
+
val ptr = engine.nativeCreate(this, id)
|
|
39
|
+
ptrFuture.complete(ptr)
|
|
48
40
|
} catch (e: Exception) {
|
|
49
|
-
|
|
50
|
-
} finally {
|
|
51
|
-
RhinoContext.exit()
|
|
41
|
+
ptrFuture.completeExceptionally(e)
|
|
52
42
|
}
|
|
53
43
|
}
|
|
54
44
|
|
|
55
|
-
val
|
|
56
|
-
threads[id] = ThreadEntry(executor,
|
|
45
|
+
val ptr = ptrFuture.get()
|
|
46
|
+
threads[id] = ThreadEntry(executor, ptr)
|
|
57
47
|
return id.toDouble()
|
|
58
48
|
}
|
|
59
49
|
|
|
@@ -64,91 +54,40 @@ class ReactNativeThreadModule(reactContext: ReactApplicationContext) :
|
|
|
64
54
|
return
|
|
65
55
|
}
|
|
66
56
|
entry.executor.execute {
|
|
67
|
-
val cx = RhinoContext.enter()
|
|
68
57
|
try {
|
|
69
|
-
|
|
70
|
-
cx.evaluateString(entry.scope, code, "RNThread-${threadId.toLong()}", 1, null)
|
|
58
|
+
engine.nativeEvaluate(entry.runtimePtr, code, "RNThread-${threadId.toLong()}")
|
|
71
59
|
} catch (e: Exception) {
|
|
72
60
|
Log.e(NAME, "JS exception on thread $threadId: ${e.message}")
|
|
73
|
-
} finally {
|
|
74
|
-
RhinoContext.exit()
|
|
75
61
|
}
|
|
76
62
|
}
|
|
77
63
|
}
|
|
78
64
|
|
|
79
65
|
override fun destroyThread(threadId: Double) {
|
|
80
66
|
val entry = threads.remove(threadId.toLong()) ?: return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
scope as ScriptableObject,
|
|
88
|
-
"resolveThreadMessage",
|
|
89
|
-
object : BaseFunction() {
|
|
90
|
-
override fun call(
|
|
91
|
-
cx: RhinoContext,
|
|
92
|
-
callScope: Scriptable,
|
|
93
|
-
thisObj: Scriptable?,
|
|
94
|
-
args: Array<out Any?>
|
|
95
|
-
): Any {
|
|
96
|
-
val raw = args.getOrNull(0)
|
|
97
|
-
val jsonString: String = try {
|
|
98
|
-
val jsonObj = ScriptableObject.getProperty(callScope, "JSON") as? Scriptable
|
|
99
|
-
val stringify = jsonObj?.let { ScriptableObject.getProperty(it, "stringify") }
|
|
100
|
-
if (stringify is org.mozilla.javascript.Function) {
|
|
101
|
-
ScriptRuntime.toString(stringify.call(cx, callScope, jsonObj, arrayOf(raw)))
|
|
102
|
-
} else {
|
|
103
|
-
raw?.toString() ?: "null"
|
|
104
|
-
}
|
|
105
|
-
} catch (e: Exception) {
|
|
106
|
-
raw?.toString() ?: "null"
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
val params = Arguments.createMap().apply {
|
|
110
|
-
putDouble("threadId", threadId.toDouble())
|
|
111
|
-
putString("data", jsonString)
|
|
112
|
-
}
|
|
113
|
-
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
114
|
-
.emit("RNThreadMessage", params)
|
|
115
|
-
|
|
116
|
-
return RhinoContext.getUndefinedValue()
|
|
117
|
-
}
|
|
67
|
+
// Destroy the native runtime on its owning thread, then shut down.
|
|
68
|
+
entry.executor.execute {
|
|
69
|
+
try {
|
|
70
|
+
engine.nativeDestroy(entry.runtimePtr)
|
|
71
|
+
} catch (e: Exception) {
|
|
72
|
+
Log.w(NAME, "Error destroying runtime for thread $threadId: ${e.message}")
|
|
118
73
|
}
|
|
119
|
-
|
|
74
|
+
}
|
|
75
|
+
entry.executor.shutdown()
|
|
120
76
|
}
|
|
121
77
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
): Any {
|
|
132
|
-
val msg = args.joinToString(" ") { it?.toString() ?: "null" }
|
|
133
|
-
when (level) {
|
|
134
|
-
Log.DEBUG -> Log.d(tag, msg)
|
|
135
|
-
Log.INFO -> Log.i(tag, msg)
|
|
136
|
-
Log.WARN -> Log.w(tag, msg)
|
|
137
|
-
Log.ERROR -> Log.e(tag, msg)
|
|
138
|
-
else -> Log.v(tag, msg)
|
|
139
|
-
}
|
|
140
|
-
return RhinoContext.getUndefinedValue()
|
|
141
|
-
}
|
|
78
|
+
/**
|
|
79
|
+
* Called from C++ via JNI when resolveThreadMessage() is invoked
|
|
80
|
+
* inside a background thread.
|
|
81
|
+
*/
|
|
82
|
+
@Suppress("unused")
|
|
83
|
+
fun onThreadMessage(threadId: Double, data: String) {
|
|
84
|
+
val params = Arguments.createMap().apply {
|
|
85
|
+
putDouble("threadId", threadId)
|
|
86
|
+
putString("data", data)
|
|
142
87
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
ScriptableObject.putProperty(console, "log", makeLogFn(Log.INFO))
|
|
147
|
-
ScriptableObject.putProperty(console, "info", makeLogFn(Log.INFO))
|
|
148
|
-
ScriptableObject.putProperty(console, "warn", makeLogFn(Log.WARN))
|
|
149
|
-
ScriptableObject.putProperty(console, "error", makeLogFn(Log.ERROR))
|
|
150
|
-
ScriptableObject.putProperty(console, "debug", makeLogFn(Log.DEBUG))
|
|
151
|
-
ScriptableObject.putProperty(scope as ScriptableObject, "console", console)
|
|
88
|
+
reactApplicationContext
|
|
89
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
90
|
+
.emit("RNThreadMessage", params)
|
|
152
91
|
}
|
|
153
92
|
|
|
154
93
|
companion object {
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#include "HermesThreadEngine.h"
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
#include <thread>
|
|
5
|
+
|
|
6
|
+
using namespace facebook;
|
|
7
|
+
|
|
8
|
+
namespace rnthread {
|
|
9
|
+
|
|
10
|
+
// ──────────────────────────────────────────────────────────────────
|
|
11
|
+
// HermesThreadRuntime
|
|
12
|
+
// ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
HermesThreadRuntime::HermesThreadRuntime(long threadId, MessageCallback onMessage)
|
|
15
|
+
: runtime_(facebook::hermes::makeHermesRuntime(
|
|
16
|
+
::hermes::vm::RuntimeConfig::Builder()
|
|
17
|
+
.withES6Promise(true)
|
|
18
|
+
.withMicrotaskQueue(true)
|
|
19
|
+
.build())),
|
|
20
|
+
onMessage_(std::move(onMessage)) {
|
|
21
|
+
installConsole(threadId);
|
|
22
|
+
installTimers();
|
|
23
|
+
installResolveThreadMessage(threadId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
HermesThreadRuntime::~HermesThreadRuntime() {
|
|
27
|
+
runtime_.reset();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
void HermesThreadRuntime::evaluate(
|
|
31
|
+
const std::string &code,
|
|
32
|
+
const std::string &sourceURL) {
|
|
33
|
+
auto buf = std::make_shared<jsi::StringBuffer>(code);
|
|
34
|
+
runtime_->evaluateJavaScript(buf, sourceURL);
|
|
35
|
+
drainTimerLoop();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ──────────────────────────────────────────────────────────────────
|
|
39
|
+
// Timer event loop
|
|
40
|
+
// ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
void HermesThreadRuntime::drainTimerLoop() {
|
|
43
|
+
auto &rt = *runtime_;
|
|
44
|
+
auto callbacks =
|
|
45
|
+
rt.global()
|
|
46
|
+
.getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
47
|
+
|
|
48
|
+
while (true) {
|
|
49
|
+
rt.drainMicrotasks();
|
|
50
|
+
|
|
51
|
+
if (timerQueue_.empty()) break;
|
|
52
|
+
|
|
53
|
+
auto now = std::chrono::steady_clock::now();
|
|
54
|
+
auto &next = timerQueue_.top();
|
|
55
|
+
|
|
56
|
+
if (next.fireAt > now) {
|
|
57
|
+
std::this_thread::sleep_until(next.fireAt);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
bool fired = false;
|
|
61
|
+
while (!timerQueue_.empty()) {
|
|
62
|
+
now = std::chrono::steady_clock::now();
|
|
63
|
+
if (timerQueue_.top().fireAt > now) break;
|
|
64
|
+
|
|
65
|
+
auto entry = timerQueue_.top();
|
|
66
|
+
timerQueue_.pop();
|
|
67
|
+
|
|
68
|
+
if (cancelledTimers_.count(entry.id)) {
|
|
69
|
+
cancelledTimers_.erase(entry.id);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
auto idStr = std::to_string(entry.id);
|
|
74
|
+
auto cb = callbacks.getProperty(rt, idStr.c_str());
|
|
75
|
+
if (cb.isObject() && cb.getObject(rt).isFunction(rt)) {
|
|
76
|
+
cb.getObject(rt).asFunction(rt).call(rt);
|
|
77
|
+
fired = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (entry.repeating) {
|
|
81
|
+
entry.fireAt = std::chrono::steady_clock::now() +
|
|
82
|
+
std::chrono::milliseconds(entry.intervalMs);
|
|
83
|
+
timerQueue_.push(entry);
|
|
84
|
+
} else {
|
|
85
|
+
callbacks.setProperty(rt, idStr.c_str(), jsi::Value::undefined());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (fired) {
|
|
90
|
+
rt.drainMicrotasks();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (timerQueue_.empty()) break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ──────────────────────────────────────────────────────────────────
|
|
98
|
+
// setTimeout / setInterval / clearTimeout / clearInterval
|
|
99
|
+
// ──────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
void HermesThreadRuntime::installTimers() {
|
|
102
|
+
auto &rt = *runtime_;
|
|
103
|
+
|
|
104
|
+
rt.global().setProperty(
|
|
105
|
+
rt, "__rnTimerCallbacks__", jsi::Object(rt));
|
|
106
|
+
|
|
107
|
+
auto *self = this;
|
|
108
|
+
|
|
109
|
+
// ── setTimeout(fn, ms) → id ──
|
|
110
|
+
auto setTimeoutFn = jsi::Function::createFromHostFunction(
|
|
111
|
+
rt, jsi::PropNameID::forAscii(rt, "setTimeout"), 2,
|
|
112
|
+
[self](jsi::Runtime &rt, const jsi::Value &,
|
|
113
|
+
const jsi::Value *args, size_t count) -> jsi::Value {
|
|
114
|
+
if (count < 1 || !args[0].isObject() ||
|
|
115
|
+
!args[0].getObject(rt).isFunction(rt))
|
|
116
|
+
return jsi::Value::undefined();
|
|
117
|
+
|
|
118
|
+
int ms = count >= 2 ? static_cast<int>(args[1].asNumber()) : 0;
|
|
119
|
+
int id = self->nextTimerId_++;
|
|
120
|
+
|
|
121
|
+
auto cbs = rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
122
|
+
cbs.setProperty(rt, std::to_string(id).c_str(),
|
|
123
|
+
args[0].getObject(rt));
|
|
124
|
+
|
|
125
|
+
TimerEntry entry;
|
|
126
|
+
entry.id = id;
|
|
127
|
+
entry.fireAt = std::chrono::steady_clock::now() +
|
|
128
|
+
std::chrono::milliseconds(ms);
|
|
129
|
+
entry.repeating = false;
|
|
130
|
+
entry.intervalMs = 0;
|
|
131
|
+
self->timerQueue_.push(entry);
|
|
132
|
+
|
|
133
|
+
return jsi::Value(id);
|
|
134
|
+
});
|
|
135
|
+
rt.global().setProperty(rt, "setTimeout", std::move(setTimeoutFn));
|
|
136
|
+
|
|
137
|
+
// ── setInterval(fn, ms) → id ──
|
|
138
|
+
auto setIntervalFn = jsi::Function::createFromHostFunction(
|
|
139
|
+
rt, jsi::PropNameID::forAscii(rt, "setInterval"), 2,
|
|
140
|
+
[self](jsi::Runtime &rt, const jsi::Value &,
|
|
141
|
+
const jsi::Value *args, size_t count) -> jsi::Value {
|
|
142
|
+
if (count < 1 || !args[0].isObject() ||
|
|
143
|
+
!args[0].getObject(rt).isFunction(rt))
|
|
144
|
+
return jsi::Value::undefined();
|
|
145
|
+
|
|
146
|
+
int ms = count >= 2 ? static_cast<int>(args[1].asNumber()) : 0;
|
|
147
|
+
if (ms <= 0) ms = 1;
|
|
148
|
+
int id = self->nextTimerId_++;
|
|
149
|
+
|
|
150
|
+
auto cbs = rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
151
|
+
cbs.setProperty(rt, std::to_string(id).c_str(),
|
|
152
|
+
args[0].getObject(rt));
|
|
153
|
+
|
|
154
|
+
TimerEntry entry;
|
|
155
|
+
entry.id = id;
|
|
156
|
+
entry.fireAt = std::chrono::steady_clock::now() +
|
|
157
|
+
std::chrono::milliseconds(ms);
|
|
158
|
+
entry.repeating = true;
|
|
159
|
+
entry.intervalMs = ms;
|
|
160
|
+
self->timerQueue_.push(entry);
|
|
161
|
+
|
|
162
|
+
return jsi::Value(id);
|
|
163
|
+
});
|
|
164
|
+
rt.global().setProperty(rt, "setInterval", std::move(setIntervalFn));
|
|
165
|
+
|
|
166
|
+
// ── clearTimeout(id) / clearInterval(id) ──
|
|
167
|
+
auto clearTimerFn = jsi::Function::createFromHostFunction(
|
|
168
|
+
rt, jsi::PropNameID::forAscii(rt, "clearTimeout"), 1,
|
|
169
|
+
[self](jsi::Runtime &rt, const jsi::Value &,
|
|
170
|
+
const jsi::Value *args, size_t count) -> jsi::Value {
|
|
171
|
+
if (count >= 1 && args[0].isNumber()) {
|
|
172
|
+
int id = static_cast<int>(args[0].asNumber());
|
|
173
|
+
self->cancelledTimers_.insert(id);
|
|
174
|
+
auto cbs =
|
|
175
|
+
rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
|
|
176
|
+
cbs.setProperty(rt, std::to_string(id).c_str(),
|
|
177
|
+
jsi::Value::undefined());
|
|
178
|
+
}
|
|
179
|
+
return jsi::Value::undefined();
|
|
180
|
+
});
|
|
181
|
+
rt.global().setProperty(rt, "clearTimeout", clearTimerFn);
|
|
182
|
+
rt.global().setProperty(rt, "clearInterval", std::move(clearTimerFn));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ──────────────────────────────────────────────────────────────────
|
|
186
|
+
// console.{log,info,warn,error,debug}
|
|
187
|
+
// ──────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
void HermesThreadRuntime::installConsole(long threadId) {
|
|
190
|
+
auto &rt = *runtime_;
|
|
191
|
+
|
|
192
|
+
auto makeLogFn = [&rt, threadId](const char *level) {
|
|
193
|
+
return jsi::Function::createFromHostFunction(
|
|
194
|
+
rt,
|
|
195
|
+
jsi::PropNameID::forAscii(rt, "log"),
|
|
196
|
+
1,
|
|
197
|
+
[threadId, level](
|
|
198
|
+
jsi::Runtime &rt,
|
|
199
|
+
const jsi::Value &,
|
|
200
|
+
const jsi::Value *args,
|
|
201
|
+
size_t count) -> jsi::Value {
|
|
202
|
+
std::string msg;
|
|
203
|
+
for (size_t i = 0; i < count; ++i) {
|
|
204
|
+
if (i > 0) msg += ' ';
|
|
205
|
+
msg += args[i].toString(rt).utf8(rt);
|
|
206
|
+
}
|
|
207
|
+
// NSLog is not available in pure C++; use fprintf which
|
|
208
|
+
// shows up in Xcode console.
|
|
209
|
+
fprintf(stderr, "[RNThread-%ld] %s: %s\n", threadId, level, msg.c_str());
|
|
210
|
+
return jsi::Value::undefined();
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
auto console = jsi::Object(rt);
|
|
215
|
+
console.setProperty(rt, "log", makeLogFn("LOG"));
|
|
216
|
+
console.setProperty(rt, "info", makeLogFn("INFO"));
|
|
217
|
+
console.setProperty(rt, "warn", makeLogFn("WARN"));
|
|
218
|
+
console.setProperty(rt, "error", makeLogFn("ERROR"));
|
|
219
|
+
console.setProperty(rt, "debug", makeLogFn("DEBUG"));
|
|
220
|
+
rt.global().setProperty(rt, "console", std::move(console));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ──────────────────────────────────────────────────────────────────
|
|
224
|
+
// resolveThreadMessage(data)
|
|
225
|
+
// ──────────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
void HermesThreadRuntime::installResolveThreadMessage(long threadId) {
|
|
228
|
+
auto &rt = *runtime_;
|
|
229
|
+
|
|
230
|
+
auto fn = jsi::Function::createFromHostFunction(
|
|
231
|
+
rt,
|
|
232
|
+
jsi::PropNameID::forAscii(rt, "resolveThreadMessage"),
|
|
233
|
+
1,
|
|
234
|
+
[this](
|
|
235
|
+
jsi::Runtime &rt,
|
|
236
|
+
const jsi::Value &,
|
|
237
|
+
const jsi::Value *args,
|
|
238
|
+
size_t count) -> jsi::Value {
|
|
239
|
+
auto json = rt.global().getPropertyAsObject(rt, "JSON");
|
|
240
|
+
auto stringify = json.getPropertyAsFunction(rt, "stringify");
|
|
241
|
+
std::string serialised;
|
|
242
|
+
if (count > 0) {
|
|
243
|
+
auto result = stringify.call(rt, args[0]);
|
|
244
|
+
if (result.isString()) {
|
|
245
|
+
serialised = result.getString(rt).utf8(rt);
|
|
246
|
+
} else {
|
|
247
|
+
serialised = "null";
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
serialised = "null";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (onMessage_) {
|
|
254
|
+
onMessage_(serialised);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return jsi::Value::undefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
rt.global().setProperty(rt, "resolveThreadMessage", std::move(fn));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
} // namespace rnthread
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <jsi/jsi.h>
|
|
4
|
+
#include <hermes/hermes.h>
|
|
5
|
+
#include <chrono>
|
|
6
|
+
#include <functional>
|
|
7
|
+
#include <memory>
|
|
8
|
+
#include <queue>
|
|
9
|
+
#include <string>
|
|
10
|
+
#include <unordered_set>
|
|
11
|
+
|
|
12
|
+
namespace rnthread {
|
|
13
|
+
|
|
14
|
+
using MessageCallback = std::function<void(const std::string &serialised)>;
|
|
15
|
+
|
|
16
|
+
class HermesThreadRuntime {
|
|
17
|
+
public:
|
|
18
|
+
HermesThreadRuntime(long threadId, MessageCallback onMessage);
|
|
19
|
+
~HermesThreadRuntime();
|
|
20
|
+
|
|
21
|
+
void evaluate(const std::string &code, const std::string &sourceURL);
|
|
22
|
+
|
|
23
|
+
private:
|
|
24
|
+
void installConsole(long threadId);
|
|
25
|
+
void installResolveThreadMessage(long threadId);
|
|
26
|
+
void installTimers();
|
|
27
|
+
void drainTimerLoop();
|
|
28
|
+
|
|
29
|
+
std::unique_ptr<facebook::hermes::HermesRuntime> runtime_;
|
|
30
|
+
MessageCallback onMessage_;
|
|
31
|
+
|
|
32
|
+
// ── Timer state ──
|
|
33
|
+
struct TimerEntry {
|
|
34
|
+
int id;
|
|
35
|
+
std::chrono::steady_clock::time_point fireAt;
|
|
36
|
+
bool repeating;
|
|
37
|
+
int intervalMs;
|
|
38
|
+
|
|
39
|
+
bool operator>(const TimerEntry &o) const { return fireAt > o.fireAt; }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
std::priority_queue<TimerEntry, std::vector<TimerEntry>,
|
|
43
|
+
std::greater<TimerEntry>>
|
|
44
|
+
timerQueue_;
|
|
45
|
+
std::unordered_set<int> cancelledTimers_;
|
|
46
|
+
int nextTimerId_ = 1;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
} // namespace rnthread
|
package/ios/ReactNativeThread.mm
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#import "ReactNativeThread.h"
|
|
2
|
-
#import
|
|
2
|
+
#import "HermesThreadEngine.h"
|
|
3
|
+
|
|
4
|
+
#include <dispatch/dispatch.h>
|
|
5
|
+
#include <memory>
|
|
6
|
+
#include <string>
|
|
3
7
|
|
|
4
8
|
@interface RNThread : NSObject
|
|
5
|
-
@property (nonatomic,
|
|
6
|
-
@property (nonatomic, strong) JSContext *context;
|
|
9
|
+
@property (nonatomic, assign) std::shared_ptr<rnthread::HermesThreadRuntime> engine;
|
|
7
10
|
@property (nonatomic, strong) dispatch_queue_t queue;
|
|
8
11
|
@property (nonatomic) dispatch_semaphore_t ready;
|
|
9
12
|
@end
|
|
@@ -44,59 +47,25 @@
|
|
|
44
47
|
t.queue = dispatch_queue_create(label.UTF8String, attr);
|
|
45
48
|
t.ready = dispatch_semaphore_create(0);
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
long tidForLog = (long)_nextId;
|
|
51
|
+
__weak ReactNativeThread *weakSelf = self;
|
|
48
52
|
|
|
49
53
|
dispatch_async(t.queue, ^{
|
|
50
|
-
|
|
51
|
-
t.context = [[JSContext alloc] initWithVirtualMachine:t.vm];
|
|
52
|
-
t.context.exceptionHandler = ^(JSContext *ctx, JSValue *exception) {
|
|
53
|
-
NSLog(@"[RNThread-%lu] JS exception: %@", (unsigned long)tidForLog, exception);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
NSString *tag = [NSString stringWithFormat:@"RNThread-%lu", (unsigned long)tidForLog];
|
|
57
|
-
NSMutableDictionary *console = [NSMutableDictionary new];
|
|
58
|
-
console[@"log"] = ^{
|
|
59
|
-
NSArray *args = [JSContext currentArguments];
|
|
60
|
-
NSMutableArray *parts = [NSMutableArray new];
|
|
61
|
-
for (JSValue *v in args) [parts addObject:[v toString]];
|
|
62
|
-
NSLog(@"[%@] %@", tag, [parts componentsJoinedByString:@" "]);
|
|
63
|
-
};
|
|
64
|
-
console[@"info"] = console[@"log"];
|
|
65
|
-
console[@"debug"] = console[@"log"];
|
|
66
|
-
console[@"warn"] = ^{
|
|
67
|
-
NSArray *args = [JSContext currentArguments];
|
|
68
|
-
NSMutableArray *parts = [NSMutableArray new];
|
|
69
|
-
for (JSValue *v in args) [parts addObject:[v toString]];
|
|
70
|
-
NSLog(@"[%@] WARN: %@", tag, [parts componentsJoinedByString:@" "]);
|
|
71
|
-
};
|
|
72
|
-
console[@"error"] = ^{
|
|
73
|
-
NSArray *args = [JSContext currentArguments];
|
|
74
|
-
NSMutableArray *parts = [NSMutableArray new];
|
|
75
|
-
for (JSValue *v in args) [parts addObject:[v toString]];
|
|
76
|
-
NSLog(@"[%@] ERROR: %@", tag, [parts componentsJoinedByString:@" "]);
|
|
77
|
-
};
|
|
78
|
-
t.context[@"console"] = console;
|
|
79
|
-
|
|
80
|
-
NSUInteger capturedTid = tidForLog;
|
|
81
|
-
__weak ReactNativeThread *weakSelf = self;
|
|
82
|
-
t.context[@"resolveThreadMessage"] = ^(JSValue *data) {
|
|
83
|
-
JSValue *jsonStr = [data.context[@"JSON"] invokeMethod:@"stringify"
|
|
84
|
-
withArguments:@[data]];
|
|
85
|
-
NSString *serialised = [jsonStr toString];
|
|
86
|
-
if (!serialised || [serialised isEqualToString:@"undefined"]) {
|
|
87
|
-
serialised = @"null";
|
|
88
|
-
}
|
|
89
|
-
|
|
54
|
+
auto onMessage = [weakSelf, tidForLog](const std::string &serialised) {
|
|
90
55
|
ReactNativeThread *strongSelf = weakSelf;
|
|
91
56
|
if (!strongSelf) return;
|
|
92
57
|
|
|
58
|
+
NSString *nsData = [NSString stringWithUTF8String:serialised.c_str()];
|
|
59
|
+
if (!nsData) nsData = @"null";
|
|
60
|
+
|
|
93
61
|
[strongSelf sendEventWithName:@"RNThreadMessage"
|
|
94
62
|
body:@{
|
|
95
|
-
@"threadId": @(
|
|
96
|
-
@"data":
|
|
63
|
+
@"threadId": @(tidForLog),
|
|
64
|
+
@"data": nsData
|
|
97
65
|
}];
|
|
98
66
|
};
|
|
99
67
|
|
|
68
|
+
t.engine = std::make_shared<rnthread::HermesThreadRuntime>(tidForLog, onMessage);
|
|
100
69
|
dispatch_semaphore_signal(t.ready);
|
|
101
70
|
});
|
|
102
71
|
|
|
@@ -119,12 +88,18 @@
|
|
|
119
88
|
return;
|
|
120
89
|
}
|
|
121
90
|
|
|
122
|
-
|
|
91
|
+
std::string codeCpp = std::string([code UTF8String]);
|
|
92
|
+
std::string sourceURL = "RNThread-" + std::to_string((long)threadId);
|
|
123
93
|
dispatch_semaphore_t ready = t.ready;
|
|
94
|
+
|
|
124
95
|
dispatch_async(t.queue, ^{
|
|
125
96
|
dispatch_semaphore_wait(ready, DISPATCH_TIME_FOREVER);
|
|
126
97
|
dispatch_semaphore_signal(ready);
|
|
127
|
-
|
|
98
|
+
try {
|
|
99
|
+
t.engine->evaluate(codeCpp, sourceURL);
|
|
100
|
+
} catch (const std::exception &e) {
|
|
101
|
+
NSLog(@"[RNThread-%.0f] JS exception: %s", threadId, e.what());
|
|
102
|
+
}
|
|
128
103
|
});
|
|
129
104
|
}
|
|
130
105
|
|
|
@@ -137,7 +112,7 @@
|
|
|
137
112
|
if (!t) return;
|
|
138
113
|
|
|
139
114
|
dispatch_async(t.queue, ^{
|
|
140
|
-
(
|
|
115
|
+
t.engine.reset();
|
|
141
116
|
});
|
|
142
117
|
}
|
|
143
118
|
|