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 +44 -0
- package/dist/index.d.ts +80 -1
- package/dist/index.js +140 -1
- package/dist/native.d.ts +2 -2
- package/dist/native.js +2 -2
- package/package.json +4 -3
- package/prebuilds/darwin-arm64/node.napi.armv8.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/src/native/ObjcObject.h +7 -1
- package/src/native/ObjcObject.mm +42 -5
- package/src/native/bridge.h +1 -1
- package/src/native/nobjc.mm +31 -1
- package/src/native/nobjc_block.h +179 -36
- package/src/native/subclass-impl.mm +2 -2
- package/src/native/type-conversion.h +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
"test:node": "
|
|
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.
|
|
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
|
|
Binary file
|
package/src/native/ObjcObject.h
CHANGED
|
@@ -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()) {
|
package/src/native/ObjcObject.mm
CHANGED
|
@@ -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::
|
|
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
|
-
|
|
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 =
|
|
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
|
+
}
|
package/src/native/bridge.h
CHANGED
|
@@ -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 (
|
|
184
|
+
if (ObjcObject::IsInstance(value.Env(), obj)) {
|
|
185
185
|
ObjcObject *objcObj = Napi::ObjectWrap<ObjcObject>::Unwrap(obj);
|
|
186
186
|
return objcObj->objcObject;
|
|
187
187
|
}
|
package/src/native/nobjc.mm
CHANGED
|
@@ -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 (!
|
|
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
|
|
package/src/native/nobjc_block.h
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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: -
|
|
339
|
+
// MARK: - Block Lifetime Management
|
|
325
340
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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,
|
|
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 =
|
|
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 =
|
|
692
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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];
|