objc-js 1.0.3 → 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 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
@@ -39,11 +39,11 @@
39
39
  "make-clangd-config": "node ./scripts/make-clangd-config.js",
40
40
  "format": "prettier --write \"**/*.{ts,js,json,md}\"",
41
41
  "preinstall-disabled": "npm run build-scripts && npm run make-clangd-config",
42
- "prebuild:arm64": "prebuildify --napi --strip --arch arm64",
43
- "prebuild:x64": "prebuildify --napi --strip --arch x64",
42
+ "prebuild:arm64": "prebuildify --napi --strip --arch arm64 -r node --tagArmv",
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.3",
46
+ "version": "1.1.0",
47
47
  "description": "Objective-C bridge for Node.js",
48
48
  "main": "dist/index.js",
49
49
  "dependencies": {
@@ -58,5 +58,8 @@
58
58
  "prettier": "^3.7.4",
59
59
  "typescript": "^5.9.3"
60
60
  },
61
- "gypfile": true
61
+ "gypfile": true,
62
+ "patchedDependencies": {
63
+ "prebuildify@6.0.1": "patches/prebuildify@6.0.1.patch"
64
+ }
62
65
  }
@@ -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 (strlen(returnType) != 1 ||
82
- strchr(validReturnTypes, *returnType) == nullptr) {
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 goes out of scope here, after invoke has completed
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
 
@@ -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
- if (encoding[0] != '{') {
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 (*ptr == '}') {
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 (*ptr != '=') {
206
- NOBJC_ERROR("ParseStructEncoding: Expected '=' after struct name");
195
+ if (!header.fieldsStart) {
196
+ NOBJC_ERROR("ParseStructEncoding: Invalid struct encoding '%s'", encoding);
207
197
  return nullptr;
208
198
  }
209
199
 
210
- ptr++; // Skip '='
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
- while (*ptr && (*ptr == 'r' || *ptr == 'n' || *ptr == 'N' ||
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
- // Determine field encoding length
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
Binary file
Binary file