objc-js 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.d.ts +77 -1
- package/dist/index.js +321 -58
- package/dist/native.d.ts +2 -2
- package/dist/native.js +2 -2
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/node.napi.armv8.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/src/native/ObjcObject.h +40 -3
- package/src/native/ObjcObject.mm +630 -18
- package/src/native/bridge.h +6 -2
- package/src/native/call-function.h +274 -0
- package/src/native/forwarding-common.h +42 -5
- package/src/native/forwarding-common.mm +18 -15
- package/src/native/method-forwarding.mm +51 -55
- package/src/native/nobjc.mm +4 -0
- package/src/native/protocol-impl.mm +7 -6
- package/src/native/protocol-manager.h +9 -8
- package/src/native/protocol-storage.h +19 -8
- package/src/native/struct-utils.h +205 -5
- package/src/native/subclass-impl.mm +39 -37
- package/src/native/subclass-manager.h +10 -9
|
@@ -20,6 +20,7 @@ using nobjc::ProtocolManager;
|
|
|
20
20
|
// NOTE: This function takes ownership of data and must clean it up
|
|
21
21
|
void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
22
22
|
InvocationData *data) {
|
|
23
|
+
@autoreleasepool {
|
|
23
24
|
// Use RAII to ensure cleanup even if we return early or throw
|
|
24
25
|
// The guard will release the invocation and delete data
|
|
25
26
|
InvocationDataGuard guard(data);
|
|
@@ -128,6 +129,7 @@ void CallJSCallback(Napi::Env env, Napi::Function jsCallback,
|
|
|
128
129
|
NOBJC_LOG("CallJSCallback: Signaled completion for %s", data->selectorName.c_str());
|
|
129
130
|
|
|
130
131
|
// guard destructor cleans up invocation and data
|
|
132
|
+
} // @autoreleasepool
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
// MARK: - Fallback Helper
|
|
@@ -174,19 +176,18 @@ bool FallbackToTSFN(Napi::ThreadSafeFunction &tsfn, InvocationData *data,
|
|
|
174
176
|
|
|
175
177
|
// Override respondsToSelector to return YES for methods we implement
|
|
176
178
|
BOOL RespondsToSelector(id self, SEL _cmd, SEL selector) {
|
|
179
|
+
@autoreleasepool {
|
|
177
180
|
void *ptr = (__bridge void *)self;
|
|
178
181
|
|
|
179
|
-
// Check if this is one of our implemented methods
|
|
180
|
-
bool found = ProtocolManager::Instance().
|
|
182
|
+
// Check if this is one of our implemented methods (read-only, shared lock)
|
|
183
|
+
bool found = ProtocolManager::Instance().WithLockConst([ptr, selector](const auto& map) {
|
|
181
184
|
auto it = map.find(ptr);
|
|
182
185
|
if (it != map.end()) {
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
186
|
+
auto methodIt = it->second.methods.find(selector);
|
|
187
|
+
if (methodIt != it->second.methods.end()) {
|
|
188
|
+
// Cache type encoding for subsequent MethodSignatureForSelector call
|
|
189
|
+
GetForwardingCache().store(ptr, selector, methodIt->second.typeEncoding.c_str());
|
|
190
|
+
return true;
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
return false;
|
|
@@ -199,20 +200,29 @@ BOOL RespondsToSelector(id self, SEL _cmd, SEL selector) {
|
|
|
199
200
|
// For methods we don't implement, check if NSObject responds to them
|
|
200
201
|
// This handles standard NSObject methods like description, isEqual:, etc.
|
|
201
202
|
return [NSObject instancesRespondToSelector:selector];
|
|
203
|
+
} // @autoreleasepool
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
// Provide method signature for message forwarding
|
|
205
207
|
NSMethodSignature *MethodSignatureForSelector(id self, SEL _cmd, SEL selector) {
|
|
206
208
|
void *ptr = (__bridge void *)self;
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
// Check forwarding pipeline cache first (populated by RespondsToSelector)
|
|
211
|
+
auto& cache = GetForwardingCache();
|
|
212
|
+
if (cache.matches(ptr, selector)) {
|
|
213
|
+
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:cache.typeEncoding];
|
|
214
|
+
cache.invalidate();
|
|
215
|
+
if (sig != nil) {
|
|
216
|
+
return sig;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
NSMethodSignature *sig = ProtocolManager::Instance().WithLockConst([ptr, selector](const auto& map) -> NSMethodSignature* {
|
|
209
221
|
auto it = map.find(ptr);
|
|
210
222
|
if (it != map.end()) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (encIt != it->second.typeEncodings.end()) {
|
|
215
|
-
return [NSMethodSignature signatureWithObjCTypes:encIt->second.c_str()];
|
|
223
|
+
auto methodIt = it->second.methods.find(selector);
|
|
224
|
+
if (methodIt != it->second.methods.end()) {
|
|
225
|
+
return [NSMethodSignature signatureWithObjCTypes:methodIt->second.typeEncoding.c_str()];
|
|
216
226
|
}
|
|
217
227
|
}
|
|
218
228
|
return nil;
|
|
@@ -227,6 +237,7 @@ NSMethodSignature *MethodSignatureForSelector(id self, SEL _cmd, SEL selector) {
|
|
|
227
237
|
|
|
228
238
|
// Handle forwarded invocations
|
|
229
239
|
void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
240
|
+
@autoreleasepool {
|
|
230
241
|
if (!invocation) {
|
|
231
242
|
NOBJC_ERROR("ForwardInvocation called with nil invocation");
|
|
232
243
|
return;
|
|
@@ -237,14 +248,7 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
237
248
|
[invocation retain];
|
|
238
249
|
|
|
239
250
|
SEL selector = [invocation selector];
|
|
240
|
-
NSString *selectorString = NSStringFromSelector(selector);
|
|
241
|
-
if (!selectorString) {
|
|
242
|
-
NOBJC_ERROR("Failed to convert selector to string");
|
|
243
|
-
[invocation release];
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
251
|
|
|
247
|
-
std::string selectorName = [selectorString UTF8String];
|
|
248
252
|
void *ptr = (__bridge void *)self;
|
|
249
253
|
|
|
250
254
|
// Set up callbacks for protocol-specific storage access
|
|
@@ -253,25 +257,25 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
253
257
|
|
|
254
258
|
// Lookup context and acquire TSFN
|
|
255
259
|
callbacks.lookupContext = [](void *lookupKey,
|
|
256
|
-
|
|
257
|
-
return ProtocolManager::Instance().WithLock([lookupKey,
|
|
260
|
+
SEL sel) -> std::optional<ForwardingContext> {
|
|
261
|
+
return ProtocolManager::Instance().WithLock([lookupKey, sel](auto& map) -> std::optional<ForwardingContext> {
|
|
258
262
|
auto it = map.find(lookupKey);
|
|
259
263
|
if (it == map.end()) {
|
|
260
264
|
NOBJC_WARN("Protocol implementation not found for instance %p", lookupKey);
|
|
261
265
|
return std::nullopt;
|
|
262
266
|
}
|
|
263
267
|
|
|
264
|
-
auto
|
|
265
|
-
if (
|
|
266
|
-
NOBJC_WARN("Callback not found for selector %s",
|
|
268
|
+
auto methodIt = it->second.methods.find(sel);
|
|
269
|
+
if (methodIt == it->second.methods.end()) {
|
|
270
|
+
NOBJC_WARN("Callback not found for selector %s", sel_getName(sel));
|
|
267
271
|
return std::nullopt;
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
// Acquire the TSFN
|
|
271
|
-
Napi::ThreadSafeFunction tsfn =
|
|
275
|
+
Napi::ThreadSafeFunction tsfn = methodIt->second.tsfn;
|
|
272
276
|
napi_status acq_status = tsfn.Acquire();
|
|
273
277
|
if (acq_status != napi_ok) {
|
|
274
|
-
NOBJC_WARN("Failed to acquire ThreadSafeFunction for selector %s",
|
|
278
|
+
NOBJC_WARN("Failed to acquire ThreadSafeFunction for selector %s", sel_getName(sel));
|
|
275
279
|
return std::nullopt;
|
|
276
280
|
}
|
|
277
281
|
|
|
@@ -284,53 +288,46 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
284
288
|
ctx.superClassPtr = nullptr; // Not used for protocols
|
|
285
289
|
|
|
286
290
|
// Cache the JS callback reference to avoid mutex re-acquisition
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
ctx.cachedJsCallback = &jsCallbackIt->second;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
auto encIt = it->second.typeEncodings.find(selName);
|
|
293
|
-
if (encIt != it->second.typeEncodings.end()) {
|
|
294
|
-
ctx.typeEncoding = encIt->second;
|
|
295
|
-
}
|
|
291
|
+
ctx.cachedJsCallback = &methodIt->second.jsCallback;
|
|
292
|
+
ctx.typeEncoding = methodIt->second.typeEncoding;
|
|
296
293
|
|
|
297
294
|
return ctx;
|
|
298
295
|
});
|
|
299
296
|
};
|
|
300
297
|
|
|
301
298
|
// Get JS function for direct call path
|
|
302
|
-
callbacks.getJSFunction = [](void *lookupKey,
|
|
303
|
-
|
|
304
|
-
return ProtocolManager::Instance().WithLock([lookupKey,
|
|
299
|
+
callbacks.getJSFunction = [](void *lookupKey, SEL sel,
|
|
300
|
+
Napi::Env /*env*/) -> Napi::Function {
|
|
301
|
+
return ProtocolManager::Instance().WithLock([lookupKey, sel](auto& map) -> Napi::Function {
|
|
305
302
|
auto it = map.find(lookupKey);
|
|
306
303
|
if (it == map.end()) {
|
|
307
304
|
return Napi::Function();
|
|
308
305
|
}
|
|
309
306
|
|
|
310
|
-
auto
|
|
311
|
-
if (
|
|
307
|
+
auto methodIt = it->second.methods.find(sel);
|
|
308
|
+
if (methodIt == it->second.methods.end()) {
|
|
312
309
|
return Napi::Function();
|
|
313
310
|
}
|
|
314
311
|
|
|
315
|
-
return
|
|
312
|
+
return methodIt->second.jsCallback.Value();
|
|
316
313
|
});
|
|
317
314
|
};
|
|
318
315
|
|
|
319
316
|
// Re-acquire TSFN for fallback path
|
|
320
317
|
callbacks.reacquireTSFN = [](void *lookupKey,
|
|
321
|
-
|
|
322
|
-
return ProtocolManager::Instance().WithLock([lookupKey,
|
|
318
|
+
SEL sel) -> std::optional<Napi::ThreadSafeFunction> {
|
|
319
|
+
return ProtocolManager::Instance().WithLock([lookupKey, sel](auto& map) -> std::optional<Napi::ThreadSafeFunction> {
|
|
323
320
|
auto it = map.find(lookupKey);
|
|
324
321
|
if (it == map.end()) {
|
|
325
322
|
return std::nullopt;
|
|
326
323
|
}
|
|
327
324
|
|
|
328
|
-
auto
|
|
329
|
-
if (
|
|
325
|
+
auto methodIt = it->second.methods.find(sel);
|
|
326
|
+
if (methodIt == it->second.methods.end()) {
|
|
330
327
|
return std::nullopt;
|
|
331
328
|
}
|
|
332
329
|
|
|
333
|
-
Napi::ThreadSafeFunction tsfn =
|
|
330
|
+
Napi::ThreadSafeFunction tsfn = methodIt->second.tsfn;
|
|
334
331
|
napi_status acq_status = tsfn.Acquire();
|
|
335
332
|
if (acq_status != napi_ok) {
|
|
336
333
|
return std::nullopt;
|
|
@@ -340,7 +337,8 @@ void ForwardInvocation(id self, SEL _cmd, NSInvocation *invocation) {
|
|
|
340
337
|
});
|
|
341
338
|
};
|
|
342
339
|
|
|
343
|
-
ForwardInvocationCommon(invocation,
|
|
340
|
+
ForwardInvocationCommon(invocation, selector, ptr, callbacks);
|
|
341
|
+
} // @autoreleasepool
|
|
344
342
|
}
|
|
345
343
|
|
|
346
344
|
// Deallocation implementation
|
|
@@ -354,13 +352,11 @@ void DeallocImplementation(id self, SEL _cmd) {
|
|
|
354
352
|
// Release all ThreadSafeFunctions and JS callbacks
|
|
355
353
|
// Do this carefully to avoid issues during shutdown
|
|
356
354
|
try {
|
|
357
|
-
for (auto &pair : it->second.
|
|
355
|
+
for (auto &pair : it->second.methods) {
|
|
358
356
|
// Release the ThreadSafeFunction
|
|
359
|
-
pair.second.Release();
|
|
357
|
+
pair.second.tsfn.Release();
|
|
360
358
|
}
|
|
361
|
-
it->second.
|
|
362
|
-
it->second.jsCallbacks.clear();
|
|
363
|
-
it->second.typeEncodings.clear();
|
|
359
|
+
it->second.methods.clear();
|
|
364
360
|
} catch (...) {
|
|
365
361
|
// Ignore errors during cleanup
|
|
366
362
|
NOBJC_WARN("Exception during callback cleanup for instance %p", self);
|
package/src/native/nobjc.mm
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "ObjcObject.h"
|
|
2
|
+
#include "call-function.h"
|
|
2
3
|
#include "pointer-utils.h"
|
|
3
4
|
#include "protocol-impl.h"
|
|
4
5
|
#include "subclass-impl.h"
|
|
@@ -21,6 +22,7 @@ Napi::Value LoadLibrary(const Napi::CallbackInfo &info) {
|
|
|
21
22
|
|
|
22
23
|
Napi::Value GetClassObject(const Napi::CallbackInfo &info) {
|
|
23
24
|
Napi::Env env = info.Env();
|
|
25
|
+
@autoreleasepool {
|
|
24
26
|
if (info.Length() != 1 || !info[0].IsString()) {
|
|
25
27
|
throw Napi::TypeError::New(env, "Expected a single string argument");
|
|
26
28
|
}
|
|
@@ -31,6 +33,7 @@ Napi::Value GetClassObject(const Napi::CallbackInfo &info) {
|
|
|
31
33
|
return env.Undefined();
|
|
32
34
|
}
|
|
33
35
|
return ObjcObject::NewInstance(env, cls);
|
|
36
|
+
} // @autoreleasepool
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
Napi::Value GetPointer(const Napi::CallbackInfo &info) {
|
|
@@ -86,6 +89,7 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
|
|
|
86
89
|
Napi::Function::New(env, CreateProtocolImplementation));
|
|
87
90
|
exports.Set("DefineClass", Napi::Function::New(env, DefineClass));
|
|
88
91
|
exports.Set("CallSuper", Napi::Function::New(env, CallSuper));
|
|
92
|
+
exports.Set("CallFunction", Napi::Function::New(env, CallFunction));
|
|
89
93
|
return exports;
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -134,8 +134,7 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
134
134
|
|
|
135
135
|
// Store callbacks for this instance (we'll set the instance pointer later)
|
|
136
136
|
ProtocolImplementation impl{
|
|
137
|
-
.
|
|
138
|
-
.typeEncodings = {},
|
|
137
|
+
.methods = {},
|
|
139
138
|
.className = className,
|
|
140
139
|
.env = env,
|
|
141
140
|
.js_thread = pthread_self(), // Store the current (JS) thread ID
|
|
@@ -215,10 +214,12 @@ Napi::Value CreateProtocolImplementation(const Napi::CallbackInfo &info) {
|
|
|
215
214
|
[](Napi::Env) {} // Finalizer (no context to clean up)
|
|
216
215
|
);
|
|
217
216
|
|
|
218
|
-
// Store
|
|
219
|
-
impl.
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
// Store method info (TSFN, JS callback, type encoding) in single map
|
|
218
|
+
impl.methods[selector] = ProtocolMethodInfo{
|
|
219
|
+
.tsfn = tsfn,
|
|
220
|
+
.jsCallback = Napi::Persistent(jsCallback),
|
|
221
|
+
.typeEncoding = std::string(typeEncoding),
|
|
222
|
+
};
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// Override respondsToSelector
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
#include "protocol-storage.h"
|
|
26
26
|
#include <functional>
|
|
27
27
|
#include <mutex>
|
|
28
|
+
#include <shared_mutex>
|
|
28
29
|
#include <optional>
|
|
29
30
|
#include <unordered_map>
|
|
30
31
|
|
|
@@ -60,7 +61,7 @@ public:
|
|
|
60
61
|
* @note Caller must NOT hold the lock. This method acquires the lock.
|
|
61
62
|
*/
|
|
62
63
|
ProtocolImplementation* Find(void* instancePtr) {
|
|
63
|
-
std::
|
|
64
|
+
std::shared_lock<std::shared_mutex> lock(mutex_);
|
|
64
65
|
auto it = implementations_.find(instancePtr);
|
|
65
66
|
if (it != implementations_.end()) {
|
|
66
67
|
return &it->second;
|
|
@@ -74,7 +75,7 @@ public:
|
|
|
74
75
|
* @param impl The implementation to register (moved).
|
|
75
76
|
*/
|
|
76
77
|
void Register(void* instancePtr, ProtocolImplementation&& impl) {
|
|
77
|
-
std::
|
|
78
|
+
std::unique_lock<std::shared_mutex> lock(mutex_);
|
|
78
79
|
implementations_.emplace(instancePtr, std::move(impl));
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -84,7 +85,7 @@ public:
|
|
|
84
85
|
* @return true if the implementation was found and removed, false otherwise.
|
|
85
86
|
*/
|
|
86
87
|
bool Unregister(void* instancePtr) {
|
|
87
|
-
std::
|
|
88
|
+
std::unique_lock<std::shared_mutex> lock(mutex_);
|
|
88
89
|
return implementations_.erase(instancePtr) > 0;
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -98,7 +99,7 @@ public:
|
|
|
98
99
|
template <typename Callback>
|
|
99
100
|
auto WithLock(Callback&& callback)
|
|
100
101
|
-> decltype(callback(std::declval<std::unordered_map<void*, ProtocolImplementation>&>())) {
|
|
101
|
-
std::
|
|
102
|
+
std::unique_lock<std::shared_mutex> lock(mutex_);
|
|
102
103
|
return callback(implementations_);
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -109,7 +110,7 @@ public:
|
|
|
109
110
|
template <typename Callback>
|
|
110
111
|
auto WithLockConst(Callback&& callback) const
|
|
111
112
|
-> decltype(callback(std::declval<const std::unordered_map<void*, ProtocolImplementation>&>())) {
|
|
112
|
-
std::
|
|
113
|
+
std::shared_lock<std::shared_mutex> lock(mutex_);
|
|
113
114
|
return callback(implementations_);
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -118,7 +119,7 @@ public:
|
|
|
118
119
|
* @return Count of implementations.
|
|
119
120
|
*/
|
|
120
121
|
size_t Size() const {
|
|
121
|
-
std::
|
|
122
|
+
std::shared_lock<std::shared_mutex> lock(mutex_);
|
|
122
123
|
return implementations_.size();
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -128,7 +129,7 @@ public:
|
|
|
128
129
|
* @return true if registered, false otherwise.
|
|
129
130
|
*/
|
|
130
131
|
bool Contains(void* instancePtr) const {
|
|
131
|
-
std::
|
|
132
|
+
std::shared_lock<std::shared_mutex> lock(mutex_);
|
|
132
133
|
return implementations_.find(instancePtr) != implementations_.end();
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -136,7 +137,7 @@ private:
|
|
|
136
137
|
ProtocolManager() = default;
|
|
137
138
|
~ProtocolManager() = default;
|
|
138
139
|
|
|
139
|
-
mutable std::
|
|
140
|
+
mutable std::shared_mutex mutex_;
|
|
140
141
|
std::unordered_map<void*, ProtocolImplementation> implementations_;
|
|
141
142
|
};
|
|
142
143
|
|
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
#include <condition_variable>
|
|
5
5
|
#include <mutex>
|
|
6
6
|
#include <napi.h>
|
|
7
|
+
#include <objc/runtime.h>
|
|
7
8
|
#include <pthread.h>
|
|
8
9
|
#include <string>
|
|
9
10
|
#include <unordered_map>
|
|
10
11
|
|
|
12
|
+
// Hash functor for SEL (interned pointer — O(1) hash, O(1) compare)
|
|
13
|
+
struct SELHash {
|
|
14
|
+
size_t operator()(SEL sel) const noexcept {
|
|
15
|
+
return std::hash<const void *>{}(sel);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
11
19
|
// Forward declarations for Objective-C types
|
|
12
20
|
#ifdef __OBJC__
|
|
13
21
|
@class NSInvocation;
|
|
@@ -42,14 +50,17 @@ struct InvocationData {
|
|
|
42
50
|
void *superClassPtr;
|
|
43
51
|
};
|
|
44
52
|
|
|
53
|
+
// Information about a single protocol method (combines TSFN, JS callback, and type encoding)
|
|
54
|
+
struct ProtocolMethodInfo {
|
|
55
|
+
Napi::ThreadSafeFunction tsfn;
|
|
56
|
+
Napi::FunctionReference jsCallback;
|
|
57
|
+
std::string typeEncoding;
|
|
58
|
+
};
|
|
59
|
+
|
|
45
60
|
// Stores information about a protocol implementation instance
|
|
46
61
|
struct ProtocolImplementation {
|
|
47
|
-
//
|
|
48
|
-
std::unordered_map<
|
|
49
|
-
// Original JS functions for direct calls (kept alive by persistent refs)
|
|
50
|
-
std::unordered_map<std::string, Napi::FunctionReference> jsCallbacks;
|
|
51
|
-
// Type encodings for each selector
|
|
52
|
-
std::unordered_map<std::string, std::string> typeEncodings;
|
|
62
|
+
// All method info keyed by SEL (interned pointer — O(1) hash/compare)
|
|
63
|
+
std::unordered_map<SEL, ProtocolMethodInfo, SELHash> methods;
|
|
53
64
|
// Dynamically generated class name
|
|
54
65
|
std::string className;
|
|
55
66
|
// Store the environment for direct calls
|
|
@@ -79,8 +90,8 @@ struct SubclassImplementation {
|
|
|
79
90
|
void *objcClass; // Class
|
|
80
91
|
// The superclass for super calls
|
|
81
92
|
void *superClass; // Class
|
|
82
|
-
// Methods defined by JS (
|
|
83
|
-
std::unordered_map<
|
|
93
|
+
// Methods defined by JS (keyed by SEL — O(1) hash/compare)
|
|
94
|
+
std::unordered_map<SEL, SubclassMethodInfo, SELHash> methods;
|
|
84
95
|
// Store the environment for direct calls
|
|
85
96
|
napi_env env;
|
|
86
97
|
// Store the JS thread ID for thread detection
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
#include "type-conversion.h"
|
|
24
24
|
#include <Foundation/Foundation.h>
|
|
25
25
|
#include <map>
|
|
26
|
+
#include <unordered_map>
|
|
26
27
|
#include <napi.h>
|
|
27
28
|
#include <objc/runtime.h>
|
|
28
29
|
#include <string>
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
* field1, etc.
|
|
38
39
|
*/
|
|
39
40
|
// clang-format off
|
|
40
|
-
static const std::
|
|
41
|
+
static const std::unordered_map<std::string, std::vector<std::string>> KNOWN_STRUCT_FIELDS = {
|
|
41
42
|
// CoreGraphics / AppKit geometry
|
|
42
43
|
{"CGPoint", {"x", "y"}},
|
|
43
44
|
{"NSPoint", {"x", "y"}},
|
|
@@ -273,6 +274,24 @@ inline ParsedStructType ParseStructEncodingWithNames(const char *encoding) {
|
|
|
273
274
|
return result;
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
// MARK: - Cached Struct Encoding Lookup
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Return a cached ParsedStructType for the given encoding string.
|
|
281
|
+
* Avoids re-parsing identical struct type encodings (CGRect, NSRange, etc.)
|
|
282
|
+
* on every struct argument or return value.
|
|
283
|
+
*/
|
|
284
|
+
inline const ParsedStructType& GetOrParseStructEncoding(const char *encoding) {
|
|
285
|
+
static std::unordered_map<std::string, ParsedStructType> cache;
|
|
286
|
+
std::string key(encoding);
|
|
287
|
+
auto it = cache.find(key);
|
|
288
|
+
if (it != cache.end()) {
|
|
289
|
+
return it->second;
|
|
290
|
+
}
|
|
291
|
+
auto [inserted_it, _] = cache.emplace(key, ParseStructEncodingWithNames(encoding));
|
|
292
|
+
return inserted_it->second;
|
|
293
|
+
}
|
|
294
|
+
|
|
276
295
|
// MARK: - JS Object → Struct Buffer (for arguments)
|
|
277
296
|
|
|
278
297
|
/**
|
|
@@ -498,15 +517,178 @@ inline bool IsStructTypeEncoding(const char *typeEncoding) {
|
|
|
498
517
|
return *SimplifyTypeEncoding(typeEncoding) == '{';
|
|
499
518
|
}
|
|
500
519
|
|
|
520
|
+
// MARK: - Fast Path: Specialized Struct Pack/Unpack (H3)
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Extract struct name from a type encoding string.
|
|
524
|
+
* E.g., "{CGRect=...}" → "CGRect", "{_NSRange=QQ}" → "_NSRange"
|
|
525
|
+
* Returns empty string_view if encoding is malformed.
|
|
526
|
+
*/
|
|
527
|
+
inline std::string_view ExtractStructName(const char *encoding) {
|
|
528
|
+
if (!encoding || *encoding != '{') return {};
|
|
529
|
+
const char *start = encoding + 1;
|
|
530
|
+
const char *end = start;
|
|
531
|
+
while (*end && *end != '=' && *end != '}') end++;
|
|
532
|
+
return {start, static_cast<size_t>(end - start)};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Fast-path pack for CGPoint / NSPoint: { double x, y }
|
|
537
|
+
* Reads jsValue.x and jsValue.y directly into buffer.
|
|
538
|
+
* Returns true if handled, false to fall through to generic path.
|
|
539
|
+
*/
|
|
540
|
+
inline bool TryPackCGPoint(Napi::Env env, const Napi::Value &jsValue,
|
|
541
|
+
uint8_t *buffer) {
|
|
542
|
+
if (!jsValue.IsObject() || jsValue.IsArray()) return false;
|
|
543
|
+
Napi::Object obj = jsValue.As<Napi::Object>();
|
|
544
|
+
double x = obj.Get("x").As<Napi::Number>().DoubleValue();
|
|
545
|
+
double y = obj.Get("y").As<Napi::Number>().DoubleValue();
|
|
546
|
+
memcpy(buffer, &x, sizeof(double));
|
|
547
|
+
memcpy(buffer + sizeof(double), &y, sizeof(double));
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Fast-path unpack for CGPoint / NSPoint: { double x, y }
|
|
553
|
+
*/
|
|
554
|
+
inline Napi::Value TryUnpackCGPoint(Napi::Env env, const uint8_t *buffer) {
|
|
555
|
+
double x, y;
|
|
556
|
+
memcpy(&x, buffer, sizeof(double));
|
|
557
|
+
memcpy(&y, buffer + sizeof(double), sizeof(double));
|
|
558
|
+
Napi::Object result = Napi::Object::New(env);
|
|
559
|
+
result.Set("x", Napi::Number::New(env, x));
|
|
560
|
+
result.Set("y", Napi::Number::New(env, y));
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Fast-path pack for CGSize / NSSize: { double width, height }
|
|
566
|
+
*/
|
|
567
|
+
inline bool TryPackCGSize(Napi::Env env, const Napi::Value &jsValue,
|
|
568
|
+
uint8_t *buffer) {
|
|
569
|
+
if (!jsValue.IsObject() || jsValue.IsArray()) return false;
|
|
570
|
+
Napi::Object obj = jsValue.As<Napi::Object>();
|
|
571
|
+
double w = obj.Get("width").As<Napi::Number>().DoubleValue();
|
|
572
|
+
double h = obj.Get("height").As<Napi::Number>().DoubleValue();
|
|
573
|
+
memcpy(buffer, &w, sizeof(double));
|
|
574
|
+
memcpy(buffer + sizeof(double), &h, sizeof(double));
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Fast-path unpack for CGSize / NSSize: { double width, height }
|
|
580
|
+
*/
|
|
581
|
+
inline Napi::Value TryUnpackCGSize(Napi::Env env, const uint8_t *buffer) {
|
|
582
|
+
double w, h;
|
|
583
|
+
memcpy(&w, buffer, sizeof(double));
|
|
584
|
+
memcpy(&h, buffer + sizeof(double), sizeof(double));
|
|
585
|
+
Napi::Object result = Napi::Object::New(env);
|
|
586
|
+
result.Set("width", Napi::Number::New(env, w));
|
|
587
|
+
result.Set("height", Napi::Number::New(env, h));
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Fast-path pack for CGRect / NSRect: { CGPoint origin, CGSize size }
|
|
593
|
+
* Memory layout: [origin.x, origin.y, size.width, size.height] — 4 doubles
|
|
594
|
+
*/
|
|
595
|
+
inline bool TryPackCGRect(Napi::Env env, const Napi::Value &jsValue,
|
|
596
|
+
uint8_t *buffer) {
|
|
597
|
+
if (!jsValue.IsObject() || jsValue.IsArray()) return false;
|
|
598
|
+
Napi::Object obj = jsValue.As<Napi::Object>();
|
|
599
|
+
Napi::Object origin = obj.Get("origin").As<Napi::Object>();
|
|
600
|
+
Napi::Object size = obj.Get("size").As<Napi::Object>();
|
|
601
|
+
double vals[4];
|
|
602
|
+
vals[0] = origin.Get("x").As<Napi::Number>().DoubleValue();
|
|
603
|
+
vals[1] = origin.Get("y").As<Napi::Number>().DoubleValue();
|
|
604
|
+
vals[2] = size.Get("width").As<Napi::Number>().DoubleValue();
|
|
605
|
+
vals[3] = size.Get("height").As<Napi::Number>().DoubleValue();
|
|
606
|
+
memcpy(buffer, vals, sizeof(vals));
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Fast-path unpack for CGRect / NSRect.
|
|
612
|
+
*/
|
|
613
|
+
inline Napi::Value TryUnpackCGRect(Napi::Env env, const uint8_t *buffer) {
|
|
614
|
+
double vals[4];
|
|
615
|
+
memcpy(vals, buffer, sizeof(vals));
|
|
616
|
+
|
|
617
|
+
Napi::Object origin = Napi::Object::New(env);
|
|
618
|
+
origin.Set("x", Napi::Number::New(env, vals[0]));
|
|
619
|
+
origin.Set("y", Napi::Number::New(env, vals[1]));
|
|
620
|
+
|
|
621
|
+
Napi::Object size = Napi::Object::New(env);
|
|
622
|
+
size.Set("width", Napi::Number::New(env, vals[2]));
|
|
623
|
+
size.Set("height", Napi::Number::New(env, vals[3]));
|
|
624
|
+
|
|
625
|
+
Napi::Object result = Napi::Object::New(env);
|
|
626
|
+
result.Set("origin", origin);
|
|
627
|
+
result.Set("size", size);
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Fast-path pack for NSRange / _NSRange: { NSUInteger location, length }
|
|
633
|
+
* On 64-bit, NSUInteger = unsigned long long = 8 bytes.
|
|
634
|
+
*/
|
|
635
|
+
inline bool TryPackNSRange(Napi::Env env, const Napi::Value &jsValue,
|
|
636
|
+
uint8_t *buffer) {
|
|
637
|
+
if (!jsValue.IsObject() || jsValue.IsArray()) return false;
|
|
638
|
+
Napi::Object obj = jsValue.As<Napi::Object>();
|
|
639
|
+
uint64_t location = static_cast<uint64_t>(
|
|
640
|
+
obj.Get("location").As<Napi::Number>().Int64Value());
|
|
641
|
+
uint64_t length = static_cast<uint64_t>(
|
|
642
|
+
obj.Get("length").As<Napi::Number>().Int64Value());
|
|
643
|
+
memcpy(buffer, &location, sizeof(uint64_t));
|
|
644
|
+
memcpy(buffer + sizeof(uint64_t), &length, sizeof(uint64_t));
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Fast-path unpack for NSRange / _NSRange.
|
|
650
|
+
*/
|
|
651
|
+
inline Napi::Value TryUnpackNSRange(Napi::Env env, const uint8_t *buffer) {
|
|
652
|
+
uint64_t location, length;
|
|
653
|
+
memcpy(&location, buffer, sizeof(uint64_t));
|
|
654
|
+
memcpy(&length, buffer + sizeof(uint64_t), sizeof(uint64_t));
|
|
655
|
+
Napi::Object result = Napi::Object::New(env);
|
|
656
|
+
result.Set("location", Napi::Number::New(env, static_cast<double>(location)));
|
|
657
|
+
result.Set("length", Napi::Number::New(env, static_cast<double>(length)));
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
501
661
|
/**
|
|
502
662
|
* Pack a JS value into a newly allocated struct buffer.
|
|
503
663
|
* Returns a vector<uint8_t> that must be kept alive until after the
|
|
504
664
|
* NSInvocation is invoked.
|
|
665
|
+
*
|
|
666
|
+
* Tries specialized fast paths for CGRect/CGPoint/CGSize/NSRange first,
|
|
667
|
+
* falling through to the generic parser for other struct types.
|
|
505
668
|
*/
|
|
506
669
|
inline std::vector<uint8_t>
|
|
507
670
|
PackJSValueAsStruct(Napi::Env env, const Napi::Value &jsValue,
|
|
508
671
|
const char *typeEncoding) {
|
|
509
|
-
|
|
672
|
+
// Fast path: check struct name for well-known types
|
|
673
|
+
auto name = ExtractStructName(typeEncoding);
|
|
674
|
+
if (!name.empty()) {
|
|
675
|
+
if (name == "CGRect" || name == "NSRect") {
|
|
676
|
+
std::vector<uint8_t> buffer(4 * sizeof(double), 0);
|
|
677
|
+
if (TryPackCGRect(env, jsValue, buffer.data())) return buffer;
|
|
678
|
+
} else if (name == "CGPoint" || name == "NSPoint") {
|
|
679
|
+
std::vector<uint8_t> buffer(2 * sizeof(double), 0);
|
|
680
|
+
if (TryPackCGPoint(env, jsValue, buffer.data())) return buffer;
|
|
681
|
+
} else if (name == "CGSize" || name == "NSSize") {
|
|
682
|
+
std::vector<uint8_t> buffer(2 * sizeof(double), 0);
|
|
683
|
+
if (TryPackCGSize(env, jsValue, buffer.data())) return buffer;
|
|
684
|
+
} else if (name == "_NSRange" || name == "NSRange") {
|
|
685
|
+
std::vector<uint8_t> buffer(2 * sizeof(uint64_t), 0);
|
|
686
|
+
if (TryPackNSRange(env, jsValue, buffer.data())) return buffer;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Generic path: parse encoding and pack recursively
|
|
691
|
+
const ParsedStructType &parsed = GetOrParseStructEncoding(typeEncoding);
|
|
510
692
|
|
|
511
693
|
if (parsed.fields.empty()) {
|
|
512
694
|
throw Napi::Error::New(
|
|
@@ -520,11 +702,29 @@ PackJSValueAsStruct(Napi::Env env, const Napi::Value &jsValue,
|
|
|
520
702
|
|
|
521
703
|
/**
|
|
522
704
|
* Unpack a struct byte buffer into a JS object.
|
|
705
|
+
*
|
|
706
|
+
* Tries specialized fast paths for CGRect/CGPoint/CGSize/NSRange first,
|
|
707
|
+
* falling through to the generic parser for other struct types.
|
|
523
708
|
*/
|
|
524
709
|
inline Napi::Value UnpackStructToJSValue(Napi::Env env,
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
710
|
+
const uint8_t *buffer,
|
|
711
|
+
const char *typeEncoding) {
|
|
712
|
+
// Fast path: check struct name for well-known types
|
|
713
|
+
auto name = ExtractStructName(typeEncoding);
|
|
714
|
+
if (!name.empty()) {
|
|
715
|
+
if (name == "CGRect" || name == "NSRect") {
|
|
716
|
+
return TryUnpackCGRect(env, buffer);
|
|
717
|
+
} else if (name == "CGPoint" || name == "NSPoint") {
|
|
718
|
+
return TryUnpackCGPoint(env, buffer);
|
|
719
|
+
} else if (name == "CGSize" || name == "NSSize") {
|
|
720
|
+
return TryUnpackCGSize(env, buffer);
|
|
721
|
+
} else if (name == "_NSRange" || name == "NSRange") {
|
|
722
|
+
return TryUnpackNSRange(env, buffer);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Generic path
|
|
727
|
+
const ParsedStructType &parsed = GetOrParseStructEncoding(typeEncoding);
|
|
528
728
|
|
|
529
729
|
if (parsed.fields.empty()) {
|
|
530
730
|
NOBJC_ERROR("UnpackStructToJSValue: Failed to parse struct encoding '%s'",
|