objc-js 0.0.15 → 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/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 -246
- package/src/native/type-dispatch.h +241 -0
|
@@ -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
|
package/src/native/nobjc.mm
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "ObjcObject.h"
|
|
2
|
+
#include "pointer-utils.h"
|
|
2
3
|
#include "protocol-impl.h"
|
|
3
4
|
#include "subclass-impl.h"
|
|
4
5
|
#include <Foundation/Foundation.h>
|
|
@@ -44,18 +45,7 @@ Napi::Value GetPointer(const Napi::CallbackInfo &info) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
ObjcObject *objcObj = Napi::ObjectWrap<ObjcObject>::Unwrap(obj);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Create a Buffer to hold the pointer (8 bytes on 64-bit macOS)
|
|
50
|
-
Napi::Buffer<uint8_t> buffer = Napi::Buffer<uint8_t>::New(env, sizeof(void*));
|
|
51
|
-
|
|
52
|
-
// Write the pointer value to the buffer in little-endian format
|
|
53
|
-
uint8_t* data = buffer.Data();
|
|
54
|
-
for (size_t i = 0; i < sizeof(void*); ++i) {
|
|
55
|
-
data[i] = static_cast<uint8_t>((ptrValue >> (i * 8)) & 0xFF);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return buffer;
|
|
48
|
+
return PointerToBuffer(env, objcObj->objcObject);
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
Napi::Value FromPointer(const Napi::CallbackInfo &info) {
|
|
@@ -65,39 +55,25 @@ Napi::Value FromPointer(const Napi::CallbackInfo &info) {
|
|
|
65
55
|
throw Napi::TypeError::New(env, "Expected a single Buffer or BigInt argument");
|
|
66
56
|
}
|
|
67
57
|
|
|
68
|
-
|
|
58
|
+
void *ptr = nullptr;
|
|
69
59
|
|
|
70
60
|
if (info[0].IsBuffer()) {
|
|
71
|
-
// Read pointer from Buffer
|
|
72
61
|
Napi::Buffer<uint8_t> buffer = info[0].As<Napi::Buffer<uint8_t>>();
|
|
73
62
|
if (buffer.Length() != sizeof(void*)) {
|
|
74
63
|
throw Napi::TypeError::New(env, "Buffer must be exactly 8 bytes for a 64-bit pointer");
|
|
75
64
|
}
|
|
76
|
-
|
|
77
|
-
uint8_t* data = buffer.Data();
|
|
78
|
-
for (size_t i = 0; i < sizeof(void*); ++i) {
|
|
79
|
-
ptrValue |= (static_cast<uintptr_t>(data[i]) << (i * 8));
|
|
80
|
-
}
|
|
65
|
+
ptr = ReadPointerFromBuffer(buffer.Data());
|
|
81
66
|
} else if (info[0].IsBigInt()) {
|
|
82
|
-
|
|
83
|
-
bool lossless = false;
|
|
84
|
-
uint64_t value = info[0].As<Napi::BigInt>().Uint64Value(&lossless);
|
|
85
|
-
if (!lossless) {
|
|
86
|
-
throw Napi::RangeError::New(env, "BigInt value out of range for pointer");
|
|
87
|
-
}
|
|
88
|
-
ptrValue = static_cast<uintptr_t>(value);
|
|
67
|
+
ptr = BigIntToPointer(env, info[0].As<Napi::BigInt>());
|
|
89
68
|
} else {
|
|
90
69
|
throw Napi::TypeError::New(env, "Expected a Buffer or BigInt argument");
|
|
91
70
|
}
|
|
92
71
|
|
|
93
|
-
if (
|
|
72
|
+
if (ptr == nullptr) {
|
|
94
73
|
return env.Null();
|
|
95
74
|
}
|
|
96
75
|
|
|
97
|
-
|
|
98
|
-
id obj = reinterpret_cast<id>(ptrValue);
|
|
99
|
-
|
|
100
|
-
return ObjcObject::NewInstance(env, obj);
|
|
76
|
+
return ObjcObject::NewInstance(env, reinterpret_cast<id>(ptr));
|
|
101
77
|
}
|
|
102
78
|
|
|
103
79
|
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#ifndef POINTER_UTILS_H
|
|
2
|
+
#define POINTER_UTILS_H
|
|
3
|
+
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <cstddef>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
#include <napi.h>
|
|
8
|
+
|
|
9
|
+
// MARK: - Pointer Conversion Utilities
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Write a pointer value to a buffer in little-endian format.
|
|
13
|
+
* The buffer must be at least sizeof(void*) bytes.
|
|
14
|
+
*/
|
|
15
|
+
inline void WritePointerToBuffer(const void *ptr, uint8_t *buffer) {
|
|
16
|
+
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(ptr);
|
|
17
|
+
for (size_t i = 0; i < sizeof(void *); ++i) {
|
|
18
|
+
buffer[i] = static_cast<uint8_t>((ptrValue >> (i * 8)) & 0xFF);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read a pointer value from a buffer in little-endian format.
|
|
24
|
+
* The buffer must be at least sizeof(void*) bytes.
|
|
25
|
+
*/
|
|
26
|
+
inline void *ReadPointerFromBuffer(const uint8_t *buffer) {
|
|
27
|
+
uintptr_t ptrValue = 0;
|
|
28
|
+
for (size_t i = 0; i < sizeof(void *); ++i) {
|
|
29
|
+
ptrValue |= static_cast<uintptr_t>(buffer[i]) << (i * 8);
|
|
30
|
+
}
|
|
31
|
+
return reinterpret_cast<void *>(ptrValue);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a N-API BigInt from a pointer value.
|
|
36
|
+
*/
|
|
37
|
+
inline Napi::BigInt PointerToBigInt(Napi::Env env, const void *ptr) {
|
|
38
|
+
return Napi::BigInt::New(env, reinterpret_cast<uint64_t>(ptr));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract a pointer value from a N-API BigInt.
|
|
43
|
+
* Throws if the value is out of range.
|
|
44
|
+
*/
|
|
45
|
+
inline void *BigIntToPointer(Napi::Env env, const Napi::BigInt &bigint) {
|
|
46
|
+
bool lossless;
|
|
47
|
+
uint64_t value = bigint.Uint64Value(&lossless);
|
|
48
|
+
if (!lossless) {
|
|
49
|
+
throw Napi::RangeError::New(env, "BigInt value out of range for pointer");
|
|
50
|
+
}
|
|
51
|
+
return reinterpret_cast<void *>(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a N-API Buffer containing a pointer value (little-endian).
|
|
56
|
+
*/
|
|
57
|
+
inline Napi::Buffer<uint8_t> PointerToBuffer(Napi::Env env, const void *ptr) {
|
|
58
|
+
Napi::Buffer<uint8_t> buffer = Napi::Buffer<uint8_t>::New(env, sizeof(void *));
|
|
59
|
+
WritePointerToBuffer(ptr, buffer.Data());
|
|
60
|
+
return buffer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#endif // POINTER_UTILS_H
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
#include "debug.h"
|
|
3
3
|
#include "method-forwarding.h"
|
|
4
4
|
#include "ObjcObject.h"
|
|
5
|
+
#include "protocol-manager.h"
|
|
5
6
|
#include "protocol-storage.h"
|
|
7
|
+
#include "runtime-detection.h"
|
|
6
8
|
#include "type-conversion.h"
|
|
7
9
|
#include <Foundation/Foundation.h>
|
|
8
10
|
#include <atomic>
|
|
@@ -11,10 +13,7 @@
|
|
|
11
13
|
#include <objc/runtime.h>
|
|
12
14
|
#include <sstream>
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
std::unordered_map<void *, ProtocolImplementation> g_implementations;
|
|
17
|
-
std::mutex g_implementations_mutex;
|
|
16
|
+
using nobjc::ProtocolManager;
|
|
18
17
|
|
|
19
18
|
// MARK: - Helper Functions
|
|
20
19
|
|
|
@@ -130,24 +129,8 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
130
129
|
// Get the method implementations object's property names
|
|
131
130
|
Napi::Array propertyNames = methodImplementations.GetPropertyNames();
|
|
132
131
|
|
|
133
|
-
// Detect if we're running in Electron
|
|
134
|
-
bool isElectron =
|
|
135
|
-
try {
|
|
136
|
-
Napi::Object global = env.Global();
|
|
137
|
-
if (global.Has("process")) {
|
|
138
|
-
Napi::Object process = global.Get("process").As<Napi::Object>();
|
|
139
|
-
if (process.Has("versions")) {
|
|
140
|
-
Napi::Object versions = process.Get("versions").As<Napi::Object>();
|
|
141
|
-
isElectron = versions.Has("electron");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
} catch (...) {
|
|
145
|
-
// If detection fails, assume not Electron
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (isElectron) {
|
|
149
|
-
// Electron runtime detected, will always use TSFN path
|
|
150
|
-
}
|
|
132
|
+
// Detect if we're running in Electron
|
|
133
|
+
bool isElectron = IsElectronRuntime(env);
|
|
151
134
|
|
|
152
135
|
// Store callbacks for this instance (we'll set the instance pointer later)
|
|
153
136
|
ProtocolImplementation impl{
|
|
@@ -270,12 +253,9 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
270
253
|
throw Napi::Error::New(env, "Failed to instantiate class");
|
|
271
254
|
}
|
|
272
255
|
|
|
273
|
-
// Store the implementation in the
|
|
256
|
+
// Store the implementation in the manager
|
|
274
257
|
void *instancePtr = (__bridge void *)instance;
|
|
275
|
-
|
|
276
|
-
std::lock_guard<std::mutex> lock(g_implementations_mutex);
|
|
277
|
-
g_implementations.emplace(instancePtr, std::move(impl));
|
|
278
|
-
}
|
|
258
|
+
ProtocolManager::Instance().Register(instancePtr, std::move(impl));
|
|
279
259
|
|
|
280
260
|
// Return wrapped object
|
|
281
261
|
return ObjcObject::NewInstance(env, instance);
|