@derec-alliance/react-native 0.0.1
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/DeRec.podspec +25 -0
- package/LICENSE +201 -0
- package/README.md +222 -0
- package/android/CMakeLists.txt +38 -0
- package/android/build.gradle +79 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/org/derec/DeRecModule.kt +56 -0
- package/android/src/main/java/org/derec/DeRecPackage.kt +24 -0
- package/android/src/main/jni/OnLoad.cpp +39 -0
- package/android/src/main/jniLibs/arm64-v8a/libderec_library.a +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libderec_library.a +0 -0
- package/android/src/main/jniLibs/x86_64/libderec_library.a +0 -0
- package/cpp/Convert.cpp +50 -0
- package/cpp/Convert.h +38 -0
- package/cpp/DeRecInstaller.cpp +186 -0
- package/cpp/DeRecInstaller.h +28 -0
- package/cpp/Invoker.h +20 -0
- package/cpp/JsCallbackBridge.cpp +144 -0
- package/cpp/JsCallbackBridge.h +148 -0
- package/cpp/Primitives.cpp +1193 -0
- package/cpp/Primitives.h +39 -0
- package/cpp/ProtocolHost.cpp +613 -0
- package/cpp/ProtocolHost.h +88 -0
- package/cpp/RuntimeInvoker.h +29 -0
- package/cpp/StoreCallbacks.cpp +1293 -0
- package/cpp/StoreCallbacks.h +111 -0
- package/cpp/WorkerThread.cpp +74 -0
- package/cpp/WorkerThread.h +54 -0
- package/cpp/derec_ffi.h +2145 -0
- package/ios/DeRec.h +23 -0
- package/ios/DeRec.mm +39 -0
- package/ios/DeRecFFI.xcframework/Info.plist +48 -0
- package/ios/DeRecFFI.xcframework/ios-arm64/Headers/derec_ffi.h +2145 -0
- package/ios/DeRecFFI.xcframework/ios-arm64/libderec_library.a +0 -0
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/Headers/derec_ffi.h +2145 -0
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/libderec_library.a +0 -0
- package/lib/binary.d.ts +19 -0
- package/lib/binary.js +164 -0
- package/lib/codec.d.ts +16 -0
- package/lib/codec.js +80 -0
- package/lib/errors.d.ts +23 -0
- package/lib/errors.js +35 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +27 -0
- package/lib/messages.d.ts +41 -0
- package/lib/messages.js +192 -0
- package/lib/native.d.ts +11 -0
- package/lib/native.js +40 -0
- package/lib/primitives.d.ts +137 -0
- package/lib/primitives.js +329 -0
- package/lib/protocol.d.ts +188 -0
- package/lib/protocol.js +514 -0
- package/lib/types.d.ts +1014 -0
- package/lib/types.js +63 -0
- package/package.json +55 -0
- package/react-native.config.js +19 -0
- package/src/binary.ts +182 -0
- package/src/codec.ts +79 -0
- package/src/errors.ts +45 -0
- package/src/index.ts +7 -0
- package/src/messages.ts +192 -0
- package/src/native.ts +52 -0
- package/src/primitives.ts +770 -0
- package/src/protocol.ts +621 -0
- package/src/types.ts +1040 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#include <fbjni/fbjni.h>
|
|
5
|
+
#include <jni.h>
|
|
6
|
+
#include <jsi/jsi.h>
|
|
7
|
+
#include <ReactCommon/CallInvokerHolder.h>
|
|
8
|
+
|
|
9
|
+
#include <memory>
|
|
10
|
+
|
|
11
|
+
#include "DeRecInstaller.h"
|
|
12
|
+
#include "RuntimeInvoker.h"
|
|
13
|
+
|
|
14
|
+
extern "C" JNIEXPORT void JNICALL
|
|
15
|
+
Java_org_derec_DeRecModule_nativeInstall(JNIEnv* env,
|
|
16
|
+
jobject /*thiz*/,
|
|
17
|
+
jlong runtimePtr,
|
|
18
|
+
jobject callInvokerHolder) {
|
|
19
|
+
auto* runtime = reinterpret_cast<facebook::jsi::Runtime*>(runtimePtr);
|
|
20
|
+
if (runtime == nullptr) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
auto holder = facebook::jni::alias_ref<
|
|
24
|
+
facebook::react::CallInvokerHolder::javaobject>{
|
|
25
|
+
static_cast<facebook::react::CallInvokerHolder::javaobject>(
|
|
26
|
+
callInvokerHolder)};
|
|
27
|
+
auto invoker = std::make_shared<derec::RuntimeInvoker>(
|
|
28
|
+
holder->cthis()->getCallInvoker());
|
|
29
|
+
derec::install(*runtime, invoker);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
extern "C" JNIEXPORT void JNICALL
|
|
33
|
+
Java_org_derec_DeRecModule_nativeInvalidate(JNIEnv*, jobject) {
|
|
34
|
+
derec::invalidateAll();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
|
38
|
+
return facebook::jni::initialize(vm, [] {});
|
|
39
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/cpp/Convert.cpp
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#include "Convert.h"
|
|
5
|
+
|
|
6
|
+
#include <cstdlib>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
|
|
9
|
+
namespace derec {
|
|
10
|
+
|
|
11
|
+
OwnedBytes allocBytes(size_t len) {
|
|
12
|
+
if (len == 0) {
|
|
13
|
+
return OwnedBytes{nullptr, 0};
|
|
14
|
+
}
|
|
15
|
+
auto* ptr = static_cast<uint8_t*>(std::malloc(len));
|
|
16
|
+
return OwnedBytes{ptr, ptr == nullptr ? 0 : len};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
void freeBytes(uint8_t* ptr, size_t /*len*/) {
|
|
20
|
+
if (ptr != nullptr) {
|
|
21
|
+
std::free(ptr);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
std::vector<uint8_t> takeBuffer(DeRecBuffer& buffer) {
|
|
26
|
+
std::vector<uint8_t> out;
|
|
27
|
+
if (buffer.ptr != nullptr && buffer.len > 0) {
|
|
28
|
+
out.assign(buffer.ptr, buffer.ptr + buffer.len);
|
|
29
|
+
}
|
|
30
|
+
derec_free_buffer(buffer.ptr, buffer.len);
|
|
31
|
+
buffer.ptr = nullptr;
|
|
32
|
+
buffer.len = 0;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
std::string takeString(char* owned) {
|
|
37
|
+
if (owned == nullptr) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
std::string out(owned);
|
|
41
|
+
derec_free_string(owned);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
std::pair<std::string, std::string> errorName(int32_t category, int32_t code) {
|
|
46
|
+
return {std::string(derec_error_category_name(category)),
|
|
47
|
+
std::string(derec_error_code_name(code))};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} // namespace derec
|
package/cpp/Convert.h
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <cstddef>
|
|
7
|
+
#include <cstdint>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <utility>
|
|
10
|
+
#include <vector>
|
|
11
|
+
|
|
12
|
+
extern "C" {
|
|
13
|
+
#include "derec_ffi.h"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
namespace derec {
|
|
17
|
+
|
|
18
|
+
/// A byte buffer allocated by this binding. Ownership passes to Rust across a
|
|
19
|
+
/// store callback out-parameter; Rust copies the contents and hands the
|
|
20
|
+
/// pointer back through the callback struct's `free_buffer`.
|
|
21
|
+
struct OwnedBytes {
|
|
22
|
+
uint8_t* ptr;
|
|
23
|
+
size_t len;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
OwnedBytes allocBytes(size_t len);
|
|
27
|
+
void freeBytes(uint8_t* ptr, size_t len);
|
|
28
|
+
|
|
29
|
+
/// Copy `buffer` into a vector and release the Rust allocation.
|
|
30
|
+
std::vector<uint8_t> takeBuffer(DeRecBuffer& buffer);
|
|
31
|
+
|
|
32
|
+
/// Copy a Rust-owned C string and release it via `derec_free_string`.
|
|
33
|
+
std::string takeString(char* owned);
|
|
34
|
+
|
|
35
|
+
/// Resolve the `(category, code)` name pair from the crate's accessors.
|
|
36
|
+
std::pair<std::string, std::string> errorName(int32_t category, int32_t code);
|
|
37
|
+
|
|
38
|
+
} // namespace derec
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#include "DeRecInstaller.h"
|
|
5
|
+
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <utility>
|
|
10
|
+
#include <vector>
|
|
11
|
+
|
|
12
|
+
#include "Convert.h"
|
|
13
|
+
#include "JsCallbackBridge.h"
|
|
14
|
+
#include "Primitives.h"
|
|
15
|
+
#include "ProtocolHost.h"
|
|
16
|
+
#include "StoreCallbacks.h"
|
|
17
|
+
|
|
18
|
+
using namespace facebook;
|
|
19
|
+
|
|
20
|
+
namespace derec {
|
|
21
|
+
|
|
22
|
+
namespace {
|
|
23
|
+
|
|
24
|
+
std::mutex& registryMutex() {
|
|
25
|
+
static std::mutex m;
|
|
26
|
+
return m;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
std::vector<std::weak_ptr<InstanceState>>& registry() {
|
|
30
|
+
static std::vector<std::weak_ptr<InstanceState>> instances;
|
|
31
|
+
return instances;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void requireArgs(jsi::Runtime& rt, const char* name, size_t count, size_t required) {
|
|
35
|
+
if (count < required) {
|
|
36
|
+
throw jsi::JSError(rt, std::string(name) + " expects " +
|
|
37
|
+
std::to_string(required) + " arguments");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// `secret_id` travels through `configJson` as a quoted decimal string (the
|
|
42
|
+
/// same field `DeRecProtocolBuilder.build()` always writes first).
|
|
43
|
+
/// `derec_protocol_new`'s result carries only the opaque handle, not the id,
|
|
44
|
+
/// so `ProtocolHost::secretId()` — which has no FFI accessor of its own —
|
|
45
|
+
/// needs this recovered independently. `JSON.stringify` emits no internal
|
|
46
|
+
/// whitespace, so a direct scan for the field is sufficient; this mirrors
|
|
47
|
+
/// the bare-JSON-integer scanning `StoreCallbacks.cpp` already relies on for
|
|
48
|
+
/// the wire's other compact JSON payloads.
|
|
49
|
+
uint64_t parseSecretIdFromConfigJson(const std::string& json) {
|
|
50
|
+
static const std::string needle = "\"secret_id\":\"";
|
|
51
|
+
size_t pos = json.find(needle);
|
|
52
|
+
if (pos == std::string::npos) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
pos += needle.size();
|
|
56
|
+
uint64_t value = 0;
|
|
57
|
+
while (pos < json.size() && json[pos] >= '0' && json[pos] <= '9') {
|
|
58
|
+
value = value * 10 + static_cast<uint64_t>(json[pos] - '0');
|
|
59
|
+
++pos;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// `protocol_new(configJson: string, communicationInfo: ArrayBuffer, stores:
|
|
65
|
+
/// object) -> HostObject`. The one place that constructs a `ProtocolHost`:
|
|
66
|
+
/// builds the per-instance `InstanceState` and `JsCallbackBridge`, resolves
|
|
67
|
+
/// the six store/transport callback structs via `StoreBindings::create`, and
|
|
68
|
+
/// hands them to `derec_protocol_new` in the FFI's own declared parameter
|
|
69
|
+
/// order — channel, secret, share, user_secret, state, transport, which is
|
|
70
|
+
/// neither alphabetical nor the order the six `with*Store` builder methods
|
|
71
|
+
/// appear in (see `library/src/interop/ffi/protocol/handle/mod.rs`'s
|
|
72
|
+
/// `derec_protocol_new` declaration).
|
|
73
|
+
jsi::Value protocolNew(jsi::Runtime& rt,
|
|
74
|
+
const std::shared_ptr<Invoker>& invoker,
|
|
75
|
+
const jsi::Value&,
|
|
76
|
+
const jsi::Value* args,
|
|
77
|
+
size_t count) {
|
|
78
|
+
requireArgs(rt, "protocol_new", count, 3);
|
|
79
|
+
std::string configJson = args[0].asString(rt).utf8(rt);
|
|
80
|
+
ByteView info = asBytes(rt, args[1]);
|
|
81
|
+
jsi::Object storesObj = args[2].asObject(rt);
|
|
82
|
+
|
|
83
|
+
auto state = std::make_shared<InstanceState>();
|
|
84
|
+
auto bridge = std::make_shared<JsCallbackBridge>(*invoker, state);
|
|
85
|
+
std::shared_ptr<StoreBindings> stores = StoreBindings::create(rt, storesObj, *bridge, state);
|
|
86
|
+
|
|
87
|
+
DeRecProtocolNewResult result = derec_protocol_new(
|
|
88
|
+
reinterpret_cast<const uint8_t*>(configJson.data()), configJson.size(), info.ptr, info.len,
|
|
89
|
+
stores->channel(), stores->secret(), stores->share(), stores->userSecret(), stores->state(),
|
|
90
|
+
stores->transport());
|
|
91
|
+
if (result.error.code != DEREC_CODE_OK) {
|
|
92
|
+
throwDeRecError(rt, result.error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
uint64_t secretId = parseSecretIdFromConfigJson(configJson);
|
|
96
|
+
|
|
97
|
+
// Ownership: `stores` (a `StoreBindings`) holds `bridge`'s referent as a
|
|
98
|
+
// plain `JsCallbackBridge&` (see `StoreBindings.h`), not a `shared_ptr` —
|
|
99
|
+
// and a store-callback trampoline can keep `StoreBindings` alive via
|
|
100
|
+
// `shared_from_this()` on the JavaScript `CallInvoker`'s queue after this
|
|
101
|
+
// `ProtocolHost` itself has been fully destroyed. `bridge` therefore must
|
|
102
|
+
// not be scoped to die *with* `ProtocolHost` in the ordinary sense of "a
|
|
103
|
+
// member that is destroyed inside its owner's destructor" — it must
|
|
104
|
+
// outlive `ProtocolHost`'s own destruction, not merely coincide with it.
|
|
105
|
+
//
|
|
106
|
+
// The custom deleter below achieves that without adding a member to
|
|
107
|
+
// `ProtocolHost` (its constructor is fixed): it ties `bridge`'s release to
|
|
108
|
+
// the *same* `shared_ptr` control block that owns the `ProtocolHost`
|
|
109
|
+
// object, but strictly *after* that control block's normal deletion step.
|
|
110
|
+
// A `shared_ptr`'s custom deleter call (`delete p`, which runs
|
|
111
|
+
// `~ProtocolHost` to completion — invalidate, drain the worker, free the
|
|
112
|
+
// handle) always finishes before the control block releases whatever the
|
|
113
|
+
// deleter closure itself captured. So `bridge` is released only once
|
|
114
|
+
// `ProtocolHost`'s entire teardown has already run, which is later than
|
|
115
|
+
// any point at which a worker-thread trampoline could still call
|
|
116
|
+
// `StoreBindings::bridge()` — that call only ever happens synchronously,
|
|
117
|
+
// from inside a `derec_protocol_*` call on the worker thread, and
|
|
118
|
+
// `drainAndJoin` (part of that same teardown) does not return until every
|
|
119
|
+
// such call has finished.
|
|
120
|
+
std::shared_ptr<ProtocolHost> host(
|
|
121
|
+
new ProtocolHost(result.handle, secretId, invoker, state, std::move(stores)),
|
|
122
|
+
[bridge](ProtocolHost* p) mutable {
|
|
123
|
+
delete p;
|
|
124
|
+
// `bridge` (captured by value) is released only after the `delete`
|
|
125
|
+
// above returns, i.e. only after `~ProtocolHost` has fully run.
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return jsi::Object::createFromHostObject(rt, std::move(host));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
} // namespace
|
|
132
|
+
|
|
133
|
+
void registerInstance(const std::shared_ptr<InstanceState>& state) {
|
|
134
|
+
std::lock_guard<std::mutex> lock(registryMutex());
|
|
135
|
+
registry().push_back(state);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
void invalidateAll() {
|
|
139
|
+
std::lock_guard<std::mutex> lock(registryMutex());
|
|
140
|
+
for (auto& weak : registry()) {
|
|
141
|
+
if (auto state = weak.lock()) {
|
|
142
|
+
// The runtime itself is going away, not just one protocol instance:
|
|
143
|
+
// work already queued onto the JavaScript thread must be abandoned
|
|
144
|
+
// rather than run, and the JavaScript values it retains must be leaked
|
|
145
|
+
// rather than released.
|
|
146
|
+
state->invalidateRuntime();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
registry().clear();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
void install(jsi::Runtime& rt, std::shared_ptr<Invoker> invoker) {
|
|
153
|
+
// `install` runs on the JavaScript thread by construction, so this is the
|
|
154
|
+
// one place that can record which thread that is. `JsCallbackBridge::
|
|
155
|
+
// callSync` asserts against it in debug builds.
|
|
156
|
+
noteJsThread();
|
|
157
|
+
|
|
158
|
+
auto host = jsi::Object(rt);
|
|
159
|
+
|
|
160
|
+
host.setProperty(
|
|
161
|
+
rt, "version",
|
|
162
|
+
jsi::Function::createFromHostFunction(
|
|
163
|
+
rt, jsi::PropNameID::forAscii(rt, "version"), 0,
|
|
164
|
+
[](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t)
|
|
165
|
+
-> jsi::Value {
|
|
166
|
+
DeRecProtocolVersion version = derec_protocol_version();
|
|
167
|
+
std::string text = std::to_string(version.major) + "." +
|
|
168
|
+
std::to_string(version.minor);
|
|
169
|
+
return jsi::String::createFromUtf8(rt, text);
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
installPrimitives(rt, host);
|
|
173
|
+
|
|
174
|
+
host.setProperty(
|
|
175
|
+
rt, "protocol_new",
|
|
176
|
+
jsi::Function::createFromHostFunction(
|
|
177
|
+
rt, jsi::PropNameID::forAscii(rt, "protocol_new"), 3,
|
|
178
|
+
[invoker](jsi::Runtime& rt2, const jsi::Value& thisVal, const jsi::Value* args,
|
|
179
|
+
size_t count) -> jsi::Value {
|
|
180
|
+
return protocolNew(rt2, invoker, thisVal, args, count);
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
rt.global().setProperty(rt, "__DeRec", std::move(host));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
} // namespace derec
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <jsi/jsi.h>
|
|
7
|
+
|
|
8
|
+
#include <memory>
|
|
9
|
+
|
|
10
|
+
#include "Invoker.h"
|
|
11
|
+
#include "JsCallbackBridge.h"
|
|
12
|
+
|
|
13
|
+
namespace derec {
|
|
14
|
+
|
|
15
|
+
/// Install the `__DeRec` host object into `rt`.
|
|
16
|
+
///
|
|
17
|
+
/// `invoker` must schedule work on the same JavaScript thread that owns `rt`.
|
|
18
|
+
void install(facebook::jsi::Runtime& rt, std::shared_ptr<Invoker> invoker);
|
|
19
|
+
|
|
20
|
+
/// Signal that the JavaScript runtime is going away. Wakes every protocol
|
|
21
|
+
/// instance blocked in a store callback so their workers can unwind instead
|
|
22
|
+
/// of hanging.
|
|
23
|
+
void invalidateAll();
|
|
24
|
+
|
|
25
|
+
/// Track a protocol instance so `invalidateAll` can reach it at teardown.
|
|
26
|
+
void registerInstance(const std::shared_ptr<InstanceState>& state);
|
|
27
|
+
|
|
28
|
+
} // namespace derec
|
package/cpp/Invoker.h
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <functional>
|
|
7
|
+
|
|
8
|
+
namespace derec {
|
|
9
|
+
|
|
10
|
+
/// Abstraction over React Native's `CallInvoker`. Declared as an interface so
|
|
11
|
+
/// the callback bridge can be exercised on the host with no JSI runtime.
|
|
12
|
+
class Invoker {
|
|
13
|
+
public:
|
|
14
|
+
virtual ~Invoker() = default;
|
|
15
|
+
|
|
16
|
+
/// Schedule `work` to run on the JavaScript thread. Must not block.
|
|
17
|
+
virtual void invokeAsync(std::function<void()> work) = 0;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
} // namespace derec
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#include "JsCallbackBridge.h"
|
|
5
|
+
|
|
6
|
+
#include <cassert>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
#include <utility>
|
|
9
|
+
|
|
10
|
+
namespace derec {
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
/// One in-flight callback. Heap-allocated and shared so a settlement arriving
|
|
15
|
+
/// after a timeout writes into live memory rather than a dead stack frame.
|
|
16
|
+
///
|
|
17
|
+
/// Both fields are guarded by the owning `InstanceState`'s mutex; the slot
|
|
18
|
+
/// carries no lock of its own so that a waiter and `InstanceState::invalidate`
|
|
19
|
+
/// synchronise on the same condition variable.
|
|
20
|
+
struct CallSlot {
|
|
21
|
+
bool done = false;
|
|
22
|
+
CallResult result{kBackendFailure, {}};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/// Written once, from `noteJsThread`, and published by the release store to
|
|
26
|
+
/// `jsThreadKnown` below; readers acquire it. Not a `thread::id` atomic
|
|
27
|
+
/// because `std::thread::id` is not guaranteed lock-free.
|
|
28
|
+
std::thread::id& jsThreadId() {
|
|
29
|
+
static std::thread::id id;
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
std::atomic<bool>& jsThreadKnown() {
|
|
34
|
+
static std::atomic<bool> known{false};
|
|
35
|
+
return known;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
int& storeCallbackDepth() {
|
|
39
|
+
static thread_local int depth = 0;
|
|
40
|
+
return depth;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
} // namespace
|
|
44
|
+
|
|
45
|
+
void InstanceState::invalidate() {
|
|
46
|
+
{
|
|
47
|
+
std::lock_guard<std::mutex> lock(m);
|
|
48
|
+
invalidated.store(true);
|
|
49
|
+
}
|
|
50
|
+
cv.notify_all();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
void InstanceState::invalidateRuntime() {
|
|
54
|
+
{
|
|
55
|
+
std::lock_guard<std::mutex> lock(m);
|
|
56
|
+
runtimeInvalidated.store(true);
|
|
57
|
+
invalidated.store(true);
|
|
58
|
+
}
|
|
59
|
+
cv.notify_all();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
void noteJsThread() {
|
|
63
|
+
jsThreadId() = std::this_thread::get_id();
|
|
64
|
+
jsThreadKnown().store(true, std::memory_order_release);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
bool isJsThread() {
|
|
68
|
+
if (!jsThreadKnown().load(std::memory_order_acquire)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return std::this_thread::get_id() == jsThreadId();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
bool isInsideStoreCallback() { return storeCallbackDepth() > 0; }
|
|
75
|
+
|
|
76
|
+
StoreCallbackScope::StoreCallbackScope() { ++storeCallbackDepth(); }
|
|
77
|
+
|
|
78
|
+
StoreCallbackScope::~StoreCallbackScope() { --storeCallbackDepth(); }
|
|
79
|
+
|
|
80
|
+
JsCallbackBridge::JsCallbackBridge(Invoker& invoker,
|
|
81
|
+
std::shared_ptr<InstanceState> state,
|
|
82
|
+
std::chrono::milliseconds timeout)
|
|
83
|
+
: invoker_(invoker), state_(std::move(state)), timeout_(timeout) {}
|
|
84
|
+
|
|
85
|
+
CallResult JsCallbackBridge::callSync(
|
|
86
|
+
std::function<void(std::function<void(CallResult)>)> body) {
|
|
87
|
+
// Blocking here on the JavaScript thread would wait for a settlement that
|
|
88
|
+
// only the JavaScript thread can produce. Debug-only: a release build must
|
|
89
|
+
// not pay for the check, and there is no correct recovery anyway.
|
|
90
|
+
assert(!isJsThread() &&
|
|
91
|
+
"JsCallbackBridge::callSync must not run on the JavaScript thread");
|
|
92
|
+
|
|
93
|
+
if (state_->invalidated.load()) {
|
|
94
|
+
return CallResult{kBackendFailure, {}};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
auto slot = std::make_shared<CallSlot>();
|
|
98
|
+
// The scheduled work owns a reference to the instance state: it must be able
|
|
99
|
+
// to take the mutex even if the bridge and its owner are already gone.
|
|
100
|
+
auto state = state_;
|
|
101
|
+
|
|
102
|
+
invoker_.invokeAsync([slot, state, body = std::move(body)]() mutable {
|
|
103
|
+
auto settle = [slot, state](CallResult result) {
|
|
104
|
+
{
|
|
105
|
+
std::lock_guard<std::mutex> lock(state->m);
|
|
106
|
+
if (slot->done) {
|
|
107
|
+
return; // a timeout or an invalidation already resolved this slot
|
|
108
|
+
}
|
|
109
|
+
slot->result = std::move(result);
|
|
110
|
+
slot->done = true;
|
|
111
|
+
}
|
|
112
|
+
state->cv.notify_all();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// A JavaScript store that throws synchronously must not escape into the
|
|
116
|
+
// runtime's dispatch loop; report it as a backend failure instead.
|
|
117
|
+
try {
|
|
118
|
+
// Marks exactly the window in which a protocol call made from this
|
|
119
|
+
// thread would be genuinely re-entrant: the store body's own execution.
|
|
120
|
+
// It ends when `body` returns, even though the worker stays parked
|
|
121
|
+
// until the Promise the store handed back settles.
|
|
122
|
+
StoreCallbackScope scope;
|
|
123
|
+
body(settle);
|
|
124
|
+
} catch (...) {
|
|
125
|
+
settle(CallResult{kBackendFailure, {}});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
std::unique_lock<std::mutex> lock(state_->m);
|
|
130
|
+
state_->cv.wait_for(lock, timeout_, [this, &slot] {
|
|
131
|
+
return slot->done || state_->invalidated.load();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!slot->done) {
|
|
135
|
+
// Timed out or invalidated. Mark the slot resolved under the same lock so
|
|
136
|
+
// a late settlement becomes a no-op, and report a backend failure — never
|
|
137
|
+
// `1`, which would tell the protocol a stored share is absent.
|
|
138
|
+
slot->done = true;
|
|
139
|
+
return CallResult{kBackendFailure, {}};
|
|
140
|
+
}
|
|
141
|
+
return std::move(slot->result);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
} // namespace derec
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 DeRec Alliance. All rights reserved.
|
|
3
|
+
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <atomic>
|
|
7
|
+
#include <chrono>
|
|
8
|
+
#include <condition_variable>
|
|
9
|
+
#include <cstdint>
|
|
10
|
+
#include <functional>
|
|
11
|
+
#include <memory>
|
|
12
|
+
#include <mutex>
|
|
13
|
+
#include <thread>
|
|
14
|
+
#include <vector>
|
|
15
|
+
|
|
16
|
+
#include "Invoker.h"
|
|
17
|
+
|
|
18
|
+
namespace derec {
|
|
19
|
+
|
|
20
|
+
/// Store-callback return code for a backend failure. `0` means success and
|
|
21
|
+
/// `1` means "not found" (load-family only); every other value is a backend
|
|
22
|
+
/// failure, so any value >= 2 is equivalent here.
|
|
23
|
+
constexpr int32_t kBackendFailure = 2;
|
|
24
|
+
|
|
25
|
+
/// Liveness backstop for a JavaScript store that never settles. Generous
|
|
26
|
+
/// enough for disk and keychain access. Not application-configurable: it is a
|
|
27
|
+
/// binding-level guard, not a protocol parameter.
|
|
28
|
+
constexpr std::chrono::seconds kStoreTimeout{30};
|
|
29
|
+
|
|
30
|
+
struct CallResult {
|
|
31
|
+
int32_t code;
|
|
32
|
+
std::vector<uint8_t> bytes;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/// Shared per-protocol-instance liveness flag. Set when the JavaScript
|
|
36
|
+
/// runtime goes away so blocked workers can unwind instead of hanging.
|
|
37
|
+
///
|
|
38
|
+
/// The instance also owns the synchronisation primitives that every in-flight
|
|
39
|
+
/// callback for that instance waits on. Keeping the mutex and condition
|
|
40
|
+
/// variable here — rather than on the individual call — is what lets
|
|
41
|
+
/// `invalidate` wake a worker that is blocked inside `JsCallbackBridge::
|
|
42
|
+
/// callSync`: a waiter and the invalidator must share one condition variable
|
|
43
|
+
/// for the notification to reach the waiter at all.
|
|
44
|
+
///
|
|
45
|
+
/// Held by `shared_ptr` because a settlement scheduled onto the JavaScript
|
|
46
|
+
/// thread may arrive after the bridge that started it is gone, and that
|
|
47
|
+
/// settlement still needs the mutex.
|
|
48
|
+
struct InstanceState {
|
|
49
|
+
std::atomic<bool> invalidated{false};
|
|
50
|
+
|
|
51
|
+
/// Set only when the JavaScript runtime itself is going away (the React
|
|
52
|
+
/// Native reload / bridge-invalidate path), never by an ordinary
|
|
53
|
+
/// `free()`/destructor teardown of a single protocol instance.
|
|
54
|
+
///
|
|
55
|
+
/// The distinction matters because the two cases demand opposite
|
|
56
|
+
/// behaviour from work already scheduled onto the JavaScript thread. After
|
|
57
|
+
/// a plain instance teardown the runtime is still alive, so a settlement
|
|
58
|
+
/// that is already queued must still run — otherwise the Promise a caller
|
|
59
|
+
/// is awaiting never settles. Once the runtime is gone, that same
|
|
60
|
+
/// settlement must not run at all, and the JavaScript values it retains
|
|
61
|
+
/// must not be released either, because both would call back into a
|
|
62
|
+
/// runtime that is being torn down.
|
|
63
|
+
std::atomic<bool> runtimeInvalidated{false};
|
|
64
|
+
|
|
65
|
+
std::mutex m;
|
|
66
|
+
std::condition_variable cv;
|
|
67
|
+
|
|
68
|
+
/// Mark the instance dead and wake every blocked waiter. The flag is set
|
|
69
|
+
/// under `m` so a waiter cannot evaluate its predicate between the store and
|
|
70
|
+
/// the notification and then sleep through the wake-up.
|
|
71
|
+
void invalidate();
|
|
72
|
+
|
|
73
|
+
/// `invalidate`, plus the stronger statement that the JavaScript runtime is
|
|
74
|
+
/// going away. Both flags are set under `m` in one critical section so a
|
|
75
|
+
/// waiter that wakes on `invalidated` can never observe it without also
|
|
76
|
+
/// observing `runtimeInvalidated`.
|
|
77
|
+
void invalidateRuntime();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/// Record the calling thread as the JavaScript thread. Called once from
|
|
81
|
+
/// `install`, which by construction runs on it.
|
|
82
|
+
void noteJsThread();
|
|
83
|
+
|
|
84
|
+
/// True when the calling thread is the one `noteJsThread` recorded. False
|
|
85
|
+
/// until `install` has run, so a caller must treat it as advisory.
|
|
86
|
+
bool isJsThread();
|
|
87
|
+
|
|
88
|
+
/// True while this thread is executing the synchronous body of a store
|
|
89
|
+
/// callback — that is, the JavaScript call into an application store, up to
|
|
90
|
+
/// the point that call returns (typically a pending Promise).
|
|
91
|
+
///
|
|
92
|
+
/// This is deliberately narrower than "the worker is parked in a callback".
|
|
93
|
+
/// A protocol call issued from ordinary JavaScript-thread work that merely
|
|
94
|
+
/// happens to overlap a parked worker is safe: the serial worker runs it
|
|
95
|
+
/// after the current call finishes. Only a call issued from inside the store
|
|
96
|
+
/// body itself can deadlock, because that body's own completion is what the
|
|
97
|
+
/// worker is waiting for.
|
|
98
|
+
bool isInsideStoreCallback();
|
|
99
|
+
|
|
100
|
+
/// Raises `isInsideStoreCallback` for the calling thread over its lifetime.
|
|
101
|
+
/// Nests, so a store body that legitimately drives another one is counted
|
|
102
|
+
/// correctly.
|
|
103
|
+
class StoreCallbackScope {
|
|
104
|
+
public:
|
|
105
|
+
StoreCallbackScope();
|
|
106
|
+
~StoreCallbackScope();
|
|
107
|
+
|
|
108
|
+
StoreCallbackScope(const StoreCallbackScope&) = delete;
|
|
109
|
+
StoreCallbackScope& operator=(const StoreCallbackScope&) = delete;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/// Adapts Rust's synchronous store/transport callbacks to the SDK's
|
|
113
|
+
/// asynchronous JavaScript store interfaces.
|
|
114
|
+
///
|
|
115
|
+
/// Called from the protocol worker thread, never from the JavaScript thread —
|
|
116
|
+
/// blocking the JavaScript thread here would deadlock, since the settlement
|
|
117
|
+
/// that unblocks the wait is itself scheduled onto that thread. `callSync`
|
|
118
|
+
/// asserts this in debug builds against the thread `noteJsThread` recorded.
|
|
119
|
+
class JsCallbackBridge {
|
|
120
|
+
public:
|
|
121
|
+
JsCallbackBridge(Invoker& invoker,
|
|
122
|
+
std::shared_ptr<InstanceState> state,
|
|
123
|
+
std::chrono::milliseconds timeout =
|
|
124
|
+
std::chrono::duration_cast<std::chrono::milliseconds>(kStoreTimeout));
|
|
125
|
+
|
|
126
|
+
/// Run `body` on the JavaScript thread and block until it settles.
|
|
127
|
+
///
|
|
128
|
+
/// `body` receives a settle function it must invoke exactly once, from any
|
|
129
|
+
/// thread. Returns a backend failure if the instance is invalidated or the
|
|
130
|
+
/// timeout elapses — never `1`, because a fabricated "not found" would make
|
|
131
|
+
/// a stored share look absent and corrupt a recovery outcome.
|
|
132
|
+
///
|
|
133
|
+
/// `body` must capture everything it touches by value or by `shared_ptr`,
|
|
134
|
+
/// never by reference to the caller's stack. Giving up on a call does not
|
|
135
|
+
/// cancel it: on timeout or invalidation `callSync` returns while `body` is
|
|
136
|
+
/// still queued on the JavaScript thread, so `body` and its captures must
|
|
137
|
+
/// stay valid after `callSync` has returned and the caller's frame is gone.
|
|
138
|
+
/// The same applies to the settle function, which may be invoked after the
|
|
139
|
+
/// return; a late settlement is discarded, but it still runs.
|
|
140
|
+
CallResult callSync(std::function<void(std::function<void(CallResult)>)> body);
|
|
141
|
+
|
|
142
|
+
private:
|
|
143
|
+
Invoker& invoker_;
|
|
144
|
+
std::shared_ptr<InstanceState> state_;
|
|
145
|
+
std::chrono::milliseconds timeout_;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
} // namespace derec
|