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.
@@ -4,6 +4,7 @@
4
4
  #include <format>
5
5
  #include <napi.h>
6
6
  #include <objc/objc.h>
7
+ #include <string_view>
7
8
 
8
9
  #ifndef NATIVE_BRIDGE_H
9
10
  #define NATIVE_BRIDGE_H
@@ -66,8 +67,8 @@ template <typename T1, typename T2> bool IsInRange(T1 value) {
66
67
  // MARK: - Conversion Implementation
67
68
 
68
69
  struct ObjcArgumentContext {
69
- std::string className;
70
- std::string selectorName;
70
+ std::string_view className;
71
+ std::string_view selectorName;
71
72
  int argumentIndex;
72
73
  };
73
74
 
@@ -277,6 +278,9 @@ inline auto AsObjCArgument(const Napi::Value &value, const char *typeEncoding,
277
278
  return ConvertToNativeValue<SEL>(value, context);
278
279
  case '@':
279
280
  return ConvertToNativeValue<id>(value, context);
281
+ case '#':
282
+ // Class is also an id in ObjC — an ObjcObject wrapping a Class works here
283
+ return ConvertToNativeValue<id>(value, context);
280
284
  case '^': // Pointer type (^v, ^c, etc.)
281
285
  if (value.IsBuffer()) {
282
286
  Napi::Buffer<uint8_t> buffer = value.As<Napi::Buffer<uint8_t>>();
@@ -0,0 +1,274 @@
1
+ #pragma once
2
+
3
+ // ============================================================================
4
+ // call-function.h - C Function Calling via dlsym + libffi
5
+ // ============================================================================
6
+ //
7
+ // Provides the ability to call C functions (like NSLog, CGRectMake, etc.)
8
+ // exported from loaded frameworks. Uses dlsym to look up the function
9
+ // symbol and libffi to perform the call with correct ABI handling.
10
+ //
11
+ // NOTE: No @autoreleasepool is used here. C functions like NSHomeDirectory()
12
+ // return autoreleased objects, and wrapping the call in @autoreleasepool would
13
+ // drain the pool before ObjcObject::NewInstance can retain the returned object,
14
+ // leaving the ObjcObject holding a dangling pointer. The caller's autorelease
15
+ // pool (from Node's/Bun's event loop) handles cleanup instead.
16
+ //
17
+
18
+ #include "constants.h"
19
+ #include "debug.h"
20
+ #include "ffi-utils.h"
21
+ #include "struct-utils.h"
22
+ #include "type-conversion.h"
23
+ #include <Foundation/Foundation.h>
24
+ #include <dlfcn.h>
25
+ #include <ffi.h>
26
+ #include <memory>
27
+ #include <napi.h>
28
+ #include <string>
29
+ #include <vector>
30
+
31
+ // MARK: - Argument Extraction
32
+
33
+ /// Extract a JS argument into a buffer based on the type encoding.
34
+ /// Handles struct types via PackJSValueAsStruct, and simple types via
35
+ /// ExtractJSArgumentToBuffer.
36
+ ///
37
+ /// Returns the owned buffer (for structs) so the caller can keep it alive.
38
+ inline std::unique_ptr<uint8_t[]>
39
+ ExtractFunctionArgument(Napi::Env env, const Napi::Value &jsValue,
40
+ const std::string &typeEncoding, void **outArgPtr,
41
+ const std::string &functionName, int argIndex,
42
+ FFITypeGuard &guard) {
43
+ const char *simplified = SimplifyTypeEncoding(typeEncoding.c_str());
44
+
45
+ if (*simplified == '{') {
46
+ // Struct argument: pack JS value into struct buffer
47
+ auto structBytes = PackJSValueAsStruct(env, jsValue, typeEncoding.c_str());
48
+ auto buffer = std::make_unique<uint8_t[]>(structBytes.size());
49
+ memcpy(buffer.get(), structBytes.data(), structBytes.size());
50
+ *outArgPtr = buffer.get();
51
+ NOBJC_LOG("ExtractFunctionArgument: Packed struct arg %d (%zu bytes)",
52
+ argIndex, structBytes.size());
53
+ return buffer;
54
+ }
55
+
56
+ // Simple type: compute size and extract
57
+ size_t argSize = GetSizeForTypeEncoding(*simplified);
58
+ if (argSize == 0 && *simplified != 'v') {
59
+ // Complex type - use NSGetSizeAndAlignment
60
+ NSUInteger nsSize, nsAlignment;
61
+ NSGetSizeAndAlignment(typeEncoding.c_str(), &nsSize, &nsAlignment);
62
+ argSize = nsSize;
63
+ }
64
+
65
+ if (argSize == 0) {
66
+ argSize = nobjc::kDefaultArgBufferSize;
67
+ }
68
+
69
+ auto buffer = std::make_unique<uint8_t[]>(argSize);
70
+ memset(buffer.get(), 0, argSize);
71
+
72
+ ObjcArgumentContext context = {
73
+ .className = functionName,
74
+ .selectorName = functionName,
75
+ .argumentIndex = argIndex,
76
+ };
77
+
78
+ ExtractJSArgumentToBuffer(env, jsValue, typeEncoding.c_str(), buffer.get(),
79
+ context);
80
+ *outArgPtr = buffer.get();
81
+
82
+ NOBJC_LOG("ExtractFunctionArgument: Extracted arg %d (type=%s, size=%zu)",
83
+ argIndex, typeEncoding.c_str(), argSize);
84
+ return buffer;
85
+ }
86
+
87
+ // MARK: - Return Value Conversion
88
+
89
+ /// Convert an FFI return buffer to a JS value, handling structs.
90
+ inline Napi::Value ConvertFunctionReturnToJS(Napi::Env env, void *returnBuffer,
91
+ const std::string &typeEncoding) {
92
+ const char *simplified = SimplifyTypeEncoding(typeEncoding.c_str());
93
+
94
+ if (*simplified == '{') {
95
+ // Struct return: unpack to JS object
96
+ return UnpackStructToJSValue(env, static_cast<const uint8_t *>(returnBuffer),
97
+ typeEncoding.c_str());
98
+ }
99
+
100
+ // Simple type
101
+ return ConvertFFIReturnToJS(env, returnBuffer, typeEncoding.c_str());
102
+ }
103
+
104
+ // MARK: - CallFunction Implementation
105
+
106
+ /// Native implementation of CallFunction.
107
+ ///
108
+ /// Arguments:
109
+ /// info[0]: function name (string)
110
+ /// info[1]: return type encoding (string)
111
+ /// info[2]: argument type encodings (array of strings)
112
+ /// info[3]: fixed argument count (number) - if < total args, uses
113
+ /// ffi_prep_cif_var for variadic calling convention
114
+ /// info[4+]: actual arguments
115
+ inline Napi::Value CallFunction(const Napi::CallbackInfo &info) {
116
+ Napi::Env env = info.Env();
117
+
118
+ // Validate minimum arguments
119
+ if (info.Length() < 4) {
120
+ throw Napi::TypeError::New(
121
+ env,
122
+ "CallFunction requires at least 4 arguments: name, returnType, "
123
+ "argTypes, fixedArgCount");
124
+ }
125
+
126
+ // Parse function name
127
+ if (!info[0].IsString()) {
128
+ throw Napi::TypeError::New(env, "First argument (function name) must be "
129
+ "a string");
130
+ }
131
+ std::string functionName = info[0].As<Napi::String>().Utf8Value();
132
+
133
+ // Parse return type encoding
134
+ if (!info[1].IsString()) {
135
+ throw Napi::TypeError::New(
136
+ env, "Second argument (return type) must be a string");
137
+ }
138
+ std::string returnType = info[1].As<Napi::String>().Utf8Value();
139
+
140
+ // Parse argument type encodings
141
+ if (!info[2].IsArray()) {
142
+ throw Napi::TypeError::New(
143
+ env, "Third argument (arg types) must be an array of strings");
144
+ }
145
+ Napi::Array argTypesArray = info[2].As<Napi::Array>();
146
+ uint32_t argCount = argTypesArray.Length();
147
+
148
+ std::vector<std::string> argTypes;
149
+ argTypes.reserve(argCount);
150
+ for (uint32_t i = 0; i < argCount; i++) {
151
+ Napi::Value v = argTypesArray.Get(i);
152
+ if (!v.IsString()) {
153
+ throw Napi::TypeError::New(
154
+ env,
155
+ "Each element of argTypes must be a string (ObjC type encoding)");
156
+ }
157
+ argTypes.push_back(v.As<Napi::String>().Utf8Value());
158
+ }
159
+
160
+ // Parse fixed arg count
161
+ if (!info[3].IsNumber()) {
162
+ throw Napi::TypeError::New(
163
+ env, "Fourth argument (fixedArgCount) must be a number");
164
+ }
165
+ int fixedArgCount = info[3].As<Napi::Number>().Int32Value();
166
+
167
+ // Validate argument count
168
+ uint32_t providedArgs = info.Length() - 4;
169
+ if (providedArgs != argCount) {
170
+ throw Napi::Error::New(
171
+ env, "Expected " + std::to_string(argCount) +
172
+ " arguments but got " + std::to_string(providedArgs) +
173
+ " for function '" + functionName + "'");
174
+ }
175
+
176
+ NOBJC_LOG("CallFunction: Looking up '%s' (return=%s, %u args, %d fixed)",
177
+ functionName.c_str(), returnType.c_str(), argCount,
178
+ fixedArgCount);
179
+
180
+ // Look up the function symbol
181
+ void *funcPtr = dlsym(RTLD_DEFAULT, functionName.c_str());
182
+ if (!funcPtr) {
183
+ throw Napi::Error::New(
184
+ env, "Function '" + functionName +
185
+ "' not found. Make sure the framework is loaded first. "
186
+ "dlsym error: " +
187
+ std::string(dlerror() ?: "unknown"));
188
+ }
189
+
190
+ NOBJC_LOG("CallFunction: Found '%s' at %p", functionName.c_str(), funcPtr);
191
+
192
+ // Build FFI type arrays
193
+ FFITypeGuard guard;
194
+
195
+ // Return type
196
+ size_t returnSize = 0;
197
+ ffi_type *returnFFIType =
198
+ GetFFITypeForEncoding(returnType.c_str(), &returnSize, guard);
199
+
200
+ // Argument types
201
+ std::vector<ffi_type *> argFFITypes;
202
+ argFFITypes.reserve(argCount);
203
+ for (uint32_t i = 0; i < argCount; i++) {
204
+ ffi_type *argType =
205
+ GetFFITypeForEncoding(argTypes[i].c_str(), nullptr, guard);
206
+ argFFITypes.push_back(argType);
207
+ }
208
+
209
+ // Prepare the FFI CIF
210
+ ffi_cif cif;
211
+ ffi_status status;
212
+ bool isVariadic =
213
+ (fixedArgCount >= 0 && static_cast<uint32_t>(fixedArgCount) < argCount);
214
+
215
+ if (isVariadic) {
216
+ NOBJC_LOG("CallFunction: Using variadic CIF (%d fixed, %u total)",
217
+ fixedArgCount, argCount);
218
+ status = ffi_prep_cif_var(&cif, FFI_DEFAULT_ABI, fixedArgCount, argCount,
219
+ returnFFIType, argFFITypes.data());
220
+ } else {
221
+ status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, argCount, returnFFIType,
222
+ argFFITypes.data());
223
+ }
224
+
225
+ if (status != FFI_OK) {
226
+ throw Napi::Error::New(env,
227
+ "Failed to prepare FFI call for function '" +
228
+ functionName + "' (ffi_prep_cif status: " +
229
+ std::to_string(status) + ")");
230
+ }
231
+
232
+ // Extract arguments from JS values
233
+ std::vector<void *> argValues(argCount);
234
+ std::vector<std::unique_ptr<uint8_t[]>> argBuffers;
235
+ argBuffers.reserve(argCount);
236
+
237
+ for (uint32_t i = 0; i < argCount; i++) {
238
+ void *argPtr = nullptr;
239
+ auto buffer = ExtractFunctionArgument(env, info[4 + i], argTypes[i],
240
+ &argPtr, functionName, i, guard);
241
+ argValues[i] = argPtr;
242
+ argBuffers.push_back(std::move(buffer));
243
+ }
244
+
245
+ // Prepare return buffer
246
+ std::unique_ptr<uint8_t[]> returnBuffer;
247
+ const char *simplifiedReturn = SimplifyTypeEncoding(returnType.c_str());
248
+ bool isVoidReturn = (*simplifiedReturn == 'v');
249
+
250
+ if (!isVoidReturn) {
251
+ size_t bufferSize =
252
+ returnSize > 0 ? returnSize : nobjc::kMinReturnBufferSize;
253
+ // Ensure minimum size for ffi_call (must be at least ffi_arg size)
254
+ if (bufferSize < sizeof(ffi_arg)) {
255
+ bufferSize = sizeof(ffi_arg);
256
+ }
257
+ returnBuffer = std::make_unique<uint8_t[]>(bufferSize);
258
+ memset(returnBuffer.get(), 0, bufferSize);
259
+ }
260
+
261
+ // Make the FFI call
262
+ NOBJC_LOG("CallFunction: Calling '%s' with %u args...",
263
+ functionName.c_str(), argCount);
264
+ ffi_call(&cif, FFI_FN(funcPtr), returnBuffer ? returnBuffer.get() : nullptr,
265
+ argCount > 0 ? argValues.data() : nullptr);
266
+ NOBJC_LOG("CallFunction: '%s' returned successfully", functionName.c_str());
267
+
268
+ // Convert return value
269
+ if (isVoidReturn) {
270
+ return env.Undefined();
271
+ }
272
+
273
+ return ConvertFunctionReturnToJS(env, returnBuffer.get(), returnType);
274
+ }
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include "memory-utils.h"
5
5
  #include "protocol-storage.h"
6
+ #include <cstring>
6
7
  #include <functional>
7
8
  #include <napi.h>
8
9
  #include <optional>
@@ -13,6 +14,42 @@
13
14
  typedef struct NSInvocation NSInvocation;
14
15
  #endif
15
16
 
17
+ // MARK: - Forwarding Pipeline Cache
18
+
19
+ /**
20
+ * Thread-local cache to avoid redundant lock acquisition in the
21
+ * RespondsToSelector -> MethodSignatureForSelector pipeline.
22
+ *
23
+ * A single forwarded call triggers both methods sequentially on the same thread.
24
+ * By caching the type encoding from RespondsToSelector, MethodSignatureForSelector
25
+ * can skip the lock entirely on cache hit.
26
+ */
27
+ struct ForwardingPipelineCache {
28
+ void* key; // instance ptr (protocols) or class ptr (subclasses)
29
+ SEL selector;
30
+ char typeEncoding[128];
31
+ bool valid;
32
+
33
+ void store(void* k, SEL sel, const char* encoding) {
34
+ key = k;
35
+ selector = sel;
36
+ std::strncpy(typeEncoding, encoding, sizeof(typeEncoding) - 1);
37
+ typeEncoding[sizeof(typeEncoding) - 1] = '\0';
38
+ valid = true;
39
+ }
40
+
41
+ void invalidate() { valid = false; }
42
+
43
+ bool matches(void* k, SEL sel) const {
44
+ return valid && key == k && selector == sel;
45
+ }
46
+ };
47
+
48
+ inline ForwardingPipelineCache& GetForwardingCache() {
49
+ static thread_local ForwardingPipelineCache cache = {nullptr, nullptr, "", false};
50
+ return cache;
51
+ }
52
+
16
53
  // MARK: - Forwarding Context
17
54
 
18
55
  /**
@@ -51,18 +88,18 @@ struct ForwardingCallbacks {
51
88
  // Look up context data under lock. Returns nullopt if not found.
52
89
  // Also acquires the TSFN.
53
90
  std::function<std::optional<ForwardingContext>(
54
- void *lookupKey, const std::string &selectorName)>
91
+ void *lookupKey, SEL selector)>
55
92
  lookupContext;
56
93
 
57
94
  // Get the JS function for direct call (called within HandleScope).
58
95
  // Returns empty function if not found.
59
- std::function<Napi::Function(void *lookupKey, const std::string &selectorName,
60
- Napi::Env env)>
96
+ std::function<Napi::Function(void *lookupKey, SEL selector,
97
+ Napi::Env env)>
61
98
  getJSFunction;
62
99
 
63
100
  // Re-acquire TSFN for fallback path. Returns nullopt if not found.
64
101
  std::function<std::optional<Napi::ThreadSafeFunction>(
65
- void *lookupKey, const std::string &selectorName)>
102
+ void *lookupKey, SEL selector)>
66
103
  reacquireTSFN;
67
104
 
68
105
  // What callback type to use
@@ -81,7 +118,7 @@ struct ForwardingCallbacks {
81
118
  * @param callbacks The storage-specific callback functions
82
119
  */
83
120
  void ForwardInvocationCommon(NSInvocation *invocation,
84
- const std::string &selectorName, void *lookupKey,
121
+ SEL selector, void *lookupKey,
85
122
  const ForwardingCallbacks &callbacks);
86
123
 
87
124
  #endif // FORWARDING_COMMON_H
@@ -9,12 +9,14 @@
9
9
  // MARK: - ForwardInvocationCommon Implementation
10
10
 
11
11
  void ForwardInvocationCommon(NSInvocation *invocation,
12
- const std::string &selectorName, void *lookupKey,
12
+ SEL selector, void *lookupKey,
13
13
  const ForwardingCallbacks &callbacks) {
14
+ const char *selectorCStr = sel_getName(selector);
15
+
14
16
  // Look up context data (acquires TSFN)
15
- auto contextOpt = callbacks.lookupContext(lookupKey, selectorName);
17
+ auto contextOpt = callbacks.lookupContext(lookupKey, selector);
16
18
  if (!contextOpt) {
17
- NOBJC_WARN("Lookup failed for selector %s", selectorName.c_str());
19
+ NOBJC_WARN("Lookup failed for selector %s", selectorCStr);
18
20
  [invocation release];
19
21
  return;
20
22
  }
@@ -27,7 +29,7 @@ void ForwardInvocationCommon(NSInvocation *invocation,
27
29
  // Create invocation data with RAII guard
28
30
  auto data = new InvocationData();
29
31
  data->invocation = invocation;
30
- data->selectorName = selectorName;
32
+ data->selectorName = selectorCStr;
31
33
  data->typeEncoding = ctx.typeEncoding;
32
34
  data->callbackType = callbacks.callbackType;
33
35
  data->instancePtr = ctx.instancePtr;
@@ -45,7 +47,7 @@ void ForwardInvocationCommon(NSInvocation *invocation,
45
47
 
46
48
  if (use_direct_call) {
47
49
  NOBJC_LOG("ForwardInvocationCommon: Using direct call path for selector %s",
48
- selectorName.c_str());
50
+ selectorCStr);
49
51
 
50
52
  // Release the TSFN since we're calling directly
51
53
  ctx.tsfn.Release();
@@ -66,28 +68,28 @@ void ForwardInvocationCommon(NSInvocation *invocation,
66
68
 
67
69
  // Fallback to callback lookup if cache miss (shouldn't happen)
68
70
  if (jsFn.IsEmpty()) {
69
- jsFn = callbacks.getJSFunction(lookupKey, selectorName, callEnv);
71
+ jsFn = callbacks.getJSFunction(lookupKey, selector, callEnv);
70
72
  }
71
73
 
72
74
  if (jsFn.IsEmpty()) {
73
75
  NOBJC_WARN("JS function not found for selector %s (direct path)",
74
- selectorName.c_str());
76
+ selectorCStr);
75
77
  return; // dataGuard cleans up
76
78
  }
77
79
 
78
80
  // Transfer ownership to CallJSCallback - it will clean up
79
81
  CallJSCallback(callEnv, jsFn, dataGuard.release());
80
82
  NOBJC_LOG("ForwardInvocationCommon: Direct call succeeded for %s",
81
- selectorName.c_str());
83
+ selectorCStr);
82
84
  } catch (const std::exception &e) {
83
85
  NOBJC_ERROR("Error calling JS callback directly: %s", e.what());
84
86
  NOBJC_LOG("Falling back to ThreadSafeFunction for selector %s",
85
- selectorName.c_str());
87
+ selectorCStr);
86
88
 
87
89
  // Re-create data for fallback since we may have released it
88
90
  auto fallbackData = new InvocationData();
89
91
  fallbackData->invocation = invocation;
90
- fallbackData->selectorName = selectorName;
92
+ fallbackData->selectorName = selectorCStr;
91
93
  fallbackData->typeEncoding = ctx.typeEncoding;
92
94
  fallbackData->callbackType = callbacks.callbackType;
93
95
  fallbackData->instancePtr = ctx.instancePtr;
@@ -95,20 +97,21 @@ void ForwardInvocationCommon(NSInvocation *invocation,
95
97
  InvocationDataGuard fallbackGuard(fallbackData);
96
98
 
97
99
  // Re-acquire TSFN for fallback
98
- auto tsfnOpt = callbacks.reacquireTSFN(lookupKey, selectorName);
100
+ auto tsfnOpt = callbacks.reacquireTSFN(lookupKey, selector);
99
101
  if (tsfnOpt) {
100
- if (FallbackToTSFN(*tsfnOpt, fallbackGuard.release(), selectorName)) {
102
+ std::string selectorStr(selectorCStr);
103
+ if (FallbackToTSFN(*tsfnOpt, fallbackGuard.release(), selectorStr)) {
101
104
  return; // Data cleaned up in callback
102
105
  }
103
106
  NOBJC_ERROR("ForwardInvocationCommon: Fallback failed for %s",
104
- selectorName.c_str());
107
+ selectorCStr);
105
108
  }
106
109
  // If we get here, fallbackGuard cleans up
107
110
  }
108
111
  } else {
109
112
  // Cross-thread call via TSFN (or Electron forcing TSFN)
110
113
  NOBJC_LOG("ForwardInvocationCommon: Using TSFN+runloop path for selector %s",
111
- selectorName.c_str());
114
+ selectorCStr);
112
115
 
113
116
  std::mutex completionMutex;
114
117
  std::condition_variable completionCv;
@@ -124,7 +127,7 @@ void ForwardInvocationCommon(NSInvocation *invocation,
124
127
 
125
128
  if (status != napi_ok) {
126
129
  NOBJC_ERROR("Failed to call ThreadSafeFunction for selector %s (status: %d)",
127
- selectorName.c_str(), status);
130
+ selectorCStr, status);
128
131
  // We already released from guard, so clean up manually
129
132
  [invocation release];
130
133
  delete data;