objc-js 1.0.4 → 1.1.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/README.md +1 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/node.napi.armv8.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/src/native/ObjcObject.mm +29 -3
- package/src/native/ffi-utils.h +7 -55
- package/src/native/struct-utils.h +538 -0
- package/src/native/type-conversion.h +103 -0
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ bun add objc-js
|
|
|
33
33
|
The documentation is organized into several guides:
|
|
34
34
|
|
|
35
35
|
- **[Basic Usage](./docs/basic-usage.md)** - Getting started with loading frameworks and calling methods
|
|
36
|
+
- **[Structs](./docs/structs.md)** - Passing and receiving C structs (CGRect, NSRange, etc.)
|
|
36
37
|
- **[Subclassing Objective-C Classes](./docs/subclassing.md)** - Creating and subclassing Objective-C classes from JavaScript
|
|
37
38
|
- **[Protocol Implementation](./docs/protocol-implementation.md)** - Creating delegate objects that implement protocols
|
|
38
39
|
- **[API Reference](./docs/api-reference.md)** - Complete API documentation for all classes and functions
|
package/package.json
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"prebuild:x64": "prebuildify --napi --strip --arch x64 -r node",
|
|
44
44
|
"prebuild:all": "bun run prebuild:arm64 && bun run prebuild:x64"
|
|
45
45
|
},
|
|
46
|
-
"version": "1.0
|
|
46
|
+
"version": "1.1.0",
|
|
47
47
|
"description": "Objective-C bridge for Node.js",
|
|
48
48
|
"main": "dist/index.js",
|
|
49
49
|
"dependencies": {
|
|
Binary file
|
|
Binary file
|
package/src/native/ObjcObject.mm
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "ObjcObject.h"
|
|
2
2
|
#include "bridge.h"
|
|
3
3
|
#include "pointer-utils.h"
|
|
4
|
+
#include "struct-utils.h"
|
|
4
5
|
#include <Foundation/Foundation.h>
|
|
5
6
|
#include <napi.h>
|
|
6
7
|
#include <objc/objc.h>
|
|
@@ -77,9 +78,11 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
|
|
|
77
78
|
}
|
|
78
79
|
const char *returnType =
|
|
79
80
|
SimplifyTypeEncoding([methodSignature methodReturnType]);
|
|
81
|
+
const bool isStructReturn = (*returnType == '{');
|
|
80
82
|
const char *validReturnTypes = "cislqCISLQfdB*v@#:";
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
+
if (!isStructReturn &&
|
|
84
|
+
(strlen(returnType) != 1 ||
|
|
85
|
+
strchr(validReturnTypes, *returnType) == nullptr)) {
|
|
83
86
|
Napi::TypeError::New(env, "Unsupported return type (pre-invoke)")
|
|
84
87
|
.ThrowAsJavaScriptException();
|
|
85
88
|
return env.Null();
|
|
@@ -97,6 +100,9 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
|
|
|
97
100
|
std::vector<ObjcType> storedArgs;
|
|
98
101
|
storedArgs.reserve(info.Length() - 1);
|
|
99
102
|
|
|
103
|
+
// Store struct argument buffers to keep them alive until after invoke.
|
|
104
|
+
std::vector<std::vector<uint8_t>> structBuffers;
|
|
105
|
+
|
|
100
106
|
for (size_t i = 1; i < info.Length(); ++i) {
|
|
101
107
|
const ObjcArgumentContext context = {
|
|
102
108
|
.className = std::string(object_getClassName(objcObject)),
|
|
@@ -105,6 +111,17 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
|
|
|
105
111
|
};
|
|
106
112
|
const char *typeEncoding =
|
|
107
113
|
SimplifyTypeEncoding([methodSignature getArgumentTypeAtIndex:i + 1]);
|
|
114
|
+
|
|
115
|
+
if (IsStructTypeEncoding(typeEncoding)) {
|
|
116
|
+
// Struct argument: pack JS object into a byte buffer and set directly
|
|
117
|
+
auto buffer = PackJSValueAsStruct(env, info[i], typeEncoding);
|
|
118
|
+
[invocation setArgument:buffer.data() atIndex:i + 1];
|
|
119
|
+
structBuffers.push_back(std::move(buffer));
|
|
120
|
+
// Push a placeholder into storedArgs to keep indices aligned
|
|
121
|
+
storedArgs.push_back(BaseObjcType{std::monostate{}});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
auto arg = AsObjCArgument(info[i], typeEncoding, context);
|
|
109
126
|
if (!arg.has_value()) {
|
|
110
127
|
std::string errorMessageStr = std::format("Unsupported argument type {}",
|
|
@@ -128,7 +145,16 @@ Napi::Value ObjcObject::$MsgSend(const Napi::CallbackInfo &info) {
|
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
[invocation invoke];
|
|
131
|
-
// storedArgs
|
|
148
|
+
// storedArgs and structBuffers go out of scope here, after invoke
|
|
149
|
+
|
|
150
|
+
if (isStructReturn) {
|
|
151
|
+
// Struct return: read bytes from invocation and convert to JS object
|
|
152
|
+
NSUInteger returnLength = [methodSignature methodReturnLength];
|
|
153
|
+
std::vector<uint8_t> returnBuffer(returnLength, 0);
|
|
154
|
+
[invocation getReturnValue:returnBuffer.data()];
|
|
155
|
+
return UnpackStructToJSValue(env, returnBuffer.data(), returnType);
|
|
156
|
+
}
|
|
157
|
+
|
|
132
158
|
return ConvertReturnValueToJSValue(env, invocation, methodSignature);
|
|
133
159
|
}
|
|
134
160
|
|
package/src/native/ffi-utils.h
CHANGED
|
@@ -185,78 +185,30 @@ inline ffi_type* ParseStructEncoding(const char* encoding, size_t* outSize,
|
|
|
185
185
|
std::vector<ffi_type*>& allocatedTypes) {
|
|
186
186
|
NOBJC_LOG("ParseStructEncoding: parsing struct '%s'", encoding);
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
NOBJC_ERROR("ParseStructEncoding: Expected '{' at start");
|
|
190
|
-
return nullptr;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Skip struct name (format: {StructName=...})
|
|
194
|
-
const char* ptr = encoding + 1;
|
|
195
|
-
while (*ptr && *ptr != '=' && *ptr != '}') {
|
|
196
|
-
ptr++;
|
|
197
|
-
}
|
|
188
|
+
auto header = ParseStructEncodingHeader(encoding);
|
|
198
189
|
|
|
199
|
-
if (
|
|
200
|
-
// Empty struct
|
|
190
|
+
if (header.empty) {
|
|
201
191
|
NOBJC_LOG("ParseStructEncoding: empty struct");
|
|
202
192
|
return &ffi_type_void;
|
|
203
193
|
}
|
|
204
194
|
|
|
205
|
-
if (
|
|
206
|
-
NOBJC_ERROR("ParseStructEncoding:
|
|
195
|
+
if (!header.fieldsStart) {
|
|
196
|
+
NOBJC_ERROR("ParseStructEncoding: Invalid struct encoding '%s'", encoding);
|
|
207
197
|
return nullptr;
|
|
208
198
|
}
|
|
209
199
|
|
|
210
|
-
|
|
200
|
+
const char* ptr = header.fieldsStart;
|
|
211
201
|
|
|
212
202
|
// Parse field types
|
|
213
203
|
std::vector<ffi_type*> fieldTypes;
|
|
214
204
|
|
|
215
205
|
while (*ptr && *ptr != '}') {
|
|
216
206
|
// Skip any qualifiers
|
|
217
|
-
|
|
218
|
-
*ptr == 'o' || *ptr == 'O' || *ptr == 'R' || *ptr == 'V')) {
|
|
219
|
-
ptr++;
|
|
220
|
-
}
|
|
207
|
+
ptr = SimplifyTypeEncoding(ptr);
|
|
221
208
|
|
|
222
209
|
if (*ptr == '}') break;
|
|
223
210
|
|
|
224
|
-
|
|
225
|
-
const char* fieldStart = ptr;
|
|
226
|
-
size_t fieldLen = 1;
|
|
227
|
-
|
|
228
|
-
if (*ptr == '{') {
|
|
229
|
-
// Nested struct - find matching '}'
|
|
230
|
-
int depth = 1;
|
|
231
|
-
ptr++;
|
|
232
|
-
while (*ptr && depth > 0) {
|
|
233
|
-
if (*ptr == '{') depth++;
|
|
234
|
-
else if (*ptr == '}') depth--;
|
|
235
|
-
ptr++;
|
|
236
|
-
}
|
|
237
|
-
fieldLen = ptr - fieldStart;
|
|
238
|
-
} else if (*ptr == '(') {
|
|
239
|
-
// Union - find matching ')'
|
|
240
|
-
int depth = 1;
|
|
241
|
-
ptr++;
|
|
242
|
-
while (*ptr && depth > 0) {
|
|
243
|
-
if (*ptr == '(') depth++;
|
|
244
|
-
else if (*ptr == ')') depth--;
|
|
245
|
-
ptr++;
|
|
246
|
-
}
|
|
247
|
-
fieldLen = ptr - fieldStart;
|
|
248
|
-
} else {
|
|
249
|
-
// Simple type - just one character (potentially with digits after)
|
|
250
|
-
ptr++;
|
|
251
|
-
// Skip any size/alignment digits
|
|
252
|
-
while (*ptr && isdigit(*ptr)) {
|
|
253
|
-
ptr++;
|
|
254
|
-
}
|
|
255
|
-
fieldLen = ptr - fieldStart;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Create temporary null-terminated string for field encoding
|
|
259
|
-
std::string fieldEncoding(fieldStart, fieldLen);
|
|
211
|
+
std::string fieldEncoding = SkipOneFieldEncoding(ptr);
|
|
260
212
|
NOBJC_LOG("ParseStructEncoding: parsing field '%s'", fieldEncoding.c_str());
|
|
261
213
|
|
|
262
214
|
// Recursively get FFI type for this field
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#ifndef STRUCT_UTILS_H
|
|
2
|
+
#define STRUCT_UTILS_H
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @file struct-utils.h
|
|
6
|
+
* @brief Struct encoding parser and JS ↔ ObjC struct conversion utilities.
|
|
7
|
+
*
|
|
8
|
+
* Provides support for converting between JavaScript objects and Objective-C
|
|
9
|
+
* structs (CGRect, CGPoint, CGSize, NSRange, etc.) in $msgSend calls.
|
|
10
|
+
*
|
|
11
|
+
* Type encodings with field names look like:
|
|
12
|
+
* {CGRect="origin"{CGPoint="x"d"y"d}"size"{CGSize="width"d"height"d}}
|
|
13
|
+
*
|
|
14
|
+
* Type encodings without field names look like:
|
|
15
|
+
* {CGRect={CGPoint=dd}{CGSize=dd}}
|
|
16
|
+
*
|
|
17
|
+
* Both formats are supported. When field names are present, JS objects use
|
|
18
|
+
* named properties. When absent, fields are accessed by index from an array.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
#include "debug.h"
|
|
22
|
+
#include "ObjcObject.h"
|
|
23
|
+
#include "type-conversion.h"
|
|
24
|
+
#include <Foundation/Foundation.h>
|
|
25
|
+
#include <map>
|
|
26
|
+
#include <napi.h>
|
|
27
|
+
#include <objc/runtime.h>
|
|
28
|
+
#include <string>
|
|
29
|
+
#include <vector>
|
|
30
|
+
|
|
31
|
+
// MARK: - Well-Known Struct Field Names
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Maps ObjC struct names to their field names, since the runtime type
|
|
35
|
+
* encodings typically don't include field names (only the compiler's
|
|
36
|
+
* @encode() does). Without this mapping, fields would be named field0,
|
|
37
|
+
* field1, etc.
|
|
38
|
+
*/
|
|
39
|
+
// clang-format off
|
|
40
|
+
static const std::map<std::string, std::vector<std::string>> KNOWN_STRUCT_FIELDS = {
|
|
41
|
+
// CoreGraphics / AppKit geometry
|
|
42
|
+
{"CGPoint", {"x", "y"}},
|
|
43
|
+
{"NSPoint", {"x", "y"}},
|
|
44
|
+
{"CGSize", {"width", "height"}},
|
|
45
|
+
{"NSSize", {"width", "height"}},
|
|
46
|
+
{"CGRect", {"origin", "size"}},
|
|
47
|
+
{"NSRect", {"origin", "size"}},
|
|
48
|
+
{"CGVector", {"dx", "dy"}},
|
|
49
|
+
// Foundation
|
|
50
|
+
{"_NSRange", {"location", "length"}},
|
|
51
|
+
{"NSRange", {"location", "length"}},
|
|
52
|
+
// Edge insets
|
|
53
|
+
{"NSEdgeInsets", {"top", "left", "bottom", "right"}},
|
|
54
|
+
{"NSDirectionalEdgeInsets", {"top", "leading", "bottom", "trailing"}},
|
|
55
|
+
// Affine transforms
|
|
56
|
+
{"CGAffineTransform", {"a", "b", "c", "d", "tx", "ty"}},
|
|
57
|
+
};
|
|
58
|
+
// clang-format on
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Look up well-known field names for a struct by its name.
|
|
62
|
+
* Returns nullptr if the struct name is not recognized.
|
|
63
|
+
*/
|
|
64
|
+
inline const std::vector<std::string> *
|
|
65
|
+
LookupKnownFieldNames(const std::string &structName) {
|
|
66
|
+
auto it = KNOWN_STRUCT_FIELDS.find(structName);
|
|
67
|
+
if (it != KNOWN_STRUCT_FIELDS.end()) {
|
|
68
|
+
return &it->second;
|
|
69
|
+
}
|
|
70
|
+
return nullptr;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// MARK: - Data Structures
|
|
74
|
+
|
|
75
|
+
struct StructFieldInfo {
|
|
76
|
+
std::string name; // Field name (e.g., "origin", "x"), empty if unnamed
|
|
77
|
+
std::string typeEncoding; // Full type encoding for this field
|
|
78
|
+
size_t offset; // Byte offset within parent struct
|
|
79
|
+
size_t size; // Size in bytes
|
|
80
|
+
size_t alignment; // Alignment requirement
|
|
81
|
+
bool isStruct; // True if this field is a nested struct
|
|
82
|
+
std::vector<StructFieldInfo> subfields; // Non-empty if isStruct
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
struct ParsedStructType {
|
|
86
|
+
std::string name; // Struct name (e.g., "CGRect")
|
|
87
|
+
std::vector<StructFieldInfo> fields; // Top-level fields
|
|
88
|
+
size_t totalSize; // Total struct size in bytes
|
|
89
|
+
size_t alignment; // Struct alignment
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// MARK: - Offset Computation
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compute byte offsets for all fields (and recursively for subfields)
|
|
96
|
+
* using alignment rules.
|
|
97
|
+
*/
|
|
98
|
+
inline void ComputeFieldOffsets(std::vector<StructFieldInfo> &fields) {
|
|
99
|
+
size_t currentOffset = 0;
|
|
100
|
+
for (auto &field : fields) {
|
|
101
|
+
// Align to field's alignment requirement
|
|
102
|
+
if (field.alignment > 0) {
|
|
103
|
+
currentOffset =
|
|
104
|
+
(currentOffset + field.alignment - 1) & ~(field.alignment - 1);
|
|
105
|
+
}
|
|
106
|
+
field.offset = currentOffset;
|
|
107
|
+
currentOffset += field.size;
|
|
108
|
+
|
|
109
|
+
// Recursively compute offsets for nested struct subfields
|
|
110
|
+
if (field.isStruct && !field.subfields.empty()) {
|
|
111
|
+
ComputeFieldOffsets(field.subfields);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// MARK: - Encoding Parser
|
|
117
|
+
|
|
118
|
+
// Forward declaration
|
|
119
|
+
inline bool ParseStructFields(const char *&ptr,
|
|
120
|
+
std::vector<StructFieldInfo> &fields);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse struct fields from the encoding, handling both named and unnamed
|
|
124
|
+
* formats.
|
|
125
|
+
*
|
|
126
|
+
* Named: "fieldName"type"fieldName2"type2
|
|
127
|
+
* Unnamed: type1type2
|
|
128
|
+
*
|
|
129
|
+
* ptr should point just past the '=' in {StructName=...}
|
|
130
|
+
* Advances ptr to the closing '}'.
|
|
131
|
+
*/
|
|
132
|
+
inline bool ParseStructFields(const char *&ptr,
|
|
133
|
+
std::vector<StructFieldInfo> &fields) {
|
|
134
|
+
int fieldIndex = 0;
|
|
135
|
+
|
|
136
|
+
while (*ptr && *ptr != '}') {
|
|
137
|
+
// Skip type qualifiers
|
|
138
|
+
ptr = SimplifyTypeEncoding(ptr);
|
|
139
|
+
|
|
140
|
+
if (*ptr == '}')
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
StructFieldInfo field;
|
|
144
|
+
|
|
145
|
+
// Check for quoted field name: "fieldName"
|
|
146
|
+
if (*ptr == '"') {
|
|
147
|
+
ptr++; // Skip opening quote
|
|
148
|
+
const char *nameStart = ptr;
|
|
149
|
+
while (*ptr && *ptr != '"') {
|
|
150
|
+
ptr++;
|
|
151
|
+
}
|
|
152
|
+
field.name = std::string(nameStart, ptr - nameStart);
|
|
153
|
+
if (*ptr == '"')
|
|
154
|
+
ptr++; // Skip closing quote
|
|
155
|
+
} else {
|
|
156
|
+
// No field name — generate a positional name
|
|
157
|
+
field.name = "field" + std::to_string(fieldIndex);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Parse the field's type encoding
|
|
161
|
+
field.typeEncoding = SkipOneFieldEncoding(ptr);
|
|
162
|
+
field.isStruct = (!field.typeEncoding.empty() && field.typeEncoding[0] == '{');
|
|
163
|
+
|
|
164
|
+
// If this is a nested struct, recursively parse its subfields
|
|
165
|
+
if (field.isStruct) {
|
|
166
|
+
auto subHeader = ParseStructEncodingHeader(field.typeEncoding.c_str());
|
|
167
|
+
if (subHeader.fieldsStart) {
|
|
168
|
+
const char *subPtr = subHeader.fieldsStart;
|
|
169
|
+
ParseStructFields(subPtr, field.subfields);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get size and alignment from the runtime
|
|
174
|
+
NSUInteger nsSize, nsAlignment;
|
|
175
|
+
NSGetSizeAndAlignment(field.typeEncoding.c_str(), &nsSize, &nsAlignment);
|
|
176
|
+
field.size = nsSize;
|
|
177
|
+
field.alignment = nsAlignment;
|
|
178
|
+
|
|
179
|
+
fields.push_back(std::move(field));
|
|
180
|
+
fieldIndex++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return !fields.empty();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parse a complete struct type encoding into a ParsedStructType.
|
|
188
|
+
*
|
|
189
|
+
* Handles both named and unnamed field formats:
|
|
190
|
+
* {CGRect="origin"{CGPoint="x"d"y"d}"size"{CGSize="width"d"height"d}}
|
|
191
|
+
* {CGRect={CGPoint=dd}{CGSize=dd}}
|
|
192
|
+
*
|
|
193
|
+
* For unnamed fields, looks up the struct name in KNOWN_STRUCT_FIELDS
|
|
194
|
+
* to apply proper field names (e.g., CGRect → origin, size).
|
|
195
|
+
*/
|
|
196
|
+
inline ParsedStructType ParseStructEncodingWithNames(const char *encoding) {
|
|
197
|
+
ParsedStructType result;
|
|
198
|
+
|
|
199
|
+
auto header = ParseStructEncodingHeader(encoding);
|
|
200
|
+
|
|
201
|
+
if (!header.fieldsStart && !header.empty) {
|
|
202
|
+
NOBJC_ERROR("ParseStructEncodingWithNames: Invalid struct encoding '%s'",
|
|
203
|
+
encoding ? encoding : "(null)");
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
result.name = header.name;
|
|
208
|
+
|
|
209
|
+
if (header.empty) {
|
|
210
|
+
result.totalSize = 0;
|
|
211
|
+
result.alignment = 1;
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Parse all fields
|
|
216
|
+
const char *ptr = header.fieldsStart;
|
|
217
|
+
ParseStructFields(ptr, result.fields);
|
|
218
|
+
|
|
219
|
+
// Apply well-known field names if fields don't have names from the encoding
|
|
220
|
+
// (i.e., they have generated names like "field0", "field1", etc.)
|
|
221
|
+
if (!result.fields.empty() && result.fields[0].name.substr(0, 5) == "field") {
|
|
222
|
+
const auto *knownNames = LookupKnownFieldNames(result.name);
|
|
223
|
+
if (knownNames && knownNames->size() == result.fields.size()) {
|
|
224
|
+
for (size_t i = 0; i < result.fields.size(); i++) {
|
|
225
|
+
result.fields[i].name = (*knownNames)[i];
|
|
226
|
+
}
|
|
227
|
+
NOBJC_LOG("ParseStructEncodingWithNames: Applied known field names for "
|
|
228
|
+
"'%s'",
|
|
229
|
+
result.name.c_str());
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Recursively apply known names to subfields of nested structs
|
|
234
|
+
for (auto &field : result.fields) {
|
|
235
|
+
if (field.isStruct && !field.subfields.empty() &&
|
|
236
|
+
!field.subfields[0].name.empty() &&
|
|
237
|
+
field.subfields[0].name.substr(0, 5) == "field") {
|
|
238
|
+
// Extract the nested struct name from its type encoding
|
|
239
|
+
std::string nestedName;
|
|
240
|
+
if (field.typeEncoding.size() > 1 && field.typeEncoding[0] == '{') {
|
|
241
|
+
const char *p = field.typeEncoding.c_str() + 1;
|
|
242
|
+
const char *ns = p;
|
|
243
|
+
while (*p && *p != '=' && *p != '}') {
|
|
244
|
+
p++;
|
|
245
|
+
}
|
|
246
|
+
nestedName = std::string(ns, p - ns);
|
|
247
|
+
}
|
|
248
|
+
if (!nestedName.empty()) {
|
|
249
|
+
const auto *nestedKnown = LookupKnownFieldNames(nestedName);
|
|
250
|
+
if (nestedKnown && nestedKnown->size() == field.subfields.size()) {
|
|
251
|
+
for (size_t i = 0; i < field.subfields.size(); i++) {
|
|
252
|
+
field.subfields[i].name = (*nestedKnown)[i];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Compute field offsets (recursively handles nested structs)
|
|
260
|
+
ComputeFieldOffsets(result.fields);
|
|
261
|
+
|
|
262
|
+
// Get total struct size and alignment from the runtime
|
|
263
|
+
NSUInteger nsSize, nsAlignment;
|
|
264
|
+
NSGetSizeAndAlignment(encoding, &nsSize, &nsAlignment);
|
|
265
|
+
result.totalSize = nsSize;
|
|
266
|
+
result.alignment = nsAlignment;
|
|
267
|
+
|
|
268
|
+
NOBJC_LOG("ParseStructEncodingWithNames: '%s' has %zu fields, size=%zu, "
|
|
269
|
+
"alignment=%zu",
|
|
270
|
+
result.name.c_str(), result.fields.size(), result.totalSize,
|
|
271
|
+
result.alignment);
|
|
272
|
+
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// MARK: - JS Object → Struct Buffer (for arguments)
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Visitor for writing a single primitive JS value into a struct buffer.
|
|
280
|
+
* Used with DispatchByTypeCode to eliminate per-type switch boilerplate.
|
|
281
|
+
*/
|
|
282
|
+
struct WriteLeafToBufferVisitor {
|
|
283
|
+
Napi::Env env;
|
|
284
|
+
const Napi::Value &jsValue;
|
|
285
|
+
uint8_t *dest;
|
|
286
|
+
|
|
287
|
+
// Numeric types (excluding bool) — integers use Int64Value, floats use
|
|
288
|
+
// DoubleValue
|
|
289
|
+
template <typename T>
|
|
290
|
+
auto operator()(std::type_identity<T>) const
|
|
291
|
+
-> std::enable_if_t<is_numeric_v<T> && !std::is_same_v<T, bool>, void> {
|
|
292
|
+
T val;
|
|
293
|
+
if constexpr (is_floating_point_v<T>) {
|
|
294
|
+
val = static_cast<T>(jsValue.As<Napi::Number>().DoubleValue());
|
|
295
|
+
} else {
|
|
296
|
+
val = static_cast<T>(jsValue.As<Napi::Number>().Int64Value());
|
|
297
|
+
}
|
|
298
|
+
memcpy(dest, &val, sizeof(val));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Bool → uint8_t (ObjC BOOL is 1 byte)
|
|
302
|
+
void operator()(std::type_identity<bool>) const {
|
|
303
|
+
bool val = jsValue.ToBoolean().Value();
|
|
304
|
+
uint8_t boolByte = val ? 1 : 0;
|
|
305
|
+
memcpy(dest, &boolByte, sizeof(boolByte));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// id (@) — extract from ObjcObject wrapper
|
|
309
|
+
void operator()(std::type_identity<ObjCIdTag>) const {
|
|
310
|
+
id objcObj = nil;
|
|
311
|
+
if (!jsValue.IsNull() && !jsValue.IsUndefined() && jsValue.IsObject()) {
|
|
312
|
+
ObjcObject *wrapper = ObjcObject::Unwrap(jsValue.As<Napi::Object>());
|
|
313
|
+
if (wrapper) {
|
|
314
|
+
objcObj = wrapper->objcObject;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
memcpy(dest, &objcObj, sizeof(objcObj));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Class (#) — not typically used in struct fields
|
|
321
|
+
void operator()(std::type_identity<ObjCClassTag>) const {
|
|
322
|
+
Class cls = nil;
|
|
323
|
+
memcpy(dest, &cls, sizeof(cls));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// SEL (:)
|
|
327
|
+
void operator()(std::type_identity<ObjCSELTag>) const {
|
|
328
|
+
SEL sel = nullptr;
|
|
329
|
+
if (jsValue.IsString()) {
|
|
330
|
+
std::string selName = jsValue.As<Napi::String>().Utf8Value();
|
|
331
|
+
sel = sel_registerName(selName.c_str());
|
|
332
|
+
}
|
|
333
|
+
memcpy(dest, &sel, sizeof(sel));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// C string (*) — can't safely handle lifetime, set to nullptr
|
|
337
|
+
void operator()(std::type_identity<ObjCCStringTag>) const {
|
|
338
|
+
const char *val = nullptr;
|
|
339
|
+
memcpy(dest, &val, sizeof(val));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Pointer (^)
|
|
343
|
+
void operator()(std::type_identity<ObjCPointerTag>) const {
|
|
344
|
+
void *val = nullptr;
|
|
345
|
+
if (!jsValue.IsNull() && !jsValue.IsUndefined() && jsValue.IsBuffer()) {
|
|
346
|
+
Napi::Buffer<uint8_t> buf = jsValue.As<Napi::Buffer<uint8_t>>();
|
|
347
|
+
val = buf.Data();
|
|
348
|
+
}
|
|
349
|
+
memcpy(dest, &val, sizeof(val));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Void — should not appear in struct fields
|
|
353
|
+
void operator()(std::type_identity<ObjCVoidTag>) const {
|
|
354
|
+
NOBJC_ERROR("WriteLeafToBufferVisitor: void type in struct field");
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Write a single primitive JS value into a buffer at the given offset,
|
|
360
|
+
* according to the type encoding character.
|
|
361
|
+
*/
|
|
362
|
+
inline void WriteLeafValueToBuffer(Napi::Env env, const Napi::Value &jsValue,
|
|
363
|
+
const char *typeEncoding, uint8_t *buffer,
|
|
364
|
+
size_t offset) {
|
|
365
|
+
const char *simplified = SimplifyTypeEncoding(typeEncoding);
|
|
366
|
+
DispatchByTypeCode(simplified[0],
|
|
367
|
+
WriteLeafToBufferVisitor{env, jsValue, buffer + offset});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Recursively pack a JS object/array into a struct byte buffer.
|
|
372
|
+
*
|
|
373
|
+
* For named fields, reads JS object properties by name.
|
|
374
|
+
* Falls back to iterating JS object properties in order if names don't match.
|
|
375
|
+
* Arrays are indexed positionally.
|
|
376
|
+
*/
|
|
377
|
+
inline void PackJSValueToStructBuffer(Napi::Env env,
|
|
378
|
+
const Napi::Value &jsValue,
|
|
379
|
+
const std::vector<StructFieldInfo> &fields,
|
|
380
|
+
uint8_t *buffer, size_t baseOffset) {
|
|
381
|
+
bool isArray = jsValue.IsArray();
|
|
382
|
+
bool isObject = jsValue.IsObject() && !isArray;
|
|
383
|
+
|
|
384
|
+
if (!isArray && !isObject) {
|
|
385
|
+
throw Napi::TypeError::New(
|
|
386
|
+
env, "Struct argument must be an object or array");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
Napi::Object jsObj = jsValue.As<Napi::Object>();
|
|
390
|
+
|
|
391
|
+
// Check if the JS object has the expected field names
|
|
392
|
+
bool namesMatch = false;
|
|
393
|
+
if (isObject && !fields.empty()) {
|
|
394
|
+
namesMatch = jsObj.Has(fields[0].name);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// If names don't match and it's an object, get property names to iterate
|
|
398
|
+
// in order
|
|
399
|
+
Napi::Array propNames;
|
|
400
|
+
if (isObject && !namesMatch) {
|
|
401
|
+
propNames = jsObj.GetPropertyNames();
|
|
402
|
+
if (propNames.Length() < fields.size()) {
|
|
403
|
+
throw Napi::Error::New(
|
|
404
|
+
env,
|
|
405
|
+
std::string("Object has ") + std::to_string(propNames.Length()) +
|
|
406
|
+
" properties but struct expects " +
|
|
407
|
+
std::to_string(fields.size()) + " fields");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (size_t i = 0; i < fields.size(); i++) {
|
|
412
|
+
const StructFieldInfo &field = fields[i];
|
|
413
|
+
Napi::Value fieldValue;
|
|
414
|
+
|
|
415
|
+
if (isArray) {
|
|
416
|
+
Napi::Array arr = jsValue.As<Napi::Array>();
|
|
417
|
+
if (i < arr.Length()) {
|
|
418
|
+
fieldValue = arr.Get(static_cast<uint32_t>(i));
|
|
419
|
+
} else {
|
|
420
|
+
throw Napi::Error::New(
|
|
421
|
+
env,
|
|
422
|
+
std::string("Struct array too short: expected at least ") +
|
|
423
|
+
std::to_string(fields.size()) + " elements, got " +
|
|
424
|
+
std::to_string(arr.Length()));
|
|
425
|
+
}
|
|
426
|
+
} else if (namesMatch) {
|
|
427
|
+
// Object with matching field names — look up by name
|
|
428
|
+
fieldValue = jsObj.Get(field.name);
|
|
429
|
+
} else {
|
|
430
|
+
// Object without matching names — use property order
|
|
431
|
+
Napi::Value key = propNames.Get(static_cast<uint32_t>(i));
|
|
432
|
+
fieldValue = jsObj.Get(key.As<Napi::String>());
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (field.isStruct && !field.subfields.empty()) {
|
|
436
|
+
// Nested struct — recurse
|
|
437
|
+
PackJSValueToStructBuffer(env, fieldValue, field.subfields, buffer,
|
|
438
|
+
baseOffset + field.offset);
|
|
439
|
+
} else {
|
|
440
|
+
// Leaf field — write the value
|
|
441
|
+
WriteLeafValueToBuffer(env, fieldValue, field.typeEncoding.c_str(),
|
|
442
|
+
buffer, baseOffset + field.offset);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// MARK: - Struct Buffer → JS Object (for return values)
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Read a single primitive value from a buffer and convert to JS.
|
|
451
|
+
* Delegates to ObjCToJS from type-conversion.h.
|
|
452
|
+
*/
|
|
453
|
+
inline Napi::Value ReadLeafValueFromBuffer(Napi::Env env,
|
|
454
|
+
const char *typeEncoding,
|
|
455
|
+
const uint8_t *buffer,
|
|
456
|
+
size_t offset) {
|
|
457
|
+
const char *simplified = SimplifyTypeEncoding(typeEncoding);
|
|
458
|
+
return ObjCToJS(env, const_cast<uint8_t *>(buffer) + offset, simplified[0]);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Recursively unpack a struct byte buffer into a JS object with named fields.
|
|
463
|
+
*/
|
|
464
|
+
inline Napi::Value
|
|
465
|
+
UnpackStructBufferToJSObject(Napi::Env env,
|
|
466
|
+
const std::vector<StructFieldInfo> &fields,
|
|
467
|
+
const uint8_t *buffer, size_t baseOffset) {
|
|
468
|
+
Napi::Object result = Napi::Object::New(env);
|
|
469
|
+
|
|
470
|
+
for (const auto &field : fields) {
|
|
471
|
+
Napi::Value fieldValue;
|
|
472
|
+
|
|
473
|
+
if (field.isStruct && !field.subfields.empty()) {
|
|
474
|
+
// Nested struct — recurse
|
|
475
|
+
fieldValue = UnpackStructBufferToJSObject(env, field.subfields, buffer,
|
|
476
|
+
baseOffset + field.offset);
|
|
477
|
+
} else {
|
|
478
|
+
// Leaf field — read the value
|
|
479
|
+
fieldValue = ReadLeafValueFromBuffer(env, field.typeEncoding.c_str(),
|
|
480
|
+
buffer, baseOffset + field.offset);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
result.Set(field.name, fieldValue);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// MARK: - High-Level API
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Check if a type encoding represents a struct type.
|
|
493
|
+
* Handles leading type qualifiers.
|
|
494
|
+
*/
|
|
495
|
+
inline bool IsStructTypeEncoding(const char *typeEncoding) {
|
|
496
|
+
if (!typeEncoding)
|
|
497
|
+
return false;
|
|
498
|
+
return *SimplifyTypeEncoding(typeEncoding) == '{';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Pack a JS value into a newly allocated struct buffer.
|
|
503
|
+
* Returns a vector<uint8_t> that must be kept alive until after the
|
|
504
|
+
* NSInvocation is invoked.
|
|
505
|
+
*/
|
|
506
|
+
inline std::vector<uint8_t>
|
|
507
|
+
PackJSValueAsStruct(Napi::Env env, const Napi::Value &jsValue,
|
|
508
|
+
const char *typeEncoding) {
|
|
509
|
+
ParsedStructType parsed = ParseStructEncodingWithNames(typeEncoding);
|
|
510
|
+
|
|
511
|
+
if (parsed.fields.empty()) {
|
|
512
|
+
throw Napi::Error::New(
|
|
513
|
+
env, std::string("Failed to parse struct encoding: ") + typeEncoding);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
std::vector<uint8_t> buffer(parsed.totalSize, 0);
|
|
517
|
+
PackJSValueToStructBuffer(env, jsValue, parsed.fields, buffer.data(), 0);
|
|
518
|
+
return buffer;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Unpack a struct byte buffer into a JS object.
|
|
523
|
+
*/
|
|
524
|
+
inline Napi::Value UnpackStructToJSValue(Napi::Env env,
|
|
525
|
+
const uint8_t *buffer,
|
|
526
|
+
const char *typeEncoding) {
|
|
527
|
+
ParsedStructType parsed = ParseStructEncodingWithNames(typeEncoding);
|
|
528
|
+
|
|
529
|
+
if (parsed.fields.empty()) {
|
|
530
|
+
NOBJC_ERROR("UnpackStructToJSValue: Failed to parse struct encoding '%s'",
|
|
531
|
+
typeEncoding);
|
|
532
|
+
return env.Undefined();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return UnpackStructBufferToJSObject(env, parsed.fields, buffer, 0);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#endif // STRUCT_UTILS_H
|
|
@@ -84,6 +84,109 @@ inline const char *SimplifyTypeEncoding(const char *typeEncoding) {
|
|
|
84
84
|
return ptr;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// MARK: - Struct Encoding Parsing Utilities
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Result of parsing a struct encoding header ({Name=...).
|
|
91
|
+
*/
|
|
92
|
+
struct StructEncodingHeader {
|
|
93
|
+
std::string name; // Struct name (e.g., "CGRect")
|
|
94
|
+
const char *fieldsStart; // Pointer to first field encoding (past '='), or
|
|
95
|
+
// nullptr if error/empty
|
|
96
|
+
bool empty; // True if struct has no fields
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse the header of a struct type encoding.
|
|
101
|
+
* Extracts the struct name and advances past the '=' separator.
|
|
102
|
+
* Returns a header with fieldsStart pointing to the first field,
|
|
103
|
+
* or nullptr if the encoding is malformed or the struct is empty.
|
|
104
|
+
*/
|
|
105
|
+
inline StructEncodingHeader ParseStructEncodingHeader(const char *encoding) {
|
|
106
|
+
StructEncodingHeader result{"", nullptr, false};
|
|
107
|
+
|
|
108
|
+
if (!encoding || encoding[0] != '{') {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const char *ptr = encoding + 1;
|
|
113
|
+
const char *nameStart = ptr;
|
|
114
|
+
while (*ptr && *ptr != '=' && *ptr != '}') {
|
|
115
|
+
ptr++;
|
|
116
|
+
}
|
|
117
|
+
result.name = std::string(nameStart, ptr - nameStart);
|
|
118
|
+
|
|
119
|
+
if (*ptr == '}') {
|
|
120
|
+
result.empty = true;
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (*ptr != '=') {
|
|
125
|
+
return result; // Malformed
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ptr++; // Skip '='
|
|
129
|
+
result.fieldsStart = ptr;
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Advance a pointer past one complete field type encoding.
|
|
135
|
+
* Handles nested structs {}, unions (), pointers ^, and simple types.
|
|
136
|
+
* Returns the encoding string for the field.
|
|
137
|
+
*/
|
|
138
|
+
inline std::string SkipOneFieldEncoding(const char *&ptr) {
|
|
139
|
+
const char *start = ptr;
|
|
140
|
+
|
|
141
|
+
if (*ptr == '{') {
|
|
142
|
+
// Nested struct — find matching '}'
|
|
143
|
+
int depth = 1;
|
|
144
|
+
ptr++;
|
|
145
|
+
while (*ptr && depth > 0) {
|
|
146
|
+
if (*ptr == '{')
|
|
147
|
+
depth++;
|
|
148
|
+
else if (*ptr == '}')
|
|
149
|
+
depth--;
|
|
150
|
+
ptr++;
|
|
151
|
+
}
|
|
152
|
+
} else if (*ptr == '(') {
|
|
153
|
+
// Union — find matching ')'
|
|
154
|
+
int depth = 1;
|
|
155
|
+
ptr++;
|
|
156
|
+
while (*ptr && depth > 0) {
|
|
157
|
+
if (*ptr == '(')
|
|
158
|
+
depth++;
|
|
159
|
+
else if (*ptr == ')')
|
|
160
|
+
depth--;
|
|
161
|
+
ptr++;
|
|
162
|
+
}
|
|
163
|
+
} else if (*ptr == '^') {
|
|
164
|
+
// Pointer type — skip '^' and the pointed-to type
|
|
165
|
+
ptr++;
|
|
166
|
+
if (*ptr == '{') {
|
|
167
|
+
int depth = 1;
|
|
168
|
+
ptr++;
|
|
169
|
+
while (*ptr && depth > 0) {
|
|
170
|
+
if (*ptr == '{')
|
|
171
|
+
depth++;
|
|
172
|
+
else if (*ptr == '}')
|
|
173
|
+
depth--;
|
|
174
|
+
ptr++;
|
|
175
|
+
}
|
|
176
|
+
} else if (*ptr) {
|
|
177
|
+
ptr++;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Simple type — single character, skip any trailing digits
|
|
181
|
+
ptr++;
|
|
182
|
+
while (*ptr && isdigit(*ptr)) {
|
|
183
|
+
ptr++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return std::string(start, ptr - start);
|
|
188
|
+
}
|
|
189
|
+
|
|
87
190
|
// MARK: - ObjC to JS Conversion
|
|
88
191
|
|
|
89
192
|
// Visitor for converting ObjC values to JS
|