objc-js 0.0.14 → 1.0.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/binding.gyp +2 -1
- package/build/Release/nobjc_native.node +0 -0
- package/package.json +2 -1
- package/src/native/ObjcObject.mm +2 -14
- package/src/native/bridge.h +20 -0
- package/src/native/constants.h +42 -0
- package/src/native/ffi-utils.h +103 -1
- package/src/native/forwarding-common.h +87 -0
- package/src/native/forwarding-common.mm +155 -0
- package/src/native/memory-utils.h +197 -0
- package/src/native/method-forwarding.mm +137 -208
- package/src/native/nobjc.mm +7 -31
- package/src/native/pointer-utils.h +63 -0
- package/src/native/protocol-impl.mm +7 -27
- package/src/native/protocol-manager.h +145 -0
- package/src/native/protocol-storage.h +12 -33
- package/src/native/runtime-detection.h +54 -0
- package/src/native/subclass-impl.mm +232 -566
- package/src/native/subclass-manager.h +170 -0
- package/src/native/super-call-helpers.h +361 -0
- package/src/native/type-conversion.h +200 -252
- package/src/native/type-dispatch.h +241 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#ifndef MEMORY_UTILS_H
|
|
2
|
+
#define MEMORY_UTILS_H
|
|
3
|
+
|
|
4
|
+
#include "debug.h"
|
|
5
|
+
#include "protocol-storage.h"
|
|
6
|
+
#include <Foundation/Foundation.h>
|
|
7
|
+
|
|
8
|
+
// MARK: - InvocationDataGuard RAII Wrapper
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RAII wrapper for InvocationData to ensure proper cleanup.
|
|
12
|
+
*
|
|
13
|
+
* This class manages the lifetime of InvocationData*, ensuring that:
|
|
14
|
+
* 1. The NSInvocation is released
|
|
15
|
+
* 2. The InvocationData is deleted
|
|
16
|
+
* 3. Cleanup happens even when exceptions are thrown
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* auto data = new InvocationData();
|
|
20
|
+
* InvocationDataGuard guard(data);
|
|
21
|
+
* // ... use data ...
|
|
22
|
+
* guard.release(); // Transfer ownership on success
|
|
23
|
+
*
|
|
24
|
+
* If release() is not called, cleanup happens in destructor.
|
|
25
|
+
*/
|
|
26
|
+
class InvocationDataGuard {
|
|
27
|
+
public:
|
|
28
|
+
explicit InvocationDataGuard(InvocationData* data) : data_(data), released_(false) {
|
|
29
|
+
#if NOBJC_DEBUG
|
|
30
|
+
if (data_) {
|
|
31
|
+
NOBJC_LOG("InvocationDataGuard: acquired data=%p, selector=%s",
|
|
32
|
+
data_, data_->selectorName.c_str());
|
|
33
|
+
}
|
|
34
|
+
#endif
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Non-copyable
|
|
38
|
+
InvocationDataGuard(const InvocationDataGuard&) = delete;
|
|
39
|
+
InvocationDataGuard& operator=(const InvocationDataGuard&) = delete;
|
|
40
|
+
|
|
41
|
+
// Movable
|
|
42
|
+
InvocationDataGuard(InvocationDataGuard&& other) noexcept
|
|
43
|
+
: data_(other.data_), released_(other.released_) {
|
|
44
|
+
other.data_ = nullptr;
|
|
45
|
+
other.released_ = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
InvocationDataGuard& operator=(InvocationDataGuard&& other) noexcept {
|
|
49
|
+
if (this != &other) {
|
|
50
|
+
cleanup();
|
|
51
|
+
data_ = other.data_;
|
|
52
|
+
released_ = other.released_;
|
|
53
|
+
other.data_ = nullptr;
|
|
54
|
+
other.released_ = true;
|
|
55
|
+
}
|
|
56
|
+
return *this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
~InvocationDataGuard() {
|
|
60
|
+
cleanup();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Release ownership of the data without cleanup.
|
|
65
|
+
* Call this when you're transferring ownership elsewhere (e.g., to a callback).
|
|
66
|
+
* @return The raw pointer (caller takes ownership)
|
|
67
|
+
*/
|
|
68
|
+
InvocationData* release() {
|
|
69
|
+
released_ = true;
|
|
70
|
+
InvocationData* ptr = data_;
|
|
71
|
+
data_ = nullptr;
|
|
72
|
+
#if NOBJC_DEBUG
|
|
73
|
+
if (ptr) {
|
|
74
|
+
NOBJC_LOG("InvocationDataGuard: released ownership of data=%p", ptr);
|
|
75
|
+
}
|
|
76
|
+
#endif
|
|
77
|
+
return ptr;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the raw pointer without releasing ownership.
|
|
82
|
+
*/
|
|
83
|
+
InvocationData* get() const { return data_; }
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if the guard has valid data.
|
|
87
|
+
*/
|
|
88
|
+
explicit operator bool() const { return data_ != nullptr && !released_; }
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Arrow operator for convenient access.
|
|
92
|
+
*/
|
|
93
|
+
InvocationData* operator->() const { return data_; }
|
|
94
|
+
|
|
95
|
+
private:
|
|
96
|
+
void cleanup() {
|
|
97
|
+
if (data_ && !released_) {
|
|
98
|
+
#if NOBJC_DEBUG
|
|
99
|
+
NOBJC_LOG("InvocationDataGuard: cleaning up data=%p, selector=%s",
|
|
100
|
+
data_, data_->selectorName.c_str());
|
|
101
|
+
#endif
|
|
102
|
+
// Release the NSInvocation if present
|
|
103
|
+
if (data_->invocation) {
|
|
104
|
+
[data_->invocation release];
|
|
105
|
+
data_->invocation = nil;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Delete the data
|
|
109
|
+
delete data_;
|
|
110
|
+
data_ = nullptr;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
InvocationData* data_;
|
|
115
|
+
bool released_;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// MARK: - Cleanup Callback for CallJSCallback
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Helper function to clean up InvocationData after a JS callback completes.
|
|
122
|
+
* This is called from CallJSCallback to ensure proper cleanup regardless of
|
|
123
|
+
* success or failure.
|
|
124
|
+
*
|
|
125
|
+
* @param data The InvocationData to clean up (takes ownership)
|
|
126
|
+
*/
|
|
127
|
+
inline void CleanupInvocationData(InvocationData* data) {
|
|
128
|
+
if (!data) return;
|
|
129
|
+
|
|
130
|
+
#if NOBJC_DEBUG
|
|
131
|
+
NOBJC_LOG("CleanupInvocationData: cleaning up selector=%s",
|
|
132
|
+
data->selectorName.c_str());
|
|
133
|
+
#endif
|
|
134
|
+
|
|
135
|
+
// Signal completion if we have synchronization primitives
|
|
136
|
+
SignalInvocationComplete(data);
|
|
137
|
+
|
|
138
|
+
// Release the invocation
|
|
139
|
+
if (data->invocation) {
|
|
140
|
+
[data->invocation release];
|
|
141
|
+
data->invocation = nil;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Delete the data
|
|
145
|
+
delete data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// MARK: - ScopeGuard for Generic Cleanup
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generic scope guard for executing cleanup code on scope exit.
|
|
152
|
+
*
|
|
153
|
+
* Usage:
|
|
154
|
+
* auto guard = MakeScopeGuard([&] { cleanup_code(); });
|
|
155
|
+
* // ... do work ...
|
|
156
|
+
* guard.dismiss(); // Don't run cleanup on success
|
|
157
|
+
*/
|
|
158
|
+
template<typename Func>
|
|
159
|
+
class ScopeGuard {
|
|
160
|
+
public:
|
|
161
|
+
explicit ScopeGuard(Func&& func) : func_(std::forward<Func>(func)), active_(true) {}
|
|
162
|
+
|
|
163
|
+
ScopeGuard(ScopeGuard&& other) noexcept
|
|
164
|
+
: func_(std::move(other.func_)), active_(other.active_) {
|
|
165
|
+
other.active_ = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
~ScopeGuard() {
|
|
169
|
+
if (active_) {
|
|
170
|
+
try {
|
|
171
|
+
func_();
|
|
172
|
+
} catch (...) {
|
|
173
|
+
// Suppress exceptions in destructor
|
|
174
|
+
#if NOBJC_DEBUG
|
|
175
|
+
NOBJC_ERROR("ScopeGuard: exception during cleanup (suppressed)");
|
|
176
|
+
#endif
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
void dismiss() { active_ = false; }
|
|
182
|
+
|
|
183
|
+
// Non-copyable
|
|
184
|
+
ScopeGuard(const ScopeGuard&) = delete;
|
|
185
|
+
ScopeGuard& operator=(const ScopeGuard&) = delete;
|
|
186
|
+
|
|
187
|
+
private:
|
|
188
|
+
Func func_;
|
|
189
|
+
bool active_;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
template<typename Func>
|
|
193
|
+
ScopeGuard<Func> MakeScopeGuard(Func&& func) {
|
|
194
|
+
return ScopeGuard<Func>(std::forward<Func>(func));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#endif // MEMORY_UTILS_H
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#include "method-forwarding.h"
|
|
2
2
|
#include "debug.h"
|
|
3
|
+
#include "forwarding-common.h"
|
|
4
|
+
#include "memory-utils.h"
|
|
3
5
|
#include "ObjcObject.h"
|
|
6
|
+
#include "protocol-manager.h"
|
|
4
7
|
#include "protocol-storage.h"
|
|
5
8
|
#include "type-conversion.h"
|
|
9
|
+
|
|
10
|
+
using nobjc::ProtocolManager;
|
|
6
11
|
#include <CoreFoundation/CoreFoundation.h>
|
|
7
12
|
#include <Foundation/Foundation.h>
|
|
8
13
|
#include <napi.h>
|
|
@@ -12,8 +17,13 @@
|
|
|
12
17
|
|
|
13
18
|
// This function runs on the JavaScript thread
|
|
14
19
|
// Handles both protocol implementation and subclass method forwarding
|
|
20
|
+
// NOTE: This function takes ownership of data and must clean it up
|
|
15
21
|
void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
16
22
|
InvocationData *data) {
|
|
23
|
+
// Use RAII to ensure cleanup even if we return early or throw
|
|
24
|
+
// The guard will release the invocation and delete data
|
|
25
|
+
InvocationDataGuard guard(data);
|
|
26
|
+
|
|
17
27
|
if (!data) {
|
|
18
28
|
NOBJC_ERROR("InvocationData is null in CallJSCallback");
|
|
19
29
|
return;
|
|
@@ -26,20 +36,15 @@ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
|
26
36
|
if (jsCallback.IsEmpty()) {
|
|
27
37
|
NOBJC_ERROR("jsCallback is null/empty in CallJSCallback for selector %s",
|
|
28
38
|
data->selectorName.c_str());
|
|
29
|
-
if (data->invocation) {
|
|
30
|
-
[data->invocation release];
|
|
31
|
-
}
|
|
32
39
|
SignalInvocationComplete(data);
|
|
33
|
-
|
|
34
|
-
return;
|
|
40
|
+
return; // guard cleans up
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
NSInvocation *invocation = data->invocation;
|
|
38
44
|
if (!invocation) {
|
|
39
45
|
NOBJC_ERROR("NSInvocation is null in CallJSCallback");
|
|
40
46
|
SignalInvocationComplete(data);
|
|
41
|
-
|
|
42
|
-
return;
|
|
47
|
+
return; // guard cleans up
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
NOBJC_LOG("CallJSCallback: About to get method signature");
|
|
@@ -50,9 +55,7 @@ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
|
50
55
|
NOBJC_ERROR("Failed to get method signature for selector %s",
|
|
51
56
|
data->selectorName.c_str());
|
|
52
57
|
SignalInvocationComplete(data);
|
|
53
|
-
|
|
54
|
-
delete data;
|
|
55
|
-
return;
|
|
58
|
+
return; // guard cleans up
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
NOBJC_LOG("CallJSCallback: Method signature: %s, numArgs: %lu",
|
|
@@ -124,10 +127,7 @@ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
|
124
127
|
SignalInvocationComplete(data);
|
|
125
128
|
NOBJC_LOG("CallJSCallback: Signaled completion for %s", data->selectorName.c_str());
|
|
126
129
|
|
|
127
|
-
//
|
|
128
|
-
// Release the invocation that we retained in ForwardInvocation
|
|
129
|
-
[invocation release];
|
|
130
|
-
delete data;
|
|
130
|
+
// guard destructor cleans up invocation and data
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// MARK: - Fallback Helper
|
|
@@ -177,19 +177,23 @@ BOOL RespondsToSelector(id self, SEL _cmd, SEL selector) {
|
|
|
177
177
|
void *ptr = (__bridge void *)self;
|
|
178
178
|
|
|
179
179
|
// Check if this is one of our implemented methods
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (it != g_implementations.end()) {
|
|
180
|
+
bool found = ProtocolManager::Instance().WithLock([ptr, selector](auto& map) {
|
|
181
|
+
auto it = map.find(ptr);
|
|
182
|
+
if (it != map.end()) {
|
|
184
183
|
NSString *selectorString = NSStringFromSelector(selector);
|
|
185
184
|
if (selectorString != nil) {
|
|
186
185
|
std::string selName = [selectorString UTF8String];
|
|
187
186
|
auto callbackIt = it->second.callbacks.find(selName);
|
|
188
187
|
if (callbackIt != it->second.callbacks.end()) {
|
|
189
|
-
return
|
|
188
|
+
return true;
|
|
190
189
|
}
|
|
191
190
|
}
|
|
192
191
|
}
|
|
192
|
+
return false;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (found) {
|
|
196
|
+
return YES;
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// For methods we don't implement, check if NSObject responds to them
|
|
@@ -201,15 +205,21 @@ BOOL RespondsToSelector(id self, SEL _cmd, SEL selector) {
|
|
|
201
205
|
NSMethodSignature *MethodSignatureForSelector(id self, SEL _cmd, SEL selector) {
|
|
202
206
|
void *ptr = (__bridge void *)self;
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
NSMethodSignature *sig = ProtocolManager::Instance().WithLock([ptr, selector](auto& map) -> NSMethodSignature* {
|
|
209
|
+
auto it = map.find(ptr);
|
|
210
|
+
if (it != map.end()) {
|
|
211
|
+
NSString *selectorString = NSStringFromSelector(selector);
|
|
212
|
+
std::string selName = [selectorString UTF8String];
|
|
213
|
+
auto encIt = it->second.typeEncodings.find(selName);
|
|
214
|
+
if (encIt != it->second.typeEncodings.end()) {
|
|
215
|
+
return [NSMethodSignature signatureWithObjCTypes:encIt->second.c_str()];
|
|
216
|
+
}
|
|
212
217
|
}
|
|
218
|
+
return nil;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (sig != nil) {
|
|
222
|
+
return sig;
|
|
213
223
|
}
|
|
214
224
|
// Fall back to superclass for methods we don't implement
|
|
215
225
|
return [NSObject instanceMethodSignatureForSelector:selector];
|
|
@@ -223,222 +233,141 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
223
233
|
}
|
|
224
234
|
|
|
225
235
|
// Retain the invocation to keep it alive during async call
|
|
226
|
-
// retainArguments only retains the arguments, not the invocation itself
|
|
227
236
|
[invocation retainArguments];
|
|
228
|
-
[invocation retain];
|
|
237
|
+
[invocation retain];
|
|
229
238
|
|
|
230
239
|
SEL selector = [invocation selector];
|
|
231
240
|
NSString *selectorString = NSStringFromSelector(selector);
|
|
232
241
|
if (!selectorString) {
|
|
233
242
|
NOBJC_ERROR("Failed to convert selector to string");
|
|
243
|
+
[invocation release];
|
|
234
244
|
return;
|
|
235
245
|
}
|
|
236
246
|
|
|
237
247
|
std::string selectorName = [selectorString UTF8String];
|
|
238
|
-
|
|
239
|
-
// Store self pointer for later lookups
|
|
240
248
|
void *ptr = (__bridge void *)self;
|
|
241
249
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
std::
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
auto callbackIt = it->second.callbacks.find(selectorName);
|
|
257
|
-
if (callbackIt == it->second.callbacks.end()) {
|
|
258
|
-
NOBJC_WARN("Callback not found for selector %s", selectorName.c_str());
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Get the ThreadSafeFunction - this is thread-safe by design
|
|
263
|
-
// IMPORTANT: We must Acquire() to increment the ref count, because copying
|
|
264
|
-
// a ThreadSafeFunction does NOT increment it. If DeallocImplementation
|
|
265
|
-
// runs and calls Release() on the original, our copy would become invalid.
|
|
266
|
-
tsfn = callbackIt->second;
|
|
267
|
-
napi_status acq_status = tsfn.Acquire();
|
|
268
|
-
if (acq_status != napi_ok) {
|
|
269
|
-
NOBJC_WARN("Failed to acquire ThreadSafeFunction for selector %s",
|
|
270
|
-
selectorName.c_str());
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Get the type encoding for return value handling
|
|
275
|
-
auto encIt = it->second.typeEncodings.find(selectorName);
|
|
276
|
-
if (encIt != it->second.typeEncodings.end()) {
|
|
277
|
-
typeEncoding = encIt->second;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Get the JS thread ID to check if we're on the same thread
|
|
281
|
-
js_thread = it->second.js_thread;
|
|
282
|
-
isElectron = it->second.isElectron;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Check if we're on the JS thread
|
|
286
|
-
bool is_js_thread = pthread_equal(pthread_self(), js_thread);
|
|
287
|
-
|
|
288
|
-
// IMPORTANT: We call directly on the JS thread so return values are set
|
|
289
|
-
// synchronously; otherwise we use a ThreadSafeFunction to marshal work.
|
|
290
|
-
// EXCEPTION: In Electron, we ALWAYS use TSFN even on the JS thread because
|
|
291
|
-
// Electron's V8 context isn't properly set up for direct handle creation.
|
|
250
|
+
// Set up callbacks for protocol-specific storage access
|
|
251
|
+
ForwardingCallbacks callbacks;
|
|
252
|
+
callbacks.callbackType = CallbackType::Protocol;
|
|
253
|
+
|
|
254
|
+
// Lookup context and acquire TSFN
|
|
255
|
+
callbacks.lookupContext = [](void *lookupKey,
|
|
256
|
+
const std::string &selName) -> std::optional<ForwardingContext> {
|
|
257
|
+
return ProtocolManager::Instance().WithLock([lookupKey, &selName](auto& map) -> std::optional<ForwardingContext> {
|
|
258
|
+
auto it = map.find(lookupKey);
|
|
259
|
+
if (it == map.end()) {
|
|
260
|
+
NOBJC_WARN("Protocol implementation not found for instance %p", lookupKey);
|
|
261
|
+
return std::nullopt;
|
|
262
|
+
}
|
|
292
263
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
data->callbackType = CallbackType::Protocol;
|
|
264
|
+
auto callbackIt = it->second.callbacks.find(selName);
|
|
265
|
+
if (callbackIt == it->second.callbacks.end()) {
|
|
266
|
+
NOBJC_WARN("Callback not found for selector %s", selName.c_str());
|
|
267
|
+
return std::nullopt;
|
|
268
|
+
}
|
|
299
269
|
|
|
300
|
-
|
|
270
|
+
// Acquire the TSFN
|
|
271
|
+
Napi::ThreadSafeFunction tsfn = callbackIt->second;
|
|
272
|
+
napi_status acq_status = tsfn.Acquire();
|
|
273
|
+
if (acq_status != napi_ok) {
|
|
274
|
+
NOBJC_WARN("Failed to acquire ThreadSafeFunction for selector %s", selName.c_str());
|
|
275
|
+
return std::nullopt;
|
|
276
|
+
}
|
|
301
277
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
278
|
+
ForwardingContext ctx;
|
|
279
|
+
ctx.tsfn = tsfn;
|
|
280
|
+
ctx.js_thread = it->second.js_thread;
|
|
281
|
+
ctx.env = it->second.env;
|
|
282
|
+
ctx.skipDirectCallForElectron = it->second.isElectron;
|
|
283
|
+
ctx.instancePtr = nullptr; // Not used for protocols
|
|
284
|
+
ctx.superClassPtr = nullptr; // Not used for protocols
|
|
285
|
+
|
|
286
|
+
// Cache the JS callback reference to avoid mutex re-acquisition
|
|
287
|
+
auto jsCallbackIt = it->second.jsCallbacks.find(selName);
|
|
288
|
+
if (jsCallbackIt != it->second.jsCallbacks.end()) {
|
|
289
|
+
ctx.cachedJsCallback = &jsCallbackIt->second;
|
|
290
|
+
}
|
|
308
291
|
|
|
309
|
-
|
|
292
|
+
auto encIt = it->second.typeEncodings.find(selName);
|
|
293
|
+
if (encIt != it->second.typeEncodings.end()) {
|
|
294
|
+
ctx.typeEncoding = encIt->second;
|
|
295
|
+
}
|
|
310
296
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
297
|
+
return ctx;
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Get JS function for direct call path
|
|
302
|
+
callbacks.getJSFunction = [](void *lookupKey, const std::string &selName,
|
|
303
|
+
Napi::Env /*env*/) -> Napi::Function {
|
|
304
|
+
return ProtocolManager::Instance().WithLock([lookupKey, &selName](auto& map) -> Napi::Function {
|
|
305
|
+
auto it = map.find(lookupKey);
|
|
306
|
+
if (it == map.end()) {
|
|
307
|
+
return Napi::Function();
|
|
321
308
|
}
|
|
322
309
|
|
|
323
|
-
auto jsCallbackIt = it->second.jsCallbacks.find(
|
|
310
|
+
auto jsCallbackIt = it->second.jsCallbacks.find(selName);
|
|
324
311
|
if (jsCallbackIt == it->second.jsCallbacks.end()) {
|
|
325
|
-
|
|
326
|
-
selectorName.c_str());
|
|
327
|
-
[invocation release];
|
|
328
|
-
delete data;
|
|
329
|
-
return;
|
|
312
|
+
return Napi::Function();
|
|
330
313
|
}
|
|
331
314
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// This is critical for Electron which may have multiple V8 contexts
|
|
344
|
-
Napi::HandleScope scope(callEnv);
|
|
345
|
-
|
|
346
|
-
CallJSCallback(callEnv, jsFn, data);
|
|
347
|
-
// CallJSCallback releases invocation and deletes data.
|
|
348
|
-
} catch (const std::exception &e) {
|
|
349
|
-
NOBJC_ERROR("Error calling JS callback directly (likely invalid env in Electron): %s", e.what());
|
|
350
|
-
NOBJC_LOG("Falling back to ThreadSafeFunction for selector %s", selectorName.c_str());
|
|
351
|
-
|
|
352
|
-
// Fallback to TSFN if direct call fails (e.g., invalid env in Electron)
|
|
353
|
-
// We need to re-acquire the TSFN
|
|
354
|
-
{
|
|
355
|
-
std::lock_guard<std::mutex> lock(g_implementations_mutex);
|
|
356
|
-
auto it = g_implementations.find(ptr);
|
|
357
|
-
if (it != g_implementations.end()) {
|
|
358
|
-
auto callbackIt = it->second.callbacks.find(selectorName);
|
|
359
|
-
if (callbackIt != it->second.callbacks.end()) {
|
|
360
|
-
tsfn = callbackIt->second;
|
|
361
|
-
napi_status acq_status = tsfn.Acquire();
|
|
362
|
-
if (acq_status == napi_ok) {
|
|
363
|
-
// Use helper function for fallback
|
|
364
|
-
if (FallbackToTSFN(tsfn, data, selectorName)) {
|
|
365
|
-
return; // Data cleaned up in callback
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
315
|
+
return jsCallbackIt->second.Value();
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Re-acquire TSFN for fallback path
|
|
320
|
+
callbacks.reacquireTSFN = [](void *lookupKey,
|
|
321
|
+
const std::string &selName) -> std::optional<Napi::ThreadSafeFunction> {
|
|
322
|
+
return ProtocolManager::Instance().WithLock([lookupKey, &selName](auto& map) -> std::optional<Napi::ThreadSafeFunction> {
|
|
323
|
+
auto it = map.find(lookupKey);
|
|
324
|
+
if (it == map.end()) {
|
|
325
|
+
return std::nullopt;
|
|
370
326
|
}
|
|
371
|
-
|
|
372
|
-
// If fallback also failed, clean up manually
|
|
373
|
-
[invocation release];
|
|
374
|
-
delete data;
|
|
375
|
-
}
|
|
376
|
-
} else {
|
|
377
|
-
// We're on a different thread (e.g., Cocoa callback from
|
|
378
|
-
// ASAuthorizationController) Use NonBlockingCall + runloop pumping to avoid
|
|
379
|
-
// deadlocks
|
|
380
|
-
std::mutex completionMutex;
|
|
381
|
-
std::condition_variable completionCv;
|
|
382
|
-
bool isComplete = false;
|
|
383
|
-
|
|
384
|
-
data->completionMutex = &completionMutex;
|
|
385
|
-
data->completionCv = &completionCv;
|
|
386
|
-
data->isComplete = &isComplete;
|
|
387
|
-
|
|
388
|
-
status = tsfn.NonBlockingCall(data, CallJSCallback);
|
|
389
|
-
tsfn.Release();
|
|
390
|
-
|
|
391
|
-
if (status != napi_ok) {
|
|
392
|
-
NOBJC_ERROR("Failed to call ThreadSafeFunction for selector %s (status: %d)",
|
|
393
|
-
selectorName.c_str(), status);
|
|
394
|
-
[invocation release];
|
|
395
|
-
delete data;
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
327
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
328
|
+
auto callbackIt = it->second.callbacks.find(selName);
|
|
329
|
+
if (callbackIt == it->second.callbacks.end()) {
|
|
330
|
+
return std::nullopt;
|
|
331
|
+
}
|
|
402
332
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
333
|
+
Napi::ThreadSafeFunction tsfn = callbackIt->second;
|
|
334
|
+
napi_status acq_status = tsfn.Acquire();
|
|
335
|
+
if (acq_status != napi_ok) {
|
|
336
|
+
return std::nullopt;
|
|
409
337
|
}
|
|
410
|
-
CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
|
|
411
|
-
}
|
|
412
|
-
// Data cleaned up in callback
|
|
413
|
-
}
|
|
414
338
|
|
|
415
|
-
|
|
339
|
+
return tsfn;
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
ForwardInvocationCommon(invocation, selectorName, ptr, callbacks);
|
|
416
344
|
}
|
|
417
345
|
|
|
418
346
|
// Deallocation implementation
|
|
419
347
|
void DeallocImplementation(id self, SEL _cmd) {
|
|
420
348
|
@autoreleasepool {
|
|
421
|
-
// Remove the implementation from the
|
|
422
|
-
std::lock_guard<std::mutex> lock(g_implementations_mutex);
|
|
349
|
+
// Remove the implementation from the manager
|
|
423
350
|
void *ptr = (__bridge void *)self;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
351
|
+
ProtocolManager::Instance().WithLock([ptr, self](auto& map) {
|
|
352
|
+
auto it = map.find(ptr);
|
|
353
|
+
if (it != map.end()) {
|
|
354
|
+
// Release all ThreadSafeFunctions and JS callbacks
|
|
355
|
+
// Do this carefully to avoid issues during shutdown
|
|
356
|
+
try {
|
|
357
|
+
for (auto &pair : it->second.callbacks) {
|
|
358
|
+
// Release the ThreadSafeFunction
|
|
359
|
+
pair.second.Release();
|
|
360
|
+
}
|
|
361
|
+
it->second.callbacks.clear();
|
|
362
|
+
it->second.jsCallbacks.clear();
|
|
363
|
+
it->second.typeEncodings.clear();
|
|
364
|
+
} catch (...) {
|
|
365
|
+
// Ignore errors during cleanup
|
|
366
|
+
NOBJC_WARN("Exception during callback cleanup for instance %p", self);
|
|
432
367
|
}
|
|
433
|
-
|
|
434
|
-
it->second.jsCallbacks.clear();
|
|
435
|
-
it->second.typeEncodings.clear();
|
|
436
|
-
} catch (...) {
|
|
437
|
-
// Ignore errors during cleanup
|
|
438
|
-
NOBJC_WARN("Exception during callback cleanup for instance %p", self);
|
|
368
|
+
map.erase(it);
|
|
439
369
|
}
|
|
440
|
-
|
|
441
|
-
}
|
|
370
|
+
});
|
|
442
371
|
}
|
|
443
372
|
|
|
444
373
|
// Call the superclass dealloc
|