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 +2 -1
- package/build/Release/nobjc_native.node +0 -0
- package/package.json +1 -1
- package/src/native/bridge.h +4 -108
- package/src/native/method-forwarding.h +38 -0
- package/src/native/method-forwarding.mm +416 -0
- package/src/native/protocol-impl.h +6 -53
- package/src/native/protocol-impl.mm +38 -606
- package/src/native/protocol-storage.h +82 -0
- package/src/native/type-conversion.h +612 -0
package/binding.gyp
CHANGED
|
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.
|
|
42
|
+
"version": "0.0.13",
|
|
43
43
|
"description": "Objective-C bridge for Node.js",
|
|
44
44
|
"main": "dist/index.js",
|
|
45
45
|
"dependencies": {
|
package/src/native/bridge.h
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|