@qntx/wallet-native 0.0.0-stage → 0.5.2
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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +123 -2
- package/WalletNative.podspec +74 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build.gradle +177 -0
- package/android/consumer-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +29 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/AppLifecycle.kt +35 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/KernelPbkdf2.java +59 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Keystore.kt +280 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/Passkey.kt +147 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/VaultDirectory.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/qntxwallet/WalletNativePackage.java +30 -0
- package/cpp/host/HybridKernelHost.cpp +162 -0
- package/cpp/host/HybridKernelHost.hpp +49 -0
- package/cpp/host/KernelFile.cpp +120 -0
- package/cpp/host/KernelFile.hpp +24 -0
- package/cpp/host/Keystore.hpp +70 -0
- package/cpp/host/Lifecycle.cpp +77 -0
- package/cpp/host/Lifecycle.hpp +71 -0
- package/cpp/host/Passkey.hpp +39 -0
- package/cpp/host/VaultDirectory.hpp +16 -0
- package/cpp/platform/KeystoreAndroid.cpp +175 -0
- package/cpp/platform/KeystoreIos.mm +300 -0
- package/cpp/platform/LifecycleAndroid.cpp +55 -0
- package/cpp/platform/LifecycleIos.mm +36 -0
- package/cpp/platform/PasskeyAndroid.cpp +118 -0
- package/cpp/platform/PasskeyIos.mm +20 -0
- package/cpp/platform/VaultDirectory.mm +58 -0
- package/cpp/platform/VaultDirectoryAndroid.cpp +35 -0
- package/cpp/runtime/KernelBackends.cpp +297 -0
- package/cpp/runtime/KernelBackends.hpp +20 -0
- package/cpp/runtime/KernelBuffer.cpp +12 -0
- package/cpp/runtime/KernelBuffer.hpp +96 -0
- package/cpp/runtime/KernelGlobals.cpp +535 -0
- package/cpp/runtime/KernelGlobals.hpp +107 -0
- package/cpp/runtime/KernelJs.cpp +155 -0
- package/cpp/runtime/KernelJs.hpp +81 -0
- package/cpp/runtime/KernelKeystore.cpp +224 -0
- package/cpp/runtime/KernelKeystore.hpp +28 -0
- package/cpp/runtime/KernelPasskey.cpp +137 -0
- package/cpp/runtime/KernelPasskey.hpp +23 -0
- package/cpp/runtime/KernelRuntime.cpp +496 -0
- package/cpp/runtime/KernelRuntime.hpp +94 -0
- package/cpp/runtime/platform/KernelPbkdf2.cpp +198 -0
- package/cpp/runtime/platform/KernelPbkdf2.hpp +38 -0
- package/dist/index.d.mts +44 -0
- package/dist/index.mjs +170 -0
- package/dist/index.mjs.map +1 -0
- package/kernel/generated/kernel-entry.js +42967 -0
- package/kernel/generated/kernel.manifest.json +80 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/WalletNative+autolinking.cmake +81 -0
- package/nitrogen/generated/android/WalletNative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.cpp +49 -0
- package/nitrogen/generated/android/WalletNativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/qntxwallet/WalletNativeOnLoad.kt +35 -0
- package/nitrogen/generated/ios/WalletNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/WalletNative-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.mm +35 -0
- package/nitrogen/generated/ios/WalletNativeAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridKernelHostSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/KernelExitReason.hpp +84 -0
- package/package.json +65 -4
- package/react-native.config.js +14 -0
- package/scripts/compile-kernel.mjs +118 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#include "KernelPasskey.hpp"
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
#include <utility>
|
|
5
|
+
|
|
6
|
+
#include "../host/Passkey.hpp"
|
|
7
|
+
#include "KernelGlobals.hpp"
|
|
8
|
+
#include "KernelJs.hpp"
|
|
9
|
+
|
|
10
|
+
namespace qntx {
|
|
11
|
+
|
|
12
|
+
namespace jsi = facebook::jsi;
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
constexpr size_t kRequestMax = 16 * 1024;
|
|
17
|
+
|
|
18
|
+
const char* errorCode(PasskeyStatus status) {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case PasskeyStatus::cancelled:
|
|
21
|
+
return "cancelled";
|
|
22
|
+
case PasskeyStatus::unavailable:
|
|
23
|
+
return "factor-unavailable";
|
|
24
|
+
case PasskeyStatus::ok:
|
|
25
|
+
case PasskeyStatus::failed:
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
return "unavailable";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
struct PasskeyCalls {
|
|
32
|
+
int inFlight = 0; // kernel thread only
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
std::string requestFrom(jsi::Runtime& rt, const jsi::Value& value, const char* name) {
|
|
36
|
+
if (!value.isString()) {
|
|
37
|
+
throw jsi::JSError(rt, std::string(name) + " expects a request JSON string");
|
|
38
|
+
}
|
|
39
|
+
auto request = value.asString(rt).utf8(rt);
|
|
40
|
+
if (request.empty() || request.size() > kRequestMax) {
|
|
41
|
+
throw jsi::JSError(rt, std::string(name) + " request length is out of range");
|
|
42
|
+
}
|
|
43
|
+
return request;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parks `done` as a pending call; the returned completion hands the outcome
|
|
47
|
+
// back on the kernel thread. Like the keystore, the platform may hold the
|
|
48
|
+
// completion past teardown, so it keeps the globals state only weakly.
|
|
49
|
+
PasskeyDone park(
|
|
50
|
+
const std::shared_ptr<KernelGlobalsState>& state,
|
|
51
|
+
const std::shared_ptr<PasskeyCalls>& calls,
|
|
52
|
+
jsi::Function done) {
|
|
53
|
+
auto call = std::make_shared<KernelPendingCall>();
|
|
54
|
+
call->done = std::move(done);
|
|
55
|
+
state->pending.insert(call);
|
|
56
|
+
calls->inFlight += 1;
|
|
57
|
+
std::weak_ptr<KernelGlobalsState> weak = state;
|
|
58
|
+
return [weak, calls, call](PasskeyResult result) {
|
|
59
|
+
auto state = weak.lock();
|
|
60
|
+
if (state == nullptr || !state->active->load(std::memory_order_acquire)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
auto shared = std::make_shared<PasskeyResult>(std::move(result));
|
|
64
|
+
state->enqueueTask([state, calls, call, shared](jsi::Runtime& rt) {
|
|
65
|
+
calls->inFlight -= 1;
|
|
66
|
+
auto done = std::move(call->done);
|
|
67
|
+
state->pending.erase(call);
|
|
68
|
+
if (!done.has_value()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (shared->status != PasskeyStatus::ok || shared->responseJson.empty()) {
|
|
72
|
+
const auto status =
|
|
73
|
+
shared->status == PasskeyStatus::ok ? PasskeyStatus::failed : shared->status;
|
|
74
|
+
done->call(
|
|
75
|
+
rt,
|
|
76
|
+
jsi::Value::undefined(),
|
|
77
|
+
jsi::Value(rt, jsi::String::createFromAscii(rt, errorCode(status))));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
done->call(
|
|
81
|
+
rt,
|
|
82
|
+
jsi::Value(rt, jsi::String::createFromUtf8(rt, shared->responseJson)),
|
|
83
|
+
jsi::Value::undefined());
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
jsi::Function callbackFrom(jsi::Runtime& rt, const jsi::Value& value, const char* name) {
|
|
89
|
+
if (!value.isObject() || !value.asObject(rt).isFunction(rt)) {
|
|
90
|
+
throw jsi::JSError(rt, std::string(name) + " expects a completion function");
|
|
91
|
+
}
|
|
92
|
+
return value.asObject(rt).asFunction(rt);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} // namespace
|
|
96
|
+
|
|
97
|
+
void installKernelPasskey(
|
|
98
|
+
jsi::Runtime& rt,
|
|
99
|
+
jsi::Object& qntx,
|
|
100
|
+
const std::shared_ptr<KernelGlobalsState>& state) {
|
|
101
|
+
auto calls = std::make_shared<PasskeyCalls>();
|
|
102
|
+
state->cancelCeremonies.emplace_back([calls] {
|
|
103
|
+
if (calls->inFlight > 0) {
|
|
104
|
+
passkeyCancel();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
auto passkey = jsi::Object(rt);
|
|
109
|
+
passkey.setProperty(
|
|
110
|
+
rt,
|
|
111
|
+
"create",
|
|
112
|
+
hostFn(rt, "passkey.create", 2, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
113
|
+
if (count < 2) {
|
|
114
|
+
throw jsi::JSError(rt, "__qntx.passkey.create expects (requestJson, done)");
|
|
115
|
+
}
|
|
116
|
+
auto request = requestFrom(rt, args[0], "__qntx.passkey.create");
|
|
117
|
+
auto done = park(state, calls, callbackFrom(rt, args[1], "__qntx.passkey.create"));
|
|
118
|
+
passkeyCreate(std::move(request), std::move(done));
|
|
119
|
+
return jsi::Value::undefined();
|
|
120
|
+
}));
|
|
121
|
+
passkey.setProperty(
|
|
122
|
+
rt,
|
|
123
|
+
"get",
|
|
124
|
+
hostFn(rt, "passkey.get", 2, [state, calls](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) -> jsi::Value {
|
|
125
|
+
if (count < 2) {
|
|
126
|
+
throw jsi::JSError(rt, "__qntx.passkey.get expects (requestJson, done)");
|
|
127
|
+
}
|
|
128
|
+
auto request = requestFrom(rt, args[0], "__qntx.passkey.get");
|
|
129
|
+
auto done = park(state, calls, callbackFrom(rt, args[1], "__qntx.passkey.get"));
|
|
130
|
+
passkeyGet(std::move(request), std::move(done));
|
|
131
|
+
return jsi::Value::undefined();
|
|
132
|
+
}));
|
|
133
|
+
state->intrinsics->freeze.call(rt, jsi::Value(rt, passkey));
|
|
134
|
+
qntx.setProperty(rt, "passkey", jsi::Value(std::move(passkey)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} // namespace qntx
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <jsi/jsi.h>
|
|
4
|
+
|
|
5
|
+
#include <memory>
|
|
6
|
+
|
|
7
|
+
namespace qntx {
|
|
8
|
+
|
|
9
|
+
struct KernelGlobalsState;
|
|
10
|
+
|
|
11
|
+
// Installs the frozen `passkey` object on `__qntx` before the caller freezes
|
|
12
|
+
// it:
|
|
13
|
+
// create(requestJson, done) done(responseJson | undefined, code | undefined)
|
|
14
|
+
// get(requestJson, done) done(responseJson | undefined, code | undefined)
|
|
15
|
+
// The request is a non-empty string of at most 16 KiB. Completions return to
|
|
16
|
+
// the kernel thread through the pending-call machinery; `code` is the kernel
|
|
17
|
+
// error code for a failure. Teardown cancels an in-flight ceremony.
|
|
18
|
+
void installKernelPasskey(
|
|
19
|
+
facebook::jsi::Runtime& rt,
|
|
20
|
+
facebook::jsi::Object& qntx,
|
|
21
|
+
const std::shared_ptr<KernelGlobalsState>& state);
|
|
22
|
+
|
|
23
|
+
} // namespace qntx
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#include "KernelRuntime.hpp"
|
|
2
|
+
|
|
3
|
+
#include "KernelBuffer.hpp"
|
|
4
|
+
#include "KernelGlobals.hpp"
|
|
5
|
+
|
|
6
|
+
#include <jsi/hermes-interfaces.h>
|
|
7
|
+
|
|
8
|
+
#if defined(__ANDROID__)
|
|
9
|
+
#include "platform/KernelPbkdf2.hpp"
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include <jni.h>
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
#include <atomic>
|
|
15
|
+
#include <condition_variable>
|
|
16
|
+
#include <deque>
|
|
17
|
+
#include <map>
|
|
18
|
+
#include <mutex>
|
|
19
|
+
#include <optional>
|
|
20
|
+
#include <stdexcept>
|
|
21
|
+
#include <thread>
|
|
22
|
+
#include <unordered_map>
|
|
23
|
+
#include <utility>
|
|
24
|
+
|
|
25
|
+
#if defined(__APPLE__) || defined(__ANDROID__)
|
|
26
|
+
#include <pthread.h>
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
namespace qntx {
|
|
30
|
+
|
|
31
|
+
namespace jsi = facebook::jsi;
|
|
32
|
+
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
// Turns the current exception into a description. Called only inside catch
|
|
36
|
+
// blocks so the exception_ptr — and any jsi values a JSError holds — die
|
|
37
|
+
// while the runtime that owns them is still alive.
|
|
38
|
+
std::string describe(const std::exception_ptr& error) {
|
|
39
|
+
try {
|
|
40
|
+
std::rethrow_exception(error);
|
|
41
|
+
} catch (const jsi::JSError& e) {
|
|
42
|
+
std::string message = e.getMessage();
|
|
43
|
+
const std::string& stack = e.getStack();
|
|
44
|
+
if (!stack.empty()) {
|
|
45
|
+
message += "\n" + stack;
|
|
46
|
+
}
|
|
47
|
+
return "jsi::JSError: " + message;
|
|
48
|
+
} catch (const std::exception& e) {
|
|
49
|
+
return std::string(e.what());
|
|
50
|
+
} catch (...) {
|
|
51
|
+
return "unknown error";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
hermes::vm::RuntimeConfig runtimeConfigFor(KernelProfile profile) {
|
|
56
|
+
auto builder = profile == KernelProfile::hardened
|
|
57
|
+
? facebook::hermes::hardenedHermesRuntimeConfig().rebuild()
|
|
58
|
+
: hermes::vm::RuntimeConfig::Builder();
|
|
59
|
+
return builder.withMicrotaskQueue(true)
|
|
60
|
+
.withEnableEval(false)
|
|
61
|
+
.withAsyncBreakCheckInEval(true)
|
|
62
|
+
.build();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void nameKernelThread() {
|
|
66
|
+
#if defined(__APPLE__)
|
|
67
|
+
pthread_setname_np("qntx-kernel");
|
|
68
|
+
#elif defined(__ANDROID__)
|
|
69
|
+
pthread_setname_np(pthread_self(), "qntx-kernel");
|
|
70
|
+
#endif
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
} // namespace
|
|
74
|
+
|
|
75
|
+
struct KernelRuntime::Impl {
|
|
76
|
+
KernelScript script;
|
|
77
|
+
Hooks hooks;
|
|
78
|
+
KernelProfile profile = KernelProfile::standard;
|
|
79
|
+
std::shared_ptr<KernelGlobalsState> globals;
|
|
80
|
+
|
|
81
|
+
// Guards `immediate`, and the `stopping` store in destroy() so the loop's
|
|
82
|
+
// cv predicate can never miss a wakeup.
|
|
83
|
+
std::mutex mutex;
|
|
84
|
+
std::condition_variable cv;
|
|
85
|
+
std::deque<std::function<void()>> immediate;
|
|
86
|
+
|
|
87
|
+
// Inbound frame deliveries queued but not yet run; capped at
|
|
88
|
+
// kMaxInboundFrames — the kernel must keep up or the host exits integrity.
|
|
89
|
+
static constexpr size_t kMaxInboundFrames = 64;
|
|
90
|
+
size_t pendingPosts = 0;
|
|
91
|
+
|
|
92
|
+
std::atomic_bool stopping{false};
|
|
93
|
+
|
|
94
|
+
// Publishes the live runtime's IHermes so destroy() can interrupt running
|
|
95
|
+
// JS. The kernel thread sets it after makeHermesRuntime and withdraws it
|
|
96
|
+
// under the same mutex BEFORE rt.reset(), so asyncTriggerTimeout can never
|
|
97
|
+
// touch a destroyed runtime.
|
|
98
|
+
std::mutex interruptMutex;
|
|
99
|
+
facebook::hermes::IHermes* interruptable = nullptr;
|
|
100
|
+
|
|
101
|
+
std::mutex stoppedMutex;
|
|
102
|
+
std::condition_variable stoppedCv;
|
|
103
|
+
bool stopped = false;
|
|
104
|
+
|
|
105
|
+
// Kernel thread only.
|
|
106
|
+
std::unique_ptr<jsi::Runtime> rt;
|
|
107
|
+
std::optional<std::string> fatal;
|
|
108
|
+
struct Timer {
|
|
109
|
+
uint64_t id;
|
|
110
|
+
jsi::Function fn;
|
|
111
|
+
};
|
|
112
|
+
using TimedMap = std::multimap<std::chrono::steady_clock::time_point, Timer>;
|
|
113
|
+
TimedMap timed;
|
|
114
|
+
std::unordered_map<uint64_t, TimedMap::iterator> timerIndex;
|
|
115
|
+
uint64_t nextTimerId = 0;
|
|
116
|
+
|
|
117
|
+
// Read and written only under `mutex`: enqueue() may run on the KDF worker
|
|
118
|
+
// thread while teardown() runs on the kernel thread.
|
|
119
|
+
bool tornDown = false;
|
|
120
|
+
|
|
121
|
+
std::atomic_bool running{false};
|
|
122
|
+
|
|
123
|
+
void run();
|
|
124
|
+
void initRuntime();
|
|
125
|
+
void evaluate();
|
|
126
|
+
void loop();
|
|
127
|
+
void fail(std::string message);
|
|
128
|
+
void teardown();
|
|
129
|
+
void deliver(std::vector<uint8_t> bytes);
|
|
130
|
+
// Queues a task that runs fn(rt) on the kernel thread. Accepts work even
|
|
131
|
+
// after `stopping`: queued-but-unrun tasks are destroyed by teardown, which
|
|
132
|
+
// runs before the runtime itself is destroyed — so jsi values captured by
|
|
133
|
+
// tasks are always released on the kernel thread while the runtime lives.
|
|
134
|
+
void enqueue(std::function<void(jsi::Runtime&)> task);
|
|
135
|
+
KernelGlobalsState::TimerCancel scheduleTimer(
|
|
136
|
+
std::chrono::steady_clock::time_point deadline,
|
|
137
|
+
jsi::Function fn);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
KernelRuntime::KernelRuntime() : impl_(std::make_shared<Impl>()) {}
|
|
141
|
+
|
|
142
|
+
KernelRuntime::~KernelRuntime() {
|
|
143
|
+
destroy();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
std::shared_ptr<KernelRuntime> KernelRuntime::start(
|
|
147
|
+
KernelScript script,
|
|
148
|
+
Hooks hooks,
|
|
149
|
+
std::string vaultDirectory,
|
|
150
|
+
KernelProfile profile) {
|
|
151
|
+
if (script.bytes == nullptr || script.bytes->empty()) {
|
|
152
|
+
throw std::invalid_argument("KernelRuntime::start: empty script");
|
|
153
|
+
}
|
|
154
|
+
auto self = std::shared_ptr<KernelRuntime>(new KernelRuntime());
|
|
155
|
+
auto& impl = *self->impl_;
|
|
156
|
+
impl.profile = profile;
|
|
157
|
+
impl.script = std::move(script);
|
|
158
|
+
impl.hooks = std::move(hooks);
|
|
159
|
+
impl.globals = std::make_shared<KernelGlobalsState>();
|
|
160
|
+
impl.globals->vaultDirectory = std::move(vaultDirectory);
|
|
161
|
+
impl.globals->active = std::make_shared<std::atomic_bool>(true);
|
|
162
|
+
impl.globals->postToHost = impl.hooks.onPost;
|
|
163
|
+
impl.globals->reportFatal = [&impl](std::string message) {
|
|
164
|
+
impl.fatal = std::move(message);
|
|
165
|
+
};
|
|
166
|
+
impl.globals->scheduleTimer =
|
|
167
|
+
[&impl](std::chrono::steady_clock::time_point deadline, jsi::Function fn) {
|
|
168
|
+
return impl.scheduleTimer(deadline, std::move(fn));
|
|
169
|
+
};
|
|
170
|
+
// Async completions (e.g. pbkdf2) may arrive after the KernelRuntime object
|
|
171
|
+
// is destroyed, so reach the impl through a weak_ptr instead of a reference.
|
|
172
|
+
std::weak_ptr<Impl> weakImpl = self->impl_;
|
|
173
|
+
impl.globals->enqueueTask =
|
|
174
|
+
[weakImpl](std::function<void(jsi::Runtime&)> task) mutable {
|
|
175
|
+
if (auto impl = weakImpl.lock()) {
|
|
176
|
+
impl->enqueue(std::move(task));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
impl.running.store(true, std::memory_order_release);
|
|
180
|
+
// The detached thread holds the Impl, not the KernelRuntime: dropping the
|
|
181
|
+
// last public handle just requests shutdown through ~KernelRuntime →
|
|
182
|
+
// destroy(), and the kernel thread finishes teardown on its own.
|
|
183
|
+
std::thread([impl = self->impl_]() { impl->run(); }).detach();
|
|
184
|
+
return self;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
bool KernelRuntime::running() const noexcept {
|
|
188
|
+
return impl_->running.load(std::memory_order_acquire);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
bool KernelRuntime::waitUntilStopped(std::chrono::milliseconds timeout) const {
|
|
192
|
+
auto& impl = *impl_;
|
|
193
|
+
std::unique_lock<std::mutex> lock(impl.stoppedMutex);
|
|
194
|
+
return impl.stoppedCv.wait_for(lock, timeout, [&] { return impl.stopped; });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void KernelRuntime::post(std::vector<uint8_t> bytes) {
|
|
198
|
+
auto& impl = *impl_;
|
|
199
|
+
std::function<void()> integrity;
|
|
200
|
+
{
|
|
201
|
+
std::lock_guard<std::mutex> lock(impl.mutex);
|
|
202
|
+
if (impl.stopping.load(std::memory_order_acquire) ||
|
|
203
|
+
!impl.running.load(std::memory_order_acquire)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (impl.pendingPosts >= Impl::kMaxInboundFrames) {
|
|
207
|
+
integrity = impl.hooks.onIntegrity;
|
|
208
|
+
} else {
|
|
209
|
+
impl.pendingPosts += 1;
|
|
210
|
+
impl.immediate.push_back([&impl, bytes = std::move(bytes)]() mutable {
|
|
211
|
+
{
|
|
212
|
+
std::lock_guard<std::mutex> inner(impl.mutex);
|
|
213
|
+
impl.pendingPosts -= 1;
|
|
214
|
+
}
|
|
215
|
+
impl.deliver(std::move(bytes));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
impl.cv.notify_one();
|
|
220
|
+
if (integrity) {
|
|
221
|
+
integrity();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void KernelRuntime::destroy() noexcept {
|
|
226
|
+
auto& impl = *impl_;
|
|
227
|
+
{
|
|
228
|
+
std::lock_guard<std::mutex> lock(impl.mutex);
|
|
229
|
+
if (impl.stopping.load(std::memory_order_acquire)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
impl.stopping.store(true, std::memory_order_release);
|
|
233
|
+
}
|
|
234
|
+
impl.cv.notify_all();
|
|
235
|
+
{
|
|
236
|
+
std::lock_guard<std::mutex> lock(impl.interruptMutex);
|
|
237
|
+
if (impl.interruptable != nullptr) {
|
|
238
|
+
impl.interruptable->asyncTriggerTimeout();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
void KernelRuntime::Impl::run() {
|
|
244
|
+
nameKernelThread();
|
|
245
|
+
auto work = [this] {
|
|
246
|
+
std::optional<std::string> error;
|
|
247
|
+
try {
|
|
248
|
+
initRuntime();
|
|
249
|
+
loop();
|
|
250
|
+
} catch (...) {
|
|
251
|
+
// describe() runs inside the catch so a JSError (and the jsi values it
|
|
252
|
+
// holds) is destroyed while the runtime still exists.
|
|
253
|
+
try {
|
|
254
|
+
error = describe(std::current_exception());
|
|
255
|
+
} catch (...) {
|
|
256
|
+
error = "undescribable kernel error";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// An exception raised after a destroy() request — the async interrupt or
|
|
260
|
+
// anything racing it — is a normal stop, not a kernel failure.
|
|
261
|
+
if (error.has_value() && !stopping.load(std::memory_order_acquire)) {
|
|
262
|
+
fail(std::move(*error));
|
|
263
|
+
}
|
|
264
|
+
teardown();
|
|
265
|
+
};
|
|
266
|
+
#if defined(__ANDROID__)
|
|
267
|
+
// Run the kernel body inside a Java call frame via fbjni. A plain
|
|
268
|
+
// AttachCurrentThread gives the thread a JNIEnv but FindClass still resolves
|
|
269
|
+
// through the boot class loader, which cannot see app-dex classes — Hermes
|
|
270
|
+
// delegates String.prototype.normalize/localeCompare to
|
|
271
|
+
// com.facebook.hermes.unicode.AndroidUnicodeUtils, and Nitro's registry is a
|
|
272
|
+
// Java class; the former throws fbjni::JniException across a noexcept
|
|
273
|
+
// boundary (std::terminate) on a raw attached thread. WithClassLoader makes
|
|
274
|
+
// app classes resolvable and attaches the thread for the whole duration.
|
|
275
|
+
facebook::jni::ThreadScope::WithClassLoader(std::move(work));
|
|
276
|
+
#else
|
|
277
|
+
work();
|
|
278
|
+
#endif
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
void KernelRuntime::Impl::initRuntime() {
|
|
282
|
+
rt = facebook::hermes::makeHermesRuntime(runtimeConfigFor(profile));
|
|
283
|
+
{
|
|
284
|
+
std::lock_guard<std::mutex> lock(interruptMutex);
|
|
285
|
+
interruptable = jsi::castInterface<facebook::hermes::IHermes>(rt.get());
|
|
286
|
+
}
|
|
287
|
+
if (interruptable == nullptr) {
|
|
288
|
+
throw std::runtime_error("hermes runtime does not expose IHermes");
|
|
289
|
+
}
|
|
290
|
+
if (stopping.load(std::memory_order_acquire)) {
|
|
291
|
+
// destroy() won the race before the script ran: skip globals and
|
|
292
|
+
// evaluation; run() then returns from loop() at once into teardown.
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
installKernelGlobals(*rt, globals);
|
|
297
|
+
|
|
298
|
+
if (hooks.beforeEvaluate) {
|
|
299
|
+
hooks.beforeEvaluate(*rt);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
evaluate();
|
|
303
|
+
rt->drainMicrotasks();
|
|
304
|
+
if (fatal.has_value()) {
|
|
305
|
+
throw std::runtime_error(*fatal);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
void KernelRuntime::Impl::evaluate() {
|
|
310
|
+
const auto& bytes = *script.bytes;
|
|
311
|
+
if (script.kind == KernelScript::Kind::bytecode) {
|
|
312
|
+
auto* api = jsi::castInterface<facebook::hermes::IHermesRootAPI>(
|
|
313
|
+
facebook::hermes::makeHermesRootAPI());
|
|
314
|
+
if (api == nullptr) {
|
|
315
|
+
throw std::runtime_error("Hermes root API unavailable");
|
|
316
|
+
}
|
|
317
|
+
if (!api->isHermesBytecode(bytes.data(), bytes.size())) {
|
|
318
|
+
throw std::runtime_error("embedded code is not Hermes bytecode");
|
|
319
|
+
}
|
|
320
|
+
std::string sanityError;
|
|
321
|
+
if (!api->hermesBytecodeSanityCheck(bytes.data(), bytes.size(), &sanityError)) {
|
|
322
|
+
throw std::runtime_error(
|
|
323
|
+
"embedded Hermes bytecode failed sanity check (version " +
|
|
324
|
+
std::to_string(api->getBytecodeVersion()) + "): " + sanityError);
|
|
325
|
+
}
|
|
326
|
+
rt->evaluateJavaScript(
|
|
327
|
+
std::make_shared<VectorBuffer>(script.bytes), "kernel.hbc");
|
|
328
|
+
} else {
|
|
329
|
+
rt->evaluateJavaScript(
|
|
330
|
+
std::make_shared<jsi::StringBuffer>(
|
|
331
|
+
std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size())),
|
|
332
|
+
"kernel.ts");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
void KernelRuntime::Impl::loop() {
|
|
337
|
+
for (;;) {
|
|
338
|
+
std::function<void()> task;
|
|
339
|
+
std::optional<Timer> due;
|
|
340
|
+
{
|
|
341
|
+
std::unique_lock<std::mutex> lock(mutex);
|
|
342
|
+
for (;;) {
|
|
343
|
+
if (stopping.load(std::memory_order_acquire)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const auto now = std::chrono::steady_clock::now();
|
|
347
|
+
// Timers live on the kernel thread only; the mutex here just happens
|
|
348
|
+
// to be held while picking the next piece of work.
|
|
349
|
+
if (!timed.empty() && timed.begin()->first <= now) {
|
|
350
|
+
auto node = timed.extract(timed.begin());
|
|
351
|
+
timerIndex.erase(node.mapped().id);
|
|
352
|
+
due = std::move(node.mapped());
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
if (!immediate.empty()) {
|
|
356
|
+
task = std::move(immediate.front());
|
|
357
|
+
immediate.pop_front();
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (timed.empty()) {
|
|
361
|
+
cv.wait(lock);
|
|
362
|
+
} else {
|
|
363
|
+
cv.wait_until(lock, timed.begin()->first);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
std::optional<std::string> error;
|
|
369
|
+
try {
|
|
370
|
+
if (task) {
|
|
371
|
+
task();
|
|
372
|
+
} else if (due.has_value()) {
|
|
373
|
+
due->fn.call(*rt);
|
|
374
|
+
}
|
|
375
|
+
rt->drainMicrotasks();
|
|
376
|
+
} catch (...) {
|
|
377
|
+
error = describe(std::current_exception());
|
|
378
|
+
}
|
|
379
|
+
// Drop jsi values while the runtime is alive, whichever way we exit.
|
|
380
|
+
due.reset();
|
|
381
|
+
task = nullptr;
|
|
382
|
+
if (error.has_value()) {
|
|
383
|
+
if (stopping.load(std::memory_order_acquire)) {
|
|
384
|
+
// The destroy() interrupt surfaced as an exception — a normal stop.
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
fail("kernel task failed: " + std::move(*error));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (fatal.has_value()) {
|
|
391
|
+
fail(std::move(*fatal));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
void KernelRuntime::Impl::deliver(std::vector<uint8_t> bytes) {
|
|
398
|
+
if (!globals->messageHandler.has_value()) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
auto buffer = rt->createArrayBuffer(std::make_shared<OwningBuffer>(std::move(bytes)));
|
|
402
|
+
globals->messageHandler->call(*rt, jsi::Value(std::move(buffer)));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
void KernelRuntime::Impl::enqueue(std::function<void(jsi::Runtime&)> task) {
|
|
406
|
+
{
|
|
407
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
408
|
+
if (tornDown) {
|
|
409
|
+
// The loop is gone and teardown already ran: drop the task here so its
|
|
410
|
+
// captures (wiping buffers, dead pending calls) release immediately.
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
immediate.push_back(
|
|
414
|
+
[this, task = std::move(task)]() mutable { task(*rt); });
|
|
415
|
+
}
|
|
416
|
+
cv.notify_one();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
KernelGlobalsState::TimerCancel KernelRuntime::Impl::scheduleTimer(
|
|
420
|
+
std::chrono::steady_clock::time_point deadline,
|
|
421
|
+
jsi::Function fn) {
|
|
422
|
+
// Kernel thread only — host functions and the loop run there, so no lock.
|
|
423
|
+
const uint64_t id = nextTimerId++;
|
|
424
|
+
auto it = timed.emplace(deadline, Timer{id, std::move(fn)});
|
|
425
|
+
timerIndex.emplace(id, it);
|
|
426
|
+
return [this, id] {
|
|
427
|
+
if (tornDown) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
auto found = timerIndex.find(id);
|
|
431
|
+
if (found != timerIndex.end()) {
|
|
432
|
+
// Erasing releases the stored jsi::Function immediately instead of
|
|
433
|
+
// parking it until the deadline.
|
|
434
|
+
timed.erase(found->second);
|
|
435
|
+
timerIndex.erase(found);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
void KernelRuntime::Impl::fail(std::string message) {
|
|
441
|
+
// Kernel thread only.
|
|
442
|
+
stopping.store(true, std::memory_order_release);
|
|
443
|
+
if (hooks.onError) {
|
|
444
|
+
try {
|
|
445
|
+
hooks.onError(std::move(message));
|
|
446
|
+
} catch (...) {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
teardown();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
void KernelRuntime::Impl::teardown() {
|
|
453
|
+
// Kernel thread only; tornDown is shared with enqueue() on other threads,
|
|
454
|
+
// so it is set under the same mutex that guards the queue.
|
|
455
|
+
{
|
|
456
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
457
|
+
if (tornDown) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
tornDown = true;
|
|
461
|
+
// Queued-but-unrun tasks die here, on the kernel thread, while the
|
|
462
|
+
// runtime is alive; enqueue() drops anything that arrives after this.
|
|
463
|
+
immediate.clear();
|
|
464
|
+
}
|
|
465
|
+
// Drop every jsi value before the runtime itself.
|
|
466
|
+
globals->intrinsics.reset();
|
|
467
|
+
globals->messageHandler.reset();
|
|
468
|
+
globals->integrityHandler.reset();
|
|
469
|
+
for (const auto& call : globals->pending) {
|
|
470
|
+
call->done.reset();
|
|
471
|
+
}
|
|
472
|
+
globals->pending.clear();
|
|
473
|
+
// A KDF worker that sees inactive drops its job — safe only because `done`
|
|
474
|
+
// was already cleared above, on this thread, while the runtime was alive.
|
|
475
|
+
globals->active->store(false, std::memory_order_release);
|
|
476
|
+
// A system prompt still on screen belongs to nobody now: dismiss it.
|
|
477
|
+
for (const auto& cancel : globals->cancelCeremonies) {
|
|
478
|
+
cancel();
|
|
479
|
+
}
|
|
480
|
+
// Timed/timerIndex are kernel-thread-only state.
|
|
481
|
+
timed.clear();
|
|
482
|
+
timerIndex.clear();
|
|
483
|
+
{
|
|
484
|
+
std::lock_guard<std::mutex> lock(interruptMutex);
|
|
485
|
+
interruptable = nullptr;
|
|
486
|
+
}
|
|
487
|
+
rt.reset();
|
|
488
|
+
running.store(false, std::memory_order_release);
|
|
489
|
+
{
|
|
490
|
+
std::lock_guard<std::mutex> lock(stoppedMutex);
|
|
491
|
+
stopped = true;
|
|
492
|
+
}
|
|
493
|
+
stoppedCv.notify_all();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
} // namespace qntx
|