objc-js 0.0.15 → 1.0.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 +30 -288
- package/binding.gyp +2 -1
- package/dist/native.js +2 -1
- package/package.json +12 -6
- package/prebuilds/darwin-arm64/objc-js.node +0 -0
- package/prebuilds/darwin-x64/objc-js.node +0 -0
- 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/build/Release/nobjc_native.node +0 -0
|
@@ -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
|