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.
@@ -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
- 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