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 +33 -0
- package/build/Release/nobjc_native.node +0 -0
- package/dist/index.js +14 -2
- package/package.json +8 -3
- package/src/native/ObjcObject.h +34 -0
- package/src/native/ObjcObject.mm +150 -0
- package/src/native/bridge.h +391 -0
- package/src/native/nobjc.mm +113 -0
- package/src/native/protocol-impl.h +69 -0
- package/src/native/protocol-impl.mm +833 -0
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|