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.
- package/binding.gyp +2 -1
- package/build/Release/nobjc_native.node +0 -0
- package/package.json +2 -1
- package/src/native/ObjcObject.mm +2 -14
- package/src/native/bridge.h +20 -0
- package/src/native/constants.h +42 -0
- package/src/native/ffi-utils.h +103 -1
- package/src/native/forwarding-common.h +87 -0
- package/src/native/forwarding-common.mm +155 -0
- package/src/native/memory-utils.h +197 -0
- package/src/native/method-forwarding.mm +137 -208
- package/src/native/nobjc.mm +7 -31
- package/src/native/pointer-utils.h +63 -0
- package/src/native/protocol-impl.mm +7 -27
- package/src/native/protocol-manager.h +145 -0
- package/src/native/protocol-storage.h +12 -33
- package/src/native/runtime-detection.h +54 -0
- package/src/native/subclass-impl.mm +232 -566
- package/src/native/subclass-manager.h +170 -0
- package/src/native/super-call-helpers.h +361 -0
- package/src/native/type-conversion.h +200 -252
- package/src/native/type-dispatch.h +241 -0
package/binding.gyp
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"darwin"
|
|
8
8
|
],
|
|
9
9
|
"repository": "https://github.com/iamEvanYT/objc-js",
|
|
10
|
+
"homepage": "https://github.com/iamEvanYT/objc-js#readme",
|
|
10
11
|
"author": "iamEvan",
|
|
11
12
|
"imports": {
|
|
12
13
|
"#nobjc_native": "./build/Release/nobjc_native.node"
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
|
40
41
|
"preinstall-disabled": "npm run build-scripts && npm run make-clangd-config"
|
|
41
42
|
},
|
|
42
|
-
"version": "0.0
|
|
43
|
+
"version": "1.0.0",
|
|
43
44
|
"description": "Objective-C bridge for Node.js",
|
|
44
45
|
"main": "dist/index.js",
|
|
45
46
|
"dependencies": {
|
package/src/native/ObjcObject.mm
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#include "ObjcObject.h"
|
|
2
2
|
#include "bridge.h"
|
|
3
|
+
#include "pointer-utils.h"
|
|
3
4
|
#include <Foundation/Foundation.h>
|
|
4
5
|
#include <napi.h>
|
|
5
6
|
#include <objc/objc.h>
|
|
@@ -133,18 +134,5 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
|
|
|
133
134
|
|
|
134
135
|
Napi::Value ObjcObject::GetPointer(const Napi::CallbackInfo &info) {
|
|
135
136
|
Napi::Env env = info.Env();
|
|
136
|
-
|
|
137
|
-
// Get the pointer value of the Objective-C object
|
|
138
|
-
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(objcObject);
|
|
139
|
-
|
|
140
|
-
// Create a Buffer to hold the pointer (8 bytes on 64-bit macOS)
|
|
141
|
-
Napi::Buffer<uint8_t> buffer = Napi::Buffer<uint8_t>::New(env, sizeof(void*));
|
|
142
|
-
|
|
143
|
-
// Write the pointer value to the buffer in little-endian format
|
|
144
|
-
uint8_t* data = buffer.Data();
|
|
145
|
-
for (size_t i = 0; i < sizeof(void*); ++i) {
|
|
146
|
-
data[i] = static_cast<uint8_t>((ptrValue >> (i * 8)) & 0xFF);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return buffer;
|
|
137
|
+
return PointerToBuffer(env, objcObject);
|
|
150
138
|
}
|
package/src/native/bridge.h
CHANGED
|
@@ -173,6 +173,10 @@ template <typename T>
|
|
|
173
173
|
T ConvertToNativeValue(const Napi::Value &value,
|
|
174
174
|
const ObjcArgumentContext &context) {
|
|
175
175
|
if constexpr (std::is_same_v<T, id>) {
|
|
176
|
+
// Handle null/undefined as nil
|
|
177
|
+
if (value.IsNull() || value.IsUndefined()) {
|
|
178
|
+
return nil;
|
|
179
|
+
}
|
|
176
180
|
// is value an ObjcObject instance?
|
|
177
181
|
if (value.IsObject()) {
|
|
178
182
|
Napi::Object obj = value.As<Napi::Object>();
|
|
@@ -183,6 +187,10 @@ T ConvertToNativeValue(const Napi::Value &value,
|
|
|
183
187
|
}
|
|
184
188
|
}
|
|
185
189
|
if constexpr (std::is_same_v<T, SEL>) {
|
|
190
|
+
// Handle null/undefined as NULL selector
|
|
191
|
+
if (value.IsNull() || value.IsUndefined()) {
|
|
192
|
+
return nullptr;
|
|
193
|
+
}
|
|
186
194
|
if (!value.IsString()) {
|
|
187
195
|
throw Napi::TypeError::New(value.Env(),
|
|
188
196
|
CONVERT_ARG_ERROR_MSG("Expected a string"));
|
|
@@ -191,18 +199,30 @@ T ConvertToNativeValue(const Napi::Value &value,
|
|
|
191
199
|
return sel_registerName(selName.c_str());
|
|
192
200
|
}
|
|
193
201
|
if constexpr (std::is_same_v<T, bool>) {
|
|
202
|
+
// Handle null/undefined as false
|
|
203
|
+
if (value.IsNull() || value.IsUndefined()) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
194
206
|
if (!value.IsBoolean()) {
|
|
195
207
|
throw Napi::TypeError::New(value.Env(),
|
|
196
208
|
CONVERT_ARG_ERROR_MSG("Expected a boolean"));
|
|
197
209
|
}
|
|
198
210
|
return value.As<Napi::Boolean>().Value();
|
|
199
211
|
} else if constexpr (std::is_same_v<T, std::string>) {
|
|
212
|
+
// Handle null/undefined as empty string
|
|
213
|
+
if (value.IsNull() || value.IsUndefined()) {
|
|
214
|
+
return std::string();
|
|
215
|
+
}
|
|
200
216
|
if (!value.IsString()) {
|
|
201
217
|
throw Napi::TypeError::New(value.Env(),
|
|
202
218
|
CONVERT_ARG_ERROR_MSG("Expected a string"));
|
|
203
219
|
}
|
|
204
220
|
return value.As<Napi::String>().Utf8Value();
|
|
205
221
|
} else if constexpr (std::is_arithmetic_v<T>) {
|
|
222
|
+
// Handle null/undefined as 0
|
|
223
|
+
if (value.IsNull() || value.IsUndefined()) {
|
|
224
|
+
return static_cast<T>(0);
|
|
225
|
+
}
|
|
206
226
|
if (value.IsNumber()) {
|
|
207
227
|
return ConvertJSNumberToNativeValue<T>(value, context);
|
|
208
228
|
} else if (value.IsBigInt()) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// constants.h - Named Constants for nobjc
|
|
5
|
+
// ============================================================================
|
|
6
|
+
//
|
|
7
|
+
// This header centralizes magic numbers and configuration values used
|
|
8
|
+
// throughout the codebase for better maintainability and documentation.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
#include <cstddef>
|
|
12
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
13
|
+
|
|
14
|
+
namespace nobjc {
|
|
15
|
+
|
|
16
|
+
// MARK: - RunLoop Configuration
|
|
17
|
+
|
|
18
|
+
/// Time interval (in seconds) for each CFRunLoop iteration when waiting for
|
|
19
|
+
/// JS callback completion. Smaller values = more responsive but higher CPU.
|
|
20
|
+
constexpr CFTimeInterval kRunLoopPumpInterval = 0.001; // 1ms
|
|
21
|
+
|
|
22
|
+
/// Number of runloop iterations between debug log messages when waiting.
|
|
23
|
+
/// Set to 1000 = log every ~1 second at kRunLoopPumpInterval of 1ms.
|
|
24
|
+
constexpr int kRunLoopDebugLogInterval = 1000;
|
|
25
|
+
|
|
26
|
+
// MARK: - Buffer Sizes
|
|
27
|
+
|
|
28
|
+
/// Minimum size for return value buffers (handles pointer-sized returns).
|
|
29
|
+
constexpr size_t kMinReturnBufferSize = 16;
|
|
30
|
+
|
|
31
|
+
/// Buffer size for type encoding strings (stack allocation).
|
|
32
|
+
constexpr size_t kTypeEncodingBufferSize = 64;
|
|
33
|
+
|
|
34
|
+
// MARK: - FFI Configuration
|
|
35
|
+
|
|
36
|
+
/// Default buffer size for FFI argument storage when type size is unknown.
|
|
37
|
+
constexpr size_t kDefaultArgBufferSize = sizeof(void*);
|
|
38
|
+
|
|
39
|
+
/// Size of pointer storage for out-parameters (e.g., NSError**).
|
|
40
|
+
constexpr size_t kOutParamPointerSize = sizeof(void*);
|
|
41
|
+
|
|
42
|
+
} // namespace nobjc
|
package/src/native/ffi-utils.h
CHANGED
|
@@ -12,6 +12,102 @@
|
|
|
12
12
|
#include "bridge.h"
|
|
13
13
|
#include "type-conversion.h"
|
|
14
14
|
|
|
15
|
+
// MARK: - FFITypeGuard RAII Wrapper
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* RAII wrapper for managing dynamically allocated ffi_type structs.
|
|
19
|
+
*
|
|
20
|
+
* FFI struct types require heap allocation for their elements arrays.
|
|
21
|
+
* This class ensures proper cleanup when going out of scope, even if
|
|
22
|
+
* exceptions are thrown.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* FFITypeGuard guard;
|
|
26
|
+
* ffi_type* structType = ParseStructEncoding(encoding, &size, guard);
|
|
27
|
+
* // ... use structType ...
|
|
28
|
+
* // Cleanup happens automatically when guard goes out of scope
|
|
29
|
+
*/
|
|
30
|
+
class FFITypeGuard {
|
|
31
|
+
public:
|
|
32
|
+
FFITypeGuard() = default;
|
|
33
|
+
|
|
34
|
+
// Non-copyable
|
|
35
|
+
FFITypeGuard(const FFITypeGuard&) = delete;
|
|
36
|
+
FFITypeGuard& operator=(const FFITypeGuard&) = delete;
|
|
37
|
+
|
|
38
|
+
// Movable
|
|
39
|
+
FFITypeGuard(FFITypeGuard&& other) noexcept : allocatedTypes_(std::move(other.allocatedTypes_)) {
|
|
40
|
+
other.allocatedTypes_.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
FFITypeGuard& operator=(FFITypeGuard&& other) noexcept {
|
|
44
|
+
if (this != &other) {
|
|
45
|
+
cleanup();
|
|
46
|
+
allocatedTypes_ = std::move(other.allocatedTypes_);
|
|
47
|
+
other.allocatedTypes_.clear();
|
|
48
|
+
}
|
|
49
|
+
return *this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
~FFITypeGuard() {
|
|
53
|
+
cleanup();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add a dynamically allocated ffi_type to be managed.
|
|
58
|
+
* The guard takes ownership and will free it on destruction.
|
|
59
|
+
*/
|
|
60
|
+
void add(ffi_type* type) {
|
|
61
|
+
if (type) {
|
|
62
|
+
allocatedTypes_.push_back(type);
|
|
63
|
+
#if NOBJC_DEBUG
|
|
64
|
+
NOBJC_LOG("FFITypeGuard: added type=%p (total: %zu)", type, allocatedTypes_.size());
|
|
65
|
+
#endif
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the underlying vector (for passing to legacy functions).
|
|
71
|
+
*/
|
|
72
|
+
std::vector<ffi_type*>& types() { return allocatedTypes_; }
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Release ownership of all types without cleanup.
|
|
76
|
+
* Use when transferring ownership elsewhere.
|
|
77
|
+
*/
|
|
78
|
+
std::vector<ffi_type*> release() {
|
|
79
|
+
std::vector<ffi_type*> result = std::move(allocatedTypes_);
|
|
80
|
+
allocatedTypes_.clear();
|
|
81
|
+
#if NOBJC_DEBUG
|
|
82
|
+
NOBJC_LOG("FFITypeGuard: released ownership of %zu types", result.size());
|
|
83
|
+
#endif
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get number of managed types.
|
|
89
|
+
*/
|
|
90
|
+
size_t size() const { return allocatedTypes_.size(); }
|
|
91
|
+
|
|
92
|
+
private:
|
|
93
|
+
void cleanup() {
|
|
94
|
+
if (!allocatedTypes_.empty()) {
|
|
95
|
+
#if NOBJC_DEBUG
|
|
96
|
+
NOBJC_LOG("FFITypeGuard: cleaning up %zu types", allocatedTypes_.size());
|
|
97
|
+
#endif
|
|
98
|
+
for (ffi_type* type : allocatedTypes_) {
|
|
99
|
+
if (type && type->type == FFI_TYPE_STRUCT && type->elements) {
|
|
100
|
+
delete[] type->elements;
|
|
101
|
+
}
|
|
102
|
+
delete type;
|
|
103
|
+
}
|
|
104
|
+
allocatedTypes_.clear();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
std::vector<ffi_type*> allocatedTypes_;
|
|
109
|
+
};
|
|
110
|
+
|
|
15
111
|
// MARK: - Type Size Calculation
|
|
16
112
|
|
|
17
113
|
inline size_t GetSizeForTypeEncoding(char typeCode) {
|
|
@@ -73,10 +169,16 @@ inline ffi_type* GetFFITypeForSimpleEncoding(char typeCode) {
|
|
|
73
169
|
}
|
|
74
170
|
}
|
|
75
171
|
|
|
76
|
-
// Forward
|
|
172
|
+
// Forward declarations
|
|
77
173
|
inline ffi_type* GetFFITypeForEncoding(const char* encoding, size_t* outSize,
|
|
78
174
|
std::vector<ffi_type*>& allocatedTypes);
|
|
79
175
|
|
|
176
|
+
// Overload that works with FFITypeGuard (preferred)
|
|
177
|
+
inline ffi_type* GetFFITypeForEncoding(const char* encoding, size_t* outSize,
|
|
178
|
+
FFITypeGuard& guard) {
|
|
179
|
+
return GetFFITypeForEncoding(encoding, outSize, guard.types());
|
|
180
|
+
}
|
|
181
|
+
|
|
80
182
|
// MARK: - Struct Type Parsing
|
|
81
183
|
|
|
82
184
|
inline ffi_type* ParseStructEncoding(const char* encoding, size_t* outSize,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#ifndef FORWARDING_COMMON_H
|
|
2
|
+
#define FORWARDING_COMMON_H
|
|
3
|
+
|
|
4
|
+
#include "memory-utils.h"
|
|
5
|
+
#include "protocol-storage.h"
|
|
6
|
+
#include <functional>
|
|
7
|
+
#include <napi.h>
|
|
8
|
+
#include <optional>
|
|
9
|
+
|
|
10
|
+
#ifdef __OBJC__
|
|
11
|
+
@class NSInvocation;
|
|
12
|
+
#else
|
|
13
|
+
typedef struct NSInvocation NSInvocation;
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
// MARK: - Forwarding Context
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Context data gathered during the initial lookup phase.
|
|
20
|
+
* This contains everything needed to perform the invocation.
|
|
21
|
+
*
|
|
22
|
+
* Performance optimization: We cache the JS function reference here
|
|
23
|
+
* so that getJSFunction doesn't need to re-acquire the mutex.
|
|
24
|
+
*/
|
|
25
|
+
struct ForwardingContext {
|
|
26
|
+
Napi::ThreadSafeFunction tsfn;
|
|
27
|
+
std::string typeEncoding;
|
|
28
|
+
pthread_t js_thread;
|
|
29
|
+
napi_env env;
|
|
30
|
+
bool skipDirectCallForElectron; // Protocol path skips direct for Electron
|
|
31
|
+
|
|
32
|
+
// Subclass-specific (set to nullptr/0 for protocols)
|
|
33
|
+
void *instancePtr;
|
|
34
|
+
void *superClassPtr;
|
|
35
|
+
|
|
36
|
+
// Cached JS function reference (avoids re-acquiring mutex in getJSFunction)
|
|
37
|
+
// This is a raw pointer to the FunctionReference stored in the global map.
|
|
38
|
+
// It remains valid as long as the implementation exists.
|
|
39
|
+
Napi::FunctionReference* cachedJsCallback;
|
|
40
|
+
|
|
41
|
+
ForwardingContext()
|
|
42
|
+
: js_thread(0), env(nullptr), skipDirectCallForElectron(false),
|
|
43
|
+
instancePtr(nullptr), superClassPtr(nullptr), cachedJsCallback(nullptr) {}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Callbacks for storage-specific operations.
|
|
48
|
+
* This allows ForwardInvocationCommon to work with both protocols and subclasses.
|
|
49
|
+
*/
|
|
50
|
+
struct ForwardingCallbacks {
|
|
51
|
+
// Look up context data under lock. Returns nullopt if not found.
|
|
52
|
+
// Also acquires the TSFN.
|
|
53
|
+
std::function<std::optional<ForwardingContext>(
|
|
54
|
+
void *lookupKey, const std::string &selectorName)>
|
|
55
|
+
lookupContext;
|
|
56
|
+
|
|
57
|
+
// Get the JS function for direct call (called within HandleScope).
|
|
58
|
+
// Returns empty function if not found.
|
|
59
|
+
std::function<Napi::Function(void *lookupKey, const std::string &selectorName,
|
|
60
|
+
Napi::Env env)>
|
|
61
|
+
getJSFunction;
|
|
62
|
+
|
|
63
|
+
// Re-acquire TSFN for fallback path. Returns nullopt if not found.
|
|
64
|
+
std::function<std::optional<Napi::ThreadSafeFunction>(
|
|
65
|
+
void *lookupKey, const std::string &selectorName)>
|
|
66
|
+
reacquireTSFN;
|
|
67
|
+
|
|
68
|
+
// What callback type to use
|
|
69
|
+
CallbackType callbackType;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// MARK: - Common Implementation
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Common implementation for method forwarding.
|
|
76
|
+
*
|
|
77
|
+
* @param invocation The NSInvocation to forward
|
|
78
|
+
* @param selectorName The selector name as a string
|
|
79
|
+
* @param lookupKey The key to use for storage lookup (instance ptr for protocols,
|
|
80
|
+
* class ptr for subclasses)
|
|
81
|
+
* @param callbacks The storage-specific callback functions
|
|
82
|
+
*/
|
|
83
|
+
void ForwardInvocationCommon(NSInvocation *invocation,
|
|
84
|
+
const std::string &selectorName, void *lookupKey,
|
|
85
|
+
const ForwardingCallbacks &callbacks);
|
|
86
|
+
|
|
87
|
+
#endif // FORWARDING_COMMON_H
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#include "forwarding-common.h"
|
|
2
|
+
#include "constants.h"
|
|
3
|
+
#include "debug.h"
|
|
4
|
+
#include "method-forwarding.h"
|
|
5
|
+
#include <Foundation/Foundation.h>
|
|
6
|
+
#include <atomic>
|
|
7
|
+
#include <chrono>
|
|
8
|
+
|
|
9
|
+
// MARK: - ForwardInvocationCommon Implementation
|
|
10
|
+
|
|
11
|
+
void ForwardInvocationCommon(NSInvocation *invocation,
|
|
12
|
+
const std::string &selectorName, void *lookupKey,
|
|
13
|
+
const ForwardingCallbacks &callbacks) {
|
|
14
|
+
// Look up context data (acquires TSFN)
|
|
15
|
+
auto contextOpt = callbacks.lookupContext(lookupKey, selectorName);
|
|
16
|
+
if (!contextOpt) {
|
|
17
|
+
NOBJC_WARN("Lookup failed for selector %s", selectorName.c_str());
|
|
18
|
+
[invocation release];
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ForwardingContext ctx = std::move(*contextOpt);
|
|
23
|
+
|
|
24
|
+
// Check if we're on the JS thread
|
|
25
|
+
bool is_js_thread = pthread_equal(pthread_self(), ctx.js_thread);
|
|
26
|
+
|
|
27
|
+
// Create invocation data with RAII guard
|
|
28
|
+
auto data = new InvocationData();
|
|
29
|
+
data->invocation = invocation;
|
|
30
|
+
data->selectorName = selectorName;
|
|
31
|
+
data->typeEncoding = ctx.typeEncoding;
|
|
32
|
+
data->callbackType = callbacks.callbackType;
|
|
33
|
+
data->instancePtr = ctx.instancePtr;
|
|
34
|
+
data->superClassPtr = ctx.superClassPtr;
|
|
35
|
+
|
|
36
|
+
InvocationDataGuard dataGuard(data);
|
|
37
|
+
|
|
38
|
+
napi_status status;
|
|
39
|
+
|
|
40
|
+
// IMPORTANT: We call directly on the JS thread so return values are set
|
|
41
|
+
// synchronously; otherwise we use a ThreadSafeFunction to marshal work.
|
|
42
|
+
// EXCEPTION: For protocols in Electron, we ALWAYS use TSFN even on the JS
|
|
43
|
+
// thread because Electron's V8 context may not be properly set up.
|
|
44
|
+
bool use_direct_call = is_js_thread && !ctx.skipDirectCallForElectron;
|
|
45
|
+
|
|
46
|
+
if (use_direct_call) {
|
|
47
|
+
NOBJC_LOG("ForwardInvocationCommon: Using direct call path for selector %s",
|
|
48
|
+
selectorName.c_str());
|
|
49
|
+
|
|
50
|
+
// Release the TSFN since we're calling directly
|
|
51
|
+
ctx.tsfn.Release();
|
|
52
|
+
|
|
53
|
+
data->completionMutex = nullptr;
|
|
54
|
+
data->completionCv = nullptr;
|
|
55
|
+
data->isComplete = nullptr;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
Napi::Env callEnv(ctx.env);
|
|
59
|
+
Napi::HandleScope scope(callEnv);
|
|
60
|
+
|
|
61
|
+
// Use cached JS function reference (avoids re-acquiring mutex)
|
|
62
|
+
Napi::Function jsFn;
|
|
63
|
+
if (ctx.cachedJsCallback && !ctx.cachedJsCallback->IsEmpty()) {
|
|
64
|
+
jsFn = ctx.cachedJsCallback->Value();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback to callback lookup if cache miss (shouldn't happen)
|
|
68
|
+
if (jsFn.IsEmpty()) {
|
|
69
|
+
jsFn = callbacks.getJSFunction(lookupKey, selectorName, callEnv);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (jsFn.IsEmpty()) {
|
|
73
|
+
NOBJC_WARN("JS function not found for selector %s (direct path)",
|
|
74
|
+
selectorName.c_str());
|
|
75
|
+
return; // dataGuard cleans up
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Transfer ownership to CallJSCallback - it will clean up
|
|
79
|
+
CallJSCallback(callEnv, jsFn, dataGuard.release());
|
|
80
|
+
NOBJC_LOG("ForwardInvocationCommon: Direct call succeeded for %s",
|
|
81
|
+
selectorName.c_str());
|
|
82
|
+
} catch (const std::exception &e) {
|
|
83
|
+
NOBJC_ERROR("Error calling JS callback directly: %s", e.what());
|
|
84
|
+
NOBJC_LOG("Falling back to ThreadSafeFunction for selector %s",
|
|
85
|
+
selectorName.c_str());
|
|
86
|
+
|
|
87
|
+
// Re-create data for fallback since we may have released it
|
|
88
|
+
auto fallbackData = new InvocationData();
|
|
89
|
+
fallbackData->invocation = invocation;
|
|
90
|
+
fallbackData->selectorName = selectorName;
|
|
91
|
+
fallbackData->typeEncoding = ctx.typeEncoding;
|
|
92
|
+
fallbackData->callbackType = callbacks.callbackType;
|
|
93
|
+
fallbackData->instancePtr = ctx.instancePtr;
|
|
94
|
+
fallbackData->superClassPtr = ctx.superClassPtr;
|
|
95
|
+
InvocationDataGuard fallbackGuard(fallbackData);
|
|
96
|
+
|
|
97
|
+
// Re-acquire TSFN for fallback
|
|
98
|
+
auto tsfnOpt = callbacks.reacquireTSFN(lookupKey, selectorName);
|
|
99
|
+
if (tsfnOpt) {
|
|
100
|
+
if (FallbackToTSFN(*tsfnOpt, fallbackGuard.release(), selectorName)) {
|
|
101
|
+
return; // Data cleaned up in callback
|
|
102
|
+
}
|
|
103
|
+
NOBJC_ERROR("ForwardInvocationCommon: Fallback failed for %s",
|
|
104
|
+
selectorName.c_str());
|
|
105
|
+
}
|
|
106
|
+
// If we get here, fallbackGuard cleans up
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// Cross-thread call via TSFN (or Electron forcing TSFN)
|
|
110
|
+
NOBJC_LOG("ForwardInvocationCommon: Using TSFN+runloop path for selector %s",
|
|
111
|
+
selectorName.c_str());
|
|
112
|
+
|
|
113
|
+
std::mutex completionMutex;
|
|
114
|
+
std::condition_variable completionCv;
|
|
115
|
+
bool isComplete = false;
|
|
116
|
+
|
|
117
|
+
data->completionMutex = &completionMutex;
|
|
118
|
+
data->completionCv = &completionCv;
|
|
119
|
+
data->isComplete = &isComplete;
|
|
120
|
+
|
|
121
|
+
// Transfer ownership to TSFN callback
|
|
122
|
+
status = ctx.tsfn.NonBlockingCall(dataGuard.release(), CallJSCallback);
|
|
123
|
+
ctx.tsfn.Release();
|
|
124
|
+
|
|
125
|
+
if (status != napi_ok) {
|
|
126
|
+
NOBJC_ERROR("Failed to call ThreadSafeFunction for selector %s (status: %d)",
|
|
127
|
+
selectorName.c_str(), status);
|
|
128
|
+
// We already released from guard, so clean up manually
|
|
129
|
+
[invocation release];
|
|
130
|
+
delete data;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Wait for callback by pumping CFRunLoop
|
|
135
|
+
int iterations = 0;
|
|
136
|
+
|
|
137
|
+
while (true) {
|
|
138
|
+
{
|
|
139
|
+
std::unique_lock<std::mutex> lock(completionMutex);
|
|
140
|
+
if (isComplete) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
iterations++;
|
|
145
|
+
if (iterations % nobjc::kRunLoopDebugLogInterval == 0) {
|
|
146
|
+
NOBJC_LOG("ForwardInvocationCommon: Still waiting... (%d iterations)",
|
|
147
|
+
iterations);
|
|
148
|
+
}
|
|
149
|
+
CFRunLoopRunInMode(kCFRunLoopDefaultMode, nobjc::kRunLoopPumpInterval, true);
|
|
150
|
+
}
|
|
151
|
+
// Data cleaned up in callback
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Return value (if any) has been set on the invocation
|
|
155
|
+
}
|