objc-js 1.3.1 → 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/README.md CHANGED
@@ -37,6 +37,7 @@ The documentation is organized into several guides:
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
39
  - **[Blocks](./docs/blocks.md)** - Passing JavaScript functions as Objective-C blocks (closures)
40
+ - **[Run Loop](./docs/run-loop.md)** - Pumping the CFRunLoop for async callback delivery (completion handlers, etc.)
40
41
  - **[Protocol Implementation](./docs/protocol-implementation.md)** - Creating delegate objects that implement protocols
41
42
  - **[API Reference](./docs/api-reference.md)** - Complete API documentation for all classes and functions
42
43
 
@@ -55,3 +56,46 @@ console.log(str.toString());
55
56
  ```
56
57
 
57
58
  For more examples and detailed guides, see the [documentation](./docs/basic-usage.md).
59
+
60
+ ## Companion Packages
61
+
62
+ objc-js has two companion packages for TypeScript types and pure-C framework bindings:
63
+
64
+ ### objcjs-types
65
+
66
+ Auto-generated TypeScript type definitions for macOS Objective-C frameworks. Provides IntelliSense and type checking for classes, protocols, enums, and methods across all major Apple frameworks.
67
+
68
+ ```bash
69
+ npm install objcjs-types
70
+ ```
71
+
72
+ ```typescript
73
+ import type { NSWindow, NSApplicationDelegate } from "objcjs-types/AppKit";
74
+ import type { CGPoint, CGSize, CGRect } from "objcjs-types/structs";
75
+ ```
76
+
77
+ ### objcjs-extra
78
+
79
+ Hand-written FFI bindings for macOS pure-C frameworks that have no Objective-C metadata (so they can't be auto-generated by objcjs-types). Works with both Bun (`bun:ffi`) and Node.js (`koffi`).
80
+
81
+ ```bash
82
+ npm install objcjs-extra koffi # Node.js
83
+ bun add objcjs-extra # Bun
84
+ ```
85
+
86
+ Provides bindings for CoreFoundation, CoreGraphics, ApplicationServices (Accessibility), Security, CoreServices (FSEvents, Launch Services), IOKit, CoreText, ImageIO, CoreAudio, Network, CoreMedia, and Accelerate.
87
+
88
+ ```typescript
89
+ import { AXUIElementCreateApplication, AXIsProcessTrusted } from "objcjs-extra/ApplicationServices";
90
+ import { CGEventCreateKeyboardEvent, CGEventPost } from "objcjs-extra/CoreGraphics";
91
+ import { getDefaultOutputDevice, setDeviceVolume } from "objcjs-extra/CoreAudio";
92
+ import { preventSleep } from "objcjs-extra/IOKit";
93
+ ```
94
+
95
+ ### When to use which package
96
+
97
+ | Need | Package |
98
+ | --------------------------------------------------------------------- | -------------- |
99
+ | Call Objective-C methods (NSWindow, NSString, etc.) | `objc-js` |
100
+ | TypeScript types for Objective-C APIs | `objcjs-types` |
101
+ | Pure-C frameworks (CoreFoundation, CoreGraphics, Accessibility, etc.) | `objcjs-extra` |
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
  }
@@ -249,4 +280,52 @@ declare function callFunction(name: string, ...rest: any[]): any;
249
280
  * ```
250
281
  */
251
282
  declare function callVariadicFunction(name: string, ...rest: any[]): any;
252
- export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, getPointer, fromPointer, callFunction, callVariadicFunction };
283
+ /**
284
+ * Utilities for pumping the macOS CFRunLoop from a Node.js/Bun event loop.
285
+ *
286
+ * Required for async Objective-C callbacks (completion handlers, AppKit events, etc.)
287
+ * to be delivered, since Node.js/Bun don't automatically pump the CFRunLoop.
288
+ *
289
+ * Usage:
290
+ * RunLoop.run(); // Start pumping (returns a cleanup function)
291
+ * RunLoop.pump(); // Pump once (non-blocking)
292
+ */
293
+ declare const RunLoop: {
294
+ _timer: ReturnType<typeof setInterval> | null;
295
+ _mainRunLoop: any;
296
+ _defaultMode: any;
297
+ _NSDate: any;
298
+ /**
299
+ * Lazily initialise NSRunLoop and NSDefaultRunLoopMode references.
300
+ * Foundation is already loaded by the time any ObjC work happens, so
301
+ * GetClassObject will succeed without an explicit LoadLibrary call.
302
+ *
303
+ * We use proxy-wrapped NobjcObjects (via wrapObjCObjectIfNeeded) rather
304
+ * than raw native ObjcObjects + $msgSend, because Bun's N-API crashes
305
+ * when CFRunLoopRunInMode is triggered from the $msgSend C++ path.
306
+ * The proxy path ($prepareSend + $msgSendPrepared) works on both runtimes.
307
+ */
308
+ _ensureRunLoop(): void;
309
+ /**
310
+ * Pump the run loop once. Processes any pending run loop sources
311
+ * (AppKit events, dispatch_async to main queue, timers, etc.)
312
+ * without blocking.
313
+ *
314
+ * @param timeout Optional timeout in seconds (default: 0)
315
+ * @returns true if a source was processed
316
+ */
317
+ pump(timeout?: number): boolean;
318
+ /**
319
+ * Start continuously pumping the run loop on a regular interval.
320
+ * This enables async Objective-C callbacks to be delivered.
321
+ *
322
+ * @param intervalMs Pump interval in milliseconds (default: 10)
323
+ * @returns A cleanup function that stops pumping
324
+ */
325
+ run(intervalMs?: number): () => void;
326
+ /**
327
+ * Stop pumping the run loop.
328
+ */
329
+ stop(): void;
330
+ };
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 });
@@ -605,4 +652,96 @@ function callVariadicFunction(name, ...rest) {
605
652
  const result = CallFunction(name, returnType, argTypes, fixedArgCount, ...args);
606
653
  return wrapObjCObjectIfNeeded(result);
607
654
  }
608
- export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, getPointer, fromPointer, callFunction, callVariadicFunction };
655
+ /**
656
+ * Utilities for pumping the macOS CFRunLoop from a Node.js/Bun event loop.
657
+ *
658
+ * Required for async Objective-C callbacks (completion handlers, AppKit events, etc.)
659
+ * to be delivered, since Node.js/Bun don't automatically pump the CFRunLoop.
660
+ *
661
+ * Usage:
662
+ * RunLoop.run(); // Start pumping (returns a cleanup function)
663
+ * RunLoop.pump(); // Pump once (non-blocking)
664
+ */
665
+ const RunLoop = {
666
+ _timer: null,
667
+ _mainRunLoop: null,
668
+ _defaultMode: null,
669
+ _NSDate: null,
670
+ /**
671
+ * Lazily initialise NSRunLoop and NSDefaultRunLoopMode references.
672
+ * Foundation is already loaded by the time any ObjC work happens, so
673
+ * GetClassObject will succeed without an explicit LoadLibrary call.
674
+ *
675
+ * We use proxy-wrapped NobjcObjects (via wrapObjCObjectIfNeeded) rather
676
+ * than raw native ObjcObjects + $msgSend, because Bun's N-API crashes
677
+ * when CFRunLoopRunInMode is triggered from the $msgSend C++ path.
678
+ * The proxy path ($prepareSend + $msgSendPrepared) works on both runtimes.
679
+ */
680
+ _ensureRunLoop() {
681
+ if (this._mainRunLoop === null) {
682
+ // Ensure Foundation is loaded — NobjcLibrary uses lazy loading,
683
+ // so Foundation may not be loaded yet if no class has been accessed.
684
+ LoadLibrary("/System/Library/Frameworks/Foundation.framework/Foundation");
685
+ const NSRunLoopRaw = GetClassObject("NSRunLoop");
686
+ if (!NSRunLoopRaw) {
687
+ throw new Error("Foundation framework is not loaded. Create a NobjcLibrary for Foundation before using RunLoop.");
688
+ }
689
+ const NSRunLoop = wrapObjCObjectIfNeeded(NSRunLoopRaw);
690
+ this._mainRunLoop = NSRunLoop.mainRunLoop();
691
+ const NSStringRaw = GetClassObject("NSString");
692
+ const NSString = wrapObjCObjectIfNeeded(NSStringRaw);
693
+ this._defaultMode = NSString.stringWithUTF8String$("kCFRunLoopDefaultMode");
694
+ const NSDateRaw = GetClassObject("NSDate");
695
+ this._NSDate = wrapObjCObjectIfNeeded(NSDateRaw);
696
+ }
697
+ },
698
+ /**
699
+ * Pump the run loop once. Processes any pending run loop sources
700
+ * (AppKit events, dispatch_async to main queue, timers, etc.)
701
+ * without blocking.
702
+ *
703
+ * @param timeout Optional timeout in seconds (default: 0)
704
+ * @returns true if a source was processed
705
+ */
706
+ pump(timeout) {
707
+ this._ensureRunLoop();
708
+ const limitDate = this._NSDate.dateWithTimeIntervalSinceNow$(timeout ?? 0);
709
+ const handled = this._mainRunLoop.runMode$beforeDate$(this._defaultMode, limitDate);
710
+ return !!handled;
711
+ },
712
+ /**
713
+ * Start continuously pumping the run loop on a regular interval.
714
+ * This enables async Objective-C callbacks to be delivered.
715
+ *
716
+ * @param intervalMs Pump interval in milliseconds (default: 10)
717
+ * @returns A cleanup function that stops pumping
718
+ */
719
+ run(intervalMs = 10) {
720
+ if (this._timer !== null) {
721
+ clearInterval(this._timer);
722
+ }
723
+ // Eagerly initialise so the first interval tick is cheap
724
+ this._ensureRunLoop();
725
+ this._timer = setInterval(() => {
726
+ const limitDate = this._NSDate.dateWithTimeIntervalSinceNow$(0);
727
+ this._mainRunLoop.runMode$beforeDate$(this._defaultMode, limitDate);
728
+ }, intervalMs);
729
+ // Unref the timer so it doesn't prevent the process from exiting
730
+ // when there are no other active handles
731
+ if (this._timer && typeof this._timer === "object" && "unref" in this._timer) {
732
+ this._timer.unref();
733
+ }
734
+ const stop = () => this.stop();
735
+ return stop;
736
+ },
737
+ /**
738
+ * Stop pumping the run loop.
739
+ */
740
+ stop() {
741
+ if (this._timer !== null) {
742
+ clearInterval(this._timer);
743
+ this._timer = null;
744
+ }
745
+ }
746
+ };
747
+ export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, typedBlock, RunLoop, getPointer, fromPointer, callFunction, callVariadicFunction };
package/dist/native.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import * as _binding from "#nobjc_native";
2
- declare const LoadLibrary: typeof _binding.LoadLibrary, GetClassObject: typeof _binding.GetClassObject, ObjcObject: typeof _binding.ObjcObject, GetPointer: typeof _binding.GetPointer, FromPointer: typeof _binding.FromPointer, CreateProtocolImplementation: typeof _binding.CreateProtocolImplementation, DefineClass: typeof _binding.DefineClass, CallSuper: typeof _binding.CallSuper, CallFunction: typeof _binding.CallFunction;
3
- export { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction };
2
+ declare const LoadLibrary: typeof _binding.LoadLibrary, GetClassObject: typeof _binding.GetClassObject, ObjcObject: typeof _binding.ObjcObject, GetPointer: typeof _binding.GetPointer, FromPointer: typeof _binding.FromPointer, CreateProtocolImplementation: typeof _binding.CreateProtocolImplementation, DefineClass: typeof _binding.DefineClass, CallSuper: typeof _binding.CallSuper, CallFunction: typeof _binding.CallFunction, PumpRunLoop: typeof _binding.PumpRunLoop;
3
+ export { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction, PumpRunLoop };
4
4
  export type { _binding as NobjcNative };
package/dist/native.js CHANGED
@@ -5,5 +5,5 @@ const require = createRequire(import.meta.url);
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
7
  const binding = require("node-gyp-build")(join(__dirname, ".."));
8
- const { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction } = binding;
9
- export { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction };
8
+ const { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction, PumpRunLoop } = binding;
9
+ export { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction, PumpRunLoop };
package/package.json CHANGED
@@ -31,13 +31,14 @@
31
31
  "build": "npm run build-native && npm run build-scripts && npm run build-source",
32
32
  "pretest": "npm run build",
33
33
  "test": "bun run test:bun",
34
- "test:bun": "bun run build && bun test",
35
- "test:node": "bun run build && npx vitest run",
34
+ "test:bun": "bun test",
35
+ "test:node": "npx vitest run",
36
36
  "test:native": "bun test tests/test-native-code.test.ts",
37
37
  "test:js": "bun test tests/test-js-code.test.ts",
38
38
  "test:string-lifetime": "bun test tests/test-string-lifetime.test.ts",
39
39
  "test:object-arguments": "bun test tests/test-object-arguments.test.ts",
40
40
  "test:protocol-implementation": "bun test tests/test-protocol-implementation.test.ts",
41
+ "test:run-loop": "bun test tests/test-run-loop.test.ts",
41
42
  "bench": "bun run build && bun run benchmarks/bench.ts",
42
43
  "make-clangd-config": "node ./scripts/make-clangd-config.js",
43
44
  "format": "prettier --write \"**/*.{ts,js,json,md}\"",
@@ -46,7 +47,7 @@
46
47
  "prebuild:x64": "prebuildify --napi --strip --arch x64 -r node",
47
48
  "prebuild:all": "bun run prebuild:arm64 && bun run prebuild:x64"
48
49
  },
49
- "version": "1.3.1",
50
+ "version": "1.5.0",
50
51
  "description": "Objective-C bridge for Node.js",
51
52
  "main": "dist/index.js",
52
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
 
@@ -442,10 +459,19 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
442
459
  constexpr size_t kSmallArgCount = 4;
443
460
  ObjcType smallArgBuf[kSmallArgCount];
444
461
  std::vector<ObjcType> heapArgBuf;
462
+ std::vector<id> createdBlocks;
445
463
  const bool useHeap = expectedArgCount > kSmallArgCount;
446
464
  if (useHeap) {
447
465
  heapArgBuf.reserve(expectedArgCount);
448
466
  }
467
+ createdBlocks.reserve(expectedArgCount);
468
+ [[maybe_unused]] auto releaseCreatedBlocks = MakeScopeGuard([&createdBlocks] {
469
+ for (id block : createdBlocks) {
470
+ if (block != nil) {
471
+ _Block_release(block);
472
+ }
473
+ }
474
+ });
449
475
 
450
476
  // Store struct argument buffers to keep them alive until after invoke.
451
477
  std::vector<std::vector<uint8_t>> structBuffers;
@@ -492,6 +518,7 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
492
518
  id block = CreateBlockFromJSFunction(env, info[i], blockEncoding);
493
519
  if (env.IsExceptionPending()) return env.Null();
494
520
  [invocation setArgument:&block atIndex:i + 1];
521
+ createdBlocks.push_back(block);
495
522
  // Store block as id in arg buffer to keep it alive until after invoke
496
523
  if (useHeap) {
497
524
  heapArgBuf.push_back(BaseObjcType{block});
@@ -735,10 +762,19 @@ Napi::Value ObjcObject::$MsgSendPrepared(const Napi::CallbackInfo &info) {
735
762
  constexpr size_t kSmallArgCount = 4;
736
763
  ObjcType smallArgBuf[kSmallArgCount];
737
764
  std::vector<ObjcType> heapArgBuf;
765
+ std::vector<id> createdBlocks;
738
766
  const bool useHeap = prepared->expectedArgCount > kSmallArgCount;
739
767
  if (useHeap) {
740
768
  heapArgBuf.reserve(prepared->expectedArgCount);
741
769
  }
770
+ createdBlocks.reserve(prepared->expectedArgCount);
771
+ [[maybe_unused]] auto releaseCreatedBlocks = MakeScopeGuard([&createdBlocks] {
772
+ for (id block : createdBlocks) {
773
+ if (block != nil) {
774
+ _Block_release(block);
775
+ }
776
+ }
777
+ });
742
778
 
743
779
  std::vector<std::vector<uint8_t>> structBuffers;
744
780
 
@@ -779,6 +815,7 @@ Napi::Value ObjcObject::$MsgSendPrepared(const Napi::CallbackInfo &info) {
779
815
  id block = CreateBlockFromJSFunction(env, info[jsArgIdx], blockEncoding);
780
816
  if (env.IsExceptionPending()) return env.Null();
781
817
  [invocation setArgument:&block atIndex:i + 2];
818
+ createdBlocks.push_back(block);
782
819
  if (useHeap) {
783
820
  heapArgBuf.push_back(BaseObjcType{block});
784
821
  } else {
@@ -822,4 +859,4 @@ Napi::Value ObjcObject::$MsgSendPrepared(const Napi::CallbackInfo &info) {
822
859
  }
823
860
 
824
861
  return ConvertReturnValueToJSValue(env, invocation, prepared->methodSignature);
825
- }
862
+ }
@@ -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
 
@@ -79,7 +79,36 @@ Napi::Value FromPointer(const Napi::CallbackInfo &info) {
79
79
  return ObjcObject::NewInstance(env, reinterpret_cast<id>(ptr));
80
80
  }
81
81
 
82
+ Napi::Value PumpRunLoop(const Napi::CallbackInfo &info) {
83
+ Napi::Env env = info.Env();
84
+
85
+ // Default timeout
86
+ NSTimeInterval timeout = 0.0; // Don't block — just process pending sources
87
+
88
+ // Optional: accept a timeout in seconds as the first argument
89
+ if (info.Length() >= 1 && info[0].IsNumber()) {
90
+ timeout = info[0].As<Napi::Number>().DoubleValue();
91
+ }
92
+
93
+ // Pump the main run loop via NSRunLoop API.
94
+ // We use NSRunLoop instead of CFRunLoopRunInMode because the CF function
95
+ // crashes under Bun's N-API implementation (segfault in the CF call).
96
+ // NSRunLoop.runMode:beforeDate: is functionally equivalent and works
97
+ // correctly in both Node.js and Bun.
98
+ @autoreleasepool {
99
+ NSRunLoop *mainLoop = [NSRunLoop mainRunLoop];
100
+ NSDate *limitDate = [NSDate dateWithTimeIntervalSinceNow:timeout];
101
+ BOOL handled = [mainLoop runMode:NSDefaultRunLoopMode beforeDate:limitDate];
102
+ return Napi::Boolean::New(env, handled);
103
+ }
104
+ }
105
+
106
+ static void CleanupEnvData(napi_env, void *data, void *) {
107
+ delete static_cast<NobjcEnvData *>(data);
108
+ }
109
+
82
110
  Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
111
+ napi_set_instance_data(env, new NobjcEnvData(), CleanupEnvData, nullptr);
83
112
  ObjcObject::Init(env, exports);
84
113
  exports.Set("LoadLibrary", Napi::Function::New(env, LoadLibrary));
85
114
  exports.Set("GetClassObject", Napi::Function::New(env, GetClassObject));
@@ -90,6 +119,7 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
90
119
  exports.Set("DefineClass", Napi::Function::New(env, DefineClass));
91
120
  exports.Set("CallSuper", Napi::Function::New(env, CallSuper));
92
121
  exports.Set("CallFunction", Napi::Function::New(env, CallFunction));
122
+ exports.Set("PumpRunLoop", Napi::Function::New(env, PumpRunLoop));
93
123
  return exports;
94
124
  }
95
125
 
@@ -37,7 +37,10 @@
37
37
  #include "type-conversion.h"
38
38
  #include "struct-utils.h"
39
39
  #include "ffi-utils.h"
40
+ #include <Block.h>
40
41
  #include <Foundation/Foundation.h>
42
+ #include <atomic>
43
+ #include <dlfcn.h>
41
44
  #include <ffi.h>
42
45
  #include <napi.h>
43
46
  #include <objc/runtime.h>
@@ -51,10 +54,14 @@
51
54
 
52
55
  // MARK: - Block ABI Structures
53
56
 
54
- // Block ABI descriptor (minimum viable — no copy/dispose helpers)
57
+ struct BlockInfo;
58
+
59
+ // Block ABI descriptor with copy/dispose helpers for BlockInfo lifetime.
55
60
  struct NobjcBlockDescriptor {
56
61
  unsigned long reserved; // Always 0
57
62
  unsigned long size; // sizeof(NobjcBlockLiteral)
63
+ void (*copy_helper)(void *dst, const void *src);
64
+ void (*dispose_helper)(const void *src);
58
65
  };
59
66
 
60
67
  // Block ABI literal struct
@@ -65,6 +72,7 @@ struct NobjcBlockLiteral {
65
72
  int reserved; // Always 0
66
73
  void *invoke; // Function pointer (FFI closure)
67
74
  NobjcBlockDescriptor *descriptor;
75
+ BlockInfo *blockInfo; // Captured state retained/released by the block runtime
68
76
  };
69
77
 
70
78
  // _NSConcreteStackBlock is declared in <Block.h> (included via Foundation)
@@ -254,7 +262,7 @@ inline std::string GetExtendedBlockEncoding(Class cls, SEL selector, size_t argI
254
262
  * BlockInfo holds all state for a single JS-function-backed block.
255
263
  * It owns the FFI closure, CIF, arg types, JS function reference, and TSFN.
256
264
  *
257
- * Stored in a global registry for lifetime management (never freed in v1).
265
+ * Lifetime is tied to the Objective-C block copies plus any in-flight callbacks.
258
266
  */
259
267
  struct BlockInfo {
260
268
  // FFI closure and CIF
@@ -290,20 +298,27 @@ struct BlockInfo {
290
298
 
291
299
  // The heap-copied block (after _Block_copy)
292
300
  // 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
301
  void *heapBlock;
295
302
 
303
+ // One ref for the initially created block copy, plus one for each additional
304
+ // Objective-C block copy, plus one for each in-flight invocation.
305
+ std::atomic<size_t> refCount{1};
306
+
307
+ // Ensures we only release the TSFN's initial ref once.
308
+ std::atomic<bool> cleanupScheduled{false};
309
+
296
310
  ~BlockInfo() {
297
311
  // Free the FFI closure
298
312
  if (closure) {
299
313
  ffi_closure_free(closure);
300
314
  closure = nullptr;
301
315
  }
302
- // Note: tsfn and jsFunction cleanup is tricky across threads.
303
- // In v1, BlockInfo is never destroyed, so this is moot.
316
+ jsFunction.Reset();
304
317
  }
305
318
  };
306
319
 
320
+ constexpr const char *kTypedBlockEncodingProperty = "__nobjcBlockTypeEncoding";
321
+
307
322
  // MARK: - Block Call Data (transient, for cross-thread invocation)
308
323
 
309
324
  /**
@@ -321,15 +336,59 @@ struct BlockCallData {
321
336
  bool isComplete;
322
337
  };
323
338
 
324
- // MARK: - Global Block Registry
339
+ // MARK: - Block Lifetime Management
325
340
 
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;
341
+ constexpr int NOBJC_BLOCK_HAS_COPY_DISPOSE = (1 << 25);
342
+
343
+ inline void RetainBlockInfo(BlockInfo *info) {
344
+ if (!info) return;
345
+ info->refCount.fetch_add(1, std::memory_order_relaxed);
346
+ }
347
+
348
+ inline void BlockTSFNFinalize(Napi::Env /*env*/, BlockInfo *info,
349
+ BlockInfo * /*data*/) {
350
+ delete info;
351
+ }
352
+
353
+ inline void ScheduleBlockInfoCleanup(BlockInfo *info) {
354
+ if (!info) return;
355
+
356
+ bool expected = false;
357
+ if (!info->cleanupScheduled.compare_exchange_strong(
358
+ expected, true, std::memory_order_acq_rel)) {
359
+ return;
360
+ }
361
+
362
+ napi_status status = info->tsfn.Release();
363
+ if (status != napi_ok) {
364
+ NOBJC_ERROR("ScheduleBlockInfoCleanup: TSFN release failed (status=%d)", status);
365
+ }
366
+ }
367
+
368
+ inline void ReleaseBlockInfo(BlockInfo *info) {
369
+ if (!info) return;
370
+
371
+ size_t previous = info->refCount.fetch_sub(1, std::memory_order_acq_rel);
372
+ if (previous == 0) {
373
+ NOBJC_ERROR("ReleaseBlockInfo: refcount underflow");
374
+ return;
375
+ }
376
+ if (previous == 1) {
377
+ ScheduleBlockInfoCleanup(info);
378
+ }
379
+ }
380
+
381
+ inline void NobjcBlockCopyHelper(void *dst, const void *src) {
382
+ auto *dstBlock = static_cast<NobjcBlockLiteral *>(dst);
383
+ auto *srcBlock = static_cast<const NobjcBlockLiteral *>(src);
384
+ dstBlock->blockInfo = srcBlock->blockInfo;
385
+ RetainBlockInfo(dstBlock->blockInfo);
386
+ }
387
+
388
+ inline void NobjcBlockDisposeHelper(const void *src) {
389
+ auto *block = static_cast<const NobjcBlockLiteral *>(src);
390
+ ReleaseBlockInfo(block->blockInfo);
391
+ }
333
392
 
334
393
  // MARK: - Block Argument Conversion (ObjC → JS)
335
394
 
@@ -344,7 +403,64 @@ static std::mutex g_blockRegistryMutex;
344
403
  * 1. Tagged pointers (arm64: high bit set) are always valid objects
345
404
  * 2. Use malloc_zone_from_ptr() to check if it's a heap allocation
346
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
347
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
+
348
464
  inline bool LooksLikeObjCObject(uintptr_t val) {
349
465
  if (val == 0) return false; // nil
350
466
 
@@ -359,12 +475,14 @@ inline bool LooksLikeObjCObject(uintptr_t val) {
359
475
  // malloc_zone_from_ptr returns non-NULL only for valid heap allocations.
360
476
  void *ptr = (void *)val;
361
477
  malloc_zone_t *zone = malloc_zone_from_ptr(ptr);
362
- 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
+ }
363
484
 
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;
485
+ return LooksLikeImageBackedObjCObject(val);
368
486
  }
369
487
 
370
488
  /**
@@ -474,7 +592,7 @@ inline void SetBlockReturnFromJS(Napi::Value result, void *returnPtr,
474
592
  id objcVal = nil;
475
593
  if (result.IsObject()) {
476
594
  Napi::Object obj = result.As<Napi::Object>();
477
- if (obj.InstanceOf(ObjcObject::constructor.Value())) {
595
+ if (ObjcObject::IsInstance(result.Env(), obj)) {
478
596
  ObjcObject *wrapper = Napi::ObjectWrap<ObjcObject>::Unwrap(obj);
479
597
  objcVal = wrapper->objcObject;
480
598
  }
@@ -547,6 +665,8 @@ inline void BlockTSFNCallback(Napi::Env env, Napi::Function /*jsCallback*/,
547
665
  callData->isComplete = true;
548
666
  callData->completionCv.notify_one();
549
667
  }
668
+
669
+ ReleaseBlockInfo(info);
550
670
  }
551
671
 
552
672
  // MARK: - FFI Closure Callback (Block Invoke)
@@ -568,6 +688,7 @@ inline void BlockInvokeCallback(ffi_cif *cif, void *ret, void **args,
568
688
  return;
569
689
  }
570
690
 
691
+ RetainBlockInfo(info);
571
692
  bool is_js_thread = pthread_equal(pthread_self(), info->js_thread);
572
693
 
573
694
  if (is_js_thread) {
@@ -603,6 +724,7 @@ inline void BlockInvokeCallback(ffi_cif *cif, void *ret, void **args,
603
724
  NOBJC_ERROR("BlockInvokeCallback: unknown exception");
604
725
  }
605
726
  }
727
+ ReleaseBlockInfo(info);
606
728
  } else {
607
729
  // Cross-thread call via TSFN
608
730
  BlockCallData callData;
@@ -621,6 +743,7 @@ inline void BlockInvokeCallback(ffi_cif *cif, void *ret, void **args,
621
743
  napi_status acq_status = info->tsfn.Acquire();
622
744
  if (acq_status != napi_ok) {
623
745
  NOBJC_ERROR("BlockInvokeCallback: Failed to acquire TSFN");
746
+ ReleaseBlockInfo(info);
624
747
  return;
625
748
  }
626
749
 
@@ -630,6 +753,7 @@ inline void BlockInvokeCallback(ffi_cif *cif, void *ret, void **args,
630
753
 
631
754
  if (status != napi_ok) {
632
755
  NOBJC_ERROR("BlockInvokeCallback: TSFN call failed (status=%d)", status);
756
+ ReleaseBlockInfo(info);
633
757
  return;
634
758
  }
635
759
 
@@ -656,8 +780,22 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
656
780
  const char *typeEncoding) {
657
781
  NOBJC_LOG("CreateBlockFromJSFunction: encoding='%s'", typeEncoding);
658
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
+
659
797
  // Parse the block signature
660
- BlockSignature sig = ParseBlockSignature(typeEncoding);
798
+ BlockSignature sig = ParseBlockSignature(effectiveTypeEncoding);
661
799
  if (!sig.valid) {
662
800
  // No extended encoding — infer from JS function's .length
663
801
  // All params are treated as pointer-sized (heuristic detection in callback)
@@ -666,7 +804,7 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
666
804
 
667
805
  NOBJC_LOG("CreateBlockFromJSFunction: No extended block encoding, "
668
806
  "inferring %u params from JS function.length. Encoding: '%s'",
669
- jsParamCount, typeEncoding);
807
+ jsParamCount, effectiveTypeEncoding);
670
808
 
671
809
  sig.returnType = "v"; // Assume void return
672
810
  sig.paramTypes.clear();
@@ -677,7 +815,7 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
677
815
  }
678
816
 
679
817
  // Create BlockInfo
680
- auto blockInfo = std::make_unique<BlockInfo>();
818
+ auto *blockInfo = new BlockInfo();
681
819
  blockInfo->signature = sig;
682
820
  blockInfo->env = env;
683
821
  blockInfo->js_thread = pthread_self();
@@ -688,8 +826,15 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
688
826
  blockInfo->jsFunction = Napi::Persistent(jsFunction.As<Napi::Function>());
689
827
 
690
828
  // Create TSFN for cross-thread calls
691
- blockInfo->tsfn = CreateMethodTSFN(env, jsFunction.As<Napi::Function>(),
692
- "nobjc_block_tsfn");
829
+ blockInfo->tsfn = Napi::ThreadSafeFunction::New(
830
+ env,
831
+ jsFunction.As<Napi::Function>(),
832
+ "nobjc_block_tsfn",
833
+ 0,
834
+ 1,
835
+ blockInfo,
836
+ BlockTSFNFinalize,
837
+ blockInfo);
693
838
 
694
839
  // Build FFI types for the block invocation
695
840
  // Block invoke signature: returnType (blockSelf, param1, param2, ...)
@@ -731,6 +876,7 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
731
876
  if (!blockInfo->closure || !codePtr) {
732
877
  Napi::Error::New(env, "Failed to allocate FFI closure for block")
733
878
  .ThrowAsJavaScriptException();
879
+ ReleaseBlockInfo(blockInfo);
734
880
  return nil;
735
881
  }
736
882
 
@@ -743,10 +889,9 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
743
889
  blockInfo->argFFIPtrs.data());
744
890
 
745
891
  if (ffiStatus != FFI_OK) {
746
- ffi_closure_free(blockInfo->closure);
747
- blockInfo->closure = nullptr;
748
892
  Napi::Error::New(env, "ffi_prep_cif failed for block")
749
893
  .ThrowAsJavaScriptException();
894
+ ReleaseBlockInfo(blockInfo);
750
895
  return nil;
751
896
  }
752
897
 
@@ -755,32 +900,35 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
755
900
  blockInfo->closure,
756
901
  &blockInfo->cif,
757
902
  BlockInvokeCallback,
758
- blockInfo.get(), // userdata = BlockInfo*
903
+ blockInfo, // userdata = BlockInfo*
759
904
  codePtr);
760
905
 
761
906
  if (ffiStatus != FFI_OK) {
762
- ffi_closure_free(blockInfo->closure);
763
- blockInfo->closure = nullptr;
764
907
  Napi::Error::New(env, "ffi_prep_closure_loc failed for block")
765
908
  .ThrowAsJavaScriptException();
909
+ ReleaseBlockInfo(blockInfo);
766
910
  return nil;
767
911
  }
768
912
 
769
913
  // Build the block literal (stack block)
770
914
  blockInfo->descriptor.reserved = 0;
771
915
  blockInfo->descriptor.size = sizeof(NobjcBlockLiteral);
916
+ blockInfo->descriptor.copy_helper = NobjcBlockCopyHelper;
917
+ blockInfo->descriptor.dispose_helper = NobjcBlockDisposeHelper;
772
918
 
773
919
  blockInfo->blockLiteral.isa = _NSConcreteStackBlock;
774
- blockInfo->blockLiteral.flags = (1 << 30); // BLOCK_HAS_SIGNATURE (not strictly needed but harmless)
920
+ blockInfo->blockLiteral.flags = NOBJC_BLOCK_HAS_COPY_DISPOSE;
775
921
  blockInfo->blockLiteral.reserved = 0;
776
922
  blockInfo->blockLiteral.invoke = codePtr;
777
923
  blockInfo->blockLiteral.descriptor = &blockInfo->descriptor;
924
+ blockInfo->blockLiteral.blockInfo = blockInfo;
778
925
 
779
926
  // Copy to heap via _Block_copy
780
927
  void *heapBlockPtr = _Block_copy(&blockInfo->blockLiteral);
781
928
  if (!heapBlockPtr) {
782
929
  Napi::Error::New(env, "_Block_copy failed")
783
930
  .ThrowAsJavaScriptException();
931
+ ReleaseBlockInfo(blockInfo);
784
932
  return nil;
785
933
  }
786
934
 
@@ -788,12 +936,7 @@ inline id CreateBlockFromJSFunction(Napi::Env env,
788
936
  blockInfo->heapBlock = heapBlockPtr;
789
937
 
790
938
  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
- }
939
+ ReleaseBlockInfo(blockInfo);
797
940
 
798
941
  NOBJC_LOG("CreateBlockFromJSFunction: created block %p", result);
799
942
  return result;
@@ -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];