objc-js 0.0.11 → 0.0.12

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.12",
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,395 @@
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
+ // Call the JavaScript callback
61
+ try {
62
+ Napi::Value result = jsCallback.Call(jsArgs);
63
+
64
+ // Handle return value if the method expects one
65
+ const char *returnType = [sig methodReturnType];
66
+ SimplifiedTypeEncoding retType(returnType);
67
+
68
+ if (retType[0] != 'v') { // Not void
69
+ NSLog(@"Setting return value for %s, return type: %c",
70
+ data->selectorName.c_str(), retType[0]);
71
+ SetInvocationReturnFromJS(invocation, result, retType[0],
72
+ data->selectorName.c_str());
73
+ }
74
+ } catch (const Napi::Error &e) {
75
+ NSLog(@"Error calling JavaScript callback for %s: %s",
76
+ data->selectorName.c_str(), e.what());
77
+ } catch (const std::exception &e) {
78
+ NSLog(@"Exception calling JavaScript callback for %s: %s",
79
+ data->selectorName.c_str(), e.what());
80
+ } catch (...) {
81
+ NSLog(@"Unknown error calling JavaScript callback for %s",
82
+ data->selectorName.c_str());
83
+ }
84
+
85
+ // Signal completion to the waiting ForwardInvocation
86
+ SignalInvocationComplete(data);
87
+
88
+ // Clean up the invocation data
89
+ // Release the invocation that we retained in ForwardInvocation
90
+ [invocation release];
91
+ delete data;
92
+ }
93
+
94
+ // MARK: - Message Forwarding Implementation
95
+
96
+ // Override respondsToSelector to return YES for methods we implement
97
+ BOOL RespondsToSelector(id self, SEL _cmd, SEL selector) {
98
+ void *ptr = (__bridge void *)self;
99
+
100
+ // Check if this is one of our implemented methods
101
+ {
102
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
103
+ auto it = g_implementations.find(ptr);
104
+ if (it != g_implementations.end()) {
105
+ NSString *selectorString = NSStringFromSelector(selector);
106
+ if (selectorString != nil) {
107
+ std::string selName = [selectorString UTF8String];
108
+ auto callbackIt = it->second.callbacks.find(selName);
109
+ if (callbackIt != it->second.callbacks.end()) {
110
+ return YES;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // For methods we don't implement, check if NSObject responds to them
117
+ // This handles standard NSObject methods like description, isEqual:, etc.
118
+ return [NSObject instancesRespondToSelector:selector];
119
+ }
120
+
121
+ // Provide method signature for message forwarding
122
+ NSMethodSignature *MethodSignatureForSelector(id self, SEL _cmd, SEL selector) {
123
+ void *ptr = (__bridge void *)self;
124
+
125
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
126
+ auto it = g_implementations.find(ptr);
127
+ if (it != g_implementations.end()) {
128
+ NSString *selectorString = NSStringFromSelector(selector);
129
+ std::string selName = [selectorString UTF8String];
130
+ auto encIt = it->second.typeEncodings.find(selName);
131
+ if (encIt != it->second.typeEncodings.end()) {
132
+ return [NSMethodSignature signatureWithObjCTypes:encIt->second.c_str()];
133
+ }
134
+ }
135
+ // Fall back to superclass for methods we don't implement
136
+ return [NSObject instanceMethodSignatureForSelector:selector];
137
+ }
138
+
139
+ // Handle forwarded invocations
140
+ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
141
+ if (!invocation) {
142
+ NSLog(@"Error: ForwardInvocation called with nil invocation");
143
+ return;
144
+ }
145
+
146
+ // Retain the invocation to keep it alive during async call
147
+ // retainArguments only retains the arguments, not the invocation itself
148
+ [invocation retainArguments];
149
+ [invocation retain]; // Keep invocation alive until callback completes
150
+
151
+ SEL selector = [invocation selector];
152
+ NSString *selectorString = NSStringFromSelector(selector);
153
+ if (!selectorString) {
154
+ NSLog(@"Error: Failed to convert selector to string");
155
+ return;
156
+ }
157
+
158
+ std::string selectorName = [selectorString UTF8String];
159
+
160
+ // Store self pointer for later lookups
161
+ void *ptr = (__bridge void *)self;
162
+
163
+ // Get thread-safe data (TSFN, typeEncoding, js_thread)
164
+ // DO NOT access any N-API values here - we may not be on the JS thread!
165
+ Napi::ThreadSafeFunction tsfn;
166
+ std::string typeEncoding;
167
+ pthread_t js_thread;
168
+ {
169
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
170
+ auto it = g_implementations.find(ptr);
171
+ if (it == g_implementations.end()) {
172
+ NSLog(@"Warning: Protocol implementation not found for instance %p",
173
+ self);
174
+ return;
175
+ }
176
+
177
+ auto callbackIt = it->second.callbacks.find(selectorName);
178
+ if (callbackIt == it->second.callbacks.end()) {
179
+ NSLog(@"Warning: Callback not found for selector %s",
180
+ selectorName.c_str());
181
+ return;
182
+ }
183
+
184
+ // Get the ThreadSafeFunction - this is thread-safe by design
185
+ // IMPORTANT: We must Acquire() to increment the ref count, because copying
186
+ // a ThreadSafeFunction does NOT increment it. If DeallocImplementation
187
+ // runs and calls Release() on the original, our copy would become invalid.
188
+ tsfn = callbackIt->second;
189
+ napi_status acq_status = tsfn.Acquire();
190
+ if (acq_status != napi_ok) {
191
+ NSLog(@"Warning: Failed to acquire ThreadSafeFunction for selector %s",
192
+ selectorName.c_str());
193
+ return;
194
+ }
195
+
196
+ // Get the type encoding for return value handling
197
+ auto encIt = it->second.typeEncodings.find(selectorName);
198
+ if (encIt != it->second.typeEncodings.end()) {
199
+ typeEncoding = encIt->second;
200
+ }
201
+
202
+ // Get the JS thread ID to check if we're on the same thread
203
+ js_thread = it->second.js_thread;
204
+ }
205
+
206
+ // Check if we're on the JS thread
207
+ bool is_js_thread = pthread_equal(pthread_self(), js_thread);
208
+
209
+ // IMPORTANT: We call directly on the JS thread so return values are set
210
+ // synchronously; otherwise we use a ThreadSafeFunction to marshal work.
211
+
212
+ // Create invocation data
213
+ auto data = new InvocationData();
214
+ data->invocation = invocation;
215
+ data->selectorName = selectorName;
216
+ data->typeEncoding = typeEncoding;
217
+
218
+ napi_status status;
219
+
220
+ if (is_js_thread) {
221
+ // We're on the JS thread (e.g., called from performSelector in Bun/Node)
222
+ // Call directly to ensure return values are set synchronously.
223
+ data->completionMutex = nullptr;
224
+ data->completionCv = nullptr;
225
+ data->isComplete = nullptr;
226
+
227
+ tsfn.Release();
228
+
229
+ Napi::Function jsFn;
230
+ napi_env stored_env;
231
+ {
232
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
233
+ auto it = g_implementations.find(ptr);
234
+ if (it == g_implementations.end()) {
235
+ NSLog(@"Warning: Protocol implementation not found for instance %p "
236
+ @"(JS thread path)",
237
+ self);
238
+ [invocation release];
239
+ delete data;
240
+ return;
241
+ }
242
+
243
+ auto jsCallbackIt = it->second.jsCallbacks.find(selectorName);
244
+ if (jsCallbackIt == it->second.jsCallbacks.end()) {
245
+ NSLog(@"Warning: JS callback not found for selector %s "
246
+ @"(JS thread path)",
247
+ selectorName.c_str());
248
+ [invocation release];
249
+ delete data;
250
+ return;
251
+ }
252
+
253
+ stored_env = it->second.env;
254
+ jsFn = jsCallbackIt->second.Value();
255
+ }
256
+
257
+ // Safely call the JS callback with proper V8 context setup
258
+ // Wrap in try-catch to handle invalid env (e.g., in Electron when context
259
+ // is destroyed)
260
+ try {
261
+ Napi::Env callEnv(stored_env);
262
+
263
+ // Create a HandleScope to properly manage V8 handles
264
+ // This is critical for Electron which may have multiple V8 contexts
265
+ Napi::HandleScope scope(callEnv);
266
+
267
+ CallJSCallback(callEnv, jsFn, data);
268
+ // CallJSCallback releases invocation and deletes data.
269
+ } catch (const std::exception &e) {
270
+ NSLog(@"Error calling JS callback directly (likely invalid env in "
271
+ @"Electron): %s",
272
+ e.what());
273
+ NSLog(@"Falling back to ThreadSafeFunction for selector %s",
274
+ selectorName.c_str());
275
+
276
+ // Fallback to TSFN if direct call fails (e.g., invalid env in Electron)
277
+ // We need to re-acquire the TSFN and set up sync primitives
278
+ {
279
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
280
+ auto it = g_implementations.find(ptr);
281
+ if (it != g_implementations.end()) {
282
+ auto callbackIt = it->second.callbacks.find(selectorName);
283
+ if (callbackIt != it->second.callbacks.end()) {
284
+ tsfn = callbackIt->second;
285
+ napi_status acq_status = tsfn.Acquire();
286
+ if (acq_status == napi_ok) {
287
+ // Set up synchronization for fallback path
288
+ std::mutex completionMutex;
289
+ std::condition_variable completionCv;
290
+ bool isComplete = false;
291
+
292
+ data->completionMutex = &completionMutex;
293
+ data->completionCv = &completionCv;
294
+ data->isComplete = &isComplete;
295
+
296
+ status = tsfn.NonBlockingCall(data, CallJSCallback);
297
+ tsfn.Release();
298
+
299
+ if (status == napi_ok) {
300
+ // Wait for callback by pumping CFRunLoop
301
+ CFTimeInterval timeout = 0.001; // 1ms per iteration
302
+ while (true) {
303
+ {
304
+ std::unique_lock<std::mutex> lock(completionMutex);
305
+ if (isComplete) {
306
+ break;
307
+ }
308
+ }
309
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
310
+ }
311
+ return; // Data cleaned up in callback
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ // If fallback also failed, clean up manually
319
+ [invocation release];
320
+ delete data;
321
+ }
322
+ } else {
323
+ // We're on a different thread (e.g., Cocoa callback from
324
+ // ASAuthorizationController) Use NonBlockingCall + runloop pumping to avoid
325
+ // deadlocks
326
+ std::mutex completionMutex;
327
+ std::condition_variable completionCv;
328
+ bool isComplete = false;
329
+
330
+ data->completionMutex = &completionMutex;
331
+ data->completionCv = &completionCv;
332
+ data->isComplete = &isComplete;
333
+
334
+ status = tsfn.NonBlockingCall(data, CallJSCallback);
335
+ tsfn.Release();
336
+
337
+ if (status != napi_ok) {
338
+ NSLog(@"Error: Failed to call ThreadSafeFunction for selector %s "
339
+ @"(status: %d)",
340
+ selectorName.c_str(), status);
341
+ [invocation release];
342
+ delete data;
343
+ return;
344
+ }
345
+
346
+ // Wait for callback by pumping CFRunLoop
347
+ // This allows the event loop to process our callback
348
+ CFTimeInterval timeout = 0.001; // 1ms per iteration
349
+
350
+ while (true) {
351
+ {
352
+ std::unique_lock<std::mutex> lock(completionMutex);
353
+ if (isComplete) {
354
+ break;
355
+ }
356
+ }
357
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
358
+ }
359
+ // Data cleaned up in callback
360
+ }
361
+
362
+ // Return value (if any) has been set on the invocation
363
+ }
364
+
365
+ // Deallocation implementation
366
+ void DeallocImplementation(id self, SEL _cmd) {
367
+ @autoreleasepool {
368
+ // Remove the implementation from the global map
369
+ std::lock_guard<std::mutex> lock(g_implementations_mutex);
370
+ void *ptr = (__bridge void *)self;
371
+ auto it = g_implementations.find(ptr);
372
+ if (it != g_implementations.end()) {
373
+ // Release all ThreadSafeFunctions and JS callbacks
374
+ // Do this carefully to avoid issues during shutdown
375
+ try {
376
+ for (auto &pair : it->second.callbacks) {
377
+ // Release the ThreadSafeFunction
378
+ pair.second.Release();
379
+ }
380
+ it->second.callbacks.clear();
381
+ it->second.jsCallbacks.clear();
382
+ it->second.typeEncodings.clear();
383
+ } catch (...) {
384
+ // Ignore errors during cleanup
385
+ NSLog(@"Warning: Exception during callback cleanup for instance %p",
386
+ self);
387
+ }
388
+ g_implementations.erase(it);
389
+ }
390
+ }
391
+
392
+ // Call the superclass dealloc
393
+ // Note: Under ARC, we don't need to manually call [super dealloc]
394
+ // The runtime handles this automatically
395
+ }
@@ -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
-