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.
@@ -0,0 +1,1001 @@
1
+ #include "subclass-impl.h"
2
+ #include "bridge.h"
3
+ #include "debug.h"
4
+ #include "ffi-utils.h"
5
+ #include "method-forwarding.h"
6
+ #include "ObjcObject.h"
7
+ #include "protocol-storage.h"
8
+ #include "type-conversion.h"
9
+ #include <Foundation/Foundation.h>
10
+ #include <atomic>
11
+ #include <chrono>
12
+ #include <napi.h>
13
+ #include <objc/message.h>
14
+ #include <objc/runtime.h>
15
+ #include <sstream>
16
+
17
+ // MARK: - objc_msgSendSuper declarations
18
+
19
+ // Declare objc_msgSendSuper2 for super calls
20
+ // Note: On ARM64, there is no separate stret variant - it's handled automatically
21
+ // objc_msgSendSuper2 isn't in the headers but is exported
22
+ extern "C" id objc_msgSendSuper2(struct objc_super *super, SEL op, ...);
23
+
24
+ // MARK: - Global Storage Definition
25
+
26
+ std::unordered_map<void *, SubclassImplementation> g_subclasses;
27
+ std::mutex g_subclasses_mutex;
28
+
29
+ // MARK: - Forward Declarations for Method Forwarding
30
+
31
+ static BOOL SubclassRespondsToSelector(id self, SEL _cmd, SEL selector);
32
+ static NSMethodSignature *SubclassMethodSignatureForSelector(id self, SEL _cmd,
33
+ SEL selector);
34
+ static void SubclassForwardInvocation(id self, SEL _cmd,
35
+ NSInvocation *invocation);
36
+ static void SubclassDeallocImplementation(id self, SEL _cmd);
37
+
38
+ // MARK: - Subclass Method Forwarding Implementation
39
+
40
+ static BOOL SubclassRespondsToSelector(id self, SEL _cmd, SEL selector) {
41
+ Class cls = object_getClass(self);
42
+ void *clsPtr = (__bridge void *)cls;
43
+
44
+ {
45
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
46
+ auto it = g_subclasses.find(clsPtr);
47
+ if (it != g_subclasses.end()) {
48
+ NSString *selectorString = NSStringFromSelector(selector);
49
+ if (selectorString != nil) {
50
+ std::string selName = [selectorString UTF8String];
51
+ auto methodIt = it->second.methods.find(selName);
52
+ if (methodIt != it->second.methods.end()) {
53
+ return YES;
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ // Check superclass
60
+ Class superClass = class_getSuperclass(cls);
61
+ if (superClass != nil) {
62
+ return [superClass instancesRespondToSelector:selector];
63
+ }
64
+ return NO;
65
+ }
66
+
67
+ static NSMethodSignature *SubclassMethodSignatureForSelector(id self, SEL _cmd,
68
+ SEL selector) {
69
+ Class cls = object_getClass(self);
70
+ void *clsPtr = (__bridge void *)cls;
71
+
72
+ {
73
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
74
+ auto it = g_subclasses.find(clsPtr);
75
+ if (it != g_subclasses.end()) {
76
+ NSString *selectorString = NSStringFromSelector(selector);
77
+ std::string selName = [selectorString UTF8String];
78
+ auto methodIt = it->second.methods.find(selName);
79
+ if (methodIt != it->second.methods.end()) {
80
+ return [NSMethodSignature
81
+ signatureWithObjCTypes:methodIt->second.typeEncoding.c_str()];
82
+ }
83
+ }
84
+ }
85
+
86
+ // Fall back to superclass
87
+ Class superClass = class_getSuperclass(cls);
88
+ if (superClass != nil) {
89
+ return [superClass instanceMethodSignatureForSelector:selector];
90
+ }
91
+ return nil;
92
+ }
93
+
94
+ static void SubclassForwardInvocation(id self, SEL _cmd,
95
+ NSInvocation *invocation) {
96
+ if (!invocation) {
97
+ NOBJC_ERROR("SubclassForwardInvocation called with nil invocation");
98
+ return;
99
+ }
100
+
101
+ [invocation retainArguments];
102
+ [invocation retain];
103
+
104
+ SEL selector = [invocation selector];
105
+ NSString *selectorString = NSStringFromSelector(selector);
106
+ if (!selectorString) {
107
+ NOBJC_ERROR("Failed to convert selector to string");
108
+ [invocation release];
109
+ return;
110
+ }
111
+
112
+ std::string selectorName = [selectorString UTF8String];
113
+
114
+ NOBJC_LOG("SubclassForwardInvocation: Called for selector %s", selectorName.c_str());
115
+
116
+ Class cls = object_getClass(self);
117
+ void *clsPtr = (__bridge void *)cls;
118
+
119
+ NOBJC_LOG("SubclassForwardInvocation: Class=%s, clsPtr=%p", class_getName(cls), clsPtr);
120
+
121
+ Napi::ThreadSafeFunction tsfn;
122
+ std::string typeEncoding;
123
+ pthread_t js_thread;
124
+ void *superClassPtr = nullptr;
125
+
126
+ {
127
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
128
+ auto it = g_subclasses.find(clsPtr);
129
+ if (it == g_subclasses.end()) {
130
+ NOBJC_WARN("Subclass implementation not found for class %p", cls);
131
+ [invocation release];
132
+ return;
133
+ }
134
+
135
+ auto methodIt = it->second.methods.find(selectorName);
136
+ if (methodIt == it->second.methods.end()) {
137
+ NOBJC_WARN("Method not found for selector %s", selectorName.c_str());
138
+ [invocation release];
139
+ return;
140
+ }
141
+
142
+ tsfn = methodIt->second.callback;
143
+ napi_status acq_status = tsfn.Acquire();
144
+ if (acq_status != napi_ok) {
145
+ NOBJC_WARN("Failed to acquire ThreadSafeFunction for selector %s",
146
+ selectorName.c_str());
147
+ [invocation release];
148
+ return;
149
+ }
150
+
151
+ typeEncoding = methodIt->second.typeEncoding;
152
+ js_thread = it->second.js_thread;
153
+ superClassPtr = it->second.superClass;
154
+ }
155
+
156
+ bool is_js_thread = pthread_equal(pthread_self(), js_thread);
157
+
158
+ auto data = new InvocationData();
159
+ data->invocation = invocation;
160
+ data->selectorName = selectorName;
161
+ data->typeEncoding = typeEncoding;
162
+ data->instancePtr = (__bridge void *)self;
163
+ data->superClassPtr = superClassPtr;
164
+ data->callbackType = CallbackType::Subclass; // Set callback type
165
+
166
+ napi_status status;
167
+
168
+ // IMPORTANT: Always try direct call when on JS thread (including Electron)
169
+ // The direct call will catch exceptions and fall back to TSFN if needed
170
+ // Only use TSFN+runloop when on a DIFFERENT thread
171
+ if (is_js_thread) {
172
+ // Direct call on JS thread (Node/Bun/Electron)
173
+ NOBJC_LOG("SubclassForwardInvocation: Using direct call path (JS thread)");
174
+ data->completionMutex = nullptr;
175
+ data->completionCv = nullptr;
176
+ data->isComplete = nullptr;
177
+
178
+ tsfn.Release();
179
+
180
+ napi_env stored_env;
181
+ {
182
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
183
+ auto it = g_subclasses.find(clsPtr);
184
+ if (it == g_subclasses.end()) {
185
+ NOBJC_WARN("Subclass implementation not found for class %p (JS thread path)", cls);
186
+ [invocation release];
187
+ delete data;
188
+ return;
189
+ }
190
+
191
+ auto methodIt = it->second.methods.find(selectorName);
192
+ if (methodIt == it->second.methods.end()) {
193
+ NOBJC_WARN("Method not found for selector %s (JS thread path)",
194
+ selectorName.c_str());
195
+ [invocation release];
196
+ delete data;
197
+ return;
198
+ }
199
+
200
+ stored_env = it->second.env;
201
+ // Don't get the function value here - do it inside the HandleScope
202
+ }
203
+
204
+ try {
205
+ NOBJC_LOG("SubclassForwardInvocation: About to call JS callback directly");
206
+ Napi::Env callEnv(stored_env);
207
+ Napi::HandleScope scope(callEnv);
208
+
209
+ // Get the JS function within the HandleScope
210
+ Napi::Function jsFn;
211
+ {
212
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
213
+ auto it = g_subclasses.find(clsPtr);
214
+ if (it != g_subclasses.end()) {
215
+ auto methodIt = it->second.methods.find(selectorName);
216
+ if (methodIt != it->second.methods.end()) {
217
+ jsFn = methodIt->second.jsCallback.Value();
218
+ }
219
+ }
220
+ }
221
+
222
+ CallJSCallback(callEnv, jsFn, data); // Use unified CallJSCallback
223
+ // CallJSCallback releases invocation and deletes data.
224
+ NOBJC_LOG("SubclassForwardInvocation: Direct call succeeded");
225
+ } catch (const std::exception &e) {
226
+ NOBJC_ERROR("Error calling JS callback directly (likely invalid env in Electron): %s",
227
+ e.what());
228
+ NOBJC_LOG("Falling back to ThreadSafeFunction for selector %s",
229
+ selectorName.c_str());
230
+
231
+ // Fallback to TSFN if direct call fails
232
+ {
233
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
234
+ auto it = g_subclasses.find(clsPtr);
235
+ if (it != g_subclasses.end()) {
236
+ auto methodIt = it->second.methods.find(selectorName);
237
+ if (methodIt != it->second.methods.end()) {
238
+ tsfn = methodIt->second.callback;
239
+ napi_status acq_status = tsfn.Acquire();
240
+ if (acq_status == napi_ok) {
241
+ // Use helper function for fallback
242
+ if (FallbackToTSFN(tsfn, data, selectorName)) {
243
+ return; // Data cleaned up in callback
244
+ }
245
+ NOBJC_ERROR("SubclassForwardInvocation: Fallback failed");
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ // If fallback also failed, clean up manually
252
+ [invocation release];
253
+ delete data;
254
+ }
255
+ } else {
256
+ // Cross-thread call via TSFN (NOT on JS thread)
257
+ NOBJC_LOG("SubclassForwardInvocation: Using TSFN+runloop path (different thread)");
258
+ std::mutex completionMutex;
259
+ std::condition_variable completionCv;
260
+ bool isComplete = false;
261
+
262
+ data->completionMutex = &completionMutex;
263
+ data->completionCv = &completionCv;
264
+ data->isComplete = &isComplete;
265
+
266
+ NOBJC_LOG("SubclassForwardInvocation: About to call NonBlockingCall for selector %s",
267
+ selectorName.c_str());
268
+
269
+ status = tsfn.NonBlockingCall(data, CallJSCallback); // Use unified CallJSCallback
270
+ tsfn.Release();
271
+
272
+ NOBJC_LOG("SubclassForwardInvocation: NonBlockingCall returned status=%d", status);
273
+
274
+ if (status != napi_ok) {
275
+ NOBJC_ERROR("Failed to call ThreadSafeFunction for selector %s (status: %d)",
276
+ selectorName.c_str(), status);
277
+ [invocation release];
278
+ delete data;
279
+ return;
280
+ }
281
+
282
+ // Wait for callback via runloop pumping
283
+ CFTimeInterval timeout = 0.001;
284
+ int iterations = 0;
285
+ while (true) {
286
+ {
287
+ std::unique_lock<std::mutex> lock(completionMutex);
288
+ if (isComplete) {
289
+ break;
290
+ }
291
+ }
292
+ iterations++;
293
+ if (iterations % 1000 == 0) {
294
+ NOBJC_LOG("SubclassForwardInvocation: Still waiting... (%d iterations)", iterations);
295
+ }
296
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
297
+ }
298
+ }
299
+ }
300
+
301
+ static void SubclassDeallocImplementation(id self, SEL _cmd) {
302
+ // Nothing special to clean up per-instance for subclasses
303
+ // The class-level storage remains until explicitly disposed
304
+
305
+ // Call super dealloc (handled by ARC, but we need to be careful)
306
+ Class cls = object_getClass(self);
307
+ Class superClass = class_getSuperclass(cls);
308
+ if (superClass) {
309
+ // Under ARC, dealloc is handled automatically
310
+ // We don't need to call [super dealloc]
311
+ }
312
+ }
313
+
314
+ // MARK: - Main DefineClass Implementation
315
+
316
+ Napi::Value DefineClass(const Napi::CallbackInfo &info) {
317
+ Napi::Env env = info.Env();
318
+
319
+ // Validate arguments
320
+ if (info.Length() < 1 || !info[0].IsObject()) {
321
+ throw Napi::TypeError::New(
322
+ env, "Expected an object argument with class definition");
323
+ }
324
+
325
+ Napi::Object definition = info[0].As<Napi::Object>();
326
+
327
+ // Extract class name
328
+ if (!definition.Has("name") || !definition.Get("name").IsString()) {
329
+ throw Napi::TypeError::New(env, "Class definition must have 'name' string");
330
+ }
331
+ std::string className = definition.Get("name").As<Napi::String>().Utf8Value();
332
+
333
+ // Check if class already exists
334
+ if (NSClassFromString([NSString stringWithUTF8String:className.c_str()]) !=
335
+ nil) {
336
+ throw Napi::Error::New(
337
+ env, "Class '" + className + "' already exists in the Objective-C runtime");
338
+ }
339
+
340
+ // Extract superclass
341
+ Class superClass = nil;
342
+ if (!definition.Has("superclass")) {
343
+ throw Napi::TypeError::New(env,
344
+ "Class definition must have 'superclass'");
345
+ }
346
+
347
+ Napi::Value superValue = definition.Get("superclass");
348
+ if (superValue.IsString()) {
349
+ std::string superName = superValue.As<Napi::String>().Utf8Value();
350
+ superClass =
351
+ NSClassFromString([NSString stringWithUTF8String:superName.c_str()]);
352
+ if (superClass == nil) {
353
+ throw Napi::Error::New(env, "Superclass '" + superName + "' not found");
354
+ }
355
+ } else if (superValue.IsObject()) {
356
+ Napi::Object superObj = superValue.As<Napi::Object>();
357
+ if (superObj.InstanceOf(ObjcObject::constructor.Value())) {
358
+ ObjcObject *objcObj = Napi::ObjectWrap<ObjcObject>::Unwrap(superObj);
359
+ superClass = (Class)objcObj->objcObject;
360
+ }
361
+ }
362
+
363
+ if (superClass == nil) {
364
+ throw Napi::TypeError::New(
365
+ env, "'superclass' must be a string or ObjcObject representing a Class");
366
+ }
367
+
368
+ // Detect Electron (bun is commented out as it's not needed)
369
+ bool isElectron = false;
370
+ // bool isBun = false;
371
+ try {
372
+ Napi::Object global = env.Global();
373
+ if (global.Has("process")) {
374
+ Napi::Object process = global.Get("process").As<Napi::Object>();
375
+ if (process.Has("versions")) {
376
+ Napi::Object versions = process.Get("versions").As<Napi::Object>();
377
+ isElectron = versions.Has("electron");
378
+ // isBun = versions.Has("bun");
379
+ }
380
+ }
381
+ } catch (...) {
382
+ }
383
+
384
+ // Allocate the new class
385
+ Class newClass = objc_allocateClassPair(superClass, className.c_str(), 0);
386
+ if (newClass == nil) {
387
+ throw Napi::Error::New(env,
388
+ "Failed to allocate class pair for '" + className + "'");
389
+ }
390
+
391
+ // Create the subclass implementation storage
392
+ SubclassImplementation impl{
393
+ .className = className,
394
+ .objcClass = (__bridge void *)newClass,
395
+ .superClass = (__bridge void *)superClass,
396
+ .methods = {},
397
+ .env = env,
398
+ .js_thread = pthread_self(),
399
+ .isElectron = isElectron, // Only Electron needs TSFN always; Bun works with direct calls
400
+ };
401
+
402
+ // Add protocol conformance
403
+ if (definition.Has("protocols") && definition.Get("protocols").IsArray()) {
404
+ Napi::Array protocols = definition.Get("protocols").As<Napi::Array>();
405
+ for (uint32_t i = 0; i < protocols.Length(); i++) {
406
+ if (protocols.Get(i).IsString()) {
407
+ std::string protoName =
408
+ protocols.Get(i).As<Napi::String>().Utf8Value();
409
+ Protocol *proto = objc_getProtocol(protoName.c_str());
410
+ if (proto != nullptr) {
411
+ class_addProtocol(newClass, proto);
412
+ } else {
413
+ NOBJC_WARN("Protocol %s not found", protoName.c_str());
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ // Process method definitions
420
+ if (definition.Has("methods") && definition.Get("methods").IsObject()) {
421
+ Napi::Object methods = definition.Get("methods").As<Napi::Object>();
422
+ Napi::Array methodNames = methods.GetPropertyNames();
423
+
424
+ for (uint32_t i = 0; i < methodNames.Length(); i++) {
425
+ Napi::Value key = methodNames.Get(i);
426
+ if (!key.IsString())
427
+ continue;
428
+
429
+ std::string selectorName = key.As<Napi::String>().Utf8Value();
430
+ Napi::Value methodDef = methods.Get(key);
431
+
432
+ if (!methodDef.IsObject()) {
433
+ NOBJC_WARN("Method definition for %s is not an object",
434
+ selectorName.c_str());
435
+ continue;
436
+ }
437
+
438
+ Napi::Object methodObj = methodDef.As<Napi::Object>();
439
+
440
+ // Get type encoding
441
+ if (!methodObj.Has("types") || !methodObj.Get("types").IsString()) {
442
+ NOBJC_WARN("Method %s missing 'types' string", selectorName.c_str());
443
+ continue;
444
+ }
445
+ std::string typeEncoding =
446
+ methodObj.Get("types").As<Napi::String>().Utf8Value();
447
+
448
+ // Get implementation function
449
+ if (!methodObj.Has("implementation") ||
450
+ !methodObj.Get("implementation").IsFunction()) {
451
+ NOBJC_WARN("Method %s missing 'implementation' function",
452
+ selectorName.c_str());
453
+ continue;
454
+ }
455
+ Napi::Function jsImpl =
456
+ methodObj.Get("implementation").As<Napi::Function>();
457
+
458
+ SEL selector = sel_registerName(selectorName.c_str());
459
+
460
+ // Create ThreadSafeFunction
461
+ Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
462
+ env, jsImpl, "SubclassMethod_" + selectorName, 0, 1,
463
+ [](Napi::Env) {});
464
+
465
+ // Store method info
466
+ SubclassMethodInfo methodInfo{
467
+ .callback = tsfn,
468
+ .jsCallback = Napi::Persistent(jsImpl),
469
+ .typeEncoding = typeEncoding,
470
+ .selectorName = selectorName,
471
+ .isClassMethod = false,
472
+ };
473
+ impl.methods[selectorName] = std::move(methodInfo);
474
+
475
+ // Add the method with _objc_msgForward as IMP (triggers forwarding)
476
+ // This ensures our forwardInvocation: gets called
477
+ class_addMethod(newClass, selector, (IMP)_objc_msgForward,
478
+ typeEncoding.c_str());
479
+ }
480
+ }
481
+
482
+ // Add message forwarding methods
483
+ class_addMethod(newClass, @selector(respondsToSelector:),
484
+ (IMP)SubclassRespondsToSelector, "B@::");
485
+ class_addMethod(newClass, @selector(methodSignatureForSelector:),
486
+ (IMP)SubclassMethodSignatureForSelector, "@@::");
487
+ class_addMethod(newClass, @selector(forwardInvocation:),
488
+ (IMP)SubclassForwardInvocation, "v@:@");
489
+ class_addMethod(newClass, sel_registerName("dealloc"),
490
+ (IMP)SubclassDeallocImplementation, "v@:");
491
+
492
+ // Register the class
493
+ objc_registerClassPair(newClass);
494
+
495
+ // Store in global map
496
+ void *classPtr = (__bridge void *)newClass;
497
+ {
498
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
499
+ g_subclasses.emplace(classPtr, std::move(impl));
500
+ }
501
+
502
+ // Return the Class object
503
+ return ObjcObject::NewInstance(env, newClass);
504
+ }
505
+
506
+ // MARK: - CallSuper Implementation
507
+
508
+ // New FFI-based implementation to avoid infinite recursion
509
+ static Napi::Value CallSuperWithFFI(
510
+ Napi::Env env,
511
+ id self,
512
+ Class superClass,
513
+ SEL selector,
514
+ NSMethodSignature* methodSig,
515
+ const Napi::CallbackInfo& info,
516
+ size_t argStartIndex) {
517
+
518
+ std::string selectorName = NSStringFromSelector(selector).UTF8String;
519
+ NOBJC_LOG("CallSuperWithFFI: selector=%s, self=%p, superClass=%s",
520
+ selectorName.c_str(), self, class_getName(superClass));
521
+
522
+ // Vector to track allocated FFI types for cleanup
523
+ std::vector<ffi_type*> allocatedTypes;
524
+
525
+ try {
526
+ //1. Prepare objc_super struct
527
+ // Use objc_msgSendSuper (NOT Super2) with the superclass
528
+ // This is the variant that works reliably on all platforms
529
+ struct objc_super superStruct;
530
+ superStruct.receiver = self;
531
+ superStruct.super_class = superClass;
532
+ NOBJC_LOG("CallSuperWithFFI: superStruct is at address %p", &superStruct);
533
+ NOBJC_LOG("CallSuperWithFFI: receiver=%p, calling superclass=%s",
534
+ self, class_getName(superClass));
535
+
536
+ // 2. Determine which objc_msgSend variant to use
537
+ // Use objc_msgSendSuper with the superclass (matches the no-args case)
538
+ const char* returnEncoding = [methodSig methodReturnType];
539
+ SimplifiedTypeEncoding simpleReturnEncoding(returnEncoding);
540
+
541
+ void* msgSendFn = (void*)objc_msgSendSuper;
542
+
543
+ NOBJC_LOG("CallSuperWithFFI: Using objc_msgSendSuper with superclass");
544
+
545
+ // 3. Build FFI type arrays for arguments
546
+ size_t totalArgs = [methodSig numberOfArguments];
547
+ std::vector<ffi_type*> argFFITypes;
548
+
549
+ // First arg: objc_super pointer
550
+ argFFITypes.push_back(&ffi_type_pointer);
551
+ // Second arg: SEL
552
+ argFFITypes.push_back(&ffi_type_pointer);
553
+
554
+ // Remaining args: method arguments (starting from index 2)
555
+ for (size_t i = 2; i < totalArgs; i++) {
556
+ const char* argEncoding = [methodSig getArgumentTypeAtIndex:i];
557
+ ffi_type* argType = GetFFITypeForEncoding(argEncoding, nullptr, allocatedTypes);
558
+ argFFITypes.push_back(argType);
559
+ NOBJC_LOG("CallSuperWithFFI: Arg %zu type encoding: %s", i - 2, argEncoding);
560
+ }
561
+
562
+ // 4. Build return FFI type
563
+ size_t returnSize = 0;
564
+ ffi_type* returnFFIType = GetFFITypeForEncoding(simpleReturnEncoding.c_str(),
565
+ &returnSize, allocatedTypes);
566
+ NOBJC_LOG("CallSuperWithFFI: Return type encoding: %s, size: %zu",
567
+ simpleReturnEncoding.c_str(), returnSize);
568
+
569
+ // 5. Prepare FFI CIF
570
+ ffi_cif cif;
571
+ ffi_status status = ffi_prep_cif(
572
+ &cif,
573
+ FFI_DEFAULT_ABI,
574
+ argFFITypes.size(),
575
+ returnFFIType,
576
+ argFFITypes.data()
577
+ );
578
+
579
+ if (status != FFI_OK) {
580
+ CleanupAllocatedFFITypes(allocatedTypes);
581
+ NOBJC_ERROR("CallSuperWithFFI: ffi_prep_cif failed with status %d", status);
582
+ throw Napi::Error::New(env, "FFI preparation failed");
583
+ }
584
+
585
+ NOBJC_LOG("CallSuperWithFFI: FFI CIF prepared successfully");
586
+
587
+ // 6. Prepare argument value buffers
588
+ std::vector<void*> argValues;
589
+ std::vector<std::unique_ptr<uint8_t[]>> argBuffers;
590
+
591
+ NOBJC_LOG("CallSuperWithFFI: Preparing argument buffers...");
592
+
593
+ // Add objc_super pointer
594
+ // libffi expects argValues[i] to point to the actual argument value
595
+ // Store the pointer in a buffer to ensure it stays valid
596
+ auto superPtrBuffer = std::make_unique<uint8_t[]>(sizeof(objc_super*));
597
+ objc_super* superPtr = &superStruct;
598
+ memcpy(superPtrBuffer.get(), &superPtr, sizeof(objc_super*));
599
+ void* superPtrBufferRawPtr = superPtrBuffer.get(); // Get raw pointer before move
600
+ argBuffers.push_back(std::move(superPtrBuffer)); // Move to keep alive
601
+ argValues.push_back(superPtrBufferRawPtr); // Add raw pointer to argValues
602
+ NOBJC_LOG("CallSuperWithFFI: Added objc_super* buffer at %p (points to %p)",
603
+ superPtrBufferRawPtr, superPtr);
604
+
605
+ // Add selector - also store in buffer
606
+ auto selectorBuffer = std::make_unique<uint8_t[]>(sizeof(SEL));
607
+ memcpy(selectorBuffer.get(), &selector, sizeof(SEL));
608
+ void* selectorBufferRawPtr = selectorBuffer.get(); // Get raw pointer before move
609
+ argBuffers.push_back(std::move(selectorBuffer)); // Move to keep alive
610
+ argValues.push_back(selectorBufferRawPtr); // Add raw pointer to argValues
611
+ NOBJC_LOG("CallSuperWithFFI: Added SEL buffer at %p (value=%p, name=%s)",
612
+ selectorBufferRawPtr, selector, sel_getName(selector));
613
+
614
+ // Add method arguments
615
+ NOBJC_LOG("CallSuperWithFFI: Processing %zu method arguments...", info.Length() - argStartIndex);
616
+ for (size_t i = argStartIndex; i < info.Length(); i++) {
617
+ size_t argIndex = i - argStartIndex + 2; // +2 for self and _cmd
618
+ const char* argEncoding = [methodSig getArgumentTypeAtIndex:argIndex];
619
+ SimplifiedTypeEncoding simpleArgEncoding(argEncoding);
620
+
621
+ NOBJC_LOG("CallSuperWithFFI: Processing JS arg %zu (method arg %zu), encoding=%s",
622
+ i - argStartIndex, argIndex, argEncoding);
623
+
624
+ // Handle special case for ^@ (out-params like NSError**)
625
+ if (simpleArgEncoding[0] == '^' && simpleArgEncoding[1] == '@') {
626
+ NOBJC_LOG("CallSuperWithFFI: Arg %zu is out-param (^@)", i - argStartIndex);
627
+
628
+ // CRITICAL: For pointer-to-pointer types, we need TWO buffers:
629
+ // 1. The actual storage location for the id (initialized to nil)
630
+ // 2. A pointer to that storage (what we pass to the function)
631
+
632
+ // Buffer 1: Storage for the id* (initialized to nil)
633
+ auto errorStorage = std::make_unique<uint8_t[]>(sizeof(id));
634
+ id nullObj = nil;
635
+ memcpy(errorStorage.get(), &nullObj, sizeof(id));
636
+ void* errorStoragePtr = errorStorage.get();
637
+
638
+ NOBJC_LOG("CallSuperWithFFI: Allocated error storage at %p", errorStoragePtr);
639
+ NOBJC_LOG("CallSuperWithFFI: Error storage contains: %p", *(id*)errorStoragePtr);
640
+
641
+ // Buffer 2: Storage for the pointer to errorStorage (this is what argValues needs)
642
+ auto pointerBuffer = std::make_unique<uint8_t[]>(sizeof(void*));
643
+ memcpy(pointerBuffer.get(), &errorStoragePtr, sizeof(void*));
644
+ void* pointerBufferPtr = pointerBuffer.get();
645
+
646
+ NOBJC_LOG("CallSuperWithFFI: Allocated pointer buffer at %p", pointerBufferPtr);
647
+ NOBJC_LOG("CallSuperWithFFI: Pointer buffer contains: %p (address of error storage)",
648
+ *(void**)pointerBufferPtr);
649
+ NOBJC_LOG("CallSuperWithFFI: This address will be passed to the method");
650
+
651
+ // CRITICAL: argValues must point to pointerBuffer, not errorStorage
652
+ // libffi will dereference this to get the address to pass
653
+ argValues.push_back(pointerBufferPtr);
654
+ argBuffers.push_back(std::move(errorStorage));
655
+ argBuffers.push_back(std::move(pointerBuffer));
656
+ continue;
657
+ }
658
+
659
+ // Calculate size for this argument
660
+ size_t argSize = GetSizeForTypeEncoding(simpleArgEncoding[0]);
661
+ if (argSize == 0) {
662
+ // For complex types, use NSGetSizeAndAlignment
663
+ NSUInteger size, alignment;
664
+ NSGetSizeAndAlignment(argEncoding, &size, &alignment);
665
+ argSize = size;
666
+ NOBJC_LOG("CallSuperWithFFI: Complex type, size from NSGetSizeAndAlignment: %zu", argSize);
667
+ }
668
+
669
+ // Allocate buffer
670
+ NOBJC_LOG("CallSuperWithFFI: Allocating buffer of %zu bytes for arg %zu", argSize, i - argStartIndex);
671
+ auto buffer = std::make_unique<uint8_t[]>(argSize);
672
+ memset(buffer.get(), 0, argSize);
673
+ void* bufferPtr = buffer.get();
674
+ NOBJC_LOG("CallSuperWithFFI: Buffer allocated at %p", bufferPtr);
675
+
676
+ // Extract JS argument to buffer
677
+ ObjcArgumentContext context = {
678
+ .className = std::string(class_getName(superClass)),
679
+ .selectorName = selectorName,
680
+ .argumentIndex = (int)(i - argStartIndex),
681
+ };
682
+
683
+ try {
684
+ NOBJC_LOG("CallSuperWithFFI: Calling ExtractJSArgumentToBuffer...");
685
+ ExtractJSArgumentToBuffer(env, info[i], argEncoding, bufferPtr, context);
686
+ NOBJC_LOG("CallSuperWithFFI: ExtractJSArgumentToBuffer succeeded");
687
+ } catch (const std::exception& e) {
688
+ CleanupAllocatedFFITypes(allocatedTypes);
689
+ NOBJC_ERROR("CallSuperWithFFI: Failed to extract argument %zu: %s", i - argStartIndex, e.what());
690
+ throw;
691
+ }
692
+
693
+ NOBJC_LOG("CallSuperWithFFI: Extracted argument %zu (size: %zu)", i - argStartIndex, argSize);
694
+
695
+ // For object types, log the actual pointer value
696
+ if (simpleArgEncoding[0] == '@') {
697
+ [[maybe_unused]] id* objPtr = (id*)bufferPtr;
698
+ NOBJC_LOG("CallSuperWithFFI: Argument %zu is object: buffer=%p, contains id=%p",
699
+ i - argStartIndex, bufferPtr, *objPtr);
700
+ }
701
+
702
+ NOBJC_LOG("CallSuperWithFFI: Adding buffer %p to argValues (index %zu)", bufferPtr, argValues.size());
703
+ argValues.push_back(bufferPtr);
704
+ argBuffers.push_back(std::move(buffer));
705
+ NOBJC_LOG("CallSuperWithFFI: Buffer moved to argBuffers (now size %zu)", argBuffers.size());
706
+ }
707
+
708
+ NOBJC_LOG("CallSuperWithFFI: Finished preparing %zu argument buffers", argBuffers.size());
709
+
710
+ // 7. Prepare return buffer
711
+ std::unique_ptr<uint8_t[]> returnBuffer;
712
+ if (simpleReturnEncoding[0] != 'v') {
713
+ size_t bufferSize = returnSize > 0 ? returnSize : 16; // Minimum 16 bytes
714
+ returnBuffer = std::make_unique<uint8_t[]>(bufferSize);
715
+ memset(returnBuffer.get(), 0, bufferSize);
716
+ NOBJC_LOG("CallSuperWithFFI: Allocated return buffer of %zu bytes at %p",
717
+ bufferSize, returnBuffer.get());
718
+ } else {
719
+ NOBJC_LOG("CallSuperWithFFI: No return buffer needed (void return)");
720
+ }
721
+
722
+ // 8. Make the FFI call
723
+ NOBJC_LOG("CallSuperWithFFI: ========== FFI CALL SETUP ==========");
724
+ NOBJC_LOG("CallSuperWithFFI: Function to call: objc_msgSendSuper at %p", msgSendFn);
725
+ NOBJC_LOG("CallSuperWithFFI: Number of arguments: %zu", argValues.size());
726
+ NOBJC_LOG("CallSuperWithFFI: Arg 0 (objc_super*): argValues[0]=%p",
727
+ argValues[0]);
728
+ // Log what's actually stored in the buffer
729
+ [[maybe_unused]] objc_super** superPtrPtr = (objc_super**)argValues[0];
730
+ NOBJC_LOG("CallSuperWithFFI: Buffer contains pointer: %p", *superPtrPtr);
731
+ NOBJC_LOG("CallSuperWithFFI: objc_super.receiver=%p", superStruct.receiver);
732
+ NOBJC_LOG("CallSuperWithFFI: objc_super.super_class=%p (%s)",
733
+ superStruct.super_class, class_getName(superClass));
734
+ NOBJC_LOG("CallSuperWithFFI: Arg 1 (SEL*): argValues[1]=%p",
735
+ argValues[1]);
736
+ [[maybe_unused]] SEL* selPtr = (SEL*)argValues[1];
737
+ NOBJC_LOG("CallSuperWithFFI: Buffer contains SEL: %p (%s)",
738
+ *selPtr, sel_getName(*selPtr));
739
+
740
+ for (size_t i = 2; i < argValues.size(); i++) {
741
+ const char* argEncoding = [methodSig getArgumentTypeAtIndex:i];
742
+ SimplifiedTypeEncoding simpleArgEncoding(argEncoding);
743
+ NOBJC_LOG("CallSuperWithFFI: Arg %zu: argValues[%zu]=%p, encoding=%s",
744
+ i, i, argValues[i], simpleArgEncoding.c_str());
745
+ if (simpleArgEncoding[0] == '@') {
746
+ [[maybe_unused]] id* objPtrLocation = (id*)argValues[i];
747
+ NOBJC_LOG("CallSuperWithFFI: Object pointer at %p points to id=%p",
748
+ objPtrLocation, *objPtrLocation);
749
+ } else if (simpleArgEncoding[0] == '^') {
750
+ [[maybe_unused]] void** ptrLocation = (void**)argValues[i];
751
+ NOBJC_LOG("CallSuperWithFFI: Pointer at %p contains: %p",
752
+ ptrLocation, *ptrLocation);
753
+ }
754
+ }
755
+
756
+ NOBJC_LOG("CallSuperWithFFI: About to call ffi_call...");
757
+ ffi_call(&cif, FFI_FN(msgSendFn),
758
+ returnBuffer ? returnBuffer.get() : nullptr,
759
+ argValues.data());
760
+ NOBJC_LOG("CallSuperWithFFI: ffi_call completed successfully!");
761
+
762
+ // 9. Convert return value
763
+ Napi::Value result;
764
+ if (simpleReturnEncoding[0] == 'v') {
765
+ result = env.Undefined();
766
+ } else {
767
+ result = ConvertFFIReturnToJS(env, returnBuffer.get(), simpleReturnEncoding.c_str());
768
+ }
769
+
770
+ // 10. Cleanup
771
+ CleanupAllocatedFFITypes(allocatedTypes);
772
+
773
+ NOBJC_LOG("CallSuperWithFFI: Returning result");
774
+ return result;
775
+
776
+ } catch (const std::exception& e) {
777
+ CleanupAllocatedFFITypes(allocatedTypes);
778
+ NOBJC_ERROR("CallSuperWithFFI: Exception: %s", e.what());
779
+ throw;
780
+ } catch (...) {
781
+ CleanupAllocatedFFITypes(allocatedTypes);
782
+ NOBJC_ERROR("CallSuperWithFFI: Unknown exception");
783
+ throw;
784
+ }
785
+ }
786
+
787
+ Napi::Value CallSuper(const Napi::CallbackInfo &info) {
788
+ Napi::Env env = info.Env();
789
+
790
+ // Args: self, selector, ...args
791
+ if (info.Length() < 2) {
792
+ throw Napi::TypeError::New(
793
+ env, "CallSuper requires at least 2 arguments: self and selector");
794
+ }
795
+
796
+ // Get self
797
+ if (!info[0].IsObject()) {
798
+ throw Napi::TypeError::New(env, "First argument must be an ObjcObject (self)");
799
+ }
800
+ Napi::Object selfObj = info[0].As<Napi::Object>();
801
+ if (!selfObj.InstanceOf(ObjcObject::constructor.Value())) {
802
+ throw Napi::TypeError::New(env, "First argument must be an ObjcObject (self)");
803
+ }
804
+ ObjcObject *selfWrapper = Napi::ObjectWrap<ObjcObject>::Unwrap(selfObj);
805
+ id self = selfWrapper->objcObject;
806
+
807
+ // Get selector
808
+ if (!info[1].IsString()) {
809
+ throw Napi::TypeError::New(env, "Second argument must be a selector string");
810
+ }
811
+ std::string selectorName = info[1].As<Napi::String>().Utf8Value();
812
+ SEL selector = sel_registerName(selectorName.c_str());
813
+
814
+ NOBJC_LOG("CallSuper: selector=%s, self=%p, argCount=%zu",
815
+ selectorName.c_str(), self, info.Length() - 2);
816
+
817
+ // Find the superclass
818
+ Class instanceClass = object_getClass(self);
819
+ Class superClass = nil;
820
+
821
+ NOBJC_LOG("CallSuper: instanceClass=%s", class_getName(instanceClass));
822
+
823
+ {
824
+ std::lock_guard<std::mutex> lock(g_subclasses_mutex);
825
+ // Walk up the class hierarchy to find our subclass implementation
826
+ Class cls = instanceClass;
827
+ while (cls != nil) {
828
+ void *clsPtr = (__bridge void *)cls;
829
+ auto it = g_subclasses.find(clsPtr);
830
+ if (it != g_subclasses.end()) {
831
+ superClass = (__bridge Class)it->second.superClass;
832
+ NOBJC_LOG("CallSuper: Found superclass from registry: %s",
833
+ class_getName(superClass));
834
+ break;
835
+ }
836
+ cls = class_getSuperclass(cls);
837
+ }
838
+ }
839
+
840
+ if (superClass == nil) {
841
+ // Fall back to direct superclass
842
+ superClass = class_getSuperclass(instanceClass);
843
+ NOBJC_LOG("CallSuper: Using direct superclass: %s",
844
+ superClass ? class_getName(superClass) : "nil");
845
+ }
846
+
847
+ if (superClass == nil) {
848
+ NOBJC_ERROR("CallSuper: Could not determine superclass for super call");
849
+ throw Napi::Error::New(env, "Could not determine superclass for super call");
850
+ }
851
+
852
+ // Get method signature from superclass
853
+ NSMethodSignature *methodSig =
854
+ [superClass instanceMethodSignatureForSelector:selector];
855
+ if (methodSig == nil) {
856
+ NOBJC_ERROR("CallSuper: Selector '%s' not found on superclass %s",
857
+ selectorName.c_str(), class_getName(superClass));
858
+ throw Napi::Error::New(
859
+ env, "Selector '" + selectorName + "' not found on superclass");
860
+ }
861
+
862
+ NOBJC_LOG("CallSuper: Method signature: %s", [methodSig description].UTF8String);
863
+
864
+ // Get the super method's IMP directly
865
+ Method superMethod = class_getInstanceMethod(superClass, selector);
866
+ if (superMethod == nil) {
867
+ NOBJC_ERROR("CallSuper: Could not get method implementation for selector '%s'",
868
+ selectorName.c_str());
869
+ throw Napi::Error::New(
870
+ env, "Could not get method implementation for selector '" + selectorName +
871
+ "' from superclass");
872
+ }
873
+
874
+ NOBJC_LOG("CallSuper: Found method implementation at %p",
875
+ method_getImplementation(superMethod));
876
+
877
+ // Validate argument count
878
+ const size_t expectedArgCount = [methodSig numberOfArguments] - 2;
879
+ const size_t providedArgCount = info.Length() - 2;
880
+
881
+ NOBJC_LOG("CallSuper: Expected %zu args, provided %zu args",
882
+ expectedArgCount, providedArgCount);
883
+
884
+ if (providedArgCount != expectedArgCount) {
885
+ NOBJC_ERROR("CallSuper: Argument count mismatch for selector '%s'",
886
+ selectorName.c_str());
887
+ throw Napi::Error::New(
888
+ env, "Selector " + selectorName + " expected " +
889
+ std::to_string(expectedArgCount) + " argument(s), but got " +
890
+ std::to_string(providedArgCount));
891
+ }
892
+
893
+ // CRITICAL: Use FFI approach to avoid infinite recursion
894
+ // The NSInvocation approach below causes infinite recursion because
895
+ // [invocation invoke] dispatches to self's implementation, not super's.
896
+
897
+ // If we have arguments, use FFI approach
898
+ if (providedArgCount > 0) {
899
+ NOBJC_LOG("CallSuper: Using FFI approach for method with arguments");
900
+
901
+ try {
902
+ return CallSuperWithFFI(env, self, superClass, selector, methodSig, info, 2);
903
+ } catch (const std::exception& e) {
904
+ NOBJC_ERROR("CallSuper: FFI approach failed: %s", e.what());
905
+ // Re-throw - don't fall back to broken NSInvocation
906
+ throw;
907
+ }
908
+ }
909
+
910
+ // For methods WITHOUT arguments, use direct objc_msgSendSuper
911
+ const char *returnType = SimplifyTypeEncoding([methodSig methodReturnType]);
912
+ struct objc_super superStruct;
913
+ superStruct.receiver = self;
914
+ superStruct.super_class = superClass;
915
+
916
+ NOBJC_LOG("CallSuper: Using direct objc_msgSendSuper for method without arguments");
917
+
918
+ switch (returnType[0]) {
919
+ case 'v': { // void
920
+ // Call and return undefined
921
+ NOBJC_LOG("CallSuper: Calling void method");
922
+ ((void (*)(struct objc_super *, SEL))objc_msgSendSuper)(&superStruct,
923
+ selector);
924
+ NOBJC_LOG("CallSuper: Void method completed");
925
+ return env.Undefined();
926
+ }
927
+ case '@':
928
+ case '#': { // id or Class
929
+ NOBJC_LOG("CallSuper: Calling method returning id/Class");
930
+ id result = ((id(*)(struct objc_super *, SEL))objc_msgSendSuper)(
931
+ &superStruct, selector);
932
+ NOBJC_LOG("CallSuper: Method returned %p", result);
933
+ if (result == nil) {
934
+ return env.Null();
935
+ }
936
+ return ObjcObject::NewInstance(env, result);
937
+ }
938
+ case 'B': { // BOOL
939
+ BOOL result = ((BOOL(*)(struct objc_super *, SEL))objc_msgSendSuper)(
940
+ &superStruct, selector);
941
+ return Napi::Boolean::New(env, result);
942
+ }
943
+ case 'c':
944
+ case 'i':
945
+ case 's':
946
+ case 'l': { // signed integers
947
+ long result = ((long (*)(struct objc_super *, SEL))objc_msgSendSuper)(
948
+ &superStruct, selector);
949
+ return Napi::Number::New(env, result);
950
+ }
951
+ case 'C':
952
+ case 'I':
953
+ case 'S':
954
+ case 'L': { // unsigned integers
955
+ unsigned long result =
956
+ ((unsigned long (*)(struct objc_super *, SEL))objc_msgSendSuper)(
957
+ &superStruct, selector);
958
+ return Napi::Number::New(env, result);
959
+ }
960
+ case 'q': { // long long / NSInteger
961
+ long long result =
962
+ ((long long (*)(struct objc_super *, SEL))objc_msgSendSuper)(
963
+ &superStruct, selector);
964
+ return Napi::Number::New(env, result);
965
+ }
966
+ case 'Q': { // unsigned long long / NSUInteger
967
+ unsigned long long result =
968
+ ((unsigned long long (*)(struct objc_super *, SEL))objc_msgSendSuper)(
969
+ &superStruct, selector);
970
+ return Napi::Number::New(env, result);
971
+ }
972
+ case 'f': { // float
973
+ float result = ((float (*)(struct objc_super *, SEL))objc_msgSendSuper)(
974
+ &superStruct, selector);
975
+ return Napi::Number::New(env, result);
976
+ }
977
+ case 'd': { // double
978
+ double result = ((double (*)(struct objc_super *, SEL))objc_msgSendSuper)(
979
+ &superStruct, selector);
980
+ return Napi::Number::New(env, result);
981
+ }
982
+ default: {
983
+ // For complex return types or methods with arguments, fall back to
984
+ // NSInvocation but invoke the IMP directly
985
+
986
+ // This is a workaround: invoke on the superclass's implementation
987
+ // by creating a temporary object or using method_invoke
988
+
989
+ // For methods WITH arguments, we need a different approach
990
+ // Let's use method_invoke which calls the IMP with an invocation
991
+
992
+ // Actually, the safest thing is to just use the invocation
993
+ // but make sure we're calling on the right implementation
994
+
995
+ // For now, throw for unsupported return types
996
+ throw Napi::Error::New(
997
+ env, "Unsupported return type '" + std::string(1, returnType[0]) +
998
+ "' for super call. Use simpler return types.");
999
+ }
1000
+ }
1001
+ }