objc-js 0.0.7 → 0.0.9

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 ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "nobjc_native",
5
+ "sources": [
6
+ "src/native/nobjc.mm",
7
+ "src/native/ObjcObject.mm",
8
+ "src/native/protocol-impl.mm"
9
+ ],
10
+ "defines": [
11
+ "NODE_ADDON_API_CPP_EXCEPTIONS",
12
+ "NAPI_VERSION=6"
13
+ ],
14
+ "include_dirs": [
15
+ "<!@(node -p \"require('node-addon-api').include\")"
16
+ ],
17
+ "dependencies": [
18
+ "<!(node -p \"require('node-addon-api').gyp\")"
19
+ ],
20
+ "xcode_settings": {
21
+ "MACOSX_DEPLOYMENT_TARGET": "13.3",
22
+ "CLANG_CXX_LIBRARY": "libc++",
23
+ "OTHER_CPLUSPLUSFLAGS": [
24
+ "-std=c++20",
25
+ "-fexceptions"
26
+ ],
27
+ "OTHER_CFLAGS": [
28
+ "-fobjc-arc"
29
+ ]
30
+ }
31
+ }
32
+ ]
33
+ }
Binary file
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { LoadLibrary, GetClassObject, ObjcObject, GetPointer, FromPointer, CreateProtocolImplementation } from "./native.js";
2
+ const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom");
2
3
  const NATIVE_OBJC_OBJECT = Symbol("nativeObjcObject");
3
4
  class NobjcLibrary {
4
5
  constructor(library) {
@@ -24,6 +25,7 @@ function ObjcSelectorToNobjcMethodName(selector) {
24
25
  }
25
26
  class NobjcObject {
26
27
  constructor(object) {
28
+ // Create the proxy handler
27
29
  const handler = {
28
30
  has(target, p) {
29
31
  // Return true for the special Symbol to enable unwrapping
@@ -54,7 +56,12 @@ class NobjcObject {
54
56
  }
55
57
  // handle toString separately
56
58
  if (methodName === "toString") {
57
- return () => String(object.$msgSend("description"));
59
+ // if the receiver has a UTF8String method, use it to get the string representation
60
+ if ("UTF8String" in receiver) {
61
+ return () => String(object.$msgSend("UTF8String"));
62
+ }
63
+ // Otherwise, use the description method
64
+ return () => String(wrapObjCObjectIfNeeded(object.$msgSend("description")));
58
65
  }
59
66
  // handle other built-in Object.prototype properties
60
67
  const builtInProps = [
@@ -79,7 +86,12 @@ class NobjcObject {
79
86
  return NobjcMethod(object, methodName);
80
87
  }
81
88
  };
82
- return new Proxy(object, handler);
89
+ // Create the proxy
90
+ const proxy = new Proxy(object, handler);
91
+ // This is used to override the default inspect behavior for the object. (console.log)
92
+ object[customInspectSymbol] = () => proxy.toString();
93
+ // Return the proxy
94
+ return proxy;
83
95
  }
84
96
  }
85
97
  function unwrapArg(arg) {
package/package.json CHANGED
@@ -17,9 +17,12 @@
17
17
  },
18
18
  "files": [
19
19
  "dist/",
20
- "build/Release/nobjc_native.node"
20
+ "build/Release/nobjc_native.node",
21
+ "binding.gyp",
22
+ "src/native"
21
23
  ],
22
24
  "scripts": {
25
+ "install": "node-gyp rebuild",
23
26
  "build-native": "node-gyp build",
24
27
  "build-scripts": "tsc --project scripts/tsconfig.json",
25
28
  "build-source": "tsc --project src/ts/tsconfig.json",
@@ -36,13 +39,15 @@
36
39
  "format": "prettier --write \"**/*.{ts,js,json,md}\"",
37
40
  "preinstall-disabled": "npm run build-scripts && npm run make-clangd-config"
38
41
  },
39
- "version": "0.0.7",
42
+ "version": "0.0.9",
40
43
  "description": "Objective-C bridge for Node.js",
41
44
  "main": "dist/index.js",
45
+ "dependencies": {
46
+ "node-addon-api": "^8.5.0"
47
+ },
42
48
  "devDependencies": {
43
49
  "@types/bun": "latest",
44
50
  "@types/node": "^20.0.0",
45
- "node-addon-api": "^8.5.0",
46
51
  "node-gyp": "^11.4.2",
47
52
  "prettier": "^3.7.4",
48
53
  "typescript": "^5.9.3"
@@ -0,0 +1,34 @@
1
+ #include <napi.h>
2
+ #include <objc/objc.h>
3
+ #include <optional>
4
+ #include <variant>
5
+
6
+ #ifndef OBJCOBJECT_H
7
+ #define OBJCOBJECT_H
8
+
9
+ class ObjcObject : public Napi::ObjectWrap<ObjcObject> {
10
+ public:
11
+ __strong id objcObject;
12
+ static Napi::FunctionReference constructor;
13
+ static void Init(Napi::Env env, Napi::Object exports);
14
+ ObjcObject(const Napi::CallbackInfo &info)
15
+ : Napi::ObjectWrap<ObjcObject>(info), objcObject(nil) {
16
+ if (info.Length() == 1 && info[0].IsExternal()) {
17
+ // This better be an Napi::External<id>! We lost the type info at runtime.
18
+ Napi::External<id> external = info[0].As<Napi::External<id>>();
19
+ objcObject = *(external.Data());
20
+ return;
21
+ }
22
+ // If someone tries `new ObjcObject()` from JS, forbid it:
23
+ Napi::TypeError::New(info.Env(), "Cannot construct directly")
24
+ .ThrowAsJavaScriptException();
25
+ }
26
+ static Napi::Object NewInstance(Napi::Env env, id obj);
27
+ ~ObjcObject() = default;
28
+
29
+ private:
30
+ Napi::Value $MsgSend(const Napi::CallbackInfo &info);
31
+ Napi::Value GetPointer(const Napi::CallbackInfo &info);
32
+ };
33
+
34
+ #endif // OBJCOBJECT_H
@@ -0,0 +1,150 @@
1
+ #include "ObjcObject.h"
2
+ #include "bridge.h"
3
+ #include <Foundation/Foundation.h>
4
+ #include <napi.h>
5
+ #include <objc/objc.h>
6
+ #include <vector>
7
+
8
+ Napi::FunctionReference ObjcObject::constructor;
9
+
10
+ void ObjcObject::Init(Napi::Env env, Napi::Object exports) {
11
+ Napi::Function func =
12
+ DefineClass(env, "ObjcObject",
13
+ {
14
+ InstanceMethod("$msgSend", &ObjcObject::$MsgSend),
15
+ InstanceMethod("$getPointer", &ObjcObject::GetPointer),
16
+ });
17
+ constructor = Napi::Persistent(func);
18
+ constructor.SuppressDestruct();
19
+ exports.Set("ObjcObject", func);
20
+ }
21
+
22
+ Napi::Object ObjcObject::NewInstance(Napi::Env env, id obj) {
23
+ Napi::EscapableHandleScope scope(env);
24
+ // `obj` is already a pointer, technically, but the Napi::External
25
+ // API expects a pointer, so we have to pointer to the pointer.
26
+ Napi::Object jsObj = constructor.New({Napi::External<id>::New(env, &obj)});
27
+ return scope.Escape(jsObj).ToObject();
28
+ }
29
+
30
+ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
31
+ Napi::Env env = info.Env();
32
+
33
+ if (info.Length() < 1 || !info[0].IsString()) {
34
+ Napi::TypeError::New(env, "Expected at least one string argument")
35
+ .ThrowAsJavaScriptException();
36
+ return env.Null();
37
+ }
38
+
39
+ std::string selectorName = info[0].As<Napi::String>().Utf8Value();
40
+ SEL selector = sel_registerName(selectorName.c_str());
41
+
42
+ if (![objcObject respondsToSelector:selector]) {
43
+ Napi::Error::New(env, "Selector not found on object")
44
+ .ThrowAsJavaScriptException();
45
+ return env.Null();
46
+ }
47
+
48
+ NSMethodSignature *methodSignature =
49
+ [objcObject methodSignatureForSelector:selector];
50
+ if (methodSignature == nil) {
51
+ Napi::Error::New(env, "Failed to get method signature")
52
+ .ThrowAsJavaScriptException();
53
+ return env.Null();
54
+ }
55
+
56
+ // The first two arguments of the signature are the target and selector.
57
+ const size_t expectedArgCount = [methodSignature numberOfArguments] - 2;
58
+
59
+ // The first provided argument is the selector name.
60
+ const size_t providedArgCount = info.Length() - 1;
61
+
62
+ if (providedArgCount != expectedArgCount) {
63
+ std::string errorMessageStr =
64
+ std::format("Selector {} (on {}) expected {} argument(s), but got {}",
65
+ selectorName, std::string(object_getClassName(objcObject)),
66
+ expectedArgCount, providedArgCount);
67
+ const char *errorMessage = errorMessageStr.c_str();
68
+ Napi::Error::New(env, errorMessage).ThrowAsJavaScriptException();
69
+ return env.Null();
70
+ }
71
+
72
+ if ([methodSignature isOneway]) {
73
+ Napi::Error::New(env, "One-way methods are not supported")
74
+ .ThrowAsJavaScriptException();
75
+ return env.Null();
76
+ }
77
+ const char *returnType =
78
+ SimplifyTypeEncoding([methodSignature methodReturnType]);
79
+ const char *validReturnTypes = "cislqCISLQfdB*v@#:";
80
+ if (strlen(returnType) != 1 ||
81
+ strchr(validReturnTypes, *returnType) == nullptr) {
82
+ Napi::TypeError::New(env, "Unsupported return type (pre-invoke)")
83
+ .ThrowAsJavaScriptException();
84
+ return env.Null();
85
+ }
86
+
87
+ NSInvocation *invocation =
88
+ [NSInvocation invocationWithMethodSignature:methodSignature];
89
+ [invocation setSelector:selector];
90
+ [invocation setTarget:objcObject];
91
+
92
+ // Store all arguments to keep them alive until after invoke.
93
+ // This is critical for string arguments where we pass a pointer to the
94
+ // internal buffer of a std::string - if the string is destroyed before
95
+ // invoke, the pointer becomes dangling.
96
+ std::vector<ObjcType> storedArgs;
97
+ storedArgs.reserve(info.Length() - 1);
98
+
99
+ for (size_t i = 1; i < info.Length(); ++i) {
100
+ const ObjcArgumentContext context = {
101
+ .className = std::string(object_getClassName(objcObject)),
102
+ .selectorName = selectorName,
103
+ .argumentIndex = (int)i - 1,
104
+ };
105
+ const char *typeEncoding =
106
+ SimplifyTypeEncoding([methodSignature getArgumentTypeAtIndex:i + 1]);
107
+ auto arg = AsObjCArgument(info[i], typeEncoding, context);
108
+ if (!arg.has_value()) {
109
+ std::string errorMessageStr = std::format("Unsupported argument type {}",
110
+ std::string(typeEncoding));
111
+ const char *errorMessage = errorMessageStr.c_str();
112
+ Napi::TypeError::New(env, errorMessage).ThrowAsJavaScriptException();
113
+ return env.Null();
114
+ }
115
+ storedArgs.push_back(std::move(*arg));
116
+ std::visit(
117
+ [&](auto &&outer) {
118
+ using OuterT = std::decay_t<decltype(outer)>;
119
+ if constexpr (std::is_same_v<OuterT, BaseObjcType>) {
120
+ std::visit(SetObjCArgumentVisitor{invocation, i + 1}, outer);
121
+ } else if constexpr (std::is_same_v<OuterT, BaseObjcType *>) {
122
+ if (outer)
123
+ std::visit(SetObjCArgumentVisitor{invocation, i + 1}, *outer);
124
+ }
125
+ },
126
+ storedArgs.back());
127
+ }
128
+
129
+ [invocation invoke];
130
+ // storedArgs goes out of scope here, after invoke has completed
131
+ return ConvertReturnValueToJSValue(env, invocation, methodSignature);
132
+ }
133
+
134
+ Napi::Value ObjcObject::GetPointer(const Napi::CallbackInfo &info) {
135
+ 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;
150
+ }
@@ -0,0 +1,391 @@
1
+ #include "ObjcObject.h"
2
+ #include <Foundation/Foundation.h>
3
+ #include <format>
4
+ #include <napi.h>
5
+ #include <objc/objc.h>
6
+
7
+ #ifndef NATIVE_BRIDGE_H
8
+ #define NATIVE_BRIDGE_H
9
+
10
+ // MARK: - Type Variant and Visitor
11
+
12
+ using BaseObjcType = std::variant<char, // c
13
+ int, // i
14
+ short, // s
15
+ long, // l
16
+ long long, // q
17
+ unsigned char, // C
18
+ unsigned int, // I
19
+ unsigned short, // S
20
+ unsigned long, // L
21
+ unsigned long long, // Q
22
+ float, // f
23
+ double, // d
24
+ bool, // B
25
+ std::monostate, // v (c type: void)
26
+ std::string, // * (c type: char *)
27
+ id, // @
28
+ Class, // #
29
+ SEL, // :
30
+ void * // ^ (pointer)
31
+ >;
32
+ using ObjcType = std::variant<BaseObjcType, BaseObjcType *>;
33
+
34
+ struct SetObjCArgumentVisitor {
35
+ NSInvocation *invocation;
36
+ size_t index;
37
+
38
+ void operator()(const std::string &str) const {
39
+ const char *cstr = str.c_str();
40
+ [invocation setArgument:&cstr atIndex:index];
41
+ }
42
+
43
+ void operator()(std::monostate) const {
44
+ // void type, do nothing.
45
+ }
46
+
47
+ void operator()(void *ptr) const {
48
+ [invocation setArgument:&ptr atIndex:index];
49
+ }
50
+
51
+ template <typename T> void operator()(T v) const {
52
+ [invocation setArgument:&v atIndex:index];
53
+ }
54
+ };
55
+
56
+ template <typename T1, typename T2> bool IsInRange(T1 value) {
57
+ static_assert(std::is_arithmetic_v<T1> && std::is_arithmetic_v<T2>,
58
+ "IsInRange<T1, T2>: both T1 and T2 must be arithmetic types");
59
+ long double v = static_cast<long double>(value);
60
+ long double lo = static_cast<long double>(std::numeric_limits<T2>::lowest());
61
+ long double hi = static_cast<long double>(std::numeric_limits<T2>::max());
62
+ return v >= lo && v <= hi;
63
+ }
64
+
65
+ // MARK: - Conversion Implementation
66
+
67
+ struct ObjcArgumentContext {
68
+ std::string className;
69
+ std::string selectorName;
70
+ int argumentIndex;
71
+ };
72
+
73
+ #define CONVERT_ARG_ERROR_MSG(message) \
74
+ std::format("Error converting argument {} of {} (on {}): {}", \
75
+ context.argumentIndex, context.selectorName, context.className, \
76
+ message)
77
+
78
+ template <typename T>
79
+ T ConvertJSNumberToNativeValue(const Napi::Value &value,
80
+ const ObjcArgumentContext &context) {
81
+ static_assert(
82
+ std::is_arithmetic_v<T>,
83
+ "ConvertJSNumberToNativeValue<T>: T must be an arithmetic type");
84
+ if (!value.IsNumber()) {
85
+ throw Napi::TypeError::New(value.Env(),
86
+ CONVERT_ARG_ERROR_MSG("Expected a number"));
87
+ }
88
+ double d = value.As<Napi::Number>().DoubleValue();
89
+ if (std::isnan(d)) {
90
+ throw Napi::TypeError::New(value.Env(),
91
+ CONVERT_ARG_ERROR_MSG("Number cannot be NaN"));
92
+ }
93
+ if (std::isinf(d)) {
94
+ throw Napi::RangeError::New(
95
+ value.Env(), CONVERT_ARG_ERROR_MSG("Number cannot be infinite"));
96
+ }
97
+ if (!IsInRange<double, T>(d)) {
98
+ throw Napi::RangeError::New(
99
+ value.Env(), CONVERT_ARG_ERROR_MSG("Number is out of range"));
100
+ }
101
+ if constexpr (std::is_integral_v<T>) {
102
+ if (std::floor(d) != d) {
103
+ throw Napi::TypeError::New(
104
+ value.Env(), CONVERT_ARG_ERROR_MSG("Number must be an integer"));
105
+ }
106
+ }
107
+ return static_cast<T>(d);
108
+ }
109
+
110
+ template <typename T>
111
+ T ConvertJSBigIntToNativeValue(const Napi::Value &value,
112
+ const ObjcArgumentContext &context) {
113
+ static_assert(
114
+ std::is_arithmetic_v<T>,
115
+ "ConvertJSBigIntToNativeValue<T>: T must be an arithmetic type");
116
+ if (!value.IsBigInt()) {
117
+ throw Napi::TypeError::New(value.Env(),
118
+ CONVERT_ARG_ERROR_MSG("Expected a BigInt"));
119
+ }
120
+ bool lossless = false;
121
+
122
+ if constexpr (std::is_integral_v<T>) {
123
+
124
+ if constexpr (std::is_unsigned_v<T>) {
125
+ uint64_t v = value.As<Napi::BigInt>().Uint64Value(&lossless);
126
+ if (!lossless) {
127
+ throw Napi::RangeError::New(
128
+ value.Env(),
129
+ CONVERT_ARG_ERROR_MSG(
130
+ "BigInt out of range for an unsigned 64-bit integer"));
131
+ }
132
+ if (!IsInRange<uint64_t, T>(v)) {
133
+ throw Napi::RangeError::New(
134
+ value.Env(), CONVERT_ARG_ERROR_MSG("BigInt out of range"));
135
+ }
136
+ return static_cast<T>(v);
137
+ } else if constexpr (std::is_signed_v<T>) {
138
+ int64_t v = value.As<Napi::BigInt>().Int64Value(&lossless);
139
+ if (!lossless) {
140
+ throw Napi::RangeError::New(
141
+ value.Env(),
142
+ CONVERT_ARG_ERROR_MSG(
143
+ "BigInt out of range for a signed 64-bit integer"));
144
+ }
145
+ if (!IsInRange<int64_t, T>(v)) {
146
+ throw Napi::RangeError::New(
147
+ value.Env(), CONVERT_ARG_ERROR_MSG("BigInt out of range"));
148
+ }
149
+ return static_cast<T>(v);
150
+ }
151
+ } else if constexpr (std::is_floating_point_v<T>) {
152
+ int64_t vs = value.As<Napi::BigInt>().Int64Value(&lossless);
153
+ if (lossless) {
154
+ return static_cast<T>(vs);
155
+ }
156
+ uint64_t vu = value.As<Napi::BigInt>().Uint64Value(&lossless);
157
+ if (lossless) {
158
+ if (!IsInRange<long double, T>(static_cast<long double>(vu))) {
159
+ throw Napi::RangeError::New(
160
+ value.Env(),
161
+ CONVERT_ARG_ERROR_MSG("BigInt too large for floating point value"));
162
+ }
163
+ return static_cast<T>(vu);
164
+ }
165
+ throw Napi::RangeError::New(
166
+ value.Env(),
167
+ CONVERT_ARG_ERROR_MSG("BigInt out of 64-bit representable range"));
168
+ }
169
+ }
170
+
171
+ template <typename T>
172
+ T ConvertToNativeValue(const Napi::Value &value,
173
+ const ObjcArgumentContext &context) {
174
+ if constexpr (std::is_same_v<T, id>) {
175
+ // is value an ObjcObject instance?
176
+ if (value.IsObject()) {
177
+ Napi::Object obj = value.As<Napi::Object>();
178
+ if (obj.InstanceOf(ObjcObject::constructor.Value())) {
179
+ ObjcObject *objcObj = Napi::ObjectWrap<ObjcObject>::Unwrap(obj);
180
+ return objcObj->objcObject;
181
+ }
182
+ }
183
+ }
184
+ if constexpr (std::is_same_v<T, SEL>) {
185
+ if (!value.IsString()) {
186
+ throw Napi::TypeError::New(value.Env(),
187
+ CONVERT_ARG_ERROR_MSG("Expected a string"));
188
+ }
189
+ std::string selName = value.As<Napi::String>().Utf8Value();
190
+ return sel_registerName(selName.c_str());
191
+ }
192
+ if constexpr (std::is_same_v<T, bool>) {
193
+ if (!value.IsBoolean()) {
194
+ throw Napi::TypeError::New(value.Env(),
195
+ CONVERT_ARG_ERROR_MSG("Expected a boolean"));
196
+ }
197
+ return value.As<Napi::Boolean>().Value();
198
+ } else if constexpr (std::is_same_v<T, std::string>) {
199
+ if (!value.IsString()) {
200
+ throw Napi::TypeError::New(value.Env(),
201
+ CONVERT_ARG_ERROR_MSG("Expected a string"));
202
+ }
203
+ return value.As<Napi::String>().Utf8Value();
204
+ } else if constexpr (std::is_arithmetic_v<T>) {
205
+ if (value.IsNumber()) {
206
+ return ConvertJSNumberToNativeValue<T>(value, context);
207
+ } else if (value.IsBigInt()) {
208
+ return ConvertJSBigIntToNativeValue<T>(value, context);
209
+ } else {
210
+ throw Napi::TypeError::New(
211
+ value.Env(), CONVERT_ARG_ERROR_MSG("Expected a number or bigint"));
212
+ }
213
+ } else {
214
+ throw Napi::TypeError::New(
215
+ value.Env(), CONVERT_ARG_ERROR_MSG("Unsupported argument type"));
216
+ }
217
+ }
218
+
219
+ // MARK: - Conversion Top Layer
220
+
221
+ // Helper class to manage the lifetime of simplified type encodings
222
+ class SimplifiedTypeEncoding {
223
+ private:
224
+ std::string simplified;
225
+
226
+ public:
227
+ SimplifiedTypeEncoding(const char *typeEncoding) : simplified(typeEncoding) {
228
+ // Remove any leading qualifiers
229
+ while (!simplified.empty() && (simplified[0] == 'r' || simplified[0] == 'n' ||
230
+ simplified[0] == 'N' || simplified[0] == 'o' ||
231
+ simplified[0] == 'O' || simplified[0] == 'R' ||
232
+ simplified[0] == 'V')) {
233
+ simplified.erase(0, 1);
234
+ }
235
+ }
236
+
237
+ const char *c_str() const { return simplified.c_str(); }
238
+ char operator[](size_t index) const { return simplified[index]; }
239
+ operator const char *() const { return simplified.c_str(); }
240
+ };
241
+
242
+ // Legacy function for compatibility - returns pointer to internal string
243
+ // WARNING: The returned pointer is only valid as long as the typeEncoding parameter is valid
244
+ inline const char *SimplifyTypeEncoding(const char *typeEncoding) {
245
+ // For simple cases where there are no qualifiers, return the original pointer
246
+ if (typeEncoding && typeEncoding[0] != 'r' && typeEncoding[0] != 'n' &&
247
+ typeEncoding[0] != 'N' && typeEncoding[0] != 'o' &&
248
+ typeEncoding[0] != 'O' && typeEncoding[0] != 'R' &&
249
+ typeEncoding[0] != 'V') {
250
+ return typeEncoding;
251
+ }
252
+
253
+ // For complex cases, we need to skip qualifiers
254
+ // This is a temporary fix - callers should use SimplifiedTypeEncoding class
255
+ static thread_local std::string buffer;
256
+ buffer = typeEncoding;
257
+ while (!buffer.empty() && (buffer[0] == 'r' || buffer[0] == 'n' ||
258
+ buffer[0] == 'N' || buffer[0] == 'o' ||
259
+ buffer[0] == 'O' || buffer[0] == 'R' ||
260
+ buffer[0] == 'V')) {
261
+ buffer.erase(0, 1);
262
+ }
263
+ return buffer.c_str();
264
+ }
265
+
266
+ // Convert a Napi::Value to an ObjcType based on the provided type encoding.
267
+ inline auto AsObjCArgument(const Napi::Value &value, const char *typeEncoding,
268
+ const ObjcArgumentContext &context)
269
+ -> std::optional<ObjcType> {
270
+ const char *simplifiedTypeEncoding = SimplifyTypeEncoding(typeEncoding);
271
+ switch (*simplifiedTypeEncoding) {
272
+ case 'c':
273
+ return ConvertToNativeValue<char>(value, context);
274
+ case 'i':
275
+ return ConvertToNativeValue<int>(value, context);
276
+ case 's':
277
+ return ConvertToNativeValue<short>(value, context);
278
+ case 'l':
279
+ return ConvertToNativeValue<long>(value, context);
280
+ case 'q':
281
+ return ConvertToNativeValue<long long>(value, context);
282
+ case 'C':
283
+ return ConvertToNativeValue<unsigned char>(value, context);
284
+ case 'I':
285
+ return ConvertToNativeValue<unsigned int>(value, context);
286
+ case 'S':
287
+ return ConvertToNativeValue<unsigned short>(value, context);
288
+ case 'L':
289
+ return ConvertToNativeValue<unsigned long>(value, context);
290
+ case 'Q':
291
+ return ConvertToNativeValue<unsigned long long>(value, context);
292
+ case 'f':
293
+ return ConvertToNativeValue<float>(value, context);
294
+ case 'd':
295
+ return ConvertToNativeValue<double>(value, context);
296
+ case 'B':
297
+ return ConvertToNativeValue<bool>(value, context);
298
+ case '*':
299
+ return ConvertToNativeValue<std::string>(value, context);
300
+ case ':':
301
+ return ConvertToNativeValue<SEL>(value, context);
302
+ case '@':
303
+ return ConvertToNativeValue<id>(value, context);
304
+ case '^': // Pointer type (^v, ^c, etc.)
305
+ if (value.IsBuffer()) {
306
+ Napi::Buffer<uint8_t> buffer = value.As<Napi::Buffer<uint8_t>>();
307
+ return static_cast<void *>(buffer.Data());
308
+ }
309
+ if (value.IsTypedArray()) {
310
+ Napi::TypedArray typedArray = value.As<Napi::TypedArray>();
311
+ return static_cast<void *>(
312
+ reinterpret_cast<uint8_t *>(typedArray.ArrayBuffer().Data()) +
313
+ typedArray.ByteOffset());
314
+ }
315
+ if (value.IsNull() || value.IsUndefined()) {
316
+ return static_cast<void *>(nullptr);
317
+ }
318
+ return std::nullopt;
319
+ }
320
+ return std::nullopt;
321
+ }
322
+
323
+ // Convert the return value of an Objective-C method to a Napi::Value.
324
+ inline Napi::Value
325
+ ConvertReturnValueToJSValue(Napi::Env env, NSInvocation *invocation,
326
+ NSMethodSignature *methodSignature) {
327
+ #define NOBJC_NUMERIC_RETURN_CASE(encoding, ctype) \
328
+ case encoding: { \
329
+ ctype result; \
330
+ [invocation getReturnValue:&result]; \
331
+ return Napi::Number::New(env, result); \
332
+ }
333
+ switch (*SimplifyTypeEncoding([methodSignature methodReturnType])) {
334
+ NOBJC_NUMERIC_RETURN_CASE('c', char)
335
+ NOBJC_NUMERIC_RETURN_CASE('i', int)
336
+ NOBJC_NUMERIC_RETURN_CASE('s', short)
337
+ NOBJC_NUMERIC_RETURN_CASE('l', long)
338
+ NOBJC_NUMERIC_RETURN_CASE('q', long long)
339
+ NOBJC_NUMERIC_RETURN_CASE('C', unsigned char)
340
+ NOBJC_NUMERIC_RETURN_CASE('I', unsigned int)
341
+ NOBJC_NUMERIC_RETURN_CASE('S', unsigned short)
342
+ NOBJC_NUMERIC_RETURN_CASE('L', unsigned long)
343
+ NOBJC_NUMERIC_RETURN_CASE('Q', unsigned long long)
344
+ NOBJC_NUMERIC_RETURN_CASE('f', float)
345
+ NOBJC_NUMERIC_RETURN_CASE('d', double)
346
+ case 'B': {
347
+ bool result;
348
+ [invocation getReturnValue:&result];
349
+ return Napi::Boolean::New(env, result);
350
+ }
351
+ case 'v':
352
+ return env.Undefined();
353
+ case '*': {
354
+ char *result = nullptr;
355
+ [invocation getReturnValue:&result];
356
+ if (result == nullptr) {
357
+ return env.Null();
358
+ }
359
+ Napi::String jsString = Napi::String::New(env, result);
360
+ // free(result); // It might not be safe to free this memory.
361
+ return jsString;
362
+ }
363
+ case '@':
364
+ case '#': {
365
+ id result = nil;
366
+ [invocation getReturnValue:&result];
367
+ if (result == nil) {
368
+ return env.Null();
369
+ }
370
+ return ObjcObject::NewInstance(env, result);
371
+ }
372
+ case ':': {
373
+ SEL result = nullptr;
374
+ [invocation getReturnValue:&result];
375
+ if (result == nullptr) {
376
+ return env.Null();
377
+ }
378
+ NSString *selectorString = NSStringFromSelector(result);
379
+ if (selectorString == nil) {
380
+ return env.Null();
381
+ }
382
+ return Napi::String::New(env, [selectorString UTF8String]);
383
+ }
384
+ default:
385
+ Napi::TypeError::New(env, "Unsupported return type (post-invoke)")
386
+ .ThrowAsJavaScriptException();
387
+ return env.Null();
388
+ }
389
+ }
390
+
391
+ #endif // NATIVE_BRIDGE_H