objc-js 1.2.0 → 1.3.0

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 CHANGED
@@ -36,6 +36,7 @@ The documentation is organized into several guides:
36
36
  - **[C Functions](./docs/c-functions.md)** - Calling C functions like NSLog, NSHomeDirectory, NSStringFromClass
37
37
  - **[Structs](./docs/structs.md)** - Passing and receiving C structs (CGRect, NSRange, etc.)
38
38
  - **[Subclassing Objective-C Classes](./docs/subclassing.md)** - Creating and subclassing Objective-C classes from JavaScript
39
+ - **[Blocks](./docs/blocks.md)** - Passing JavaScript functions as Objective-C blocks (closures)
39
40
  - **[Protocol Implementation](./docs/protocol-implementation.md)** - Creating delegate objects that implement protocols
40
41
  - **[API Reference](./docs/api-reference.md)** - Complete API documentation for all classes and functions
41
42
 
package/dist/index.js CHANGED
@@ -137,6 +137,20 @@ function unwrapArg(arg) {
137
137
  if (arg && typeof arg === "object") {
138
138
  return nativeObjectMap.get(arg) ?? arg;
139
139
  }
140
+ // Wrap function arguments so that when called from native (e.g., as ObjC blocks),
141
+ // the native ObjcObject args are automatically wrapped in NobjcObject proxies.
142
+ if (typeof arg === "function") {
143
+ const wrapped = function (...nativeArgs) {
144
+ for (let i = 0; i < nativeArgs.length; i++) {
145
+ nativeArgs[i] = wrapObjCObjectIfNeeded(nativeArgs[i]);
146
+ }
147
+ return unwrapArg(arg(...nativeArgs));
148
+ };
149
+ // Preserve the original function's .length so the native layer can read it
150
+ // (used to infer block parameter count when extended encoding is unavailable)
151
+ Object.defineProperty(wrapped, "length", { value: arg.length });
152
+ return wrapped;
153
+ }
140
154
  return arg;
141
155
  }
142
156
  function wrapObjCObjectIfNeeded(result) {
package/package.json CHANGED
@@ -30,7 +30,9 @@
30
30
  "prebuild": "node-gyp clean && node-gyp configure",
31
31
  "build": "npm run build-native && npm run build-scripts && npm run build-source",
32
32
  "pretest": "npm run build",
33
- "test": "bun test",
33
+ "test": "bun run test:bun",
34
+ "test:bun": "bun run build && bun test",
35
+ "test:node": "bun run build && npx vitest run",
34
36
  "test:native": "bun test tests/test-native-code.test.ts",
35
37
  "test:js": "bun test tests/test-js-code.test.ts",
36
38
  "test:string-lifetime": "bun test tests/test-string-lifetime.test.ts",
@@ -44,7 +46,7 @@
44
46
  "prebuild:x64": "prebuildify --napi --strip --arch x64 -r node",
45
47
  "prebuild:all": "bun run prebuild:arm64 && bun run prebuild:x64"
46
48
  },
47
- "version": "1.2.0",
49
+ "version": "1.3.0",
48
50
  "description": "Objective-C bridge for Node.js",
49
51
  "main": "dist/index.js",
50
52
  "dependencies": {
@@ -57,7 +59,8 @@
57
59
  "node-gyp": "^12.1.0",
58
60
  "prebuildify": "^6.0.1",
59
61
  "prettier": "^3.7.4",
60
- "typescript": "^5.9.3"
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^4.0.18"
61
64
  },
62
65
  "gypfile": true,
63
66
  "patchedDependencies": {
Binary file
@@ -2,6 +2,7 @@
2
2
  #include "bridge.h"
3
3
  #include "pointer-utils.h"
4
4
  #include "struct-utils.h"
5
+ #include "nobjc_block.h"
5
6
  #include <Foundation/Foundation.h>
6
7
  #include <napi.h>
7
8
  #include <objc/objc.h>
@@ -153,6 +154,8 @@ static bool TryFastMsgSend(Napi::Env env, id target, SEL selector,
153
154
  const char *argType = SimplifyTypeEncoding(
154
155
  [methodSignature getArgumentTypeAtIndex:i + 2]);
155
156
  char code = *argType;
157
+ // Block args (@?) need special handling — bail out of fast path
158
+ if (code == '@' && argType[1] == '?') return false;
156
159
  if (!IsFastPathArgTypeCode(code)) return false;
157
160
  argTypeCodes[i] = code;
158
161
  if (code == 'f' || code == 'd') hasFloatArgs = true;
@@ -476,6 +479,28 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
476
479
  continue;
477
480
  }
478
481
 
482
+ // Block argument: convert JS function to ObjC block
483
+ if (IsBlockTypeEncoding(typeEncoding) && info[i].IsFunction()) {
484
+ // Get extended encoding from method_getTypeEncoding() which preserves @?<...>
485
+ // NSMethodSignature strips the extended encoding, so we use the runtime directly.
486
+ // Argument index in NSInvocation is i+1 (0=self, 1=_cmd, 2+=user args)
487
+ std::string extEncoding = GetExtendedBlockEncoding(
488
+ object_getClass(objcObject), selector, i + 1);
489
+ const char *blockEncoding = extEncoding.empty()
490
+ ? [methodSignature getArgumentTypeAtIndex:i + 1]
491
+ : extEncoding.c_str();
492
+ id block = CreateBlockFromJSFunction(env, info[i], blockEncoding);
493
+ if (env.IsExceptionPending()) return env.Null();
494
+ [invocation setArgument:&block atIndex:i + 1];
495
+ // Store block as id in arg buffer to keep it alive until after invoke
496
+ if (useHeap) {
497
+ heapArgBuf.push_back(BaseObjcType{block});
498
+ } else {
499
+ smallArgBuf[argIdx] = BaseObjcType{block};
500
+ }
501
+ continue;
502
+ }
503
+
479
504
  auto arg = AsObjCArgument(info[i], typeEncoding, context);
480
505
  if (!arg.has_value()) {
481
506
  std::string errorMessageStr = std::format("Unsupported argument type {}",
@@ -634,6 +659,10 @@ Napi::Value ObjcObject::$PrepareSend(const Napi::CallbackInfo &info) {
634
659
  [methodSignature getArgumentTypeAtIndex:i + 2]);
635
660
  char code = *argType;
636
661
  prepared->argInfos[i] = {code, code == '{'};
662
+ // Block args (@?) need slow path for JS function → block conversion
663
+ if (code == '@' && argType[1] == '?') {
664
+ canFast = false;
665
+ }
637
666
  if (code == '{' || !IsFastPathArgTypeCode(code)) {
638
667
  canFast = false;
639
668
  }
@@ -738,6 +767,26 @@ Napi::Value ObjcObject::$MsgSendPrepared(const Napi::CallbackInfo &info) {
738
767
  continue;
739
768
  }
740
769
 
770
+ // Block argument: convert JS function to ObjC block
771
+ if (IsBlockTypeEncoding(typeEncoding) && info[jsArgIdx].IsFunction()) {
772
+ // Get extended encoding from method_getTypeEncoding() which preserves @?<...>
773
+ // Argument index in NSInvocation is i+2 (0=self, 1=_cmd, 2+=user args)
774
+ std::string extEncoding = GetExtendedBlockEncoding(
775
+ object_getClass(objcObject), prepared->selector, i + 2);
776
+ const char *blockEncoding = extEncoding.empty()
777
+ ? [prepared->methodSignature getArgumentTypeAtIndex:i + 2]
778
+ : extEncoding.c_str();
779
+ id block = CreateBlockFromJSFunction(env, info[jsArgIdx], blockEncoding);
780
+ if (env.IsExceptionPending()) return env.Null();
781
+ [invocation setArgument:&block atIndex:i + 2];
782
+ if (useHeap) {
783
+ heapArgBuf.push_back(BaseObjcType{block});
784
+ } else {
785
+ smallArgBuf[i] = BaseObjcType{block};
786
+ }
787
+ continue;
788
+ }
789
+
741
790
  auto arg = AsObjCArgument(info[jsArgIdx], typeEncoding, context);
742
791
  if (!arg.has_value()) {
743
792
  Napi::TypeError::New(env, std::string("Unsupported argument type ") + typeEncoding)
@@ -208,7 +208,7 @@ inline ffi_type* ParseStructEncoding(const char* encoding, size_t* outSize,
208
208
 
209
209
  if (*ptr == '}') break;
210
210
 
211
- std::string fieldEncoding = SkipOneFieldEncoding(ptr);
211
+ std::string fieldEncoding = SkipOneTypeEncoding(ptr);
212
212
  NOBJC_LOG("ParseStructEncoding: parsing field '%s'", fieldEncoding.c_str());
213
213
 
214
214
  // Recursively get FFI type for this field
@@ -271,6 +271,14 @@ inline ffi_type* GetFFITypeForEncoding(const char* encoding, size_t* outSize,
271
271
 
272
272
  char firstChar = simpleEncoding[0];
273
273
 
274
+ // Handle block type (@?) — blocks are pointers
275
+ if (firstChar == '@' && simpleEncoding[1] == '?') {
276
+ if (outSize) {
277
+ *outSize = sizeof(void*);
278
+ }
279
+ return &ffi_type_pointer;
280
+ }
281
+
274
282
  // Handle structs and unions
275
283
  if (firstChar == '{') {
276
284
  return ParseStructEncoding(simpleEncoding, outSize, allocatedTypes);
@@ -3,10 +3,15 @@
3
3
 
4
4
  #include "memory-utils.h"
5
5
  #include "protocol-storage.h"
6
+ #include "constants.h"
7
+ #include "debug.h"
6
8
  #include <cstring>
9
+ #include <condition_variable>
7
10
  #include <functional>
11
+ #include <mutex>
8
12
  #include <napi.h>
9
13
  #include <optional>
14
+ #include <CoreFoundation/CoreFoundation.h>
10
15
 
11
16
  #ifdef __OBJC__
12
17
  @class NSInvocation;
@@ -106,6 +111,46 @@ struct ForwardingCallbacks {
106
111
  CallbackType callbackType;
107
112
  };
108
113
 
114
+ // MARK: - Shared Helpers
115
+
116
+ /**
117
+ * Pump the CFRunLoop until a completion flag is set.
118
+ * Used by protocol forwarding, subclass forwarding, and block invocation
119
+ * to wait for cross-thread JS callbacks to complete.
120
+ *
121
+ * @param mutex Mutex protecting the isComplete flag
122
+ * @param isComplete Flag set to true when the JS callback completes
123
+ * @param label Optional label for debug logging (nullptr to disable)
124
+ */
125
+ inline void PumpRunLoopUntilComplete(std::mutex &mutex, bool &isComplete,
126
+ const char *label = nullptr) {
127
+ int iterations = 0;
128
+ while (true) {
129
+ {
130
+ std::unique_lock<std::mutex> lock(mutex);
131
+ if (isComplete) break;
132
+ }
133
+ iterations++;
134
+ if (label && iterations % nobjc::kRunLoopDebugLogInterval == 0) {
135
+ NOBJC_LOG("%s: Still waiting... (%d iterations)", label, iterations);
136
+ }
137
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, nobjc::kRunLoopPumpInterval, true);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Create a ThreadSafeFunction for method forwarding.
143
+ * Shared factory for protocol, subclass, and block TSFN creation.
144
+ *
145
+ * @param env Napi environment
146
+ * @param fn The JS function to wrap
147
+ * @param name Resource name for debugging
148
+ */
149
+ inline Napi::ThreadSafeFunction CreateMethodTSFN(
150
+ Napi::Env env, const Napi::Function &fn, const std::string &name) {
151
+ return Napi::ThreadSafeFunction::New(env, fn, name, 0, 1, [](Napi::Env) {});
152
+ }
153
+
109
154
  // MARK: - Common Implementation
110
155
 
111
156
  /**
@@ -135,22 +135,8 @@ void ForwardInvocationCommon(NSInvocation *invocation,
135
135
  }
136
136
 
137
137
  // Wait for callback by pumping CFRunLoop
138
- int iterations = 0;
139
-
140
- while (true) {
141
- {
142
- std::unique_lock<std::mutex> lock(completionMutex);
143
- if (isComplete) {
144
- break;
145
- }
146
- }
147
- iterations++;
148
- if (iterations % nobjc::kRunLoopDebugLogInterval == 0) {
149
- NOBJC_LOG("ForwardInvocationCommon: Still waiting... (%d iterations)",
150
- iterations);
151
- }
152
- CFRunLoopRunInMode(kCFRunLoopDefaultMode, nobjc::kRunLoopPumpInterval, true);
153
- }
138
+ PumpRunLoopUntilComplete(completionMutex, isComplete,
139
+ "ForwardInvocationCommon");
154
140
  // Data cleaned up in callback
155
141
  }
156
142
 
@@ -158,16 +158,7 @@ bool FallbackToTSFN(Napi::ThreadSafeFunction &tsfn, InvocationData *data,
158
158
  }
159
159
 
160
160
  // Wait for callback by pumping CFRunLoop
161
- CFTimeInterval timeout = 0.001; // 1ms per iteration
162
- while (true) {
163
- {
164
- std::unique_lock<std::mutex> lock(completionMutex);
165
- if (isComplete) {
166
- break;
167
- }
168
- }
169
- CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true);
170
- }
161
+ PumpRunLoopUntilComplete(completionMutex, isComplete);
171
162
 
172
163
  return true;
173
164
  }
@@ -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
@@ -1,5 +1,6 @@
1
1
  #include "protocol-impl.h"
2
2
  #include "debug.h"
3
+ #include "forwarding-common.h"
3
4
  #include "method-forwarding.h"
4
5
  #include "ObjcObject.h"
5
6
  #include "protocol-manager.h"
@@ -37,10 +38,7 @@ std::vector<std::string> ParseMethodSignature(const char *typeEncoding) {
37
38
  const char *typeStart = ptr;
38
39
 
39
40
  // Handle type qualifiers
40
- while (*ptr == 'r' || *ptr == 'n' || *ptr == 'N' || *ptr == 'o' ||
41
- *ptr == 'O' || *ptr == 'R' || *ptr == 'V') {
42
- ptr++;
43
- }
41
+ SkipTypeQualifiers(ptr);
44
42
 
45
43
  // Get the main type character
46
44
  if (*ptr) {
@@ -205,14 +203,8 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
205
203
  }
206
204
 
207
205
  // Create a ThreadSafeFunction for this callback
208
- Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
209
- env,
210
- jsCallback, // The JS function to call
211
- "ProtocolCallback", // Resource name
212
- 0, // Max queue size (0 = unlimited)
213
- 1, // Initial thread count
214
- [](Napi::Env) {} // Finalizer (no context to clean up)
215
- );
206
+ Napi::ThreadSafeFunction tsfn = CreateMethodTSFN(env, jsCallback,
207
+ "ProtocolCallback");
216
208
 
217
209
  // Store method info (TSFN, JS callback, type encoding) in single map
218
210
  impl.methods[selector] = ProtocolMethodInfo{
@@ -159,7 +159,7 @@ inline bool ParseStructFields(const char *&ptr,
159
159
  }
160
160
 
161
161
  // Parse the field's type encoding
162
- field.typeEncoding = SkipOneFieldEncoding(ptr);
162
+ field.typeEncoding = SkipOneTypeEncoding(ptr);
163
163
  field.isStruct = (!field.typeEncoding.empty() && field.typeEncoding[0] == '{');
164
164
 
165
165
  // If this is a nested struct, recursively parse its subfields
@@ -370,9 +370,8 @@ Napi::Value DefineClass(const Napi::CallbackInfo &info) {
370
370
  SEL selector = sel_registerName(selectorName.c_str());
371
371
 
372
372
  // Create ThreadSafeFunction
373
- Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
374
- env, jsImpl, "SubclassMethod_" + selectorName, 0, 1,
375
- [](Napi::Env) {});
373
+ Napi::ThreadSafeFunction tsfn = CreateMethodTSFN(
374
+ env, jsImpl, "SubclassMethod_" + selectorName);
376
375
 
377
376
  // Store method info
378
377
  SubclassMethodInfo methodInfo{
@@ -35,6 +35,26 @@
35
35
 
36
36
  // MARK: - Type Encoding Utilities
37
37
 
38
+ /**
39
+ * Check if a character is an ObjC type qualifier.
40
+ * r=const, n=in, N=inout, o=out, O=bycopy, R=byref, V=oneway
41
+ */
42
+ inline bool IsTypeQualifier(char c) {
43
+ return c == 'r' || c == 'n' || c == 'N' || c == 'o' ||
44
+ c == 'O' || c == 'R' || c == 'V';
45
+ }
46
+
47
+ /**
48
+ * Advance a pointer past any leading ObjC type qualifiers (rnNoORV).
49
+ * This is the canonical helper for qualifier skipping — use it everywhere
50
+ * instead of inline while loops.
51
+ */
52
+ inline void SkipTypeQualifiers(const char *&ptr) {
53
+ while (*ptr && IsTypeQualifier(*ptr)) {
54
+ ++ptr;
55
+ }
56
+ }
57
+
38
58
  // Helper class to manage the lifetime of simplified type encodings
39
59
  // Optimized to use pointer offset instead of string::erase()
40
60
  class SimplifiedTypeEncoding {
@@ -42,21 +62,14 @@ private:
42
62
  const char* original;
43
63
  size_t offset; // Offset past any leading qualifiers
44
64
 
45
- // Check if a character is a type qualifier
46
- static bool IsQualifier(char c) {
47
- // r=const, n=in, N=inout, o=out, O=bycopy, R=byref, V=oneway
48
- return c == 'r' || c == 'n' || c == 'N' || c == 'o' ||
49
- c == 'O' || c == 'R' || c == 'V';
50
- }
51
-
52
65
  public:
53
66
  SimplifiedTypeEncoding(const char *typeEncoding)
54
67
  : original(typeEncoding), offset(0) {
55
68
  // Skip leading qualifiers using pointer arithmetic (O(k) where k = qualifier count)
56
69
  if (original) {
57
- while (original[offset] != '\0' && IsQualifier(original[offset])) {
58
- ++offset;
59
- }
70
+ const char *ptr = original;
71
+ SkipTypeQualifiers(ptr);
72
+ offset = ptr - original;
60
73
  }
61
74
  }
62
75
 
@@ -74,13 +87,8 @@ public:
74
87
  // Optimized to use pointer arithmetic instead of string mutations
75
88
  inline const char *SimplifyTypeEncoding(const char *typeEncoding) {
76
89
  if (!typeEncoding) return "";
77
-
78
- // Skip leading qualifiers using pointer arithmetic
79
90
  const char* ptr = typeEncoding;
80
- while (*ptr == 'r' || *ptr == 'n' || *ptr == 'N' || *ptr == 'o' ||
81
- *ptr == 'O' || *ptr == 'R' || *ptr == 'V') {
82
- ++ptr;
83
- }
91
+ SkipTypeQualifiers(ptr);
84
92
  return ptr;
85
93
  }
86
94
 
@@ -131,22 +139,54 @@ inline StructEncodingHeader ParseStructEncodingHeader(const char *encoding) {
131
139
  }
132
140
 
133
141
  /**
134
- * Advance a pointer past one complete field type encoding.
135
- * Handles nested structs {}, unions (), pointers ^, and simple types.
136
- * Returns the encoding string for the field.
142
+ * Advance a pointer past one complete type encoding.
143
+ *
144
+ * This is the unified type encoding parser used throughout the codebase.
145
+ * It handles all ObjC type encoding forms:
146
+ * - @? Block type (two-character encoding)
147
+ * - @?<...> Block with extended signature
148
+ * - @"..." Object with protocol/class name
149
+ * - @ Object type
150
+ * - {...} Struct type
151
+ * - (...) Union type
152
+ * - ^T Pointer to type T (recursive)
153
+ * - X Simple single-character type
154
+ *
155
+ * Does NOT skip leading type qualifiers or trailing offset digits —
156
+ * callers should use SkipTypeQualifiers() and skip digits as needed.
157
+ *
158
+ * Returns the encoding string for the parsed type.
137
159
  */
138
- inline std::string SkipOneFieldEncoding(const char *&ptr) {
160
+ inline std::string SkipOneTypeEncoding(const char *&ptr) {
139
161
  const char *start = ptr;
140
162
 
141
- if (*ptr == '{') {
142
- // Nested structfind matching '}'
163
+ if (*ptr == '@' && *(ptr + 1) == '?') {
164
+ // Block type @? possibly with extended encoding @?<...>
165
+ ptr += 2;
166
+ if (*ptr == '<') {
167
+ int depth = 1;
168
+ ptr++;
169
+ while (*ptr && depth > 0) {
170
+ if (*ptr == '<') depth++;
171
+ else if (*ptr == '>') depth--;
172
+ ptr++;
173
+ }
174
+ }
175
+ } else if (*ptr == '@') {
176
+ // Object type — possibly with quoted class/protocol name @"NSString"
177
+ ptr++;
178
+ if (*ptr == '"') {
179
+ ptr++;
180
+ while (*ptr && *ptr != '"') ptr++;
181
+ if (*ptr == '"') ptr++;
182
+ }
183
+ } else if (*ptr == '{') {
184
+ // Struct — find matching '}'
143
185
  int depth = 1;
144
186
  ptr++;
145
187
  while (*ptr && depth > 0) {
146
- if (*ptr == '{')
147
- depth++;
148
- else if (*ptr == '}')
149
- depth--;
188
+ if (*ptr == '{') depth++;
189
+ else if (*ptr == '}') depth--;
150
190
  ptr++;
151
191
  }
152
192
  } else if (*ptr == '(') {
@@ -154,39 +194,27 @@ inline std::string SkipOneFieldEncoding(const char *&ptr) {
154
194
  int depth = 1;
155
195
  ptr++;
156
196
  while (*ptr && depth > 0) {
157
- if (*ptr == '(')
158
- depth++;
159
- else if (*ptr == ')')
160
- depth--;
197
+ if (*ptr == '(') depth++;
198
+ else if (*ptr == ')') depth--;
161
199
  ptr++;
162
200
  }
163
201
  } else if (*ptr == '^') {
164
- // Pointer type — skip '^' and the pointed-to type
202
+ // Pointer type — skip '^' and the pointed-to type (recursive)
165
203
  ptr++;
166
- if (*ptr == '{') {
167
- int depth = 1;
168
- ptr++;
169
- while (*ptr && depth > 0) {
170
- if (*ptr == '{')
171
- depth++;
172
- else if (*ptr == '}')
173
- depth--;
174
- ptr++;
175
- }
176
- } else if (*ptr) {
177
- ptr++;
178
- }
179
- } else {
180
- // Simple type — single character, skip any trailing digits
204
+ SkipOneTypeEncoding(ptr);
205
+ } else if (*ptr) {
206
+ // Simple type — single character (c, i, s, l, q, C, I, S, L, Q, f, d, B, etc.)
181
207
  ptr++;
182
- while (*ptr && isdigit(*ptr)) {
183
- ptr++;
184
- }
185
208
  }
186
209
 
187
210
  return std::string(start, ptr - start);
188
211
  }
189
212
 
213
+ // Backward-compatible alias (deprecated — use SkipOneTypeEncoding instead)
214
+ inline std::string SkipOneFieldEncoding(const char *&ptr) {
215
+ return SkipOneTypeEncoding(ptr);
216
+ }
217
+
190
218
  // MARK: - ObjC to JS Conversion
191
219
 
192
220
  // Visitor for converting ObjC values to JS