objc-js 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, getPointer, fromPointer };
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
- return new NobjcObject(GetClassObject(className));
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.$msgSend("respondsToSelector:", NobjcMethodNameToObjcSelector(p.toString()));
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
- // if the receiver has a UTF8String method, use it to get the string representation
60
- if ("UTF8String" in receiver) {
61
- return () => String(object.$msgSend("UTF8String"));
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
- // Otherwise, use the description method
64
- return () => String(wrapObjCObjectIfNeeded(object.$msgSend("description")));
102
+ return fn;
65
103
  }
66
- // handle other built-in Object.prototype properties
67
- const builtInProps = [
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 (!(methodName in receiver)) {
84
- throw new Error(`Method ${methodName} not found on object ${receiver}`);
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
- return NobjcMethod(object, methodName);
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
- // This is used to override the default inspect behavior for the object. (console.log)
92
- object[customInspectSymbol] = () => proxy.toString();
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" && NATIVE_OBJC_OBJECT in arg) {
99
- return arg[NATIVE_OBJC_OBJECT];
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 Proxy
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
- // This cannot be an arrow function because we need to access `arguments`.
113
- function methodFunc() {
114
- const unwrappedArgs = Array.from(arguments).map(unwrapArg);
115
- const result = object.$msgSend(selector, ...unwrappedArgs);
116
- return wrapObjCObjectIfNeeded(result);
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
- const handler = {};
119
- return new Proxy(methodFunc, handler);
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
- const wrappedArgs = args.map((arg) => {
131
- return wrapObjCObjectIfNeeded(arg);
132
- });
133
- const result = impl(...wrappedArgs);
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
- if (obj && typeof obj === "object" && NATIVE_OBJC_OBJECT in obj) {
162
- const nativeObj = obj[NATIVE_OBJC_OBJECT];
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, but preserve out-param objects as-is
260
- const wrappedArgs = nativeArgs.map((arg) => {
261
- // Check if it's an out-param object (has 'set' method)
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
- // Keep out-param objects as-is, but wrap the error objects they handle
264
- return {
318
+ nativeArgs[i] = {
265
319
  set: (error) => arg.set(unwrapArg(error)),
266
320
  get: () => wrapObjCObjectIfNeeded(arg.get())
267
321
  };
268
322
  }
269
- return wrapObjCObjectIfNeeded(arg);
270
- });
323
+ else {
324
+ nativeArgs[i] = wrapObjCObjectIfNeeded(arg);
325
+ }
326
+ }
271
327
  // Call the user's implementation
272
- const result = methodDef.implementation(wrappedSelf, ...wrappedArgs);
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
- const unwrappedArgs = args.map(unwrapArg);
312
- const result = CallSuper(nativeSelf, normalizedSelector, ...unwrappedArgs);
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
- export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol, NobjcClass, getPointer, fromPointer };
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.1.0",
47
+ "version": "1.2.1",
47
48
  "description": "Objective-C bridge for Node.js",
48
49
  "main": "dist/index.js",
49
50
  "dependencies": {
Binary file
@@ -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
- #ifndef OBJCOBJECT_H
7
- #define OBJCOBJECT_H
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