objc-js 1.1.0 → 1.2.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 +1 -0
- package/dist/index.d.ts +77 -1
- package/dist/index.js +321 -58
- package/dist/native.d.ts +2 -2
- package/dist/native.js +2 -2
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/node.napi.armv8.node +0 -0
- package/prebuilds/darwin-arm64/objc-js.napi.armv8.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/src/native/ObjcObject.h +40 -3
- package/src/native/ObjcObject.mm +630 -18
- package/src/native/bridge.h +6 -2
- package/src/native/call-function.h +274 -0
- package/src/native/forwarding-common.h +42 -5
- package/src/native/forwarding-common.mm +18 -15
- package/src/native/method-forwarding.mm +51 -55
- package/src/native/nobjc.mm +4 -0
- package/src/native/protocol-impl.mm +7 -6
- package/src/native/protocol-manager.h +9 -8
- package/src/native/protocol-storage.h +19 -8
- package/src/native/struct-utils.h +205 -5
- package/src/native/subclass-impl.mm +39 -37
- package/src/native/subclass-manager.h +10 -9
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ bun add objc-js
|
|
|
33
33
|
The documentation is organized into several guides:
|
|
34
34
|
|
|
35
35
|
- **[Basic Usage](./docs/basic-usage.md)** - Getting started with loading frameworks and calling methods
|
|
36
|
+
- **[C Functions](./docs/c-functions.md)** - Calling C functions like NSLog, NSHomeDirectory, NSStringFromClass
|
|
36
37
|
- **[Structs](./docs/structs.md)** - Passing and receiving C structs (CGRect, NSRange, etc.)
|
|
37
38
|
- **[Subclassing Objective-C Classes](./docs/subclassing.md)** - Creating and subclassing Objective-C classes from JavaScript
|
|
38
39
|
- **[Protocol Implementation](./docs/protocol-implementation.md)** - Creating delegate objects that implement protocols
|
package/dist/index.d.ts
CHANGED
|
@@ -173,4 +173,80 @@ declare class NobjcClass {
|
|
|
173
173
|
*/
|
|
174
174
|
static super(self: NobjcObject, selector: string, ...args: any[]): any;
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Call a C function by name.
|
|
178
|
+
*
|
|
179
|
+
* The framework containing the function must be loaded first (e.g., via `new NobjcLibrary(...)`).
|
|
180
|
+
* Uses `dlsym` to look up the function symbol and `libffi` to call it with the correct ABI.
|
|
181
|
+
*
|
|
182
|
+
* Argument types are inferred from JS values by default:
|
|
183
|
+
* - NobjcObject → `@` (id)
|
|
184
|
+
* - string → `@` (auto-converted to NSString)
|
|
185
|
+
* - boolean → `B` (BOOL)
|
|
186
|
+
* - number → `d` (double/CGFloat)
|
|
187
|
+
* - null → `@` (nil)
|
|
188
|
+
*
|
|
189
|
+
* Return type defaults to `"v"` (void). Pass an options object to specify return/arg types.
|
|
190
|
+
*
|
|
191
|
+
* @param name - The function name (e.g., "NSLog", "NSHomeDirectory")
|
|
192
|
+
* @param optionsOrFirstArg - Either a CallFunctionOptions object or the first function argument
|
|
193
|
+
* @param args - The actual arguments to pass to the function
|
|
194
|
+
* @returns The return value converted to a JavaScript type, or undefined for void functions
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* import { NobjcLibrary, callFunction } from "objc-js";
|
|
199
|
+
*
|
|
200
|
+
* const Foundation = new NobjcLibrary(
|
|
201
|
+
* "/System/Library/Frameworks/Foundation.framework/Foundation"
|
|
202
|
+
* );
|
|
203
|
+
* const NSString = Foundation.NSString;
|
|
204
|
+
*
|
|
205
|
+
* // Void function — simplest form, no options needed
|
|
206
|
+
* const msg = NSString.stringWithUTF8String$("Hello!");
|
|
207
|
+
* callFunction("NSLog", msg);
|
|
208
|
+
*
|
|
209
|
+
* // Function that returns a value — specify { returns }
|
|
210
|
+
* const homeDir = callFunction("NSHomeDirectory", { returns: "@" });
|
|
211
|
+
* console.log(homeDir.toString());
|
|
212
|
+
*
|
|
213
|
+
* // Explicit arg types when inference isn't enough
|
|
214
|
+
* const selName = callFunction("NSStringFromSelector", { returns: "@", args: [":"] }, "description");
|
|
215
|
+
*
|
|
216
|
+
* // Combined type string shorthand (return + args)
|
|
217
|
+
* const className = callFunction("NSStringFromClass", { types: "@#" }, NSString);
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
declare function callFunction(name: string, ...rest: any[]): any;
|
|
221
|
+
/**
|
|
222
|
+
* Call a variadic C function by name.
|
|
223
|
+
*
|
|
224
|
+
* Correctly handles variadic calling conventions (important on Apple Silicon / ARM64
|
|
225
|
+
* where variadic args go on the stack while fixed args go in registers).
|
|
226
|
+
*
|
|
227
|
+
* @param name - The function name (e.g., "NSLog")
|
|
228
|
+
* @param optionsOrFixedCount - Either a CallFunctionOptions object or the fixedArgCount directly
|
|
229
|
+
* @param fixedArgCountOrFirstArg - Number of fixed (non-variadic) arguments, or first arg if options were provided
|
|
230
|
+
* @param args - The actual arguments to pass to the function
|
|
231
|
+
* @returns The return value converted to a JavaScript type
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* import { NobjcLibrary, callVariadicFunction } from "objc-js";
|
|
236
|
+
*
|
|
237
|
+
* const Foundation = new NobjcLibrary(
|
|
238
|
+
* "/System/Library/Frameworks/Foundation.framework/Foundation"
|
|
239
|
+
* );
|
|
240
|
+
* const NSString = Foundation.NSString;
|
|
241
|
+
*
|
|
242
|
+
* // Simplest: void return, inferred args, fixedArgCount = 1
|
|
243
|
+
* const format = NSString.stringWithUTF8String$("Hello, %@!");
|
|
244
|
+
* const name = NSString.stringWithUTF8String$("World");
|
|
245
|
+
* callVariadicFunction("NSLog", 1, format, name);
|
|
246
|
+
*
|
|
247
|
+
* // With explicit types
|
|
248
|
+
* callVariadicFunction("NSLog", { returns: "v", args: ["@", "i"] }, 1, format, 42);
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
declare function callVariadicFunction(name: string, ...rest: any[]): any;
|
|
252
|
+
export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, getPointer, fromPointer, callFunction, callVariadicFunction };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
|
-
import { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper } from "./native.js";
|
|
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
|
+
// WeakMap side-channel for O(1) proxy → native object lookup (bypasses Proxy traps)
|
|
5
|
+
const nativeObjectMap = new WeakMap();
|
|
6
|
+
// Module-scope Set for O(1) lookup instead of per-access array with O(n) .includes()
|
|
7
|
+
const BUILT_IN_PROPS = new Set([
|
|
8
|
+
"constructor",
|
|
9
|
+
"valueOf",
|
|
10
|
+
"hasOwnProperty",
|
|
11
|
+
"isPrototypeOf",
|
|
12
|
+
"propertyIsEnumerable",
|
|
13
|
+
"toLocaleString",
|
|
14
|
+
"__proto__",
|
|
15
|
+
"__defineGetter__",
|
|
16
|
+
"__defineSetter__",
|
|
17
|
+
"__lookupGetter__",
|
|
18
|
+
"__lookupSetter__"
|
|
19
|
+
]);
|
|
20
|
+
// WeakMap cache for NobjcMethod proxies per object to avoid GC pressure
|
|
21
|
+
const methodCache = new WeakMap();
|
|
4
22
|
class NobjcLibrary {
|
|
5
23
|
constructor(library) {
|
|
24
|
+
const classCache = new Map();
|
|
6
25
|
const handler = {
|
|
7
26
|
wasLoaded: false,
|
|
8
27
|
get(_, className) {
|
|
28
|
+
let cls = classCache.get(className);
|
|
29
|
+
if (cls)
|
|
30
|
+
return cls;
|
|
9
31
|
if (!this.wasLoaded) {
|
|
10
32
|
LoadLibrary(library);
|
|
11
33
|
this.wasLoaded = true;
|
|
12
34
|
}
|
|
13
|
-
|
|
35
|
+
cls = new NobjcObject(GetClassObject(className));
|
|
36
|
+
classCache.set(className, cls);
|
|
37
|
+
return cls;
|
|
14
38
|
}
|
|
15
39
|
};
|
|
16
40
|
return new Proxy({}, handler);
|
|
@@ -39,7 +63,7 @@ class NobjcObject {
|
|
|
39
63
|
return true;
|
|
40
64
|
// check if the object responds to the selector
|
|
41
65
|
try {
|
|
42
|
-
return target.$
|
|
66
|
+
return target.$respondsToSelector(NobjcMethodNameToObjcSelector(p.toString()));
|
|
43
67
|
}
|
|
44
68
|
catch (e) {
|
|
45
69
|
return false;
|
|
@@ -50,53 +74,68 @@ class NobjcObject {
|
|
|
50
74
|
if (methodName === NATIVE_OBJC_OBJECT) {
|
|
51
75
|
return target;
|
|
52
76
|
}
|
|
77
|
+
// Handle customInspectSymbol in get trap instead of mutating native object
|
|
78
|
+
// (avoids hidden class transition that deoptimizes V8 inline caches)
|
|
79
|
+
if (methodName === customInspectSymbol) {
|
|
80
|
+
return () => proxy.toString();
|
|
81
|
+
}
|
|
53
82
|
// guard against symbols
|
|
54
83
|
if (typeof methodName === "symbol") {
|
|
55
84
|
return Reflect.get(object, methodName, receiver);
|
|
56
85
|
}
|
|
57
|
-
// handle toString separately
|
|
86
|
+
// handle toString separately (cached to avoid repeated closure allocation and FFI check)
|
|
58
87
|
if (methodName === "toString") {
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
88
|
+
let cache = methodCache.get(object);
|
|
89
|
+
if (!cache) {
|
|
90
|
+
cache = new Map();
|
|
91
|
+
methodCache.set(object, cache);
|
|
92
|
+
}
|
|
93
|
+
let fn = cache.get("toString");
|
|
94
|
+
if (!fn) {
|
|
95
|
+
// Check directly on native object to avoid triggering proxy has trap
|
|
96
|
+
const hasUTF8 = target.$respondsToSelector("UTF8String");
|
|
97
|
+
fn = (hasUTF8
|
|
98
|
+
? () => String(object.$msgSend("UTF8String"))
|
|
99
|
+
: () => String(wrapObjCObjectIfNeeded(object.$msgSend("description"))));
|
|
100
|
+
cache.set("toString", fn);
|
|
62
101
|
}
|
|
63
|
-
|
|
64
|
-
return () => String(wrapObjCObjectIfNeeded(object.$msgSend("description")));
|
|
102
|
+
return fn;
|
|
65
103
|
}
|
|
66
|
-
// handle other built-in Object.prototype properties
|
|
67
|
-
|
|
68
|
-
"constructor",
|
|
69
|
-
"valueOf",
|
|
70
|
-
"hasOwnProperty",
|
|
71
|
-
"isPrototypeOf",
|
|
72
|
-
"propertyIsEnumerable",
|
|
73
|
-
"toLocaleString",
|
|
74
|
-
"__proto__",
|
|
75
|
-
"__defineGetter__",
|
|
76
|
-
"__defineSetter__",
|
|
77
|
-
"__lookupGetter__",
|
|
78
|
-
"__lookupSetter__"
|
|
79
|
-
];
|
|
80
|
-
if (builtInProps.includes(methodName)) {
|
|
104
|
+
// handle other built-in Object.prototype properties (O(1) Set lookup)
|
|
105
|
+
if (BUILT_IN_PROPS.has(methodName)) {
|
|
81
106
|
return Reflect.get(target, methodName);
|
|
82
107
|
}
|
|
83
|
-
if
|
|
84
|
-
|
|
108
|
+
// Return cached method proxy if available, otherwise create and cache
|
|
109
|
+
let cache = methodCache.get(object);
|
|
110
|
+
if (!cache) {
|
|
111
|
+
cache = new Map();
|
|
112
|
+
methodCache.set(object, cache);
|
|
85
113
|
}
|
|
86
|
-
|
|
114
|
+
let method = cache.get(methodName);
|
|
115
|
+
if (!method) {
|
|
116
|
+
// Check respondsToSelector on cache miss only, directly on native
|
|
117
|
+
// object (avoids triggering proxy 'has' trap which would be a second FFI call)
|
|
118
|
+
const selector = NobjcMethodNameToObjcSelector(methodName);
|
|
119
|
+
if (!target.$respondsToSelector(selector)) {
|
|
120
|
+
throw new Error(`Method ${methodName} not found on object`);
|
|
121
|
+
}
|
|
122
|
+
method = NobjcMethod(object, methodName);
|
|
123
|
+
cache.set(methodName, method);
|
|
124
|
+
}
|
|
125
|
+
return method;
|
|
87
126
|
}
|
|
88
127
|
};
|
|
89
128
|
// Create the proxy
|
|
90
129
|
const proxy = new Proxy(object, handler);
|
|
91
|
-
//
|
|
92
|
-
|
|
130
|
+
// Store proxy → native mapping in WeakMap for O(1) unwrap (bypasses Proxy traps)
|
|
131
|
+
nativeObjectMap.set(proxy, object);
|
|
93
132
|
// Return the proxy
|
|
94
133
|
return proxy;
|
|
95
134
|
}
|
|
96
135
|
}
|
|
97
136
|
function unwrapArg(arg) {
|
|
98
|
-
if (arg && typeof arg === "object"
|
|
99
|
-
return arg
|
|
137
|
+
if (arg && typeof arg === "object") {
|
|
138
|
+
return nativeObjectMap.get(arg) ?? arg;
|
|
100
139
|
}
|
|
101
140
|
return arg;
|
|
102
141
|
}
|
|
@@ -106,17 +145,33 @@ function wrapObjCObjectIfNeeded(result) {
|
|
|
106
145
|
}
|
|
107
146
|
return result;
|
|
108
147
|
}
|
|
109
|
-
// Note: This is actually a factory function that returns a callable
|
|
148
|
+
// Note: This is actually a factory function that returns a callable function
|
|
110
149
|
const NobjcMethod = function (object, methodName) {
|
|
111
150
|
const selector = NobjcMethodNameToObjcSelector(methodName);
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
151
|
+
// H2: Cache SEL + method signature natively via $prepareSend.
|
|
152
|
+
// This avoids re-registering the selector, respondsToSelector:, and
|
|
153
|
+
// method signature lookup on every call.
|
|
154
|
+
const handle = object.$prepareSend(selector);
|
|
155
|
+
// Fast paths for 0-3 args avoid rest param array allocation + spread overhead
|
|
156
|
+
function methodFunc(...args) {
|
|
157
|
+
switch (args.length) {
|
|
158
|
+
case 0:
|
|
159
|
+
return wrapObjCObjectIfNeeded(object.$msgSendPrepared(handle));
|
|
160
|
+
case 1:
|
|
161
|
+
return wrapObjCObjectIfNeeded(object.$msgSendPrepared(handle, unwrapArg(args[0])));
|
|
162
|
+
case 2:
|
|
163
|
+
return wrapObjCObjectIfNeeded(object.$msgSendPrepared(handle, unwrapArg(args[0]), unwrapArg(args[1])));
|
|
164
|
+
case 3:
|
|
165
|
+
return wrapObjCObjectIfNeeded(object.$msgSendPrepared(handle, unwrapArg(args[0]), unwrapArg(args[1]), unwrapArg(args[2])));
|
|
166
|
+
default:
|
|
167
|
+
for (let i = 0; i < args.length; i++) {
|
|
168
|
+
args[i] = unwrapArg(args[i]);
|
|
169
|
+
}
|
|
170
|
+
return wrapObjCObjectIfNeeded(object.$msgSendPrepared(handle, ...args));
|
|
171
|
+
}
|
|
117
172
|
}
|
|
118
|
-
|
|
119
|
-
return
|
|
173
|
+
// Return the function directly — no Proxy wrapper needed (handler was empty)
|
|
174
|
+
return methodFunc;
|
|
120
175
|
};
|
|
121
176
|
class NobjcProtocol {
|
|
122
177
|
static implement(protocolName, methodImplementations) {
|
|
@@ -126,11 +181,11 @@ class NobjcProtocol {
|
|
|
126
181
|
const selector = NobjcMethodNameToObjcSelector(methodName);
|
|
127
182
|
// Wrap the implementation to wrap args and unwrap return values
|
|
128
183
|
convertedMethods[selector] = function (...args) {
|
|
129
|
-
// Wrap native ObjcObject arguments in NobjcObject proxies
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
const result = impl(...
|
|
184
|
+
// Wrap native ObjcObject arguments in NobjcObject proxies (in-place to avoid allocation)
|
|
185
|
+
for (let i = 0; i < args.length; i++) {
|
|
186
|
+
args[i] = wrapObjCObjectIfNeeded(args[i]);
|
|
187
|
+
}
|
|
188
|
+
const result = impl(...args);
|
|
134
189
|
// If the result is already a NobjcObject, unwrap it to get the native object
|
|
135
190
|
return unwrapArg(result);
|
|
136
191
|
};
|
|
@@ -157,9 +212,9 @@ class NobjcProtocol {
|
|
|
157
212
|
* ```
|
|
158
213
|
*/
|
|
159
214
|
function getPointer(obj) {
|
|
160
|
-
// Unwrap the NobjcObject to get the native ObjcObject
|
|
161
|
-
|
|
162
|
-
|
|
215
|
+
// Unwrap the NobjcObject to get the native ObjcObject via WeakMap
|
|
216
|
+
const nativeObj = nativeObjectMap.get(obj);
|
|
217
|
+
if (nativeObj) {
|
|
163
218
|
return GetPointer(nativeObj);
|
|
164
219
|
}
|
|
165
220
|
throw new TypeError("Argument must be a NobjcObject instance");
|
|
@@ -256,20 +311,21 @@ class NobjcClass {
|
|
|
256
311
|
implementation: (nativeSelf, ...nativeArgs) => {
|
|
257
312
|
// Wrap self
|
|
258
313
|
const wrappedSelf = wrapObjCObjectIfNeeded(nativeSelf);
|
|
259
|
-
// Wrap args
|
|
260
|
-
|
|
261
|
-
|
|
314
|
+
// Wrap args in-place to avoid allocation (preserve out-param objects)
|
|
315
|
+
for (let i = 0; i < nativeArgs.length; i++) {
|
|
316
|
+
const arg = nativeArgs[i];
|
|
262
317
|
if (arg && typeof arg === "object" && typeof arg.set === "function") {
|
|
263
|
-
|
|
264
|
-
return {
|
|
318
|
+
nativeArgs[i] = {
|
|
265
319
|
set: (error) => arg.set(unwrapArg(error)),
|
|
266
320
|
get: () => wrapObjCObjectIfNeeded(arg.get())
|
|
267
321
|
};
|
|
268
322
|
}
|
|
269
|
-
|
|
270
|
-
|
|
323
|
+
else {
|
|
324
|
+
nativeArgs[i] = wrapObjCObjectIfNeeded(arg);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
271
327
|
// Call the user's implementation
|
|
272
|
-
const result = methodDef.implementation(wrappedSelf, ...
|
|
328
|
+
const result = methodDef.implementation(wrappedSelf, ...nativeArgs);
|
|
273
329
|
// Unwrap the return value
|
|
274
330
|
return unwrapArg(result);
|
|
275
331
|
}
|
|
@@ -308,9 +364,216 @@ class NobjcClass {
|
|
|
308
364
|
static super(self, selector, ...args) {
|
|
309
365
|
const normalizedSelector = NobjcMethodNameToObjcSelector(selector);
|
|
310
366
|
const nativeSelf = unwrapArg(self);
|
|
311
|
-
|
|
312
|
-
|
|
367
|
+
// Mutate args in-place to avoid allocation
|
|
368
|
+
for (let i = 0; i < args.length; i++) {
|
|
369
|
+
args[i] = unwrapArg(args[i]);
|
|
370
|
+
}
|
|
371
|
+
const result = CallSuper(nativeSelf, normalizedSelector, ...args);
|
|
313
372
|
return wrapObjCObjectIfNeeded(result);
|
|
314
373
|
}
|
|
315
374
|
}
|
|
316
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Parse a combined type encoding string into individual type encodings.
|
|
377
|
+
* Handles multi-character encodings: ^v (pointer), {CGRect=dd} (struct), etc.
|
|
378
|
+
*/
|
|
379
|
+
function parseTypeEncodings(typeStr) {
|
|
380
|
+
const result = [];
|
|
381
|
+
let i = 0;
|
|
382
|
+
while (i < typeStr.length) {
|
|
383
|
+
const start = i;
|
|
384
|
+
const ch = typeStr[i];
|
|
385
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
386
|
+
const close = ch === "{" ? "}" : ch === "[" ? "]" : ")";
|
|
387
|
+
let depth = 1;
|
|
388
|
+
i++;
|
|
389
|
+
while (i < typeStr.length && depth > 0) {
|
|
390
|
+
if (typeStr[i] === ch)
|
|
391
|
+
depth++;
|
|
392
|
+
else if (typeStr[i] === close)
|
|
393
|
+
depth--;
|
|
394
|
+
i++;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (ch === "^") {
|
|
398
|
+
i++;
|
|
399
|
+
if (i < typeStr.length) {
|
|
400
|
+
if (typeStr[i] === "{" || typeStr[i] === "[" || typeStr[i] === "(") {
|
|
401
|
+
// Pointer to compound type — parse the inner type
|
|
402
|
+
const inner = parseTypeEncodings(typeStr.substring(i));
|
|
403
|
+
i += inner[0]?.length ?? 1;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
i++; // pointer to simple type (^v, ^@, etc.)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
i++;
|
|
412
|
+
}
|
|
413
|
+
result.push(typeStr.substring(start, i));
|
|
414
|
+
}
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Infer the ObjC type encoding for a JS argument value.
|
|
419
|
+
* - NobjcObject / object → "@" (id)
|
|
420
|
+
* - string → "@" (auto-converted to NSString by native code)
|
|
421
|
+
* - boolean → "B" (BOOL)
|
|
422
|
+
* - number → "d" (double/CGFloat)
|
|
423
|
+
* - null/undefined → "@" (nil)
|
|
424
|
+
*
|
|
425
|
+
* Note: numbers always infer as "d" (double). For integer params (NSInteger, etc.),
|
|
426
|
+
* specify the type explicitly via { args: ["q"] } or { types: "..." }.
|
|
427
|
+
*/
|
|
428
|
+
function inferArgType(arg) {
|
|
429
|
+
if (arg === null || arg === undefined)
|
|
430
|
+
return "@";
|
|
431
|
+
if (typeof arg === "boolean")
|
|
432
|
+
return "B";
|
|
433
|
+
if (typeof arg === "number")
|
|
434
|
+
return "d";
|
|
435
|
+
return "@";
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Check if a value is a CallFunctionOptions object (not a NobjcObject or struct).
|
|
439
|
+
*/
|
|
440
|
+
function isCallOptions(value) {
|
|
441
|
+
if (value === null || value === undefined || typeof value !== "object")
|
|
442
|
+
return false;
|
|
443
|
+
if (nativeObjectMap.has(value))
|
|
444
|
+
return false; // It's a NobjcObject proxy
|
|
445
|
+
return "returns" in value || "types" in value || "args" in value;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Resolve return type and arg types from options + values.
|
|
449
|
+
*/
|
|
450
|
+
function resolveTypes(options, args) {
|
|
451
|
+
if (options?.types) {
|
|
452
|
+
const encodings = parseTypeEncodings(options.types);
|
|
453
|
+
const returnType = encodings[0] || "v";
|
|
454
|
+
const explicitArgTypes = encodings.slice(1);
|
|
455
|
+
return {
|
|
456
|
+
returnType,
|
|
457
|
+
argTypes: explicitArgTypes.length > 0 ? explicitArgTypes : args.map(inferArgType)
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
returnType: options?.returns || "v",
|
|
462
|
+
argTypes: options?.args || args.map(inferArgType)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Call a C function by name.
|
|
467
|
+
*
|
|
468
|
+
* The framework containing the function must be loaded first (e.g., via `new NobjcLibrary(...)`).
|
|
469
|
+
* Uses `dlsym` to look up the function symbol and `libffi` to call it with the correct ABI.
|
|
470
|
+
*
|
|
471
|
+
* Argument types are inferred from JS values by default:
|
|
472
|
+
* - NobjcObject → `@` (id)
|
|
473
|
+
* - string → `@` (auto-converted to NSString)
|
|
474
|
+
* - boolean → `B` (BOOL)
|
|
475
|
+
* - number → `d` (double/CGFloat)
|
|
476
|
+
* - null → `@` (nil)
|
|
477
|
+
*
|
|
478
|
+
* Return type defaults to `"v"` (void). Pass an options object to specify return/arg types.
|
|
479
|
+
*
|
|
480
|
+
* @param name - The function name (e.g., "NSLog", "NSHomeDirectory")
|
|
481
|
+
* @param optionsOrFirstArg - Either a CallFunctionOptions object or the first function argument
|
|
482
|
+
* @param args - The actual arguments to pass to the function
|
|
483
|
+
* @returns The return value converted to a JavaScript type, or undefined for void functions
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```typescript
|
|
487
|
+
* import { NobjcLibrary, callFunction } from "objc-js";
|
|
488
|
+
*
|
|
489
|
+
* const Foundation = new NobjcLibrary(
|
|
490
|
+
* "/System/Library/Frameworks/Foundation.framework/Foundation"
|
|
491
|
+
* );
|
|
492
|
+
* const NSString = Foundation.NSString;
|
|
493
|
+
*
|
|
494
|
+
* // Void function — simplest form, no options needed
|
|
495
|
+
* const msg = NSString.stringWithUTF8String$("Hello!");
|
|
496
|
+
* callFunction("NSLog", msg);
|
|
497
|
+
*
|
|
498
|
+
* // Function that returns a value — specify { returns }
|
|
499
|
+
* const homeDir = callFunction("NSHomeDirectory", { returns: "@" });
|
|
500
|
+
* console.log(homeDir.toString());
|
|
501
|
+
*
|
|
502
|
+
* // Explicit arg types when inference isn't enough
|
|
503
|
+
* const selName = callFunction("NSStringFromSelector", { returns: "@", args: [":"] }, "description");
|
|
504
|
+
*
|
|
505
|
+
* // Combined type string shorthand (return + args)
|
|
506
|
+
* const className = callFunction("NSStringFromClass", { types: "@#" }, NSString);
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
function callFunction(name, ...rest) {
|
|
510
|
+
let options = null;
|
|
511
|
+
let args;
|
|
512
|
+
if (rest.length > 0 && isCallOptions(rest[0])) {
|
|
513
|
+
options = rest[0];
|
|
514
|
+
args = rest.slice(1);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
args = rest;
|
|
518
|
+
}
|
|
519
|
+
const { returnType, argTypes } = resolveTypes(options, args);
|
|
520
|
+
// Unwrap NobjcObject proxies to native objects
|
|
521
|
+
for (let i = 0; i < args.length; i++) {
|
|
522
|
+
args[i] = unwrapArg(args[i]);
|
|
523
|
+
}
|
|
524
|
+
const result = CallFunction(name, returnType, argTypes, argTypes.length, ...args);
|
|
525
|
+
return wrapObjCObjectIfNeeded(result);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Call a variadic C function by name.
|
|
529
|
+
*
|
|
530
|
+
* Correctly handles variadic calling conventions (important on Apple Silicon / ARM64
|
|
531
|
+
* where variadic args go on the stack while fixed args go in registers).
|
|
532
|
+
*
|
|
533
|
+
* @param name - The function name (e.g., "NSLog")
|
|
534
|
+
* @param optionsOrFixedCount - Either a CallFunctionOptions object or the fixedArgCount directly
|
|
535
|
+
* @param fixedArgCountOrFirstArg - Number of fixed (non-variadic) arguments, or first arg if options were provided
|
|
536
|
+
* @param args - The actual arguments to pass to the function
|
|
537
|
+
* @returns The return value converted to a JavaScript type
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* import { NobjcLibrary, callVariadicFunction } from "objc-js";
|
|
542
|
+
*
|
|
543
|
+
* const Foundation = new NobjcLibrary(
|
|
544
|
+
* "/System/Library/Frameworks/Foundation.framework/Foundation"
|
|
545
|
+
* );
|
|
546
|
+
* const NSString = Foundation.NSString;
|
|
547
|
+
*
|
|
548
|
+
* // Simplest: void return, inferred args, fixedArgCount = 1
|
|
549
|
+
* const format = NSString.stringWithUTF8String$("Hello, %@!");
|
|
550
|
+
* const name = NSString.stringWithUTF8String$("World");
|
|
551
|
+
* callVariadicFunction("NSLog", 1, format, name);
|
|
552
|
+
*
|
|
553
|
+
* // With explicit types
|
|
554
|
+
* callVariadicFunction("NSLog", { returns: "v", args: ["@", "i"] }, 1, format, 42);
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
function callVariadicFunction(name, ...rest) {
|
|
558
|
+
let options = null;
|
|
559
|
+
let restIdx = 0;
|
|
560
|
+
if (rest.length > 0 && isCallOptions(rest[0])) {
|
|
561
|
+
options = rest[0];
|
|
562
|
+
restIdx = 1;
|
|
563
|
+
}
|
|
564
|
+
// fixedArgCount must be a number
|
|
565
|
+
if (restIdx >= rest.length || typeof rest[restIdx] !== "number") {
|
|
566
|
+
throw new Error("callVariadicFunction requires fixedArgCount as a number parameter");
|
|
567
|
+
}
|
|
568
|
+
const fixedArgCount = rest[restIdx];
|
|
569
|
+
restIdx++;
|
|
570
|
+
const args = rest.slice(restIdx);
|
|
571
|
+
const { returnType, argTypes } = resolveTypes(options, args);
|
|
572
|
+
// Unwrap NobjcObject proxies to native objects
|
|
573
|
+
for (let i = 0; i < args.length; i++) {
|
|
574
|
+
args[i] = unwrapArg(args[i]);
|
|
575
|
+
}
|
|
576
|
+
const result = CallFunction(name, returnType, argTypes, fixedArgCount, ...args);
|
|
577
|
+
return wrapObjCObjectIfNeeded(result);
|
|
578
|
+
}
|
|
579
|
+
export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, 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;
|
|
3
|
-
export { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper };
|
|
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 };
|
|
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 } = binding;
|
|
9
|
-
export { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper };
|
|
8
|
+
const { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction } = binding;
|
|
9
|
+
export { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation, DefineClass, CallSuper, CallFunction };
|
package/package.json
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"test:string-lifetime": "bun test tests/test-string-lifetime.test.ts",
|
|
37
37
|
"test:object-arguments": "bun test tests/test-object-arguments.test.ts",
|
|
38
38
|
"test:protocol-implementation": "bun test tests/test-protocol-implementation.test.ts",
|
|
39
|
+
"bench": "bun run build && bun run benchmarks/bench.ts",
|
|
39
40
|
"make-clangd-config": "node ./scripts/make-clangd-config.js",
|
|
40
41
|
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
|
41
42
|
"preinstall-disabled": "npm run build-scripts && npm run make-clangd-config",
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"prebuild:x64": "prebuildify --napi --strip --arch x64 -r node",
|
|
44
45
|
"prebuild:all": "bun run prebuild:arm64 && bun run prebuild:x64"
|
|
45
46
|
},
|
|
46
|
-
"version": "1.
|
|
47
|
+
"version": "1.2.0",
|
|
47
48
|
"description": "Objective-C bridge for Node.js",
|
|
48
49
|
"main": "dist/index.js",
|
|
49
50
|
"dependencies": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/native/ObjcObject.h
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
|
+
#ifndef OBJCOBJECT_H
|
|
2
|
+
#define OBJCOBJECT_H
|
|
3
|
+
|
|
1
4
|
#include <napi.h>
|
|
2
5
|
#include <objc/objc.h>
|
|
6
|
+
#include <objc/runtime.h>
|
|
3
7
|
#include <optional>
|
|
4
8
|
#include <variant>
|
|
9
|
+
#include <unordered_map>
|
|
10
|
+
#include <vector>
|
|
5
11
|
|
|
6
|
-
#
|
|
7
|
-
|
|
12
|
+
#ifdef __OBJC__
|
|
13
|
+
@class NSMethodSignature;
|
|
14
|
+
#else
|
|
15
|
+
typedef struct NSMethodSignature NSMethodSignature;
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
// MARK: - Prepared Send Handle
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Opaque handle for $prepareSend / $msgSendPrepared.
|
|
22
|
+
* Caches SEL, method signature, and fast-path eligibility so that
|
|
23
|
+
* repeated calls skip selector registration, respondsToSelector:,
|
|
24
|
+
* and method signature lookup entirely.
|
|
25
|
+
*/
|
|
26
|
+
struct PreparedSend {
|
|
27
|
+
SEL selector;
|
|
28
|
+
NSMethodSignature *methodSignature;
|
|
29
|
+
size_t expectedArgCount; // numberOfArguments - 2 (self + _cmd)
|
|
30
|
+
const char *returnType; // simplified return type encoding (interned, lives in sig)
|
|
31
|
+
bool isStructReturn;
|
|
32
|
+
bool canUseFastPath; // true if direct objc_msgSend cast is possible
|
|
33
|
+
char fastReturnTypeCode; // first char of simplified return type, for fast dispatch
|
|
34
|
+
|
|
35
|
+
// Per-argument info for fast path
|
|
36
|
+
struct ArgInfo {
|
|
37
|
+
char typeCode; // simplified type code
|
|
38
|
+
bool isStruct;
|
|
39
|
+
};
|
|
40
|
+
std::vector<ArgInfo> argInfos;
|
|
41
|
+
};
|
|
8
42
|
|
|
9
43
|
class ObjcObject : public Napi::ObjectWrap<ObjcObject> {
|
|
10
44
|
public:
|
|
@@ -28,7 +62,10 @@ public:
|
|
|
28
62
|
|
|
29
63
|
private:
|
|
30
64
|
Napi::Value $MsgSend(const Napi::CallbackInfo &info);
|
|
65
|
+
Napi::Value $RespondsToSelector(const Napi::CallbackInfo &info);
|
|
66
|
+
Napi::Value $PrepareSend(const Napi::CallbackInfo &info);
|
|
67
|
+
Napi::Value $MsgSendPrepared(const Napi::CallbackInfo &info);
|
|
31
68
|
Napi::Value GetPointer(const Napi::CallbackInfo &info);
|
|
32
69
|
};
|
|
33
70
|
|
|
34
|
-
#endif // OBJCOBJECT_H
|
|
71
|
+
#endif // OBJCOBJECT_H
|