objc-js 1.4.0 → 1.5.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/dist/index.d.ts CHANGED
@@ -7,6 +7,37 @@ declare class NobjcObject {
7
7
  [key: string]: NobjcMethod;
8
8
  constructor(object: NobjcNative.ObjcObject);
9
9
  }
10
+ export interface TypedBlockOptions {
11
+ /**
12
+ * Block return type encoding. Example: "v" (void), "@" (id), "B" (BOOL).
13
+ */
14
+ returns: string;
15
+ /**
16
+ * Block argument type encodings, excluding the implicit block-self (@?) parameter.
17
+ * Example: ["@", "Q", "^B"] for enumerateObjectsUsingBlock:.
18
+ */
19
+ args?: string[];
20
+ /**
21
+ * Full block type encoding. Example: "@?<v@?@Q^B>".
22
+ * When provided, this takes precedence over returns/args.
23
+ */
24
+ types?: string;
25
+ }
26
+ /**
27
+ * Attach an explicit block signature to a JavaScript function.
28
+ *
29
+ * Use this when Objective-C only exposes `@?` for a block parameter and
30
+ * objc-js would otherwise have to guess the callback argument types.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const handler = typedBlock({ returns: "v", args: ["@", "Q", "^B"] }, (obj, idx, stop) => {
35
+ * console.log(obj.toString(), idx, stop);
36
+ * });
37
+ * array.enumerateObjectsUsingBlock$(handler);
38
+ * ```
39
+ */
40
+ declare function typedBlock<T extends (...args: any[]) => any>(signature: string | TypedBlockOptions, fn: T): T;
10
41
  interface NobjcMethod {
11
42
  (...args: any[]): any;
12
43
  }
@@ -297,4 +328,4 @@ declare const RunLoop: {
297
328
  */
298
329
  stop(): void;
299
330
  };
300
- export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, RunLoop, getPointer, fromPointer, callFunction, callVariadicFunction };
331
+ export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, typedBlock, RunLoop, getPointer, fromPointer, callFunction, callVariadicFunction };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction } from "./native.js";
2
2
  const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom");
3
3
  const NATIVE_OBJC_OBJECT = Symbol("nativeObjcObject");
4
+ const TYPED_BLOCK_ENCODING = "__nobjcBlockTypeEncoding";
4
5
  // WeakMap side-channel for O(1) proxy → native object lookup (bypasses Proxy traps)
5
6
  const nativeObjectMap = new WeakMap();
6
7
  // Module-scope Set for O(1) lookup instead of per-access array with O(n) .includes()
@@ -148,6 +149,44 @@ class NobjcObject {
148
149
  return proxy;
149
150
  }
150
151
  }
152
+ function normalizeTypedBlockEncoding(signature) {
153
+ if (typeof signature === "string") {
154
+ if (!signature.startsWith("@?")) {
155
+ throw new TypeError("typedBlock(string, fn) expects a full block type encoding starting with '@?'");
156
+ }
157
+ return signature;
158
+ }
159
+ if (signature.types !== undefined) {
160
+ if (!signature.types.startsWith("@?")) {
161
+ throw new TypeError("typedBlock({ types }, fn) expects a full block type encoding starting with '@?'");
162
+ }
163
+ return signature.types;
164
+ }
165
+ const args = signature.args ?? [];
166
+ return `@?<${signature.returns}@?${args.join("")}>`;
167
+ }
168
+ /**
169
+ * Attach an explicit block signature to a JavaScript function.
170
+ *
171
+ * Use this when Objective-C only exposes `@?` for a block parameter and
172
+ * objc-js would otherwise have to guess the callback argument types.
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * const handler = typedBlock({ returns: "v", args: ["@", "Q", "^B"] }, (obj, idx, stop) => {
177
+ * console.log(obj.toString(), idx, stop);
178
+ * });
179
+ * array.enumerateObjectsUsingBlock$(handler);
180
+ * ```
181
+ */
182
+ function typedBlock(signature, fn) {
183
+ Object.defineProperty(fn, TYPED_BLOCK_ENCODING, {
184
+ value: normalizeTypedBlockEncoding(signature),
185
+ enumerable: false,
186
+ configurable: true
187
+ });
188
+ return fn;
189
+ }
151
190
  function unwrapArg(arg) {
152
191
  if (arg && typeof arg === "object") {
153
192
  return nativeObjectMap.get(arg) ?? arg;
@@ -161,6 +200,14 @@ function unwrapArg(arg) {
161
200
  }
162
201
  return unwrapArg(arg(...nativeArgs));
163
202
  };
203
+ const explicitBlockEncoding = arg[TYPED_BLOCK_ENCODING];
204
+ if (typeof explicitBlockEncoding === "string") {
205
+ Object.defineProperty(wrapped, TYPED_BLOCK_ENCODING, {
206
+ value: explicitBlockEncoding,
207
+ enumerable: false,
208
+ configurable: true
209
+ });
210
+ }
164
211
  // Preserve the original function's .length so the native layer can read it
165
212
  // (used to infer block parameter count when extended encoding is unavailable)
166
213
  Object.defineProperty(wrapped, "length", { value: arg.length });
@@ -697,4 +744,4 @@ const RunLoop = {
697
744
  }
698
745
  }
699
746
  };
700
- export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, RunLoop, getPointer, fromPointer, callFunction, callVariadicFunction };
747
+ export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, typedBlock, RunLoop, getPointer, fromPointer, callFunction, callVariadicFunction };
package/package.json CHANGED
@@ -47,7 +47,7 @@
47
47
  "prebuild:x64": "prebuildify --napi --strip --arch x64 -r node",
48
48
  "prebuild:all": "bun run prebuild:arm64 && bun run prebuild:x64"
49
49
  },
50
- "version": "1.4.0",
50
+ "version": "1.5.0",
51
51
  "description": "Objective-C bridge for Node.js",
52
52
  "main": "dist/index.js",
53
53
  "dependencies": {
Binary file
@@ -47,11 +47,17 @@ struct PreparedSend {
47
47
  std::vector<ArgInfo> argInfos;
48
48
  };
49
49
 
50
+ struct NobjcEnvData {
51
+ Napi::FunctionReference objcObjectConstructor;
52
+ };
53
+
50
54
  class ObjcObject : public Napi::ObjectWrap<ObjcObject> {
51
55
  public:
52
56
  __strong id objcObject;
53
- static Napi::FunctionReference constructor;
54
57
  static void Init(Napi::Env env, Napi::Object exports);
58
+ static NobjcEnvData *GetEnvData(Napi::Env env);
59
+ static Napi::FunctionReference &GetConstructorRef(Napi::Env env);
60
+ static bool IsInstance(Napi::Env env, const Napi::Value &value);
55
61
  ObjcObject(const Napi::CallbackInfo &info)
56
62
  : Napi::ObjectWrap<ObjcObject>(info), objcObject(nil) {
57
63
  if (info.Length() == 1 && info[0].IsExternal()) {
@@ -311,7 +311,24 @@ struct ClassSELHash {
311
311
  static std::unordered_map<std::pair<Class, SEL>, NSMethodSignature *, ClassSELHash>
312
312
  methodSignatureCache;
313
313
 
314
- Napi::FunctionReference ObjcObject::constructor;
314
+ NobjcEnvData *ObjcObject::GetEnvData(Napi::Env env) {
315
+ NobjcEnvData *data = env.GetInstanceData<NobjcEnvData>();
316
+ if (data == nullptr) {
317
+ throw Napi::Error::New(env, "objc-js addon state is not initialized");
318
+ }
319
+ return data;
320
+ }
321
+
322
+ Napi::FunctionReference &ObjcObject::GetConstructorRef(Napi::Env env) {
323
+ return GetEnvData(env)->objcObjectConstructor;
324
+ }
325
+
326
+ bool ObjcObject::IsInstance(Napi::Env env, const Napi::Value &value) {
327
+ if (!value.IsObject()) {
328
+ return false;
329
+ }
330
+ return value.As<Napi::Object>().InstanceOf(GetConstructorRef(env).Value());
331
+ }
315
332
 
316
333
  void ObjcObject::Init(Napi::Env env, Napi::Object exports) {
317
334
  Napi::Function func =
@@ -323,8 +340,7 @@ void ObjcObject::Init(Napi::Env env, Napi::Object exports) {
323
340
  InstanceMethod("$msgSendPrepared", &ObjcObject::$MsgSendPrepared),
324
341
  InstanceMethod("$getPointer", &ObjcObject::GetPointer),
325
342
  });
326
- constructor = Napi::Persistent(func);
327
- constructor.SuppressDestruct();
343
+ GetConstructorRef(env) = Napi::Persistent(func);
328
344
  exports.Set("ObjcObject", func);
329
345
  }
330
346
 
@@ -332,7 +348,8 @@ Napi::Object ObjcObject::NewInstance(Napi::Env env, id obj) {
332
348
  Napi::EscapableHandleScope scope(env);
333
349
  // `obj` is already a pointer, technically, but the Napi::External
334
350
  // API expects a pointer, so we have to pointer to the pointer.
335
- Napi::Object jsObj = constructor.New({Napi::External<id>::New(env, &obj)});
351
+ Napi::Object jsObj =
352
+ GetConstructorRef(env).New({Napi::External<id>::New(env, &obj)});
336
353
  return scope.Escape(jsObj).ToObject();
337
354
  }
338
355
 
@@ -181,7 +181,7 @@ T ConvertToNativeValue(const Napi::Value &value,
181
181
  // is value an ObjcObject instance?
182
182
  if (value.IsObject()) {
183
183
  Napi::Object obj = value.As<Napi::Object>();
184
- if (obj.InstanceOf(ObjcObject::constructor.Value())) {
184
+ if (ObjcObject::IsInstance(value.Env(), obj)) {
185
185
  ObjcObject *objcObj = Napi::ObjectWrap<ObjcObject>::Unwrap(obj);
186
186
  return objcObj->objcObject;
187
187
  }
@@ -43,7 +43,7 @@ Napi::Value GetPointer(const Napi::CallbackInfo &info) {
43
43
  }
44
44
 
45
45
  Napi::Object obj = info[0].As<Napi::Object>();
46
- if (!obj.InstanceOf(ObjcObject::constructor.Value())) {
46
+ if (!ObjcObject::IsInstance(env, obj)) {
47
47
  throw Napi::TypeError::New(env, "Argument must be an ObjcObject instance");
48
48
  }
49
49
 
@@ -103,7 +103,12 @@ Napi::Value PumpRunLoop(const Napi::CallbackInfo &info) {
103
103
  }
104
104
  }
105
105
 
106
+ static void CleanupEnvData(napi_env, void *data, void *) {
107
+ delete static_cast<NobjcEnvData *>(data);
108
+ }
109
+
106
110
  Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
111
+ napi_set_instance_data(env, new NobjcEnvData(), CleanupEnvData, nullptr);
107
112
  ObjcObject::Init(env, exports);
108
113
  exports.Set("LoadLibrary", Napi::Function::New(env, LoadLibrary));
109
114
  exports.Set("GetClassObject", Napi::Function::New(env, GetClassObject));
@@ -40,6 +40,7 @@
40
40
  #include <Block.h>
41
41
  #include <Foundation/Foundation.h>
42
42
  #include <atomic>
43
+ #include <dlfcn.h>
43
44
  #include <ffi.h>
44
45
  #include <napi.h>
45
46
  #include <objc/runtime.h>
@@ -316,6 +317,8 @@ struct BlockInfo {
316
317
  }
317
318
  };
318
319
 
320
+ constexpr const char *kTypedBlockEncodingProperty = "__nobjcBlockTypeEncoding";
321
+
319
322
  // MARK: - Block Call Data (transient, for cross-thread invocation)
320
323
 
321
324
  /**
@@ -400,7 +403,64 @@ inline void NobjcBlockDisposeHelper(const void *src) {
400
403
  * 1. Tagged pointers (arm64: high bit set) are always valid objects
401
404
  * 2. Use malloc_zone_from_ptr() to check if it's a heap allocation
402
405
  * 3. If it is, verify it has a valid class pointer
406
+ * 4. Fall back to image-backed singleton detection for constant objects like
407
+ * __NSArray0 that live in the dyld shared cache instead of malloc heap
403
408
  */
409
+ inline bool PointerResolvesToLoadedImage(const void *ptr) {
410
+ if (!ptr) return false;
411
+ Dl_info info;
412
+ return dladdr(ptr, &info) != 0;
413
+ }
414
+
415
+ inline uintptr_t GetObjCIsaClassMask() {
416
+ static uintptr_t mask = []() -> uintptr_t {
417
+ void *symbol = dlsym(RTLD_DEFAULT, "objc_debug_isa_class_mask");
418
+ if (symbol) {
419
+ return *static_cast<const uintptr_t *>(symbol);
420
+ }
421
+
422
+ #if defined(__aarch64__) || defined(__arm64__)
423
+ return 0x0000000ffffffff8ULL;
424
+ #elif defined(__x86_64__)
425
+ return 0x00007ffffffffff8ULL;
426
+ #else
427
+ return 0;
428
+ #endif
429
+ }();
430
+ return mask;
431
+ }
432
+
433
+ inline bool LooksLikeImageBackedObjCObject(uintptr_t val) {
434
+ void *ptr = reinterpret_cast<void *>(val);
435
+ if (!PointerResolvesToLoadedImage(ptr)) return false;
436
+
437
+ // Image-backed singleton objects (for example __NSArray0) are not malloc
438
+ // allocations, so inspect the isa word and see if it resolves to something
439
+ // that also looks like a class pointer in a loaded image.
440
+ uintptr_t isaBits = *reinterpret_cast<const uintptr_t *>(ptr);
441
+ if (isaBits < 4096) return false;
442
+
443
+ const uintptr_t isaMask = GetObjCIsaClassMask();
444
+ const uintptr_t isaCandidates[] = {
445
+ isaBits,
446
+ isaMask == 0 ? 0 : (isaBits & isaMask),
447
+ };
448
+
449
+ for (uintptr_t candidate : isaCandidates) {
450
+ if (candidate < 4096 || (candidate & (alignof(void *) - 1)) != 0) {
451
+ continue;
452
+ }
453
+
454
+ void *candidatePtr = reinterpret_cast<void *>(candidate);
455
+ if (malloc_zone_from_ptr(candidatePtr) ||
456
+ PointerResolvesToLoadedImage(candidatePtr)) {
457
+ return true;
458
+ }
459
+ }
460
+
461
+ return false;
462
+ }
463
+
404
464
  inline bool LooksLikeObjCObject(uintptr_t val) {
405
465
  if (val == 0) return false; // nil
406
466
 
@@ -415,12 +475,14 @@ inline bool LooksLikeObjCObject(uintptr_t val) {
415
475
  // malloc_zone_from_ptr returns non-NULL only for valid heap allocations.
416
476
  void *ptr = (void *)val;
417
477
  malloc_zone_t *zone = malloc_zone_from_ptr(ptr);
418
- if (!zone) return false;
478
+ if (zone) {
479
+ // It's a heap allocation — very likely an ObjC object.
480
+ // Do a final check: object_getClass should return a valid class.
481
+ Class cls = object_getClass((__bridge id)ptr);
482
+ if (cls != nil) return true;
483
+ }
419
484
 
420
- // It's a heap allocation — very likely an ObjC object.
421
- // Do a final check: object_getClass should return a valid class.
422
- Class cls = object_getClass((__bridge id)ptr);
423
- return cls != nil;
485
+ return LooksLikeImageBackedObjCObject(val);
424
486
  }
425
487
 
426
488
  /**
@@ -530,7 +592,7 @@ inline void SetBlockReturnFromJS(Napi::Value result, void *returnPtr,
530
592
  id objcVal = nil;
531
593
  if (result.IsObject()) {
532
594
  Napi::Object obj = result.As<Napi::Object>();
533
- if (obj.InstanceOf(ObjcObject::constructor.Value())) {
595
+ if (ObjcObject::IsInstance(result.Env(), obj)) {
534
596
  ObjcObject *wrapper = Napi::ObjectWrap<ObjcObject>::Unwrap(obj);
535
597
  objcVal = wrapper->objcObject;
536
598
  }
@@ -718,8 +780,22 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
718
780
  const char *typeEncoding) {
719
781
  NOBJC_LOG("CreateBlockFromJSFunction: encoding='%s'", typeEncoding);
720
782
 
783
+ std::string explicitTypeEncoding;
784
+ if (jsFunction.IsFunction()) {
785
+ Napi::Value encodingValue =
786
+ jsFunction.As<Napi::Function>().Get(kTypedBlockEncodingProperty);
787
+ if (encodingValue.IsString()) {
788
+ explicitTypeEncoding = encodingValue.As<Napi::String>().Utf8Value();
789
+ NOBJC_LOG("CreateBlockFromJSFunction: using explicit encoding='%s'",
790
+ explicitTypeEncoding.c_str());
791
+ }
792
+ }
793
+
794
+ const char *effectiveTypeEncoding =
795
+ explicitTypeEncoding.empty() ? typeEncoding : explicitTypeEncoding.c_str();
796
+
721
797
  // Parse the block signature
722
- BlockSignature sig = ParseBlockSignature(typeEncoding);
798
+ BlockSignature sig = ParseBlockSignature(effectiveTypeEncoding);
723
799
  if (!sig.valid) {
724
800
  // No extended encoding — infer from JS function's .length
725
801
  // All params are treated as pointer-sized (heuristic detection in callback)
@@ -728,7 +804,7 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
728
804
 
729
805
  NOBJC_LOG("CreateBlockFromJSFunction: No extended block encoding, "
730
806
  "inferring %u params from JS function.length. Encoding: '%s'",
731
- jsParamCount, typeEncoding);
807
+ jsParamCount, effectiveTypeEncoding);
732
808
 
733
809
  sig.returnType = "v"; // Assume void return
734
810
  sig.paramTypes.clear();
@@ -279,7 +279,7 @@ Napi::Value DefineClass(const Napi::CallbackInfo &info) {
279
279
  }
280
280
  } else if (superValue.IsObject()) {
281
281
  Napi::Object superObj = superValue.As<Napi::Object>();
282
- if (superObj.InstanceOf(ObjcObject::constructor.Value())) {
282
+ if (ObjcObject::IsInstance(env, superObj)) {
283
283
  ObjcObject *objcObj = Napi::ObjectWrap<ObjcObject>::Unwrap(superObj);
284
284
  superClass = (Class)objcObj->objcObject;
285
285
  }
@@ -627,7 +627,7 @@ Napi::Value CallSuper(const Napi::CallbackInfo &info) {
627
627
  throw Napi::TypeError::New(env, "First argument must be an ObjcObject (self)");
628
628
  }
629
629
  Napi::Object selfObj = info[0].As<Napi::Object>();
630
- if (!selfObj.InstanceOf(ObjcObject::constructor.Value())) {
630
+ if (!ObjcObject::IsInstance(env, selfObj)) {
631
631
  throw Napi::TypeError::New(env, "First argument must be an ObjcObject (self)");
632
632
  }
633
633
  ObjcObject *selfWrapper = Napi::ObjectWrap<ObjcObject>::Unwrap(selfObj);
@@ -583,7 +583,7 @@ inline void SetInvocationReturnFromJS(NSInvocation *invocation,
583
583
  case '@': {
584
584
  if (result.IsObject()) {
585
585
  Napi::Object resultObj = result.As<Napi::Object>();
586
- if (resultObj.InstanceOf(ObjcObject::constructor.Value())) {
586
+ if (ObjcObject::IsInstance(result.Env(), resultObj)) {
587
587
  ObjcObject *objcObj = Napi::ObjectWrap<ObjcObject>::Unwrap(resultObj);
588
588
  id objcValue = objcObj->objcObject;
589
589
  [invocation setReturnValue:&objcValue];