objc-js 0.0.12 → 0.0.14
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/README.md +56 -1
- package/binding.gyp +7 -2
- package/build/Release/nobjc_native.node +0 -0
- package/dist/index.d.ts +116 -1
- package/dist/index.js +117 -2
- package/dist/native.d.ts +2 -2
- package/dist/native.js +2 -2
- package/package.json +1 -1
- package/src/native/debug.h +19 -0
- package/src/native/ffi-utils.h +342 -0
- package/src/native/method-forwarding.h +5 -0
- package/src/native/method-forwarding.mm +110 -58
- package/src/native/nobjc.mm +3 -0
- package/src/native/protocol-impl.mm +26 -7
- package/src/native/protocol-storage.h +61 -0
- package/src/native/subclass-impl.h +20 -0
- package/src/native/subclass-impl.mm +1001 -0
- package/src/native/type-conversion.h +6 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "method-forwarding.h"
|
|
2
|
+
#include "debug.h"
|
|
2
3
|
#include "ObjcObject.h"
|
|
3
4
|
#include "protocol-storage.h"
|
|
4
5
|
#include "type-conversion.h"
|
|
@@ -10,16 +11,20 @@
|
|
|
10
11
|
// MARK: - ThreadSafeFunction Callback Handler
|
|
11
12
|
|
|
12
13
|
// This function runs on the JavaScript thread
|
|
14
|
+
// Handles both protocol implementation and subclass method forwarding
|
|
13
15
|
void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
14
16
|
InvocationData *data) {
|
|
15
17
|
if (!data) {
|
|
16
|
-
|
|
18
|
+
NOBJC_ERROR("InvocationData is null in CallJSCallback");
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
NOBJC_LOG("CallJSCallback: Called for selector %s, callbackType=%d",
|
|
23
|
+
data->selectorName.c_str(), (int)data->callbackType);
|
|
24
|
+
|
|
20
25
|
// Check if the callback is valid before proceeding
|
|
21
26
|
if (jsCallback.IsEmpty()) {
|
|
22
|
-
|
|
27
|
+
NOBJC_ERROR("jsCallback is null/empty in CallJSCallback for selector %s",
|
|
23
28
|
data->selectorName.c_str());
|
|
24
29
|
if (data->invocation) {
|
|
25
30
|
[data->invocation release];
|
|
@@ -31,16 +36,18 @@ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
|
31
36
|
|
|
32
37
|
NSInvocation *invocation = data->invocation;
|
|
33
38
|
if (!invocation) {
|
|
34
|
-
|
|
39
|
+
NOBJC_ERROR("NSInvocation is null in CallJSCallback");
|
|
35
40
|
SignalInvocationComplete(data);
|
|
36
41
|
delete data;
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
NOBJC_LOG("CallJSCallback: About to get method signature");
|
|
46
|
+
|
|
40
47
|
// Extract arguments using NSInvocation
|
|
41
48
|
NSMethodSignature *sig = [invocation methodSignature];
|
|
42
49
|
if (!sig) {
|
|
43
|
-
|
|
50
|
+
NOBJC_ERROR("Failed to get method signature for selector %s",
|
|
44
51
|
data->selectorName.c_str());
|
|
45
52
|
SignalInvocationComplete(data);
|
|
46
53
|
[invocation release];
|
|
@@ -48,42 +55,74 @@ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
|
48
55
|
return;
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
NOBJC_LOG("CallJSCallback: Method signature: %s, numArgs: %lu",
|
|
59
|
+
[sig description].UTF8String, (unsigned long)[sig numberOfArguments]);
|
|
60
|
+
|
|
51
61
|
std::vector<napi_value> jsArgs;
|
|
52
62
|
|
|
53
|
-
//
|
|
63
|
+
// For subclass methods, include 'self' as first JavaScript argument
|
|
64
|
+
if (data->callbackType == CallbackType::Subclass) {
|
|
65
|
+
NOBJC_LOG("CallJSCallback: Extracting 'self' argument for subclass method");
|
|
66
|
+
__unsafe_unretained id selfObj;
|
|
67
|
+
[invocation getArgument:&selfObj atIndex:0];
|
|
68
|
+
NOBJC_LOG("CallJSCallback: About to create ObjcObject for self=%p", selfObj);
|
|
69
|
+
Napi::Value selfValue = ObjcObject::NewInstance(env, selfObj);
|
|
70
|
+
NOBJC_LOG("CallJSCallback: Created ObjcObject for self (JS wrapper created)");
|
|
71
|
+
jsArgs.push_back(selfValue);
|
|
72
|
+
NOBJC_LOG("CallJSCallback: Added self to jsArgs");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Extract remaining arguments (skip self and _cmd, start at index 2)
|
|
76
|
+
NOBJC_LOG("CallJSCallback: Extracting %lu method arguments", (unsigned long)[sig numberOfArguments] - 2);
|
|
54
77
|
for (NSUInteger i = 2; i < [sig numberOfArguments]; i++) {
|
|
55
78
|
const char *type = [sig getArgumentTypeAtIndex:i];
|
|
56
79
|
SimplifiedTypeEncoding argType(type);
|
|
80
|
+
|
|
81
|
+
NOBJC_LOG("CallJSCallback: Processing arg %lu, type=%s, simplified=%s",
|
|
82
|
+
(unsigned long)i, type, argType.c_str());
|
|
83
|
+
|
|
84
|
+
// Handle out-parameters (e.g., NSError**) by passing null
|
|
85
|
+
// This avoids creating N-API Function objects which triggers Bun crashes
|
|
86
|
+
if (argType[0] == '^' && argType[1] == '@') {
|
|
87
|
+
NOBJC_LOG("CallJSCallback: Arg %lu is out-param (^@), passing null", (unsigned long)i);
|
|
88
|
+
jsArgs.push_back(env.Null());
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
NOBJC_LOG("CallJSCallback: About to extract arg %lu to JS", (unsigned long)i);
|
|
57
93
|
jsArgs.push_back(ExtractInvocationArgumentToJS(env, invocation, i, argType[0]));
|
|
94
|
+
NOBJC_LOG("CallJSCallback: Successfully extracted arg %lu", (unsigned long)i);
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
// Call the JavaScript callback
|
|
61
98
|
try {
|
|
99
|
+
NOBJC_LOG("CallJSCallback: About to call JS function with %zu args", jsArgs.size());
|
|
62
100
|
Napi::Value result = jsCallback.Call(jsArgs);
|
|
101
|
+
NOBJC_LOG("CallJSCallback: JS function returned successfully");
|
|
63
102
|
|
|
64
103
|
// Handle return value if the method expects one
|
|
65
104
|
const char *returnType = [sig methodReturnType];
|
|
66
105
|
SimplifiedTypeEncoding retType(returnType);
|
|
67
106
|
|
|
68
107
|
if (retType[0] != 'v') { // Not void
|
|
69
|
-
NSLog(@"Setting return value for %s, return type: %c",
|
|
70
|
-
data->selectorName.c_str(), retType[0]);
|
|
71
108
|
SetInvocationReturnFromJS(invocation, result, retType[0],
|
|
72
109
|
data->selectorName.c_str());
|
|
73
110
|
}
|
|
74
111
|
} catch (const Napi::Error &e) {
|
|
75
|
-
|
|
112
|
+
NOBJC_ERROR("Error calling JavaScript callback for %s: %s",
|
|
76
113
|
data->selectorName.c_str(), e.what());
|
|
77
114
|
} catch (const std::exception &e) {
|
|
78
|
-
|
|
115
|
+
NOBJC_ERROR("Exception calling JavaScript callback for %s: %s",
|
|
79
116
|
data->selectorName.c_str(), e.what());
|
|
80
117
|
} catch (...) {
|
|
81
|
-
|
|
118
|
+
NOBJC_ERROR("Unknown error calling JavaScript callback for %s",
|
|
82
119
|
data->selectorName.c_str());
|
|
83
120
|
}
|
|
84
121
|
|
|
85
122
|
// Signal completion to the waiting ForwardInvocation
|
|
123
|
+
NOBJC_LOG("CallJSCallback: About to signal completion for %s", data->selectorName.c_str());
|
|
86
124
|
SignalInvocationComplete(data);
|
|
125
|
+
NOBJC_LOG("CallJSCallback: Signaled completion for %s", data->selectorName.c_str());
|
|
87
126
|
|
|
88
127
|
// Clean up the invocation data
|
|
89
128
|
// Release the invocation that we retained in ForwardInvocation
|
|
@@ -91,6 +130,46 @@ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
|
91
130
|
delete data;
|
|
92
131
|
}
|
|
93
132
|
|
|
133
|
+
// MARK: - Fallback Helper
|
|
134
|
+
|
|
135
|
+
// Helper function to fallback to ThreadSafeFunction when direct call fails
|
|
136
|
+
// This is used when direct JS callback invocation fails (e.g., in Electron)
|
|
137
|
+
bool FallbackToTSFN(Napi::ThreadSafeFunction &tsfn, InvocationData *data,
|
|
138
|
+
const std::string &selectorName) {
|
|
139
|
+
// Set up synchronization primitives
|
|
140
|
+
std::mutex completionMutex;
|
|
141
|
+
std::condition_variable completionCv;
|
|
142
|
+
bool isComplete = false;
|
|
143
|
+
|
|
144
|
+
data->completionMutex = &completionMutex;
|
|
145
|
+
data->completionCv = &completionCv;
|
|
146
|
+
data->isComplete = &isComplete;
|
|
147
|
+
|
|
148
|
+
// Call via ThreadSafeFunction
|
|
149
|
+
napi_status status = tsfn.NonBlockingCall(data, CallJSCallback);
|
|
150
|
+
tsfn.Release();
|
|
151
|
+
|
|
152
|
+
if (status != napi_ok) {
|
|
153
|
+
NOBJC_ERROR("FallbackToTSFN failed for selector %s (status: %d)",
|
|
154
|
+
selectorName.c_str(), status);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Wait for callback by pumping CFRunLoop
|
|
159
|
+
CFTimeInterval timeout = 0.001; // 1ms per iteration
|
|
160
|
+
while (true) {
|
|
161
|
+
{
|
|
162
|
+
std::unique_lock<std::mutex> lock(completionMutex);
|
|
163
|
+
if (isComplete) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
94
173
|
// MARK: - Message Forwarding Implementation
|
|
95
174
|
|
|
96
175
|
// Override respondsToSelector to return YES for methods we implement
|
|
@@ -139,7 +218,7 @@ NSMethodSignature *MethodSignatureForSelector(id self, SEL _cmd, SEL selector) {
|
|
|
139
218
|
// Handle forwarded invocations
|
|
140
219
|
void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
141
220
|
if (!invocation) {
|
|
142
|
-
|
|
221
|
+
NOBJC_ERROR("ForwardInvocation called with nil invocation");
|
|
143
222
|
return;
|
|
144
223
|
}
|
|
145
224
|
|
|
@@ -151,7 +230,7 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
151
230
|
SEL selector = [invocation selector];
|
|
152
231
|
NSString *selectorString = NSStringFromSelector(selector);
|
|
153
232
|
if (!selectorString) {
|
|
154
|
-
|
|
233
|
+
NOBJC_ERROR("Failed to convert selector to string");
|
|
155
234
|
return;
|
|
156
235
|
}
|
|
157
236
|
|
|
@@ -165,19 +244,18 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
165
244
|
Napi::ThreadSafeFunction tsfn;
|
|
166
245
|
std::string typeEncoding;
|
|
167
246
|
pthread_t js_thread;
|
|
247
|
+
bool isElectron;
|
|
168
248
|
{
|
|
169
249
|
std::lock_guard<std::mutex> lock(g_implementations_mutex);
|
|
170
250
|
auto it = g_implementations.find(ptr);
|
|
171
251
|
if (it == g_implementations.end()) {
|
|
172
|
-
|
|
173
|
-
self);
|
|
252
|
+
NOBJC_WARN("Protocol implementation not found for instance %p", self);
|
|
174
253
|
return;
|
|
175
254
|
}
|
|
176
255
|
|
|
177
256
|
auto callbackIt = it->second.callbacks.find(selectorName);
|
|
178
257
|
if (callbackIt == it->second.callbacks.end()) {
|
|
179
|
-
|
|
180
|
-
selectorName.c_str());
|
|
258
|
+
NOBJC_WARN("Callback not found for selector %s", selectorName.c_str());
|
|
181
259
|
return;
|
|
182
260
|
}
|
|
183
261
|
|
|
@@ -188,7 +266,7 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
188
266
|
tsfn = callbackIt->second;
|
|
189
267
|
napi_status acq_status = tsfn.Acquire();
|
|
190
268
|
if (acq_status != napi_ok) {
|
|
191
|
-
|
|
269
|
+
NOBJC_WARN("Failed to acquire ThreadSafeFunction for selector %s",
|
|
192
270
|
selectorName.c_str());
|
|
193
271
|
return;
|
|
194
272
|
}
|
|
@@ -201,6 +279,7 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
201
279
|
|
|
202
280
|
// Get the JS thread ID to check if we're on the same thread
|
|
203
281
|
js_thread = it->second.js_thread;
|
|
282
|
+
isElectron = it->second.isElectron;
|
|
204
283
|
}
|
|
205
284
|
|
|
206
285
|
// Check if we're on the JS thread
|
|
@@ -208,17 +287,20 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
208
287
|
|
|
209
288
|
// IMPORTANT: We call directly on the JS thread so return values are set
|
|
210
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.
|
|
211
292
|
|
|
212
293
|
// Create invocation data
|
|
213
294
|
auto data = new InvocationData();
|
|
214
295
|
data->invocation = invocation;
|
|
215
296
|
data->selectorName = selectorName;
|
|
216
297
|
data->typeEncoding = typeEncoding;
|
|
298
|
+
data->callbackType = CallbackType::Protocol;
|
|
217
299
|
|
|
218
300
|
napi_status status;
|
|
219
301
|
|
|
220
|
-
if (is_js_thread) {
|
|
221
|
-
// We're on the JS thread
|
|
302
|
+
if (is_js_thread && !isElectron) {
|
|
303
|
+
// We're on the JS thread in Node/Bun (NOT Electron)
|
|
222
304
|
// Call directly to ensure return values are set synchronously.
|
|
223
305
|
data->completionMutex = nullptr;
|
|
224
306
|
data->completionCv = nullptr;
|
|
@@ -232,9 +314,7 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
232
314
|
std::lock_guard<std::mutex> lock(g_implementations_mutex);
|
|
233
315
|
auto it = g_implementations.find(ptr);
|
|
234
316
|
if (it == g_implementations.end()) {
|
|
235
|
-
|
|
236
|
-
@"(JS thread path)",
|
|
237
|
-
self);
|
|
317
|
+
NOBJC_WARN("Protocol implementation not found for instance %p (JS thread path)", self);
|
|
238
318
|
[invocation release];
|
|
239
319
|
delete data;
|
|
240
320
|
return;
|
|
@@ -242,8 +322,7 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
242
322
|
|
|
243
323
|
auto jsCallbackIt = it->second.jsCallbacks.find(selectorName);
|
|
244
324
|
if (jsCallbackIt == it->second.jsCallbacks.end()) {
|
|
245
|
-
|
|
246
|
-
@"(JS thread path)",
|
|
325
|
+
NOBJC_WARN("JS callback not found for selector %s (JS thread path)",
|
|
247
326
|
selectorName.c_str());
|
|
248
327
|
[invocation release];
|
|
249
328
|
delete data;
|
|
@@ -267,14 +346,11 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
267
346
|
CallJSCallback(callEnv, jsFn, data);
|
|
268
347
|
// CallJSCallback releases invocation and deletes data.
|
|
269
348
|
} catch (const std::exception &e) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
e.what());
|
|
273
|
-
NSLog(@"Falling back to ThreadSafeFunction for selector %s",
|
|
274
|
-
selectorName.c_str());
|
|
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());
|
|
275
351
|
|
|
276
352
|
// Fallback to TSFN if direct call fails (e.g., invalid env in Electron)
|
|
277
|
-
// We need to re-acquire the TSFN
|
|
353
|
+
// We need to re-acquire the TSFN
|
|
278
354
|
{
|
|
279
355
|
std::lock_guard<std::mutex> lock(g_implementations_mutex);
|
|
280
356
|
auto it = g_implementations.find(ptr);
|
|
@@ -284,30 +360,8 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
284
360
|
tsfn = callbackIt->second;
|
|
285
361
|
napi_status acq_status = tsfn.Acquire();
|
|
286
362
|
if (acq_status == napi_ok) {
|
|
287
|
-
//
|
|
288
|
-
|
|
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
|
-
}
|
|
363
|
+
// Use helper function for fallback
|
|
364
|
+
if (FallbackToTSFN(tsfn, data, selectorName)) {
|
|
311
365
|
return; // Data cleaned up in callback
|
|
312
366
|
}
|
|
313
367
|
}
|
|
@@ -335,8 +389,7 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
335
389
|
tsfn.Release();
|
|
336
390
|
|
|
337
391
|
if (status != napi_ok) {
|
|
338
|
-
|
|
339
|
-
@"(status: %d)",
|
|
392
|
+
NOBJC_ERROR("Failed to call ThreadSafeFunction for selector %s (status: %d)",
|
|
340
393
|
selectorName.c_str(), status);
|
|
341
394
|
[invocation release];
|
|
342
395
|
delete data;
|
|
@@ -382,8 +435,7 @@ void DeallocImplementation(id self, SEL _cmd) {
|
|
|
382
435
|
it->second.typeEncodings.clear();
|
|
383
436
|
} catch (...) {
|
|
384
437
|
// Ignore errors during cleanup
|
|
385
|
-
|
|
386
|
-
self);
|
|
438
|
+
NOBJC_WARN("Exception during callback cleanup for instance %p", self);
|
|
387
439
|
}
|
|
388
440
|
g_implementations.erase(it);
|
|
389
441
|
}
|
package/src/native/nobjc.mm
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "ObjcObject.h"
|
|
2
2
|
#include "protocol-impl.h"
|
|
3
|
+
#include "subclass-impl.h"
|
|
3
4
|
#include <Foundation/Foundation.h>
|
|
4
5
|
#include <dlfcn.h>
|
|
5
6
|
#include <napi.h>
|
|
@@ -107,6 +108,8 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
|
|
|
107
108
|
exports.Set("FromPointer", Napi::Function::New(env, FromPointer));
|
|
108
109
|
exports.Set("CreateProtocolImplementation",
|
|
109
110
|
Napi::Function::New(env, CreateProtocolImplementation));
|
|
111
|
+
exports.Set("DefineClass", Napi::Function::New(env, DefineClass));
|
|
112
|
+
exports.Set("CallSuper", Napi::Function::New(env, CallSuper));
|
|
110
113
|
return exports;
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "protocol-impl.h"
|
|
2
|
+
#include "debug.h"
|
|
2
3
|
#include "method-forwarding.h"
|
|
3
4
|
#include "ObjcObject.h"
|
|
4
5
|
#include "protocol-storage.h"
|
|
@@ -103,8 +104,7 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
103
104
|
protocol = objc_getProtocol(protocolName.c_str());
|
|
104
105
|
if (protocol == nullptr) {
|
|
105
106
|
// Log warning but continue (for informal protocols)
|
|
106
|
-
|
|
107
|
-
@"conformance",
|
|
107
|
+
NOBJC_WARN("Protocol %s not found, creating class without protocol conformance",
|
|
108
108
|
protocolName.c_str());
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -130,13 +130,33 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
130
130
|
// Get the method implementations object's property names
|
|
131
131
|
Napi::Array propertyNames = methodImplementations.GetPropertyNames();
|
|
132
132
|
|
|
133
|
+
// Detect if we're running in Electron by checking process.versions.electron
|
|
134
|
+
bool isElectron = false;
|
|
135
|
+
try {
|
|
136
|
+
Napi::Object global = env.Global();
|
|
137
|
+
if (global.Has("process")) {
|
|
138
|
+
Napi::Object process = global.Get("process").As<Napi::Object>();
|
|
139
|
+
if (process.Has("versions")) {
|
|
140
|
+
Napi::Object versions = process.Get("versions").As<Napi::Object>();
|
|
141
|
+
isElectron = versions.Has("electron");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (...) {
|
|
145
|
+
// If detection fails, assume not Electron
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isElectron) {
|
|
149
|
+
// Electron runtime detected, will always use TSFN path
|
|
150
|
+
}
|
|
151
|
+
|
|
133
152
|
// Store callbacks for this instance (we'll set the instance pointer later)
|
|
134
153
|
ProtocolImplementation impl{
|
|
135
154
|
.callbacks = {},
|
|
136
155
|
.typeEncodings = {},
|
|
137
156
|
.className = className,
|
|
138
157
|
.env = env,
|
|
139
|
-
.js_thread = pthread_self() // Store the current (JS) thread ID
|
|
158
|
+
.js_thread = pthread_self(), // Store the current (JS) thread ID
|
|
159
|
+
.isElectron = isElectron
|
|
140
160
|
};
|
|
141
161
|
|
|
142
162
|
// Store default type encodings to keep them alive
|
|
@@ -153,7 +173,7 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
153
173
|
Napi::Value value = methodImplementations.Get(key);
|
|
154
174
|
|
|
155
175
|
if (!value.IsFunction()) {
|
|
156
|
-
|
|
176
|
+
NOBJC_WARN("Value for selector %s is not a function, skipping",
|
|
157
177
|
selectorName.c_str());
|
|
158
178
|
continue;
|
|
159
179
|
}
|
|
@@ -198,8 +218,7 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
198
218
|
defaultTypeEncodings.push_back(std::move(defaultEncoding));
|
|
199
219
|
typeEncoding = defaultTypeEncodings.back().c_str();
|
|
200
220
|
|
|
201
|
-
|
|
202
|
-
@"%s",
|
|
221
|
+
NOBJC_WARN("No type encoding found for selector %s, using default: %s",
|
|
203
222
|
selectorName.c_str(), typeEncoding);
|
|
204
223
|
}
|
|
205
224
|
|
|
@@ -224,7 +243,7 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
224
243
|
// methods yet
|
|
225
244
|
if (!class_addMethod(newClass, @selector(respondsToSelector:),
|
|
226
245
|
(IMP)RespondsToSelector, "B@::")) {
|
|
227
|
-
|
|
246
|
+
NOBJC_WARN("Failed to add respondsToSelector: method");
|
|
228
247
|
}
|
|
229
248
|
|
|
230
249
|
// Add message forwarding methods to the class
|
|
@@ -13,20 +13,33 @@
|
|
|
13
13
|
@class NSInvocation;
|
|
14
14
|
#else
|
|
15
15
|
typedef struct NSInvocation NSInvocation;
|
|
16
|
+
typedef struct objc_class *Class;
|
|
16
17
|
#endif
|
|
17
18
|
|
|
18
19
|
// MARK: - Data Structures
|
|
19
20
|
|
|
21
|
+
// Callback type for method forwarding
|
|
22
|
+
enum class CallbackType {
|
|
23
|
+
Protocol, // Protocol implementation - args start at index 2
|
|
24
|
+
Subclass // Subclass override - include self at index 0 as first JS arg
|
|
25
|
+
};
|
|
26
|
+
|
|
20
27
|
// Data passed from native thread to JS thread for invocation handling
|
|
21
28
|
struct InvocationData {
|
|
22
29
|
NSInvocation *invocation;
|
|
23
30
|
std::string selectorName;
|
|
24
31
|
std::string typeEncoding;
|
|
32
|
+
// Type of callback (protocol or subclass)
|
|
33
|
+
CallbackType callbackType;
|
|
25
34
|
// Synchronization: we use NonBlockingCall + runloop pumping to avoid
|
|
26
35
|
// deadlocks in Electron while still getting return values
|
|
27
36
|
std::mutex *completionMutex;
|
|
28
37
|
std::condition_variable *completionCv;
|
|
29
38
|
bool *isComplete;
|
|
39
|
+
// For subclass method calls: the instance pointer (for super calls)
|
|
40
|
+
void *instancePtr;
|
|
41
|
+
// For subclass method calls: the superclass for super calls
|
|
42
|
+
void *superClassPtr;
|
|
30
43
|
};
|
|
31
44
|
|
|
32
45
|
// Stores information about a protocol implementation instance
|
|
@@ -43,6 +56,37 @@ struct ProtocolImplementation {
|
|
|
43
56
|
napi_env env;
|
|
44
57
|
// Store the JS thread ID for thread detection
|
|
45
58
|
pthread_t js_thread;
|
|
59
|
+
// Flag to indicate if running in Electron (requires TSFN path always)
|
|
60
|
+
bool isElectron;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// MARK: - Subclass Storage
|
|
64
|
+
|
|
65
|
+
// Information about an overridden method in a subclass
|
|
66
|
+
struct SubclassMethodInfo {
|
|
67
|
+
Napi::ThreadSafeFunction callback;
|
|
68
|
+
Napi::FunctionReference jsCallback;
|
|
69
|
+
std::string typeEncoding;
|
|
70
|
+
std::string selectorName;
|
|
71
|
+
bool isClassMethod;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Stores information about a JS-defined subclass
|
|
75
|
+
struct SubclassImplementation {
|
|
76
|
+
// Class name
|
|
77
|
+
std::string className;
|
|
78
|
+
// The Objective-C Class object
|
|
79
|
+
void *objcClass; // Class
|
|
80
|
+
// The superclass for super calls
|
|
81
|
+
void *superClass; // Class
|
|
82
|
+
// Methods defined by JS (selector -> info)
|
|
83
|
+
std::unordered_map<std::string, SubclassMethodInfo> methods;
|
|
84
|
+
// Store the environment for direct calls
|
|
85
|
+
napi_env env;
|
|
86
|
+
// Store the JS thread ID for thread detection
|
|
87
|
+
pthread_t js_thread;
|
|
88
|
+
// Flag to indicate if running in Electron
|
|
89
|
+
bool isElectron;
|
|
46
90
|
};
|
|
47
91
|
|
|
48
92
|
// MARK: - Global Storage
|
|
@@ -53,6 +97,11 @@ struct ProtocolImplementation {
|
|
|
53
97
|
extern std::unordered_map<void *, ProtocolImplementation> g_implementations;
|
|
54
98
|
extern std::mutex g_implementations_mutex;
|
|
55
99
|
|
|
100
|
+
// Global map: Class pointer -> subclass implementation details
|
|
101
|
+
// This stores information about JS-defined subclasses
|
|
102
|
+
extern std::unordered_map<void *, SubclassImplementation> g_subclasses;
|
|
103
|
+
extern std::mutex g_subclasses_mutex;
|
|
104
|
+
|
|
56
105
|
// MARK: - Storage Access Helpers
|
|
57
106
|
|
|
58
107
|
// Helper to signal completion of an invocation
|
|
@@ -76,5 +125,17 @@ FindImplementation(void *instancePtr) {
|
|
|
76
125
|
return nullptr;
|
|
77
126
|
}
|
|
78
127
|
|
|
128
|
+
// Look up subclass implementation for a Class pointer
|
|
129
|
+
// Returns nullptr if not found
|
|
130
|
+
// Caller must hold g_subclasses_mutex
|
|
131
|
+
inline SubclassImplementation *
|
|
132
|
+
FindSubclassImplementation(void *classPtr) {
|
|
133
|
+
auto it = g_subclasses.find(classPtr);
|
|
134
|
+
if (it != g_subclasses.end()) {
|
|
135
|
+
return &it->second;
|
|
136
|
+
}
|
|
137
|
+
return nullptr;
|
|
138
|
+
}
|
|
139
|
+
|
|
79
140
|
#endif // PROTOCOL_STORAGE_H
|
|
80
141
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#ifndef SUBCLASS_IMPL_H
|
|
2
|
+
#define SUBCLASS_IMPL_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
// Define a new Objective-C class at runtime from JavaScript
|
|
7
|
+
// Arguments: { name: string, superclass: string|Class, protocols?: string[], methods?: {...} }
|
|
8
|
+
// Returns: The new Class object
|
|
9
|
+
Napi::Value DefineClass(const Napi::CallbackInfo &info);
|
|
10
|
+
|
|
11
|
+
// Call the superclass implementation of a method
|
|
12
|
+
// Arguments: self, selectorName, ...args
|
|
13
|
+
// Returns: The result of the super call
|
|
14
|
+
Napi::Value CallSuper(const Napi::CallbackInfo &info);
|
|
15
|
+
|
|
16
|
+
// Callback handler for subclass methods (called from JS thread)
|
|
17
|
+
void CallSubclassJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
18
|
+
struct InvocationData *data);
|
|
19
|
+
|
|
20
|
+
#endif // SUBCLASS_IMPL_H
|