objc-js 0.0.14 → 1.0.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.
@@ -0,0 +1,170 @@
1
+ #ifndef SUBCLASS_MANAGER_H
2
+ #define SUBCLASS_MANAGER_H
3
+
4
+ /**
5
+ * @file subclass-manager.h
6
+ * @brief Singleton manager for subclass implementations.
7
+ *
8
+ * SubclassManager provides thread-safe access to subclass implementation storage.
9
+ * It replaces the global g_subclasses map and g_subclasses_mutex with
10
+ * an encapsulated singleton pattern.
11
+ *
12
+ * Usage:
13
+ * // Register a subclass implementation
14
+ * SubclassManager::Instance().Register(classPtr, std::move(impl));
15
+ *
16
+ * // Find an implementation
17
+ * auto* impl = SubclassManager::Instance().Find(classPtr);
18
+ *
19
+ * // Execute a callback with lock held
20
+ * SubclassManager::Instance().WithLock([](auto& map) {
21
+ * // ... work with map ...
22
+ * });
23
+ */
24
+
25
+ #include "protocol-storage.h"
26
+ #include <functional>
27
+ #include <mutex>
28
+ #include <optional>
29
+ #include <unordered_map>
30
+
31
+ namespace nobjc {
32
+
33
+ /**
34
+ * @brief Thread-safe singleton manager for subclass implementations.
35
+ *
36
+ * Encapsulates storage and synchronization for JS-defined subclasses,
37
+ * providing a clean API for registration, lookup, and iteration.
38
+ */
39
+ class SubclassManager {
40
+ public:
41
+ /**
42
+ * @brief Get the singleton instance.
43
+ * @return Reference to the singleton SubclassManager.
44
+ */
45
+ static SubclassManager& Instance() {
46
+ static SubclassManager instance;
47
+ return instance;
48
+ }
49
+
50
+ // Delete copy/move operations for singleton
51
+ SubclassManager(const SubclassManager&) = delete;
52
+ SubclassManager& operator=(const SubclassManager&) = delete;
53
+ SubclassManager(SubclassManager&&) = delete;
54
+ SubclassManager& operator=(SubclassManager&&) = delete;
55
+
56
+ /**
57
+ * @brief Find an implementation by class pointer.
58
+ * @param classPtr The Objective-C Class pointer (bridged from Class).
59
+ * @return Pointer to implementation if found, nullptr otherwise.
60
+ * @note Caller must NOT hold the lock. This method acquires the lock.
61
+ */
62
+ SubclassImplementation* Find(void* classPtr) {
63
+ std::lock_guard<std::mutex> lock(mutex_);
64
+ auto it = subclasses_.find(classPtr);
65
+ if (it != subclasses_.end()) {
66
+ return &it->second;
67
+ }
68
+ return nullptr;
69
+ }
70
+
71
+ /**
72
+ * @brief Register a new subclass implementation.
73
+ * @param classPtr The Objective-C Class pointer.
74
+ * @param impl The implementation to register (moved).
75
+ */
76
+ void Register(void* classPtr, SubclassImplementation&& impl) {
77
+ std::lock_guard<std::mutex> lock(mutex_);
78
+ subclasses_.emplace(classPtr, std::move(impl));
79
+ }
80
+
81
+ /**
82
+ * @brief Unregister a subclass implementation.
83
+ * @param classPtr The Objective-C Class pointer.
84
+ * @return true if the implementation was found and removed, false otherwise.
85
+ */
86
+ bool Unregister(void* classPtr) {
87
+ std::lock_guard<std::mutex> lock(mutex_);
88
+ return subclasses_.erase(classPtr) > 0;
89
+ }
90
+
91
+ /**
92
+ * @brief Execute a callback with the lock held.
93
+ * @param callback Function to call with the map reference.
94
+ *
95
+ * Use this for complex operations that need to access multiple map entries
96
+ * or perform conditional logic while holding the lock.
97
+ */
98
+ template <typename Callback>
99
+ auto WithLock(Callback&& callback)
100
+ -> decltype(callback(std::declval<std::unordered_map<void*, SubclassImplementation>&>())) {
101
+ std::lock_guard<std::mutex> lock(mutex_);
102
+ return callback(subclasses_);
103
+ }
104
+
105
+ /**
106
+ * @brief Execute a read-only callback with the lock held.
107
+ * @param callback Function to call with const map reference.
108
+ */
109
+ template <typename Callback>
110
+ auto WithLockConst(Callback&& callback) const
111
+ -> decltype(callback(std::declval<const std::unordered_map<void*, SubclassImplementation>&>())) {
112
+ std::lock_guard<std::mutex> lock(mutex_);
113
+ return callback(subclasses_);
114
+ }
115
+
116
+ /**
117
+ * @brief Find superclass by walking the class hierarchy.
118
+ * @param instanceClassPtr Starting class pointer.
119
+ * @return Superclass pointer if found in registry, nullptr otherwise.
120
+ *
121
+ * Walks up the class hierarchy to find a registered subclass implementation
122
+ * and returns its superclass pointer.
123
+ */
124
+ void* FindSuperClassInHierarchy(void* instanceClassPtr) {
125
+ std::lock_guard<std::mutex> lock(mutex_);
126
+
127
+ // Walk up the class hierarchy to find our subclass implementation
128
+ void* clsPtr = instanceClassPtr;
129
+ while (clsPtr != nullptr) {
130
+ auto it = subclasses_.find(clsPtr);
131
+ if (it != subclasses_.end()) {
132
+ return it->second.superClass;
133
+ }
134
+ // Get superclass - this requires Objective-C runtime, so we return nullptr
135
+ // and let the caller handle walking the hierarchy with runtime calls
136
+ return nullptr;
137
+ }
138
+ return nullptr;
139
+ }
140
+
141
+ /**
142
+ * @brief Get the number of registered subclasses.
143
+ * @return Count of subclasses.
144
+ */
145
+ size_t Size() const {
146
+ std::lock_guard<std::mutex> lock(mutex_);
147
+ return subclasses_.size();
148
+ }
149
+
150
+ /**
151
+ * @brief Check if a subclass exists for the given pointer.
152
+ * @param classPtr The Objective-C Class pointer.
153
+ * @return true if registered, false otherwise.
154
+ */
155
+ bool Contains(void* classPtr) const {
156
+ std::lock_guard<std::mutex> lock(mutex_);
157
+ return subclasses_.find(classPtr) != subclasses_.end();
158
+ }
159
+
160
+ private:
161
+ SubclassManager() = default;
162
+ ~SubclassManager() = default;
163
+
164
+ mutable std::mutex mutex_;
165
+ std::unordered_map<void*, SubclassImplementation> subclasses_;
166
+ };
167
+
168
+ } // namespace nobjc
169
+
170
+ #endif // SUBCLASS_MANAGER_H
@@ -0,0 +1,361 @@
1
+ #pragma once
2
+
3
+ // ============================================================================
4
+ // super-call-helpers.h - Helper Functions for CallSuper Implementation
5
+ // ============================================================================
6
+ //
7
+ // These functions break up the large CallSuperWithFFI function into
8
+ // smaller, focused units for better maintainability and readability.
9
+ //
10
+
11
+ #include "constants.h"
12
+ #include "debug.h"
13
+ #include "ffi-utils.h"
14
+ #include "type-conversion.h"
15
+ #include <Foundation/Foundation.h>
16
+ #include <memory>
17
+ #include <napi.h>
18
+ #include <objc/message.h>
19
+ #include <objc/runtime.h>
20
+ #include <string>
21
+ #include <vector>
22
+
23
+ // MARK: - FFI Argument Context
24
+
25
+ /// Context structure for building FFI arguments
26
+ struct FFIArgumentContext {
27
+ std::vector<ffi_type*> argFFITypes;
28
+ std::vector<void*> argValues;
29
+ std::vector<std::unique_ptr<uint8_t[]>> argBuffers;
30
+ std::vector<ffi_type*> allocatedTypes;
31
+ };
32
+
33
+ // MARK: - PrepareFFIArgumentTypes
34
+
35
+ /// Build FFI type arrays for the method arguments.
36
+ /// Returns the FFI return type and populates ctx.argFFITypes.
37
+ inline ffi_type* PrepareFFIArgumentTypes(
38
+ NSMethodSignature* methodSig,
39
+ const char* returnEncoding,
40
+ size_t* outReturnSize,
41
+ FFIArgumentContext& ctx) {
42
+
43
+ size_t totalArgs = [methodSig numberOfArguments];
44
+
45
+ // First arg: objc_super pointer
46
+ ctx.argFFITypes.push_back(&ffi_type_pointer);
47
+ // Second arg: SEL
48
+ ctx.argFFITypes.push_back(&ffi_type_pointer);
49
+
50
+ // Remaining args: method arguments (starting from index 2)
51
+ for (size_t i = 2; i < totalArgs; i++) {
52
+ const char* argEncoding = [methodSig getArgumentTypeAtIndex:i];
53
+ ffi_type* argType = GetFFITypeForEncoding(argEncoding, nullptr, ctx.allocatedTypes);
54
+ ctx.argFFITypes.push_back(argType);
55
+ NOBJC_LOG("PrepareFFIArgumentTypes: Arg %zu type encoding: %s", i - 2, argEncoding);
56
+ }
57
+
58
+ // Build return FFI type
59
+ ffi_type* returnFFIType = GetFFITypeForEncoding(returnEncoding, outReturnSize, ctx.allocatedTypes);
60
+ NOBJC_LOG("PrepareFFIArgumentTypes: Return type encoding: %s, size: %zu",
61
+ returnEncoding, *outReturnSize);
62
+
63
+ return returnFFIType;
64
+ }
65
+
66
+ // MARK: - AddFixedFFIArguments
67
+
68
+ /// Add the fixed arguments (objc_super* and SEL) to the argument buffers.
69
+ inline void AddFixedFFIArguments(
70
+ struct objc_super* superPtr,
71
+ SEL selector,
72
+ FFIArgumentContext& ctx) {
73
+
74
+ // Add objc_super pointer
75
+ // libffi expects argValues[i] to point to the actual argument value
76
+ auto superPtrBuffer = std::make_unique<uint8_t[]>(sizeof(objc_super*));
77
+ memcpy(superPtrBuffer.get(), &superPtr, sizeof(objc_super*));
78
+ void* superPtrBufferRawPtr = superPtrBuffer.get();
79
+ ctx.argBuffers.push_back(std::move(superPtrBuffer));
80
+ ctx.argValues.push_back(superPtrBufferRawPtr);
81
+ NOBJC_LOG("AddFixedFFIArguments: Added objc_super* buffer at %p (points to %p)",
82
+ superPtrBufferRawPtr, superPtr);
83
+
84
+ // Add selector
85
+ auto selectorBuffer = std::make_unique<uint8_t[]>(sizeof(SEL));
86
+ memcpy(selectorBuffer.get(), &selector, sizeof(SEL));
87
+ void* selectorBufferRawPtr = selectorBuffer.get();
88
+ ctx.argBuffers.push_back(std::move(selectorBuffer));
89
+ ctx.argValues.push_back(selectorBufferRawPtr);
90
+ NOBJC_LOG("AddFixedFFIArguments: Added SEL buffer at %p (value=%p, name=%s)",
91
+ selectorBufferRawPtr, selector, sel_getName(selector));
92
+ }
93
+
94
+ // MARK: - ExtractOutParamArgument
95
+
96
+ /// Handle out-param arguments (e.g., NSError**).
97
+ /// Returns true if this was an out-param that was handled.
98
+ inline bool ExtractOutParamArgument(
99
+ const SimplifiedTypeEncoding& simpleArgEncoding,
100
+ size_t argIndex,
101
+ FFIArgumentContext& ctx) {
102
+
103
+ if (simpleArgEncoding[0] != '^' || simpleArgEncoding[1] != '@') {
104
+ return false;
105
+ }
106
+
107
+ NOBJC_LOG("ExtractOutParamArgument: Arg %zu is out-param (^@)", argIndex);
108
+
109
+ // Buffer 1: Storage for the id* (initialized to nil)
110
+ auto errorStorage = std::make_unique<uint8_t[]>(sizeof(id));
111
+ id nullObj = nil;
112
+ memcpy(errorStorage.get(), &nullObj, sizeof(id));
113
+ void* errorStoragePtr = errorStorage.get();
114
+
115
+ NOBJC_LOG("ExtractOutParamArgument: Allocated error storage at %p", errorStoragePtr);
116
+
117
+ // Buffer 2: Storage for the pointer to errorStorage
118
+ auto pointerBuffer = std::make_unique<uint8_t[]>(sizeof(void*));
119
+ memcpy(pointerBuffer.get(), &errorStoragePtr, sizeof(void*));
120
+ void* pointerBufferPtr = pointerBuffer.get();
121
+
122
+ NOBJC_LOG("ExtractOutParamArgument: Allocated pointer buffer at %p", pointerBufferPtr);
123
+
124
+ ctx.argValues.push_back(pointerBufferPtr);
125
+ ctx.argBuffers.push_back(std::move(errorStorage));
126
+ ctx.argBuffers.push_back(std::move(pointerBuffer));
127
+
128
+ return true;
129
+ }
130
+
131
+ // MARK: - ExtractRegularArgument
132
+
133
+ /// Extract a regular (non-out-param) argument from JS to buffer.
134
+ inline void ExtractRegularArgument(
135
+ Napi::Env env,
136
+ Napi::Value jsValue,
137
+ const char* argEncoding,
138
+ const SimplifiedTypeEncoding& simpleArgEncoding,
139
+ const std::string& className,
140
+ const std::string& selectorName,
141
+ size_t argIndex,
142
+ FFIArgumentContext& ctx) {
143
+
144
+ // Calculate size for this argument
145
+ size_t argSize = GetSizeForTypeEncoding(simpleArgEncoding[0]);
146
+ if (argSize == 0) {
147
+ // For complex types, use NSGetSizeAndAlignment
148
+ NSUInteger size, alignment;
149
+ NSGetSizeAndAlignment(argEncoding, &size, &alignment);
150
+ argSize = size;
151
+ NOBJC_LOG("ExtractRegularArgument: Complex type, size: %zu", argSize);
152
+ }
153
+
154
+ // Allocate buffer
155
+ NOBJC_LOG("ExtractRegularArgument: Allocating buffer of %zu bytes for arg %zu",
156
+ argSize, argIndex);
157
+ auto buffer = std::make_unique<uint8_t[]>(argSize);
158
+ memset(buffer.get(), 0, argSize);
159
+ void* bufferPtr = buffer.get();
160
+
161
+ // Extract JS argument to buffer
162
+ ObjcArgumentContext context = {
163
+ .className = className,
164
+ .selectorName = selectorName,
165
+ .argumentIndex = (int)argIndex,
166
+ };
167
+
168
+ ExtractJSArgumentToBuffer(env, jsValue, argEncoding, bufferPtr, context);
169
+ NOBJC_LOG("ExtractRegularArgument: Extracted argument %zu (size: %zu)", argIndex, argSize);
170
+
171
+ // For object types, log the actual pointer value
172
+ if (simpleArgEncoding[0] == '@') {
173
+ [[maybe_unused]] id* objPtr = (id*)bufferPtr;
174
+ NOBJC_LOG("ExtractRegularArgument: Argument %zu is object: buffer=%p, contains id=%p",
175
+ argIndex, bufferPtr, *objPtr);
176
+ }
177
+
178
+ ctx.argValues.push_back(bufferPtr);
179
+ ctx.argBuffers.push_back(std::move(buffer));
180
+ }
181
+
182
+ // MARK: - ExtractMethodArguments
183
+
184
+ /// Extract all method arguments from JS values.
185
+ inline void ExtractMethodArguments(
186
+ Napi::Env env,
187
+ const Napi::CallbackInfo& info,
188
+ size_t argStartIndex,
189
+ NSMethodSignature* methodSig,
190
+ Class superClass,
191
+ const std::string& selectorName,
192
+ FFIArgumentContext& ctx) {
193
+
194
+ NOBJC_LOG("ExtractMethodArguments: Processing %zu method arguments...",
195
+ info.Length() - argStartIndex);
196
+
197
+ std::string className = class_getName(superClass);
198
+
199
+ for (size_t i = argStartIndex; i < info.Length(); i++) {
200
+ size_t argIndex = i - argStartIndex + 2; // +2 for self and _cmd
201
+ const char* argEncoding = [methodSig getArgumentTypeAtIndex:argIndex];
202
+ SimplifiedTypeEncoding simpleArgEncoding(argEncoding);
203
+
204
+ NOBJC_LOG("ExtractMethodArguments: Processing JS arg %zu (method arg %zu), encoding=%s",
205
+ i - argStartIndex, argIndex, argEncoding);
206
+
207
+ // Try out-param first
208
+ if (ExtractOutParamArgument(simpleArgEncoding, i - argStartIndex, ctx)) {
209
+ continue;
210
+ }
211
+
212
+ // Regular argument
213
+ ExtractRegularArgument(env, info[i], argEncoding, simpleArgEncoding,
214
+ className, selectorName, i - argStartIndex, ctx);
215
+ }
216
+
217
+ NOBJC_LOG("ExtractMethodArguments: Finished preparing %zu argument buffers",
218
+ ctx.argBuffers.size());
219
+ }
220
+
221
+ // MARK: - LogFFICallSetup
222
+
223
+ /// Log the FFI call setup for debugging.
224
+ inline void LogFFICallSetup(
225
+ [[maybe_unused]] void* msgSendFn,
226
+ [[maybe_unused]] const std::vector<void*>& argValues,
227
+ [[maybe_unused]] const struct objc_super& superStruct,
228
+ [[maybe_unused]] Class superClass,
229
+ [[maybe_unused]] NSMethodSignature* methodSig) {
230
+
231
+ NOBJC_LOG("LogFFICallSetup: ========== FFI CALL SETUP ==========");
232
+ NOBJC_LOG("LogFFICallSetup: Function to call: objc_msgSendSuper at %p", msgSendFn);
233
+ NOBJC_LOG("LogFFICallSetup: Number of arguments: %zu", argValues.size());
234
+
235
+ if (argValues.size() > 0) {
236
+ NOBJC_LOG("LogFFICallSetup: Arg 0 (objc_super*): argValues[0]=%p", argValues[0]);
237
+ [[maybe_unused]] objc_super** superPtrPtr = (objc_super**)argValues[0];
238
+ NOBJC_LOG("LogFFICallSetup: Buffer contains pointer: %p", *superPtrPtr);
239
+ NOBJC_LOG("LogFFICallSetup: objc_super.receiver=%p", superStruct.receiver);
240
+ NOBJC_LOG("LogFFICallSetup: objc_super.super_class=%p (%s)",
241
+ superStruct.super_class, class_getName(superClass));
242
+ }
243
+
244
+ if (argValues.size() > 1) {
245
+ NOBJC_LOG("LogFFICallSetup: Arg 1 (SEL*): argValues[1]=%p", argValues[1]);
246
+ [[maybe_unused]] SEL* selPtr = (SEL*)argValues[1];
247
+ NOBJC_LOG("LogFFICallSetup: Buffer contains SEL: %p (%s)",
248
+ *selPtr, sel_getName(*selPtr));
249
+ }
250
+
251
+ for (size_t i = 2; i < argValues.size(); i++) {
252
+ const char* argEncoding = [methodSig getArgumentTypeAtIndex:i];
253
+ SimplifiedTypeEncoding simpleArgEncoding(argEncoding);
254
+ NOBJC_LOG("LogFFICallSetup: Arg %zu: argValues[%zu]=%p, encoding=%s",
255
+ i, i, argValues[i], simpleArgEncoding.c_str());
256
+ if (simpleArgEncoding[0] == '@') {
257
+ [[maybe_unused]] id* objPtrLocation = (id*)argValues[i];
258
+ NOBJC_LOG("LogFFICallSetup: Object pointer at %p points to id=%p",
259
+ objPtrLocation, *objPtrLocation);
260
+ } else if (simpleArgEncoding[0] == '^') {
261
+ [[maybe_unused]] void** ptrLocation = (void**)argValues[i];
262
+ NOBJC_LOG("LogFFICallSetup: Pointer at %p contains: %p",
263
+ ptrLocation, *ptrLocation);
264
+ }
265
+ }
266
+ }
267
+
268
+ // MARK: - ExecuteFFICallAndConvert
269
+
270
+ /// Execute the FFI call and convert the return value to JS.
271
+ inline Napi::Value ExecuteFFICallAndConvert(
272
+ Napi::Env env,
273
+ ffi_cif* cif,
274
+ void* msgSendFn,
275
+ FFIArgumentContext& ctx,
276
+ const char* returnEncoding,
277
+ size_t returnSize) {
278
+
279
+ // Prepare return buffer
280
+ std::unique_ptr<uint8_t[]> returnBuffer;
281
+ if (returnEncoding[0] != 'v') {
282
+ size_t bufferSize = returnSize > 0 ? returnSize : nobjc::kMinReturnBufferSize;
283
+ returnBuffer = std::make_unique<uint8_t[]>(bufferSize);
284
+ memset(returnBuffer.get(), 0, bufferSize);
285
+ NOBJC_LOG("ExecuteFFICallAndConvert: Allocated return buffer of %zu bytes at %p",
286
+ bufferSize, returnBuffer.get());
287
+ } else {
288
+ NOBJC_LOG("ExecuteFFICallAndConvert: No return buffer needed (void return)");
289
+ }
290
+
291
+ // Make the FFI call
292
+ NOBJC_LOG("ExecuteFFICallAndConvert: About to call ffi_call...");
293
+ ffi_call(cif, FFI_FN(msgSendFn),
294
+ returnBuffer ? returnBuffer.get() : nullptr,
295
+ ctx.argValues.data());
296
+ NOBJC_LOG("ExecuteFFICallAndConvert: ffi_call completed successfully!");
297
+
298
+ // Convert return value
299
+ Napi::Value result;
300
+ if (returnEncoding[0] == 'v') {
301
+ result = env.Undefined();
302
+ } else {
303
+ result = ConvertFFIReturnToJS(env, returnBuffer.get(), returnEncoding);
304
+ }
305
+
306
+ NOBJC_LOG("ExecuteFFICallAndConvert: Returning result");
307
+ return result;
308
+ }
309
+
310
+ // MARK: - ValidateSuperMethod
311
+
312
+ /// Validate that the method exists on the superclass.
313
+ /// Returns the method signature on success, throws on failure.
314
+ inline NSMethodSignature* ValidateSuperMethod(
315
+ Napi::Env env,
316
+ Class superClass,
317
+ SEL selector,
318
+ const std::string& selectorName,
319
+ size_t providedArgCount) {
320
+
321
+ // Get method signature from superclass
322
+ NSMethodSignature* methodSig = [superClass instanceMethodSignatureForSelector:selector];
323
+ if (methodSig == nil) {
324
+ NOBJC_ERROR("ValidateSuperMethod: Selector '%s' not found on superclass %s",
325
+ selectorName.c_str(), class_getName(superClass));
326
+ throw Napi::Error::New(
327
+ env, "Selector '" + selectorName + "' not found on superclass");
328
+ }
329
+
330
+ NOBJC_LOG("ValidateSuperMethod: Method signature: %s", [methodSig description].UTF8String);
331
+
332
+ // Get the super method's IMP directly
333
+ Method superMethod = class_getInstanceMethod(superClass, selector);
334
+ if (superMethod == nil) {
335
+ NOBJC_ERROR("ValidateSuperMethod: Could not get method implementation for selector '%s'",
336
+ selectorName.c_str());
337
+ throw Napi::Error::New(
338
+ env, "Could not get method implementation for selector '" + selectorName +
339
+ "' from superclass");
340
+ }
341
+
342
+ NOBJC_LOG("ValidateSuperMethod: Found method implementation at %p",
343
+ method_getImplementation(superMethod));
344
+
345
+ // Validate argument count
346
+ const size_t expectedArgCount = [methodSig numberOfArguments] - 2;
347
+
348
+ NOBJC_LOG("ValidateSuperMethod: Expected %zu args, provided %zu args",
349
+ expectedArgCount, providedArgCount);
350
+
351
+ if (providedArgCount != expectedArgCount) {
352
+ NOBJC_ERROR("ValidateSuperMethod: Argument count mismatch for selector '%s'",
353
+ selectorName.c_str());
354
+ throw Napi::Error::New(
355
+ env, "Selector " + selectorName + " expected " +
356
+ std::to_string(expectedArgCount) + " argument(s), but got " +
357
+ std::to_string(providedArgCount));
358
+ }
359
+
360
+ return methodSig;
361
+ }