objc-js 0.0.11 → 0.0.13

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 CHANGED
@@ -5,7 +5,8 @@
5
5
  "sources": [
6
6
  "src/native/nobjc.mm",
7
7
  "src/native/ObjcObject.mm",
8
- "src/native/protocol-impl.mm"
8
+ "src/native/protocol-impl.mm",
9
+ "src/native/method-forwarding.mm"
9
10
  ],
10
11
  "defines": [
11
12
  "NODE_ADDON_API_CPP_EXCEPTIONS",
Binary file
package/package.json CHANGED
@@ -39,7 +39,7 @@
39
39
  "format": "prettier --write \"**/*.{ts,js,json,md}\"",
40
40
  "preinstall-disabled": "npm run build-scripts && npm run make-clangd-config"
41
41
  },
42
- "version": "0.0.11",
42
+ "version": "0.0.13",
43
43
  "description": "Objective-C bridge for Node.js",
44
44
  "main": "dist/index.js",
45
45
  "dependencies": {
@@ -1,4 +1,5 @@
1
1
  #include "ObjcObject.h"
2
+ #include "type-conversion.h"
2
3
  #include <Foundation/Foundation.h>
3
4
  #include <format>
4
5
  #include <napi.h>
@@ -218,51 +219,6 @@ T ConvertToNativeValue(const Napi::Value &value,
218
219
 
219
220
  // MARK: - Conversion Top Layer
220
221
 
221
- // Helper class to manage the lifetime of simplified type encodings
222
- class SimplifiedTypeEncoding {
223
- private:
224
- std::string simplified;
225
-
226
- public:
227
- SimplifiedTypeEncoding(const char *typeEncoding) : simplified(typeEncoding) {
228
- // Remove any leading qualifiers
229
- while (!simplified.empty() && (simplified[0] == 'r' || simplified[0] == 'n' ||
230
- simplified[0] == 'N' || simplified[0] == 'o' ||
231
- simplified[0] == 'O' || simplified[0] == 'R' ||
232
- simplified[0] == 'V')) {
233
- simplified.erase(0, 1);
234
- }
235
- }
236
-
237
- const char *c_str() const { return simplified.c_str(); }
238
- char operator[](size_t index) const { return simplified[index]; }
239
- operator const char *() const { return simplified.c_str(); }
240
- };
241
-
242
- // Legacy function for compatibility - returns pointer to internal string
243
- // WARNING: The returned pointer is only valid as long as the typeEncoding parameter is valid
244
- inline const char *SimplifyTypeEncoding(const char *typeEncoding) {
245
- // For simple cases where there are no qualifiers, return the original pointer
246
- if (typeEncoding && typeEncoding[0] != 'r' && typeEncoding[0] != 'n' &&
247
- typeEncoding[0] != 'N' && typeEncoding[0] != 'o' &&
248
- typeEncoding[0] != 'O' && typeEncoding[0] != 'R' &&
249
- typeEncoding[0] != 'V') {
250
- return typeEncoding;
251
- }
252
-
253
- // For complex cases, we need to skip qualifiers
254
- // This is a temporary fix - callers should use SimplifiedTypeEncoding class
255
- static thread_local std::string buffer;
256
- buffer = typeEncoding;
257
- while (!buffer.empty() && (buffer[0] == 'r' || buffer[0] == 'n' ||
258
- buffer[0] == 'N' || buffer[0] == 'o' ||
259
- buffer[0] == 'O' || buffer[0] == 'R' ||
260
- buffer[0] == 'V')) {
261
- buffer.erase(0, 1);
262
- }
263
- return buffer.c_str();
264
- }
265
-
266
222
  // Convert a Napi::Value to an ObjcType based on the provided type encoding.
267
223
  inline auto AsObjCArgument(const Napi::Value &value, const char *typeEncoding,
268
224
  const ObjcArgumentContext &context)
@@ -321,71 +277,11 @@ inline auto AsObjCArgument(const Napi::Value &value, const char *typeEncoding,
321
277
  }
322
278
 
323
279
  // Convert the return value of an Objective-C method to a Napi::Value.
280
+ // This is an alias for GetInvocationReturnAsJS for backward compatibility.
324
281
  inline Napi::Value
325
282
  ConvertReturnValueToJSValue(Napi::Env env, NSInvocation *invocation,
326
283
  NSMethodSignature *methodSignature) {
327
- #define NOBJC_NUMERIC_RETURN_CASE(encoding, ctype) \
328
- case encoding: { \
329
- ctype result; \
330
- [invocation getReturnValue:&result]; \
331
- return Napi::Number::New(env, result); \
332
- }
333
- switch (*SimplifyTypeEncoding([methodSignature methodReturnType])) {
334
- NOBJC_NUMERIC_RETURN_CASE('c', char)
335
- NOBJC_NUMERIC_RETURN_CASE('i', int)
336
- NOBJC_NUMERIC_RETURN_CASE('s', short)
337
- NOBJC_NUMERIC_RETURN_CASE('l', long)
338
- NOBJC_NUMERIC_RETURN_CASE('q', long long)
339
- NOBJC_NUMERIC_RETURN_CASE('C', unsigned char)
340
- NOBJC_NUMERIC_RETURN_CASE('I', unsigned int)
341
- NOBJC_NUMERIC_RETURN_CASE('S', unsigned short)
342
- NOBJC_NUMERIC_RETURN_CASE('L', unsigned long)
343
- NOBJC_NUMERIC_RETURN_CASE('Q', unsigned long long)
344
- NOBJC_NUMERIC_RETURN_CASE('f', float)
345
- NOBJC_NUMERIC_RETURN_CASE('d', double)
346
- case 'B': {
347
- bool result;
348
- [invocation getReturnValue:&result];
349
- return Napi::Boolean::New(env, result);
350
- }
351
- case 'v':
352
- return env.Undefined();
353
- case '*': {
354
- char *result = nullptr;
355
- [invocation getReturnValue:&result];
356
- if (result == nullptr) {
357
- return env.Null();
358
- }
359
- Napi::String jsString = Napi::String::New(env, result);
360
- // free(result); // It might not be safe to free this memory.
361
- return jsString;
362
- }
363
- case '@':
364
- case '#': {
365
- id result = nil;
366
- [invocation getReturnValue:&result];
367
- if (result == nil) {
368
- return env.Null();
369
- }
370
- return ObjcObject::NewInstance(env, result);
371
- }
372
- case ':': {
373
- SEL result = nullptr;
374
- [invocation getReturnValue:&result];
375
- if (result == nullptr) {
376
- return env.Null();
377
- }
378
- NSString *selectorString = NSStringFromSelector(result);
379
- if (selectorString == nil) {
380
- return env.Null();
381
- }
382
- return Napi::String::New(env, [selectorString UTF8String]);
383
- }
384
- default:
385
- Napi::TypeError::New(env, "Unsupported return type (post-invoke)")
386
- .ThrowAsJavaScriptException();
387
- return env.Null();
388
- }
284
+ return GetInvocationReturnAsJS(env, invocation, methodSignature);
389
285
  }
390
286
 
391
- #endif // NATIVE_BRIDGE_H
287
+ #endif // NATIVE_BRIDGE_H
@@ -0,0 +1,38 @@
1
+ #ifndef METHOD_FORWARDING_H
2
+ #define METHOD_FORWARDING_H
3
+
4
+ #include "protocol-storage.h"
5
+ #include <napi.h>
6
+ #include <objc/runtime.h>
7
+
8
+ // Forward declarations for Objective-C types
9
+ #ifdef __OBJC__
10
+ @class NSMethodSignature;
11
+ @class NSInvocation;
12
+ #else
13
+ typedef struct NSMethodSignature NSMethodSignature;
14
+ typedef struct NSInvocation NSInvocation;
15
+ #endif
16
+
17
+ // MARK: - ThreadSafeFunction Callback
18
+
19
+ // Callback that runs on the JavaScript thread to invoke JS functions
20
+ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
21
+ InvocationData *data);
22
+
23
+ // MARK: - ObjC Runtime Method Implementations
24
+
25
+ // Override respondsToSelector to return YES for methods we implement
26
+ BOOL RespondsToSelector(id self, SEL _cmd, SEL selector);
27
+
28
+ // Method signature provider for message forwarding
29
+ NSMethodSignature *MethodSignatureForSelector(id self, SEL _cmd, SEL selector);
30
+
31
+ // Forward invocation handler for dynamic method dispatch
32
+ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation);
33
+
34
+ // Deallocation implementation to clean up when instance is destroyed
35
+ void DeallocImplementation(id self, SEL _cmd);
36
+
37
+ #endif // METHOD_FORWARDING_H
38
+
@@ -0,0 +1,416 @@
1
+ #include "method-forwarding.h"
2
+ #include "ObjcObject.h"
3
+ #include "protocol-storage.h"
4
+ #include "type-conversion.h"
5
+ #include <CoreFoundation/CoreFoundation.h>
6
+ #include <Foundation/Foundation.h>
7
+ #include <napi.h>
8
+ #include <objc/runtime.h>
9
+
10
+ // MARK: - ThreadSafeFunction Callback Handler
11
+
12
+ // This function runs on the JavaScript thread
13
+ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
14
+ InvocationData *data) {
15
+ if (!data) {
16
+ NSLog(@"Error: InvocationData is null in CallJSCallback");
17
+ return;
18
+ }
19
+
20
+ // Check if the callback is valid before proceeding
21
+ if (jsCallback.IsEmpty()) {
22
+ NSLog(@"Error: jsCallback is null/empty in CallJSCallback for selector %s",
23
+ data->selectorName.c_str());
24
+ if (data->invocation) {
25
+ [data->invocation release];
26
+ }
27
+ SignalInvocationComplete(data);
28
+ delete data;
29
+ return;
30
+ }
31
+
32
+ NSInvocation *invocation = data->invocation;
33
+ if (!invocation) {
34
+ NSLog(@"Error: NSInvocation is null in CallJSCallback");
35
+ SignalInvocationComplete(data);
36
+ delete data;
37
+ return;
38
+ }
39
+
40
+ // Extract arguments using NSInvocation
41
+ NSMethodSignature *sig = [invocation methodSignature];
42
+ if (!sig) {
43
+ NSLog(@"Error: Failed to get method signature for selector %s",
44
+ data->selectorName.c_str());
45
+ SignalInvocationComplete(data);
46
+ [invocation release];
47
+ delete data;
48
+ return;
49
+ }
50
+
51
+ std::vector<napi_value> jsArgs;
52
+
53
+ // Skip first two arguments (self and _cmd)
54
+ for (NSUInteger i = 2; i < [sig numberOfArguments]; i++) {
55
+ const char *type = [sig getArgumentTypeAtIndex:i];
56
+ SimplifiedTypeEncoding argType(type);
57
+ jsArgs.push_back(ExtractInvocationArgumentToJS(env, invocation, i, argType[0]));
58
+ }
59
+
60
+ NSLog(@"[DEBUG] About to call JS callback for %s with %zu arguments",
61
+ data->selectorName.c_str(), jsArgs.size());
62
+
63
+ // Call the JavaScript callback
64
+ try {
65
+ Napi::Value result = jsCallback.Call(jsArgs);
66
+
67
+ NSLog(@"[DEBUG] JS callback for %s returned, result type: %d",
68
+ data->selectorName.c_str(), result.Type());
69
+
70
+ // Handle return value if the method expects one
71
+ const char *returnType = [sig methodReturnType];
72
+ SimplifiedTypeEncoding retType(returnType);
73
+
74
+ if (retType[0] != 'v') { // Not void
75
+ NSLog(@"[DEBUG] Setting return value for %s, return type: %c, JS result is %s",
76
+ data->selectorName.c_str(), retType[0],
77
+ result.IsNull() ? "null" : result.IsUndefined() ? "undefined" : "value");
78
+ SetInvocationReturnFromJS(invocation, result, retType[0],
79
+ data->selectorName.c_str());
80
+ NSLog(@"[DEBUG] Return value set for %s", data->selectorName.c_str());
81
+ }
82
+ } catch (const Napi::Error &e) {
83
+ NSLog(@"Error calling JavaScript callback for %s: %s",
84
+ data->selectorName.c_str(), e.what());
85
+ } catch (const std::exception &e) {
86
+ NSLog(@"Exception calling JavaScript callback for %s: %s",
87
+ data->selectorName.c_str(), e.what());
88
+ } catch (...) {
89
+ NSLog(@"Unknown error calling JavaScript callback for %s",
90
+ data->selectorName.c_str());
91
+ }
92
+
93
+ // Signal completion to the waiting ForwardInvocation
94
+ SignalInvocationComplete(data);
95
+
96
+ // Clean up the invocation data
97
+ // Release the invocation that we retained in ForwardInvocation
98
+ [invocation release];
99
+ delete data;
100
+ }
101
+
102
+ // MARK: - Message Forwarding Implementation
103
+
104
+ // Override respondsToSelector to return YES for methods we implement
105
+ BOOL RespondsToSelector(id self, SEL _cmd, SEL selector) {
106
+ void *ptr = (__bridge void *)self;
107
+
108
+ // Check if this is one of our implemented methods
109
+ {
110
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
111
+ auto it = g_implementations.find(ptr);
112
+ if (it != g_implementations.end()) {
113
+ NSString *selectorString = NSStringFromSelector(selector);
114
+ if (selectorString != nil) {
115
+ std::string selName = [selectorString UTF8String];
116
+ auto callbackIt = it->second.callbacks.find(selName);
117
+ if (callbackIt != it->second.callbacks.end()) {
118
+ return YES;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ // For methods we don't implement, check if NSObject responds to them
125
+ // This handles standard NSObject methods like description, isEqual:, etc.
126
+ return [NSObject instancesRespondToSelector:selector];
127
+ }
128
+
129
+ // Provide method signature for message forwarding
130
+ NSMethodSignature *MethodSignatureForSelector(id self, SEL _cmd, SEL selector) {
131
+ void *ptr = (__bridge void *)self;
132
+
133
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
134
+ auto it = g_implementations.find(ptr);
135
+ if (it != g_implementations.end()) {
136
+ NSString *selectorString = NSStringFromSelector(selector);
137
+ std::string selName = [selectorString UTF8String];
138
+ auto encIt = it->second.typeEncodings.find(selName);
139
+ if (encIt != it->second.typeEncodings.end()) {
140
+ return [NSMethodSignature signatureWithObjCTypes:encIt->second.c_str()];
141
+ }
142
+ }
143
+ // Fall back to superclass for methods we don't implement
144
+ return [NSObject instanceMethodSignatureForSelector:selector];
145
+ }
146
+
147
+ // Handle forwarded invocations
148
+ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
149
+ if (!invocation) {
150
+ NSLog(@"Error: ForwardInvocation called with nil invocation");
151
+ return;
152
+ }
153
+
154
+ // Retain the invocation to keep it alive during async call
155
+ // retainArguments only retains the arguments, not the invocation itself
156
+ [invocation retainArguments];
157
+ [invocation retain]; // Keep invocation alive until callback completes
158
+
159
+ SEL selector = [invocation selector];
160
+ NSString *selectorString = NSStringFromSelector(selector);
161
+ if (!selectorString) {
162
+ NSLog(@"Error: Failed to convert selector to string");
163
+ return;
164
+ }
165
+
166
+ std::string selectorName = [selectorString UTF8String];
167
+
168
+ // Store self pointer for later lookups
169
+ void *ptr = (__bridge void *)self;
170
+
171
+ // Get thread-safe data (TSFN, typeEncoding, js_thread)
172
+ // DO NOT access any N-API values here - we may not be on the JS thread!
173
+ Napi::ThreadSafeFunction tsfn;
174
+ std::string typeEncoding;
175
+ pthread_t js_thread;
176
+ bool isElectron;
177
+ {
178
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
179
+ auto it = g_implementations.find(ptr);
180
+ if (it == g_implementations.end()) {
181
+ NSLog(@"Warning: Protocol implementation not found for instance %p",
182
+ self);
183
+ return;
184
+ }
185
+
186
+ auto callbackIt = it->second.callbacks.find(selectorName);
187
+ if (callbackIt == it->second.callbacks.end()) {
188
+ NSLog(@"Warning: Callback not found for selector %s",
189
+ selectorName.c_str());
190
+ return;
191
+ }
192
+
193
+ // Get the ThreadSafeFunction - this is thread-safe by design
194
+ // IMPORTANT: We must Acquire() to increment the ref count, because copying
195
+ // a ThreadSafeFunction does NOT increment it. If DeallocImplementation
196
+ // runs and calls Release() on the original, our copy would become invalid.
197
+ tsfn = callbackIt->second;
198
+ napi_status acq_status = tsfn.Acquire();
199
+ if (acq_status != napi_ok) {
200
+ NSLog(@"Warning: Failed to acquire ThreadSafeFunction for selector %s",
201
+ selectorName.c_str());
202
+ return;
203
+ }
204
+
205
+ // Get the type encoding for return value handling
206
+ auto encIt = it->second.typeEncodings.find(selectorName);
207
+ if (encIt != it->second.typeEncodings.end()) {
208
+ typeEncoding = encIt->second;
209
+ }
210
+
211
+ // Get the JS thread ID to check if we're on the same thread
212
+ js_thread = it->second.js_thread;
213
+ isElectron = it->second.isElectron;
214
+ }
215
+
216
+ // Check if we're on the JS thread
217
+ bool is_js_thread = pthread_equal(pthread_self(), js_thread);
218
+
219
+ NSLog(@"[DEBUG] ForwardInvocation for %s: is_js_thread=%d, isElectron=%d, current_thread=%p, js_thread=%p",
220
+ selectorName.c_str(), is_js_thread, isElectron, pthread_self(), js_thread);
221
+
222
+ // IMPORTANT: We call directly on the JS thread so return values are set
223
+ // synchronously; otherwise we use a ThreadSafeFunction to marshal work.
224
+ // EXCEPTION: In Electron, we ALWAYS use TSFN even on the JS thread because
225
+ // Electron's V8 context isn't properly set up for direct handle creation.
226
+
227
+ // Create invocation data
228
+ auto data = new InvocationData();
229
+ data->invocation = invocation;
230
+ data->selectorName = selectorName;
231
+ data->typeEncoding = typeEncoding;
232
+
233
+ napi_status status;
234
+
235
+ if (is_js_thread && !isElectron) {
236
+ // We're on the JS thread in Node/Bun (NOT Electron)
237
+ // Call directly to ensure return values are set synchronously.
238
+ NSLog(@"[DEBUG] Taking JS thread direct call path for %s", selectorName.c_str());
239
+ data->completionMutex = nullptr;
240
+ data->completionCv = nullptr;
241
+ data->isComplete = nullptr;
242
+
243
+ tsfn.Release();
244
+
245
+ Napi::Function jsFn;
246
+ napi_env stored_env;
247
+ {
248
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
249
+ auto it = g_implementations.find(ptr);
250
+ if (it == g_implementations.end()) {
251
+ NSLog(@"Warning: Protocol implementation not found for instance %p "
252
+ @"(JS thread path)",
253
+ self);
254
+ [invocation release];
255
+ delete data;
256
+ return;
257
+ }
258
+
259
+ auto jsCallbackIt = it->second.jsCallbacks.find(selectorName);
260
+ if (jsCallbackIt == it->second.jsCallbacks.end()) {
261
+ NSLog(@"Warning: JS callback not found for selector %s "
262
+ @"(JS thread path)",
263
+ selectorName.c_str());
264
+ [invocation release];
265
+ delete data;
266
+ return;
267
+ }
268
+
269
+ stored_env = it->second.env;
270
+ jsFn = jsCallbackIt->second.Value();
271
+ }
272
+
273
+ // Safely call the JS callback with proper V8 context setup
274
+ // Wrap in try-catch to handle invalid env (e.g., in Electron when context
275
+ // is destroyed)
276
+ try {
277
+ NSLog(@"[DEBUG] Creating Napi::Env from stored_env for %s", selectorName.c_str());
278
+ Napi::Env callEnv(stored_env);
279
+
280
+ // Create a HandleScope to properly manage V8 handles
281
+ // This is critical for Electron which may have multiple V8 contexts
282
+ NSLog(@"[DEBUG] Creating HandleScope for %s", selectorName.c_str());
283
+ Napi::HandleScope scope(callEnv);
284
+
285
+ NSLog(@"[DEBUG] Calling CallJSCallback directly for %s", selectorName.c_str());
286
+ CallJSCallback(callEnv, jsFn, data);
287
+ NSLog(@"[DEBUG] CallJSCallback completed for %s", selectorName.c_str());
288
+ // CallJSCallback releases invocation and deletes data.
289
+ } catch (const std::exception &e) {
290
+ NSLog(@"Error calling JS callback directly (likely invalid env in "
291
+ @"Electron): %s",
292
+ e.what());
293
+ NSLog(@"Falling back to ThreadSafeFunction for selector %s",
294
+ selectorName.c_str());
295
+
296
+ // Fallback to TSFN if direct call fails (e.g., invalid env in Electron)
297
+ // We need to re-acquire the TSFN and set up sync primitives
298
+ {
299
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
300
+ auto it = g_implementations.find(ptr);
301
+ if (it != g_implementations.end()) {
302
+ auto callbackIt = it->second.callbacks.find(selectorName);
303
+ if (callbackIt != it->second.callbacks.end()) {
304
+ tsfn = callbackIt->second;
305
+ napi_status acq_status = tsfn.Acquire();
306
+ if (acq_status == napi_ok) {
307
+ // Set up synchronization for fallback path
308
+ std::mutex completionMutex;
309
+ std::condition_variable completionCv;
310
+ bool isComplete = false;
311
+
312
+ data->completionMutex = &completionMutex;
313
+ data->completionCv = &completionCv;
314
+ data->isComplete = &isComplete;
315
+
316
+ status = tsfn.NonBlockingCall(data, CallJSCallback);
317
+ tsfn.Release();
318
+
319
+ if (status == napi_ok) {
320
+ // Wait for callback by pumping CFRunLoop
321
+ CFTimeInterval timeout = 0.001; // 1ms per iteration
322
+ while (true) {
323
+ {
324
+ std::unique_lock<std::mutex> lock(completionMutex);
325
+ if (isComplete) {
326
+ break;
327
+ }
328
+ }
329
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
330
+ }
331
+ return; // Data cleaned up in callback
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ // If fallback also failed, clean up manually
339
+ [invocation release];
340
+ delete data;
341
+ }
342
+ } else {
343
+ // We're on a different thread (e.g., Cocoa callback from
344
+ // ASAuthorizationController) Use NonBlockingCall + runloop pumping to avoid
345
+ // deadlocks
346
+ NSLog(@"[DEBUG] Taking non-JS thread path for %s", selectorName.c_str());
347
+ std::mutex completionMutex;
348
+ std::condition_variable completionCv;
349
+ bool isComplete = false;
350
+
351
+ data->completionMutex = &completionMutex;
352
+ data->completionCv = &completionCv;
353
+ data->isComplete = &isComplete;
354
+
355
+ status = tsfn.NonBlockingCall(data, CallJSCallback);
356
+ tsfn.Release();
357
+
358
+ if (status != napi_ok) {
359
+ NSLog(@"Error: Failed to call ThreadSafeFunction for selector %s "
360
+ @"(status: %d)",
361
+ selectorName.c_str(), status);
362
+ [invocation release];
363
+ delete data;
364
+ return;
365
+ }
366
+
367
+ // Wait for callback by pumping CFRunLoop
368
+ // This allows the event loop to process our callback
369
+ CFTimeInterval timeout = 0.001; // 1ms per iteration
370
+
371
+ while (true) {
372
+ {
373
+ std::unique_lock<std::mutex> lock(completionMutex);
374
+ if (isComplete) {
375
+ break;
376
+ }
377
+ }
378
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
379
+ }
380
+ // Data cleaned up in callback
381
+ }
382
+
383
+ // Return value (if any) has been set on the invocation
384
+ }
385
+
386
+ // Deallocation implementation
387
+ void DeallocImplementation(id self, SEL _cmd) {
388
+ @autoreleasepool {
389
+ // Remove the implementation from the global map
390
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
391
+ void *ptr = (__bridge void *)self;
392
+ auto it = g_implementations.find(ptr);
393
+ if (it != g_implementations.end()) {
394
+ // Release all ThreadSafeFunctions and JS callbacks
395
+ // Do this carefully to avoid issues during shutdown
396
+ try {
397
+ for (auto &pair : it->second.callbacks) {
398
+ // Release the ThreadSafeFunction
399
+ pair.second.Release();
400
+ }
401
+ it->second.callbacks.clear();
402
+ it->second.jsCallbacks.clear();
403
+ it->second.typeEncodings.clear();
404
+ } catch (...) {
405
+ // Ignore errors during cleanup
406
+ NSLog(@"Warning: Exception during callback cleanup for instance %p",
407
+ self);
408
+ }
409
+ g_implementations.erase(it);
410
+ }
411
+ }
412
+
413
+ // Call the superclass dealloc
414
+ // Note: Under ARC, we don't need to manually call [super dealloc]
415
+ // The runtime handles this automatically
416
+ }
@@ -2,68 +2,21 @@
2
2
  #define PROTOCOL_IMPL_H
3
3
 
4
4
  #include <napi.h>
5
- #include <objc/runtime.h>
6
5
  #include <string>
7
- #include <unordered_map>
8
6
  #include <vector>
9
7
 
10
- // Forward declarations for Objective-C types
11
- #ifdef __OBJC__
12
- @class NSMethodSignature;
13
- @class NSInvocation;
14
- #else
15
- typedef struct NSMethodSignature NSMethodSignature;
16
- typedef struct NSInvocation NSInvocation;
17
- #endif
18
-
19
- // MARK: - Data Structures
20
-
21
- // Data passed from native thread to JS thread for invocation handling
22
- struct InvocationData {
23
- NSInvocation *invocation;
24
- std::string selectorName;
25
- std::string typeEncoding;
26
- // The invocation itself stores the return value, so we don't need separate storage
27
- // BlockingCall ensures the callback completes before returning, so no sync primitives needed
28
- };
29
-
30
- // Stores information about a protocol implementation instance
31
- struct ProtocolImplementation {
32
- std::unordered_map<std::string, Napi::ThreadSafeFunction> callbacks;
33
- std::unordered_map<std::string, Napi::FunctionReference> jsCallbacks; // Original JS functions for direct calls
34
- std::unordered_map<std::string, std::string> typeEncodings;
35
- std::string className;
36
- napi_env env; // Store the environment for direct calls
37
- pthread_t js_thread; // Store the JS thread ID
38
- };
39
-
40
- // Global map: instance pointer -> implementation details
41
- // This keeps JavaScript callbacks alive for the lifetime of the Objective-C object
42
- extern std::unordered_map<void *, ProtocolImplementation> g_implementations;
43
-
44
- // MARK: - Function Declarations
8
+ // MARK: - Public API
45
9
 
46
10
  // Main entry point: creates a new Objective-C class that implements a protocol
11
+ // Arguments:
12
+ // - protocolName (string): Name of the Objective-C protocol to implement
13
+ // - methodImplementations (object): Map of selector names to JS functions
14
+ // Returns: An ObjcObject wrapping the new instance
47
15
  Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info);
48
16
 
49
- // Override respondsToSelector to return YES for implemented methods
50
- BOOL RespondsToSelector(id self, SEL _cmd, SEL selector);
51
-
52
- // Method signature provider for message forwarding
53
- NSMethodSignature* MethodSignatureForSelector(id self, SEL _cmd, SEL selector);
54
-
55
- // Forward invocation handler for dynamic method dispatch
56
- void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation);
17
+ // MARK: - Utility Functions
57
18
 
58
19
  // Helper: Parses an Objective-C method signature to extract argument types
59
20
  std::vector<std::string> ParseMethodSignature(const char *typeEncoding);
60
21
 
61
- // Helper: Converts an Objective-C value to a JavaScript value
62
- Napi::Value ConvertObjCValueToJS(Napi::Env env, void *value,
63
- const char *typeEncoding);
64
-
65
- // Deallocation implementation to clean up when instance is destroyed
66
- void DeallocImplementation(id self, SEL _cmd);
67
-
68
22
  #endif // PROTOCOL_IMPL_H
69
-