objc-js 1.2.1 → 1.3.1

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,802 @@
1
+ #ifndef NOBJC_BLOCK_H
2
+ #define NOBJC_BLOCK_H
3
+
4
+ /**
5
+ * @file nobjc_block.h
6
+ * @brief Objective-C Block support for nobjc.
7
+ *
8
+ * Enables transparent conversion of JavaScript functions to Objective-C blocks.
9
+ * When a method expects a block parameter (@? type encoding) and a JS function
10
+ * is provided, it is automatically wrapped in an ObjC block.
11
+ *
12
+ * Block ABI:
13
+ * A block is a struct with { isa, flags, reserved, invoke, descriptor }.
14
+ * We use _NSConcreteStackBlock as isa (then _Block_copy to move to heap).
15
+ * The invoke function pointer is an FFI closure that calls back into JS.
16
+ *
17
+ * Extended block type encoding format:
18
+ * @?<v@?q> => return_type=v, block_self=@?, arg1=q
19
+ * @?<B@?@@> => return_type=B, block_self=@?, arg1=@, arg2=@
20
+ *
21
+ * Memory management:
22
+ * BlockInfo structs (containing FFI closure, JS function ref, TSFN) are
23
+ * stored in a global registry and never freed (v1 simplification).
24
+ * The block itself is heap-copied via _Block_copy and stored as `id`
25
+ * in the ObjcType variant, so ARC manages the block pointer lifetime.
26
+ *
27
+ * Thread safety:
28
+ * Blocks may be called from background threads (e.g., completion handlers).
29
+ * We use TSFN + CFRunLoop pumping for cross-thread calls, same as protocol
30
+ * forwarding. Direct invocation is used when already on the JS thread.
31
+ */
32
+
33
+ #include "debug.h"
34
+ #include "constants.h"
35
+ #include "forwarding-common.h"
36
+ #include "ObjcObject.h"
37
+ #include "type-conversion.h"
38
+ #include "struct-utils.h"
39
+ #include "ffi-utils.h"
40
+ #include <Foundation/Foundation.h>
41
+ #include <ffi.h>
42
+ #include <napi.h>
43
+ #include <objc/runtime.h>
44
+ #include <pthread.h>
45
+ #include <malloc/malloc.h>
46
+ #include <mutex>
47
+ #include <condition_variable>
48
+ #include <vector>
49
+ #include <memory>
50
+ #include <string>
51
+
52
+ // MARK: - Block ABI Structures
53
+
54
+ // Block ABI descriptor (minimum viable — no copy/dispose helpers)
55
+ struct NobjcBlockDescriptor {
56
+ unsigned long reserved; // Always 0
57
+ unsigned long size; // sizeof(NobjcBlockLiteral)
58
+ };
59
+
60
+ // Block ABI literal struct
61
+ // This matches the runtime layout expected by objc_msgSend and _Block_copy.
62
+ struct NobjcBlockLiteral {
63
+ void *isa; // _NSConcreteStackBlock (before copy) or _NSConcreteMallocBlock (after)
64
+ int flags; // Block flags
65
+ int reserved; // Always 0
66
+ void *invoke; // Function pointer (FFI closure)
67
+ NobjcBlockDescriptor *descriptor;
68
+ };
69
+
70
+ // _NSConcreteStackBlock is declared in <Block.h> (included via Foundation)
71
+ // as `extern void *_NSConcreteStackBlock[];`
72
+ // We use &_NSConcreteStackBlock[0] to get a void* for the isa field.
73
+
74
+ // MARK: - Block Signature Parsing
75
+
76
+ /**
77
+ * Check if a type encoding represents a block type.
78
+ * Block encodings start with @? (possibly preceded by type qualifiers).
79
+ */
80
+ inline bool IsBlockTypeEncoding(const char *typeEncoding) {
81
+ if (!typeEncoding) return false;
82
+ const char *simplified = SimplifyTypeEncoding(typeEncoding);
83
+ return simplified[0] == '@' && simplified[1] == '?';
84
+ }
85
+
86
+ /**
87
+ * Parsed block signature: return type + parameter types.
88
+ * The block self parameter (@?) is excluded from paramTypes.
89
+ */
90
+ struct BlockSignature {
91
+ std::string returnType; // e.g., "v", "B", "@"
92
+ std::vector<std::string> paramTypes; // e.g., ["@", "Q"] (excludes block self)
93
+ bool valid;
94
+ };
95
+
96
+ // SkipOneBlockEncoding is replaced by the unified SkipOneTypeEncoding
97
+ // from type-conversion.h. Use SkipTypeQualifiers() when qualifiers need
98
+ // to be skipped before parsing.
99
+
100
+ /**
101
+ * Parse a block's extended type encoding.
102
+ *
103
+ * Input: the full type encoding for the block parameter, e.g.:
104
+ * "@?<v@?q>" → ret=v, params=[q]
105
+ * "@?<B@?@@>" → ret=B, params=[@, @]
106
+ * "@?" → no extended encoding available
107
+ *
108
+ * The format inside <...> is: returnType blockSelf(=@?) param1 param2 ...
109
+ */
110
+ inline BlockSignature ParseBlockSignature(const char *encoding) {
111
+ BlockSignature result;
112
+ result.valid = false;
113
+
114
+ if (!encoding) return result;
115
+
116
+ const char *simplified = SimplifyTypeEncoding(encoding);
117
+
118
+ // Must start with @?
119
+ if (simplified[0] != '@' || simplified[1] != '?') return result;
120
+
121
+ // Check for extended encoding
122
+ if (simplified[2] != '<') {
123
+ // No extended encoding — we can't determine the signature
124
+ NOBJC_LOG("ParseBlockSignature: No extended encoding in '%s'", encoding);
125
+ return result;
126
+ }
127
+
128
+ // Parse inside <...>
129
+ const char *ptr = simplified + 3; // Skip "@?<"
130
+
131
+ // Find the closing '>'
132
+ const char *end = ptr;
133
+ int depth = 1;
134
+ while (*end && depth > 0) {
135
+ if (*end == '<') depth++;
136
+ else if (*end == '>') depth--;
137
+ end++;
138
+ }
139
+ // end now points past '>'
140
+
141
+ // Create a null-terminated copy for safe parsing
142
+ std::string inner(ptr, end - 1 - ptr); // Exclude the closing '>'
143
+ const char *innerPtr = inner.c_str();
144
+
145
+ // First encoding: return type
146
+ result.returnType = SkipOneTypeEncoding(innerPtr);
147
+
148
+ // Second encoding: block self (@?) — skip it
149
+ if (*innerPtr) {
150
+ std::string blockSelf = SkipOneTypeEncoding(innerPtr);
151
+ // Should be "@?" — we just skip it
152
+ NOBJC_LOG("ParseBlockSignature: block self = '%s'", blockSelf.c_str());
153
+ }
154
+
155
+ // Remaining encodings: parameter types
156
+ while (*innerPtr) {
157
+ std::string paramType = SkipOneTypeEncoding(innerPtr);
158
+ if (!paramType.empty()) {
159
+ result.paramTypes.push_back(paramType);
160
+ }
161
+ }
162
+
163
+ result.valid = true;
164
+ NOBJC_LOG("ParseBlockSignature: ret='%s', %zu params",
165
+ result.returnType.c_str(), result.paramTypes.size());
166
+
167
+ return result;
168
+ }
169
+
170
+ // MARK: - Extended Block Encoding Extraction from Method Type
171
+
172
+ /**
173
+ * Extract the type encoding for a specific argument index from a full method
174
+ * type encoding string (as returned by method_getTypeEncoding()).
175
+ *
176
+ * The full method type has the format:
177
+ * returnType[offset] arg0Type[offset] arg1Type[offset] ...
178
+ * where arg0 = self (@), arg1 = _cmd (:), arg2+ = user args.
179
+ *
180
+ * This is needed because [NSMethodSignature getArgumentTypeAtIndex:] strips
181
+ * the extended block encoding (<...>), but method_getTypeEncoding() preserves it.
182
+ *
183
+ * @param methodTypeEncoding Full type encoding from method_getTypeEncoding()
184
+ * @param argIndex 0-based argument index (0 = self, 1 = _cmd, 2+ = user args)
185
+ * @return The encoding for that argument, or empty string if not found.
186
+ */
187
+ inline std::string ExtractArgEncodingFromMethodType(const char *methodTypeEncoding,
188
+ size_t argIndex) {
189
+ if (!methodTypeEncoding) return "";
190
+
191
+ const char *ptr = methodTypeEncoding;
192
+
193
+ // Skip type qualifiers at start
194
+ SkipTypeQualifiers(ptr);
195
+
196
+ // First, skip the return type encoding
197
+ SkipOneTypeEncoding(ptr);
198
+ // Skip return type offset digits
199
+ while (*ptr && isdigit(*ptr)) ptr++;
200
+
201
+ // Now iterate through arguments 0, 1, ..., argIndex
202
+ for (size_t i = 0; i <= argIndex; i++) {
203
+ // Skip type qualifiers
204
+ SkipTypeQualifiers(ptr);
205
+
206
+ if (!*ptr) return "";
207
+
208
+ if (i == argIndex) {
209
+ // This is the argument we want — capture its encoding
210
+ const char *start = ptr;
211
+ SkipOneTypeEncoding(ptr);
212
+ return std::string(start, ptr - start);
213
+ }
214
+
215
+ // Skip this argument's encoding
216
+ SkipOneTypeEncoding(ptr);
217
+
218
+ // Skip trailing offset digits
219
+ while (*ptr && isdigit(*ptr)) ptr++;
220
+ }
221
+
222
+ return "";
223
+ }
224
+
225
+ /**
226
+ * Get the extended block type encoding for a specific argument of a method.
227
+ * Uses the ObjC runtime to get the full method type encoding which preserves
228
+ * extended block encodings like @?<v@?q>.
229
+ *
230
+ * @param cls The class that implements the method
231
+ * @param selector The selector of the method
232
+ * @param argIndex The NSInvocation-style argument index (0 = self, 1 = _cmd, 2+ = user args)
233
+ * @return The extended encoding for that argument, or empty string if unavailable.
234
+ */
235
+ inline std::string GetExtendedBlockEncoding(Class cls, SEL selector, size_t argIndex) {
236
+ Method method = class_getInstanceMethod(cls, selector);
237
+ if (!method) {
238
+ // Try class method
239
+ method = class_getClassMethod(cls, selector);
240
+ }
241
+ if (!method) return "";
242
+
243
+ const char *fullType = method_getTypeEncoding(method);
244
+ if (!fullType) return "";
245
+
246
+ NOBJC_LOG("GetExtendedBlockEncoding: fullType='%s', argIndex=%zu", fullType, argIndex);
247
+
248
+ std::string encoding = ExtractArgEncodingFromMethodType(fullType, argIndex);
249
+ NOBJC_LOG("GetExtendedBlockEncoding: extracted='%s'", encoding.c_str());
250
+ return encoding;
251
+ }
252
+
253
+ /**
254
+ * BlockInfo holds all state for a single JS-function-backed block.
255
+ * It owns the FFI closure, CIF, arg types, JS function reference, and TSFN.
256
+ *
257
+ * Stored in a global registry for lifetime management (never freed in v1).
258
+ */
259
+ struct BlockInfo {
260
+ // FFI closure and CIF
261
+ ffi_closure *closure;
262
+ ffi_cif cif;
263
+ ffi_type *returnFFIType;
264
+ std::vector<ffi_type *> argFFITypes; // Includes block self (pointer) as arg[0]
265
+ std::vector<ffi_type *> argFFIPtrs; // Pointer array for ffi_prep_cif
266
+
267
+ // Block signature
268
+ BlockSignature signature;
269
+
270
+ // Allocated FFI types (for struct types — cleaned up on destruction)
271
+ FFITypeGuard ffiTypeGuard;
272
+
273
+ // JS function reference (prevents GC)
274
+ Napi::FunctionReference jsFunction;
275
+
276
+ // Thread-safe function for cross-thread calls
277
+ Napi::ThreadSafeFunction tsfn;
278
+
279
+ // JS thread ID for thread detection
280
+ pthread_t js_thread;
281
+
282
+ // Napi environment
283
+ napi_env env;
284
+
285
+ // The block descriptor (must outlive the block)
286
+ NobjcBlockDescriptor descriptor;
287
+
288
+ // The block literal (stack block, before _Block_copy)
289
+ NobjcBlockLiteral blockLiteral;
290
+
291
+ // The heap-copied block (after _Block_copy)
292
+ // Stored as void* since ARC is not enabled for .mm files in this project.
293
+ // The block is never freed in v1 (stored in global registry).
294
+ void *heapBlock;
295
+
296
+ ~BlockInfo() {
297
+ // Free the FFI closure
298
+ if (closure) {
299
+ ffi_closure_free(closure);
300
+ closure = nullptr;
301
+ }
302
+ // Note: tsfn and jsFunction cleanup is tricky across threads.
303
+ // In v1, BlockInfo is never destroyed, so this is moot.
304
+ }
305
+ };
306
+
307
+ // MARK: - Block Call Data (transient, for cross-thread invocation)
308
+
309
+ /**
310
+ * Data passed from the block invoke callback to the JS thread.
311
+ * Holds argument values and synchronization primitives.
312
+ */
313
+ struct BlockCallData {
314
+ BlockInfo *blockInfo; // Non-owning pointer to the BlockInfo
315
+ std::vector<void *> argValues; // Pointers to argument values (from FFI)
316
+ void *returnValuePtr; // Where to write the return value
317
+
318
+ // Synchronization for cross-thread calls
319
+ std::mutex completionMutex;
320
+ std::condition_variable completionCv;
321
+ bool isComplete;
322
+ };
323
+
324
+ // MARK: - Global Block Registry
325
+
326
+ /**
327
+ * Global registry of all created blocks.
328
+ * Blocks are never freed in v1 — this prevents crashes from async callbacks
329
+ * referencing freed memory.
330
+ */
331
+ static std::vector<std::unique_ptr<BlockInfo>> g_blockRegistry;
332
+ static std::mutex g_blockRegistryMutex;
333
+
334
+ // MARK: - Block Argument Conversion (ObjC → JS)
335
+
336
+ /**
337
+ * Heuristic: try to determine if a pointer-sized value is an ObjC object.
338
+ * Used when we don't have type encoding info (no extended block encoding).
339
+ *
340
+ * This takes the RAW value (not a pointer to it) — already dereferenced from
341
+ * the FFI arg pointer.
342
+ *
343
+ * Strategy:
344
+ * 1. Tagged pointers (arm64: high bit set) are always valid objects
345
+ * 2. Use malloc_zone_from_ptr() to check if it's a heap allocation
346
+ * 3. If it is, verify it has a valid class pointer
347
+ */
348
+ inline bool LooksLikeObjCObject(uintptr_t val) {
349
+ if (val == 0) return false; // nil
350
+
351
+ // Tagged pointer check (arm64: high bit set)
352
+ if (val & (1ULL << 63)) return true;
353
+
354
+ // Values below 4096 are definitely not objects — they're small integers
355
+ // or null page addresses
356
+ if (val < 4096) return false;
357
+
358
+ // Check if this pointer was allocated via malloc (all ObjC heap objects are).
359
+ // malloc_zone_from_ptr returns non-NULL only for valid heap allocations.
360
+ void *ptr = (void *)val;
361
+ malloc_zone_t *zone = malloc_zone_from_ptr(ptr);
362
+ if (!zone) return false;
363
+
364
+ // It's a heap allocation — very likely an ObjC object.
365
+ // Do a final check: object_getClass should return a valid class.
366
+ Class cls = object_getClass((__bridge id)ptr);
367
+ return cls != nil;
368
+ }
369
+
370
+ /**
371
+ * Convert a block argument to JS using heuristic type detection.
372
+ * Used when no extended block encoding is available (@? without <...>).
373
+ *
374
+ * argPtr is a pointer TO the argument value (as provided by FFI).
375
+ * The argument is pointer-sized.
376
+ *
377
+ * Strategy:
378
+ * - If the value looks like an ObjC object, wrap it as ObjcObject
379
+ * - Otherwise, interpret as a number (NSUInteger/NSInteger)
380
+ */
381
+ inline Napi::Value ConvertBlockArgHeuristic(Napi::Env env, void *argPtr) {
382
+ // Read the raw pointer-sized value
383
+ uintptr_t value = *static_cast<uintptr_t *>(argPtr);
384
+
385
+ // Zero could be nil (for objects) or 0 (for integers).
386
+ // Return as number 0 — this works for NSUInteger and for nil objects
387
+ // (the proxy layer handles numeric values correctly).
388
+ if (value == 0) return Napi::Number::New(env, 0);
389
+
390
+ if (LooksLikeObjCObject(value)) {
391
+ id obj = (__bridge id)(void *)value;
392
+ return ObjcObject::NewInstance(env, obj);
393
+ }
394
+
395
+ // Treat as unsigned integer
396
+ return Napi::Number::New(env, static_cast<double>(value));
397
+ }
398
+
399
+ /**
400
+ * Convert a single block argument from ObjC to JS.
401
+ * Used inside the FFI callback when the block is invoked.
402
+ */
403
+ inline Napi::Value ConvertBlockArgToJS(Napi::Env env, void *argPtr,
404
+ const std::string &typeEncoding) {
405
+ const char *simplified = SimplifyTypeEncoding(typeEncoding.c_str());
406
+ char code = simplified[0];
407
+
408
+ // Unknown type (inferred params) — use heuristic
409
+ if (code == '?') {
410
+ return ConvertBlockArgHeuristic(env, argPtr);
411
+ }
412
+
413
+ // Handle @? (block) args as opaque objects
414
+ if (code == '@' && simplified[1] == '?') {
415
+ id value = *(static_cast<id *>(argPtr));
416
+ if (value == nil) return env.Null();
417
+ return ObjcObject::NewInstance(env, value);
418
+ }
419
+
420
+ // Handle struct types
421
+ if (code == '{') {
422
+ return UnpackStructToJSValue(env, static_cast<const uint8_t *>(argPtr),
423
+ simplified);
424
+ }
425
+
426
+ // Simple types
427
+ return ObjCToJS(env, argPtr, code);
428
+ }
429
+
430
+ // MARK: - Block Return Value Conversion (JS → ObjC)
431
+
432
+ /**
433
+ * Convert a JS return value to ObjC and write it to the return buffer.
434
+ * Used inside the FFI callback after the JS function returns.
435
+ */
436
+ inline void SetBlockReturnFromJS(Napi::Value result, void *returnPtr,
437
+ const std::string &typeEncoding) {
438
+ const char *simplified = SimplifyTypeEncoding(typeEncoding.c_str());
439
+ char code = simplified[0];
440
+
441
+ if (code == 'v') return; // Void return — nothing to do
442
+
443
+ if (result.IsNull() || result.IsUndefined()) {
444
+ // For object types, set nil
445
+ if (code == '@' || code == '#') {
446
+ id nilVal = nil;
447
+ memcpy(returnPtr, &nilVal, sizeof(id));
448
+ }
449
+ // For numeric types, leave as-is (zero-initialized by FFI)
450
+ return;
451
+ }
452
+
453
+ switch (code) {
454
+ case 'c': { char v = static_cast<char>(result.As<Napi::Number>().Int32Value()); memcpy(returnPtr, &v, sizeof(v)); break; }
455
+ case 'i': { int v = result.As<Napi::Number>().Int32Value(); memcpy(returnPtr, &v, sizeof(v)); break; }
456
+ case 's': { short v = static_cast<short>(result.As<Napi::Number>().Int32Value()); memcpy(returnPtr, &v, sizeof(v)); break; }
457
+ case 'l': { long v = static_cast<long>(result.As<Napi::Number>().Int64Value()); memcpy(returnPtr, &v, sizeof(v)); break; }
458
+ case 'q': { long long v = result.As<Napi::Number>().Int64Value(); memcpy(returnPtr, &v, sizeof(v)); break; }
459
+ case 'C': { unsigned char v = static_cast<unsigned char>(result.As<Napi::Number>().Uint32Value()); memcpy(returnPtr, &v, sizeof(v)); break; }
460
+ case 'I': { unsigned int v = result.As<Napi::Number>().Uint32Value(); memcpy(returnPtr, &v, sizeof(v)); break; }
461
+ case 'S': { unsigned short v = static_cast<unsigned short>(result.As<Napi::Number>().Uint32Value()); memcpy(returnPtr, &v, sizeof(v)); break; }
462
+ case 'L': { unsigned long v = static_cast<unsigned long>(result.As<Napi::Number>().Int64Value()); memcpy(returnPtr, &v, sizeof(v)); break; }
463
+ case 'Q': { unsigned long long v = static_cast<unsigned long long>(result.As<Napi::Number>().Int64Value()); memcpy(returnPtr, &v, sizeof(v)); break; }
464
+ case 'f': { float v = static_cast<float>(result.As<Napi::Number>().DoubleValue()); memcpy(returnPtr, &v, sizeof(v)); break; }
465
+ case 'd': { double v = result.As<Napi::Number>().DoubleValue(); memcpy(returnPtr, &v, sizeof(v)); break; }
466
+ case 'B': {
467
+ bool v = false;
468
+ if (result.IsBoolean()) v = result.As<Napi::Boolean>().Value();
469
+ else if (result.IsNumber()) v = result.As<Napi::Number>().Int32Value() != 0;
470
+ memcpy(returnPtr, &v, sizeof(v));
471
+ break;
472
+ }
473
+ case '@': case '#': {
474
+ id objcVal = nil;
475
+ if (result.IsObject()) {
476
+ Napi::Object obj = result.As<Napi::Object>();
477
+ if (obj.InstanceOf(ObjcObject::constructor.Value())) {
478
+ ObjcObject *wrapper = Napi::ObjectWrap<ObjcObject>::Unwrap(obj);
479
+ objcVal = wrapper->objcObject;
480
+ }
481
+ }
482
+ memcpy(returnPtr, &objcVal, sizeof(id));
483
+ break;
484
+ }
485
+ default:
486
+ NOBJC_WARN("SetBlockReturnFromJS: Unsupported return type '%c'", code);
487
+ break;
488
+ }
489
+ }
490
+
491
+ // MARK: - TSFN Callback for Cross-Thread Block Invocation
492
+
493
+ /**
494
+ * Called on the JS thread via ThreadSafeFunction when a block is invoked
495
+ * from a background thread.
496
+ */
497
+ inline void BlockTSFNCallback(Napi::Env env, Napi::Function /*jsCallback*/,
498
+ BlockCallData *callData) {
499
+ if (!callData || !callData->blockInfo) {
500
+ NOBJC_ERROR("BlockTSFNCallback: null callData or blockInfo");
501
+ if (callData) {
502
+ std::lock_guard<std::mutex> lock(callData->completionMutex);
503
+ callData->isComplete = true;
504
+ callData->completionCv.notify_one();
505
+ }
506
+ return;
507
+ }
508
+
509
+ BlockInfo *info = callData->blockInfo;
510
+
511
+ @autoreleasepool {
512
+ try {
513
+ Napi::HandleScope scope(env);
514
+
515
+ // Build JS arguments (skip arg[0] which is the block self)
516
+ std::vector<napi_value> jsArgs;
517
+ jsArgs.reserve(info->signature.paramTypes.size());
518
+
519
+ for (size_t i = 0; i < info->signature.paramTypes.size(); i++) {
520
+ // argValues[0] is block self, actual params start at index 1
521
+ void *argPtr = callData->argValues[i + 1];
522
+ Napi::Value jsVal = ConvertBlockArgToJS(env, argPtr,
523
+ info->signature.paramTypes[i]);
524
+ jsArgs.push_back(jsVal);
525
+ }
526
+
527
+ // Call the JS function
528
+ Napi::Value result = info->jsFunction.Value().Call(jsArgs);
529
+
530
+ // Handle return value
531
+ if (callData->returnValuePtr) {
532
+ SetBlockReturnFromJS(result, callData->returnValuePtr,
533
+ info->signature.returnType);
534
+ }
535
+ } catch (const Napi::Error &e) {
536
+ NOBJC_ERROR("BlockTSFNCallback: JS error: %s", e.what());
537
+ } catch (const std::exception &e) {
538
+ NOBJC_ERROR("BlockTSFNCallback: exception: %s", e.what());
539
+ } catch (...) {
540
+ NOBJC_ERROR("BlockTSFNCallback: unknown exception");
541
+ }
542
+ }
543
+
544
+ // Signal completion
545
+ {
546
+ std::lock_guard<std::mutex> lock(callData->completionMutex);
547
+ callData->isComplete = true;
548
+ callData->completionCv.notify_one();
549
+ }
550
+ }
551
+
552
+ // MARK: - FFI Closure Callback (Block Invoke)
553
+
554
+ /**
555
+ * This is the function pointer stored in the block's `invoke` field.
556
+ * Called by the ObjC runtime when the block is invoked.
557
+ *
558
+ * FFI closure signature: void(ffi_cif *, void *ret, void **args, void *userdata)
559
+ *
560
+ * args[0] = pointer to the block literal itself (block self)
561
+ * args[1..n] = pointers to the actual block parameters
562
+ */
563
+ inline void BlockInvokeCallback(ffi_cif *cif, void *ret, void **args,
564
+ void *userdata) {
565
+ BlockInfo *info = static_cast<BlockInfo *>(userdata);
566
+ if (!info) {
567
+ NOBJC_ERROR("BlockInvokeCallback: null userdata");
568
+ return;
569
+ }
570
+
571
+ bool is_js_thread = pthread_equal(pthread_self(), info->js_thread);
572
+
573
+ if (is_js_thread) {
574
+ // Direct call on JS thread
575
+ @autoreleasepool {
576
+ try {
577
+ Napi::Env env(info->env);
578
+ Napi::HandleScope scope(env);
579
+
580
+ // Build JS arguments (skip args[0] which is block self)
581
+ std::vector<napi_value> jsArgs;
582
+ jsArgs.reserve(info->signature.paramTypes.size());
583
+
584
+ for (size_t i = 0; i < info->signature.paramTypes.size(); i++) {
585
+ void *argPtr = args[i + 1]; // +1 to skip block self
586
+ Napi::Value jsVal = ConvertBlockArgToJS(env, argPtr,
587
+ info->signature.paramTypes[i]);
588
+ jsArgs.push_back(jsVal);
589
+ }
590
+
591
+ // Call the JS function
592
+ Napi::Value result = info->jsFunction.Value().Call(jsArgs);
593
+
594
+ // Handle return value
595
+ if (ret && info->signature.returnType != "v") {
596
+ SetBlockReturnFromJS(result, ret, info->signature.returnType);
597
+ }
598
+ } catch (const Napi::Error &e) {
599
+ NOBJC_ERROR("BlockInvokeCallback: JS error: %s", e.what());
600
+ } catch (const std::exception &e) {
601
+ NOBJC_ERROR("BlockInvokeCallback: exception: %s", e.what());
602
+ } catch (...) {
603
+ NOBJC_ERROR("BlockInvokeCallback: unknown exception");
604
+ }
605
+ }
606
+ } else {
607
+ // Cross-thread call via TSFN
608
+ BlockCallData callData;
609
+ callData.blockInfo = info;
610
+ callData.returnValuePtr = ret;
611
+ callData.isComplete = false;
612
+
613
+ // Copy arg pointers
614
+ size_t totalArgs = info->signature.paramTypes.size() + 1; // +1 for block self
615
+ callData.argValues.resize(totalArgs);
616
+ for (size_t i = 0; i < totalArgs; i++) {
617
+ callData.argValues[i] = args[i];
618
+ }
619
+
620
+ // Acquire the TSFN
621
+ napi_status acq_status = info->tsfn.Acquire();
622
+ if (acq_status != napi_ok) {
623
+ NOBJC_ERROR("BlockInvokeCallback: Failed to acquire TSFN");
624
+ return;
625
+ }
626
+
627
+ // Call via TSFN
628
+ napi_status status = info->tsfn.NonBlockingCall(&callData, BlockTSFNCallback);
629
+ info->tsfn.Release();
630
+
631
+ if (status != napi_ok) {
632
+ NOBJC_ERROR("BlockInvokeCallback: TSFN call failed (status=%d)", status);
633
+ return;
634
+ }
635
+
636
+ // Wait for completion by pumping CFRunLoop
637
+ PumpRunLoopUntilComplete(callData.completionMutex, callData.isComplete);
638
+ }
639
+ }
640
+
641
+ // MARK: - Block Creation
642
+
643
+ /**
644
+ * Create an Objective-C block from a JavaScript function.
645
+ *
646
+ * @param env Napi environment
647
+ * @param jsFunction The JS function to wrap
648
+ * @param typeEncoding The full type encoding for the block parameter (e.g., "@?<v@?q>")
649
+ * @return The heap-copied block as an `id`, or nil on failure.
650
+ *
651
+ * The returned block is heap-allocated via _Block_copy and managed by ARC
652
+ * when stored in the ObjcType variant as `id`.
653
+ */
654
+ inline id CreateBlockFromJSFunction(Napi::Env env,
655
+ const Napi::Value &jsFunction,
656
+ const char *typeEncoding) {
657
+ NOBJC_LOG("CreateBlockFromJSFunction: encoding='%s'", typeEncoding);
658
+
659
+ // Parse the block signature
660
+ BlockSignature sig = ParseBlockSignature(typeEncoding);
661
+ if (!sig.valid) {
662
+ // No extended encoding — infer from JS function's .length
663
+ // All params are treated as pointer-sized (heuristic detection in callback)
664
+ Napi::Function fn = jsFunction.As<Napi::Function>();
665
+ uint32_t jsParamCount = fn.Get("length").As<Napi::Number>().Uint32Value();
666
+
667
+ NOBJC_LOG("CreateBlockFromJSFunction: No extended block encoding, "
668
+ "inferring %u params from JS function.length. Encoding: '%s'",
669
+ jsParamCount, typeEncoding);
670
+
671
+ sig.returnType = "v"; // Assume void return
672
+ sig.paramTypes.clear();
673
+ for (uint32_t i = 0; i < jsParamCount; i++) {
674
+ sig.paramTypes.push_back("?"); // Unknown type — use heuristic in callback
675
+ }
676
+ sig.valid = true;
677
+ }
678
+
679
+ // Create BlockInfo
680
+ auto blockInfo = std::make_unique<BlockInfo>();
681
+ blockInfo->signature = sig;
682
+ blockInfo->env = env;
683
+ blockInfo->js_thread = pthread_self();
684
+ blockInfo->closure = nullptr;
685
+ blockInfo->heapBlock = nullptr;
686
+
687
+ // Store JS function reference
688
+ blockInfo->jsFunction = Napi::Persistent(jsFunction.As<Napi::Function>());
689
+
690
+ // Create TSFN for cross-thread calls
691
+ blockInfo->tsfn = CreateMethodTSFN(env, jsFunction.As<Napi::Function>(),
692
+ "nobjc_block_tsfn");
693
+
694
+ // Build FFI types for the block invocation
695
+ // Block invoke signature: returnType (blockSelf, param1, param2, ...)
696
+ // blockSelf is always a pointer (the block literal)
697
+
698
+ // Return type
699
+ blockInfo->returnFFIType = GetFFITypeForSimpleEncoding(
700
+ SimplifyTypeEncoding(sig.returnType.c_str())[0]);
701
+
702
+ // Arg types: [blockSelf(ptr), param1, param2, ...]
703
+ blockInfo->argFFITypes.push_back(&ffi_type_pointer); // block self
704
+ for (const auto &paramType : sig.paramTypes) {
705
+ const char *simplified = SimplifyTypeEncoding(paramType.c_str());
706
+ ffi_type *ffiType;
707
+ if (simplified[0] == '?') {
708
+ // Unknown type (inferred from JS function.length) — treat as pointer
709
+ ffiType = &ffi_type_pointer;
710
+ } else if (simplified[0] == '{') {
711
+ // Struct type — need full parsing
712
+ size_t structSize = 0;
713
+ ffiType = GetFFITypeForEncoding(simplified, &structSize, blockInfo->ffiTypeGuard);
714
+ } else {
715
+ ffiType = GetFFITypeForSimpleEncoding(simplified[0]);
716
+ }
717
+ blockInfo->argFFITypes.push_back(ffiType);
718
+ }
719
+
720
+ // Build pointer array for ffi_prep_cif
721
+ blockInfo->argFFIPtrs.resize(blockInfo->argFFITypes.size());
722
+ for (size_t i = 0; i < blockInfo->argFFITypes.size(); i++) {
723
+ blockInfo->argFFIPtrs[i] = blockInfo->argFFITypes[i];
724
+ }
725
+
726
+ // Allocate FFI closure
727
+ void *codePtr = nullptr;
728
+ blockInfo->closure = static_cast<ffi_closure *>(
729
+ ffi_closure_alloc(sizeof(ffi_closure), &codePtr));
730
+
731
+ if (!blockInfo->closure || !codePtr) {
732
+ Napi::Error::New(env, "Failed to allocate FFI closure for block")
733
+ .ThrowAsJavaScriptException();
734
+ return nil;
735
+ }
736
+
737
+ // Prepare the CIF
738
+ ffi_status ffiStatus = ffi_prep_cif(
739
+ &blockInfo->cif,
740
+ FFI_DEFAULT_ABI,
741
+ static_cast<unsigned int>(blockInfo->argFFIPtrs.size()),
742
+ blockInfo->returnFFIType,
743
+ blockInfo->argFFIPtrs.data());
744
+
745
+ if (ffiStatus != FFI_OK) {
746
+ ffi_closure_free(blockInfo->closure);
747
+ blockInfo->closure = nullptr;
748
+ Napi::Error::New(env, "ffi_prep_cif failed for block")
749
+ .ThrowAsJavaScriptException();
750
+ return nil;
751
+ }
752
+
753
+ // Prepare the closure
754
+ ffiStatus = ffi_prep_closure_loc(
755
+ blockInfo->closure,
756
+ &blockInfo->cif,
757
+ BlockInvokeCallback,
758
+ blockInfo.get(), // userdata = BlockInfo*
759
+ codePtr);
760
+
761
+ if (ffiStatus != FFI_OK) {
762
+ ffi_closure_free(blockInfo->closure);
763
+ blockInfo->closure = nullptr;
764
+ Napi::Error::New(env, "ffi_prep_closure_loc failed for block")
765
+ .ThrowAsJavaScriptException();
766
+ return nil;
767
+ }
768
+
769
+ // Build the block literal (stack block)
770
+ blockInfo->descriptor.reserved = 0;
771
+ blockInfo->descriptor.size = sizeof(NobjcBlockLiteral);
772
+
773
+ blockInfo->blockLiteral.isa = _NSConcreteStackBlock;
774
+ blockInfo->blockLiteral.flags = (1 << 30); // BLOCK_HAS_SIGNATURE (not strictly needed but harmless)
775
+ blockInfo->blockLiteral.reserved = 0;
776
+ blockInfo->blockLiteral.invoke = codePtr;
777
+ blockInfo->blockLiteral.descriptor = &blockInfo->descriptor;
778
+
779
+ // Copy to heap via _Block_copy
780
+ void *heapBlockPtr = _Block_copy(&blockInfo->blockLiteral);
781
+ if (!heapBlockPtr) {
782
+ Napi::Error::New(env, "_Block_copy failed")
783
+ .ThrowAsJavaScriptException();
784
+ return nil;
785
+ }
786
+
787
+ // Store the heap block pointer (no ARC — manual retain from _Block_copy)
788
+ blockInfo->heapBlock = heapBlockPtr;
789
+
790
+ id result = (id)blockInfo->heapBlock;
791
+
792
+ // Store in global registry (never freed in v1)
793
+ {
794
+ std::lock_guard<std::mutex> lock(g_blockRegistryMutex);
795
+ g_blockRegistry.push_back(std::move(blockInfo));
796
+ }
797
+
798
+ NOBJC_LOG("CreateBlockFromJSFunction: created block %p", result);
799
+ return result;
800
+ }
801
+
802
+ #endif // NOBJC_BLOCK_H