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 CHANGED
@@ -7,7 +7,8 @@
7
7
  "src/native/ObjcObject.mm",
8
8
  "src/native/protocol-impl.mm",
9
9
  "src/native/method-forwarding.mm",
10
- "src/native/subclass-impl.mm"
10
+ "src/native/subclass-impl.mm",
11
+ "src/native/forwarding-common.mm"
11
12
  ],
12
13
  "defines": [
13
14
  "NODE_ADDON_API_CPP_EXCEPTIONS",
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.15",
43
+ "version": "1.0.0",
43
44
  "description": "Objective-C bridge for Node.js",
44
45
  "main": "dist/index.js",
45
46
  "dependencies": {
@@ -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
@@ -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 declaration
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