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.
@@ -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().WithLock([ptr, selector](auto& map) {
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
- NSString *selectorString = NSStringFromSelector(selector);
184
- if (selectorString != nil) {
185
- std::string selName = [selectorString UTF8String];
186
- auto callbackIt = it->second.callbacks.find(selName);
187
- if (callbackIt != it->second.callbacks.end()) {
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
- NSMethodSignature *sig = ProtocolManager::Instance().WithLock([ptr, selector](auto& map) -> NSMethodSignature* {
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
- NSString *selectorString = NSStringFromSelector(selector);
212
- std::string selName = [selectorString UTF8String];
213
- auto encIt = it->second.typeEncodings.find(selName);
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
- const std::string &selName) -> std::optional<ForwardingContext> {
257
- return ProtocolManager::Instance().WithLock([lookupKey, &selName](auto& map) -> std::optional<ForwardingContext> {
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 callbackIt = it->second.callbacks.find(selName);
265
- if (callbackIt == it->second.callbacks.end()) {
266
- NOBJC_WARN("Callback not found for selector %s", selName.c_str());
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 = callbackIt->second;
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", selName.c_str());
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
- auto jsCallbackIt = it->second.jsCallbacks.find(selName);
288
- if (jsCallbackIt != it->second.jsCallbacks.end()) {
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, const std::string &selName,
303
- Napi::Env /*env*/) -> Napi::Function {
304
- return ProtocolManager::Instance().WithLock([lookupKey, &selName](auto& map) -> Napi::Function {
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 jsCallbackIt = it->second.jsCallbacks.find(selName);
311
- if (jsCallbackIt == it->second.jsCallbacks.end()) {
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 jsCallbackIt->second.Value();
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
- const std::string &selName) -> std::optional<Napi::ThreadSafeFunction> {
322
- return ProtocolManager::Instance().WithLock([lookupKey, &selName](auto& map) -> std::optional<Napi::ThreadSafeFunction> {
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 callbackIt = it->second.callbacks.find(selName);
329
- if (callbackIt == it->second.callbacks.end()) {
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 = callbackIt->second;
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, selectorName, ptr, callbacks);
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.callbacks) {
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.callbacks.clear();
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);
@@ -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
- .callbacks = {},
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 both the TSFN and the original JS function
219
- impl.callbacks[selectorName] = tsfn;
220
- impl.jsCallbacks[selectorName] = Napi::Persistent(jsCallback);
221
- impl.typeEncodings[selectorName] = std::string(typeEncoding);
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::lock_guard<std::mutex> lock(mutex_);
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::lock_guard<std::mutex> lock(mutex_);
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::lock_guard<std::mutex> lock(mutex_);
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::lock_guard<std::mutex> lock(mutex_);
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::lock_guard<std::mutex> lock(mutex_);
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::lock_guard<std::mutex> lock(mutex_);
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::lock_guard<std::mutex> lock(mutex_);
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::mutex mutex_;
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
- // ThreadSafeFunction for each selector - allows calling JS from any thread
48
- std::unordered_map<std::string, Napi::ThreadSafeFunction> callbacks;
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 (selector -> info)
83
- std::unordered_map<std::string, SubclassMethodInfo> methods;
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::map<std::string, std::vector<std::string>> KNOWN_STRUCT_FIELDS = {
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
- ParsedStructType parsed = ParseStructEncodingWithNames(typeEncoding);
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
- const uint8_t *buffer,
526
- const char *typeEncoding) {
527
- ParsedStructType parsed = ParseStructEncodingWithNames(typeEncoding);
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'",