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 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.14",
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
  }
@@ -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
@@ -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
+ }