objc-js 0.0.15 → 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/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 -246
- 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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#ifndef MEMORY_UTILS_H
|
|
2
|
+
#define MEMORY_UTILS_H
|
|
3
|
+
|
|
4
|
+
#include "debug.h"
|
|
5
|
+
#include "protocol-storage.h"
|
|
6
|
+
#include <Foundation/Foundation.h>
|
|
7
|
+
|
|
8
|
+
// MARK: - InvocationDataGuard RAII Wrapper
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RAII wrapper for InvocationData to ensure proper cleanup.
|
|
12
|
+
*
|
|
13
|
+
* This class manages the lifetime of InvocationData*, ensuring that:
|
|
14
|
+
* 1. The NSInvocation is released
|
|
15
|
+
* 2. The InvocationData is deleted
|
|
16
|
+
* 3. Cleanup happens even when exceptions are thrown
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* auto data = new InvocationData();
|
|
20
|
+
* InvocationDataGuard guard(data);
|
|
21
|
+
* // ... use data ...
|
|
22
|
+
* guard.release(); // Transfer ownership on success
|
|
23
|
+
*
|
|
24
|
+
* If release() is not called, cleanup happens in destructor.
|
|
25
|
+
*/
|
|
26
|
+
class InvocationDataGuard {
|
|
27
|
+
public:
|
|
28
|
+
explicit InvocationDataGuard(InvocationData* data) : data_(data), released_(false) {
|
|
29
|
+
#if NOBJC_DEBUG
|
|
30
|
+
if (data_) {
|
|
31
|
+
NOBJC_LOG("InvocationDataGuard: acquired data=%p, selector=%s",
|
|
32
|
+
data_, data_->selectorName.c_str());
|
|
33
|
+
}
|
|
34
|
+
#endif
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Non-copyable
|
|
38
|
+
InvocationDataGuard(const InvocationDataGuard&) = delete;
|
|
39
|
+
InvocationDataGuard& operator=(const InvocationDataGuard&) = delete;
|
|
40
|
+
|
|
41
|
+
// Movable
|
|
42
|
+
InvocationDataGuard(InvocationDataGuard&& other) noexcept
|
|
43
|
+
: data_(other.data_), released_(other.released_) {
|
|
44
|
+
other.data_ = nullptr;
|
|
45
|
+
other.released_ = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
InvocationDataGuard& operator=(InvocationDataGuard&& other) noexcept {
|
|
49
|
+
if (this != &other) {
|
|
50
|
+
cleanup();
|
|
51
|
+
data_ = other.data_;
|
|
52
|
+
released_ = other.released_;
|
|
53
|
+
other.data_ = nullptr;
|
|
54
|
+
other.released_ = true;
|
|
55
|
+
}
|
|
56
|
+
return *this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
~InvocationDataGuard() {
|
|
60
|
+
cleanup();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Release ownership of the data without cleanup.
|
|
65
|
+
* Call this when you're transferring ownership elsewhere (e.g., to a callback).
|
|
66
|
+
* @return The raw pointer (caller takes ownership)
|
|
67
|
+
*/
|
|
68
|
+
InvocationData* release() {
|
|
69
|
+
released_ = true;
|
|
70
|
+
InvocationData* ptr = data_;
|
|
71
|
+
data_ = nullptr;
|
|
72
|
+
#if NOBJC_DEBUG
|
|
73
|
+
if (ptr) {
|
|
74
|
+
NOBJC_LOG("InvocationDataGuard: released ownership of data=%p", ptr);
|
|
75
|
+
}
|
|
76
|
+
#endif
|
|
77
|
+
return ptr;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the raw pointer without releasing ownership.
|
|
82
|
+
*/
|
|
83
|
+
InvocationData* get() const { return data_; }
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if the guard has valid data.
|
|
87
|
+
*/
|
|
88
|
+
explicit operator bool() const { return data_ != nullptr && !released_; }
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Arrow operator for convenient access.
|
|
92
|
+
*/
|
|
93
|
+
InvocationData* operator->() const { return data_; }
|
|
94
|
+
|
|
95
|
+
private:
|
|
96
|
+
void cleanup() {
|
|
97
|
+
if (data_ && !released_) {
|
|
98
|
+
#if NOBJC_DEBUG
|
|
99
|
+
NOBJC_LOG("InvocationDataGuard: cleaning up data=%p, selector=%s",
|
|
100
|
+
data_, data_->selectorName.c_str());
|
|
101
|
+
#endif
|
|
102
|
+
// Release the NSInvocation if present
|
|
103
|
+
if (data_->invocation) {
|
|
104
|
+
[data_->invocation release];
|
|
105
|
+
data_->invocation = nil;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Delete the data
|
|
109
|
+
delete data_;
|
|
110
|
+
data_ = nullptr;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
InvocationData* data_;
|
|
115
|
+
bool released_;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// MARK: - Cleanup Callback for CallJSCallback
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Helper function to clean up InvocationData after a JS callback completes.
|
|
122
|
+
* This is called from CallJSCallback to ensure proper cleanup regardless of
|
|
123
|
+
* success or failure.
|
|
124
|
+
*
|
|
125
|
+
* @param data The InvocationData to clean up (takes ownership)
|
|
126
|
+
*/
|
|
127
|
+
inline void CleanupInvocationData(InvocationData* data) {
|
|
128
|
+
if (!data) return;
|
|
129
|
+
|
|
130
|
+
#if NOBJC_DEBUG
|
|
131
|
+
NOBJC_LOG("CleanupInvocationData: cleaning up selector=%s",
|
|
132
|
+
data->selectorName.c_str());
|
|
133
|
+
#endif
|
|
134
|
+
|
|
135
|
+
// Signal completion if we have synchronization primitives
|
|
136
|
+
SignalInvocationComplete(data);
|
|
137
|
+
|
|
138
|
+
// Release the invocation
|
|
139
|
+
if (data->invocation) {
|
|
140
|
+
[data->invocation release];
|
|
141
|
+
data->invocation = nil;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Delete the data
|
|
145
|
+
delete data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// MARK: - ScopeGuard for Generic Cleanup
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generic scope guard for executing cleanup code on scope exit.
|
|
152
|
+
*
|
|
153
|
+
* Usage:
|
|
154
|
+
* auto guard = MakeScopeGuard([&] { cleanup_code(); });
|
|
155
|
+
* // ... do work ...
|
|
156
|
+
* guard.dismiss(); // Don't run cleanup on success
|
|
157
|
+
*/
|
|
158
|
+
template<typename Func>
|
|
159
|
+
class ScopeGuard {
|
|
160
|
+
public:
|
|
161
|
+
explicit ScopeGuard(Func&& func) : func_(std::forward<Func>(func)), active_(true) {}
|
|
162
|
+
|
|
163
|
+
ScopeGuard(ScopeGuard&& other) noexcept
|
|
164
|
+
: func_(std::move(other.func_)), active_(other.active_) {
|
|
165
|
+
other.active_ = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
~ScopeGuard() {
|
|
169
|
+
if (active_) {
|
|
170
|
+
try {
|
|
171
|
+
func_();
|
|
172
|
+
} catch (...) {
|
|
173
|
+
// Suppress exceptions in destructor
|
|
174
|
+
#if NOBJC_DEBUG
|
|
175
|
+
NOBJC_ERROR("ScopeGuard: exception during cleanup (suppressed)");
|
|
176
|
+
#endif
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
void dismiss() { active_ = false; }
|
|
182
|
+
|
|
183
|
+
// Non-copyable
|
|
184
|
+
ScopeGuard(const ScopeGuard&) = delete;
|
|
185
|
+
ScopeGuard& operator=(const ScopeGuard&) = delete;
|
|
186
|
+
|
|
187
|
+
private:
|
|
188
|
+
Func func_;
|
|
189
|
+
bool active_;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
template<typename Func>
|
|
193
|
+
ScopeGuard<Func> MakeScopeGuard(Func&& func) {
|
|
194
|
+
return ScopeGuard<Func>(std::forward<Func>(func));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#endif // MEMORY_UTILS_H
|