objc-js 1.2.1 → 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 +1 -0
- package/dist/index.js +14 -0
- package/package.json +6 -3
- package/prebuilds/darwin-arm64/node.napi.armv8.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/src/native/ObjcObject.mm +49 -0
- package/src/native/ffi-utils.h +9 -1
- package/src/native/forwarding-common.h +45 -0
- package/src/native/forwarding-common.mm +2 -16
- package/src/native/method-forwarding.mm +1 -10
- package/src/native/nobjc_block.h +802 -0
- package/src/native/protocol-impl.mm +4 -12
- package/src/native/struct-utils.h +1 -1
- package/src/native/subclass-impl.mm +2 -3
- package/src/native/type-conversion.h +77 -49
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.
|
|
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
|
|
Binary file
|
package/src/native/ObjcObject.mm
CHANGED
|
@@ -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)
|
package/src/native/ffi-utils.h
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 ¶mType : 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
|
-
|
|
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 =
|
|
209
|
-
|
|
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 =
|
|
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 =
|
|
374
|
-
env, jsImpl, "SubclassMethod_" + selectorName
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
|
160
|
+
inline std::string SkipOneTypeEncoding(const char *&ptr) {
|
|
139
161
|
const char *start = ptr;
|
|
140
162
|
|
|
141
|
-
if (*ptr == '
|
|
142
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|