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.
@@ -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
- delete data;
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
- delete data;
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
- [invocation release];
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
- // Clean up the invocation data
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
- std::lock_guard<std::mutex> lock(g_implementations_mutex);
182
- auto it = g_implementations.find(ptr);
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 YES;
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
- std::lock_guard<std::mutex> lock(g_implementations_mutex);
205
- auto it = g_implementations.find(ptr);
206
- if (it != g_implementations.end()) {
207
- NSString *selectorString = NSStringFromSelector(selector);
208
- std::string selName = [selectorString UTF8String];
209
- auto encIt = it->second.typeEncodings.find(selName);
210
- if (encIt != it->second.typeEncodings.end()) {
211
- return [NSMethodSignature signatureWithObjCTypes:encIt->second.c_str()];
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]; // Keep invocation alive until callback completes
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
- // Get thread-safe data (TSFN, typeEncoding, js_thread)
243
- // DO NOT access any N-API values here - we may not be on the JS thread!
244
- Napi::ThreadSafeFunction tsfn;
245
- std::string typeEncoding;
246
- pthread_t js_thread;
247
- bool isElectron;
248
- {
249
- std::lock_guard<std::mutex> lock(g_implementations_mutex);
250
- auto it = g_implementations.find(ptr);
251
- if (it == g_implementations.end()) {
252
- NOBJC_WARN("Protocol implementation not found for instance %p", self);
253
- return;
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
- // Create invocation data
294
- auto data = new InvocationData();
295
- data->invocation = invocation;
296
- data->selectorName = selectorName;
297
- data->typeEncoding = typeEncoding;
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
- napi_status status;
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
- if (is_js_thread && !isElectron) {
303
- // We're on the JS thread in Node/Bun (NOT Electron)
304
- // Call directly to ensure return values are set synchronously.
305
- data->completionMutex = nullptr;
306
- data->completionCv = nullptr;
307
- data->isComplete = nullptr;
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
- tsfn.Release();
292
+ auto encIt = it->second.typeEncodings.find(selName);
293
+ if (encIt != it->second.typeEncodings.end()) {
294
+ ctx.typeEncoding = encIt->second;
295
+ }
310
296
 
311
- Napi::Function jsFn;
312
- napi_env stored_env;
313
- {
314
- std::lock_guard<std::mutex> lock(g_implementations_mutex);
315
- auto it = g_implementations.find(ptr);
316
- if (it == g_implementations.end()) {
317
- NOBJC_WARN("Protocol implementation not found for instance %p (JS thread path)", self);
318
- [invocation release];
319
- delete data;
320
- return;
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(selectorName);
310
+ auto jsCallbackIt = it->second.jsCallbacks.find(selName);
324
311
  if (jsCallbackIt == it->second.jsCallbacks.end()) {
325
- NOBJC_WARN("JS callback not found for selector %s (JS thread path)",
326
- selectorName.c_str());
327
- [invocation release];
328
- delete data;
329
- return;
312
+ return Napi::Function();
330
313
  }
331
314
 
332
- stored_env = it->second.env;
333
- jsFn = jsCallbackIt->second.Value();
334
- }
335
-
336
- // Safely call the JS callback with proper V8 context setup
337
- // Wrap in try-catch to handle invalid env (e.g., in Electron when context
338
- // is destroyed)
339
- try {
340
- Napi::Env callEnv(stored_env);
341
-
342
- // Create a HandleScope to properly manage V8 handles
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
- // Wait for callback by pumping CFRunLoop
400
- // This allows the event loop to process our callback
401
- CFTimeInterval timeout = 0.001; // 1ms per iteration
328
+ auto callbackIt = it->second.callbacks.find(selName);
329
+ if (callbackIt == it->second.callbacks.end()) {
330
+ return std::nullopt;
331
+ }
402
332
 
403
- while (true) {
404
- {
405
- std::unique_lock<std::mutex> lock(completionMutex);
406
- if (isComplete) {
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
- // Return value (if any) has been set on the invocation
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 global map
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
- auto it = g_implementations.find(ptr);
425
- if (it != g_implementations.end()) {
426
- // Release all ThreadSafeFunctions and JS callbacks
427
- // Do this carefully to avoid issues during shutdown
428
- try {
429
- for (auto &pair : it->second.callbacks) {
430
- // Release the ThreadSafeFunction
431
- pair.second.Release();
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
- it->second.callbacks.clear();
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
- g_implementations.erase(it);
441
- }
370
+ });
442
371
  }
443
372
 
444
373
  // Call the superclass dealloc
@@ -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
- uintptr_t ptrValue = reinterpret_cast<uintptr_t>(objcObj->objcObject);
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
- uintptr_t ptrValue = 0;
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
- // Read pointer from BigInt
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 (ptrValue == 0) {
72
+ if (ptr == nullptr) {
94
73
  return env.Null();
95
74
  }
96
75
 
97
- // Convert the pointer value back to an Objective-C object pointer
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
- // MARK: - Global Storage Definition
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 by checking process.versions.electron
134
- bool isElectron = false;
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 global map
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);