koffi 2.14.0-beta.2 → 2.14.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/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@
5
5
 
6
6
  ## Koffi 2
7
7
 
8
+ ### Koffi 2.14
9
+
10
+ #### Koffi 2.14.0
11
+
12
+ *Released on 2025-08-17*
13
+
14
+ - Improve support for structs with [flexible array member](input#flexible-arrays)
15
+ - Automatically encode/decode dynamic arrays pointers when length is known though struct member
16
+ - Fix parser crash when direction qualifier is followed by unknown type
17
+ - Add missing TypeScript types and arguments
18
+
8
19
  ### Koffi 2.13
9
20
 
10
21
  #### Koffi 2.13.0
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -15,7 +15,7 @@ These results are detailed and explained below, and compared to node-ffi/node-ff
15
15
 
16
16
  # Linux x86_64
17
17
 
18
- The results presented below were measured on my x86_64 Linux machine (Intel® Corei5-4460).
18
+ The results presented below were measured on my x86_64 Linux machine (AMD Ryzen5 2600).
19
19
 
20
20
  ## rand results
21
21
 
@@ -27,9 +27,9 @@ This test is based around repeated calls to a simple standard C function `rand`,
27
27
 
28
28
  Benchmark | Iteration time | Relative performance | Overhead
29
29
  ------------- | -------------- | -------------------- | --------
30
- rand_napi | 700 ns | x1.00 | (ref)
31
- rand_koffi | 1152 ns | x0.61 | +64%
32
- rand_node_ffi | 32750 ns | x0.02 | +4576%
30
+ rand_napi | 569 ns | x1.00 | (ref)
31
+ rand_koffi | 855 ns | x0.67 | +50%
32
+ rand_node_ffi | 58730 ns | x0.010 | +10228%
33
33
 
34
34
  Because rand is a pretty small function, the FFI overhead is clearly visible.
35
35
 
@@ -39,9 +39,9 @@ This test is similar to the rand one, but it is based on `atoi`, which takes a s
39
39
 
40
40
  Benchmark | Iteration time | Relative performance | Overhead
41
41
  ------------- | -------------- | -------------------- | --------
42
- atoi_napi | 1028 ns | x1.00 | (ref)
43
- atoi_koffi | 1730 ns | x0.59 | +68%
44
- atoi_node_ffi | 121670 ns | x0.008 | +11738%
42
+ atoi_napi | 1039 ns | x1.00 | (ref)
43
+ atoi_koffi | 1642 ns | x0.63 | +58%
44
+ atoi_node_ffi | 164790 ns | x0.006 | +15767%
45
45
 
46
46
  Because atoi is a pretty small function, the FFI overhead is clearly visible.
47
47
 
@@ -54,10 +54,10 @@ This benchmark uses the CPU-based image drawing functions in Raylib. The calls a
54
54
 
55
55
  Benchmark | Iteration time | Relative performance | Overhead
56
56
  ------------------ | -------------- | -------------------- | --------
57
- raylib_cc | 18.5 µs | x1.42 | -30%
58
- raylib_node_raylib | 26.3 µs | x1.00 | (ref)
59
- raylib_koffi | 28.0 µs | x0.94 | +6%
60
- raylib_node_ffi | 87.0 µs | x0.30 | +230%
57
+ raylib_cc | 17.5 µs | x1.34 | -25%
58
+ raylib_node_raylib | 23.4 µs | x1.00 | (ref)
59
+ raylib_koffi | 28.8 µs | x0.81 | +23%
60
+ raylib_node_ffi | 103.9 µs | x0.23 | +344%
61
61
 
62
62
  # Windows x86_64
63
63
 
@@ -396,6 +396,72 @@ Koffi can also convert JS strings to fixed-sized arrays in the following cases:
396
396
 
397
397
  The reverse case is also true, Koffi can convert a C fixed-size buffer to a JS string. This happens by default for char, char16_t and char32_t arrays, but you can also explicitly ask for this with the `String` array hint (e.g. `koffi.array('char', 8, 'String')`).
398
398
 
399
+ ## Flexible arrays
400
+
401
+ *Added in Koffi 2.14.0*
402
+
403
+ C structs ending with a flexible array member are often used for variable-sized structs, and are generally paired with dynamic memory allocation. In many cases, the number of elements is described by another struct member.
404
+
405
+ Use `koffi.array(type, countedBy, maxLen)` to make a flexible array type, for which the array length is determined by the struct member indicated by the `countedBy` parameter. Flexible array types can only be used as the last member of a struct, as shown below:
406
+
407
+ ```js
408
+ const FlexibleArray = koffi.struct('FlexibleArray', {
409
+ count: 'size_t',
410
+ numbers: koffi.array('int', 'count', 128)
411
+ });
412
+ ````
413
+
414
+ For various reasons, Koffi requires you to specify an upper bound (`maxLen`) for the number of elements in the flexible array member.
415
+
416
+ > [!WARNING]
417
+ > Also, unlike C flexible arrays, the struct size will expand to accomodate the maximum size of the flexible array.
418
+
419
+ The following example illustrates how to use a flexible array API in C from Koffi.
420
+
421
+ ```c
422
+ // Build with: clang -fPIC -o flexible.so -shared flexible.c -Wall -O2
423
+
424
+ #include <stddef.h>
425
+
426
+ struct FlexibleArray {
427
+ size_t count;
428
+ int numbers[];
429
+ };
430
+
431
+ void AppendValues(struct FlexibleArray *arr, size_t count, int start, int step)
432
+ {
433
+ for (size_t i = 0; i < count; i++) {
434
+ arr->numbers[arr->count + i] = start + i * step;
435
+ }
436
+ arr->count += count;
437
+ }
438
+ ```
439
+
440
+ ```js
441
+ // ES6 syntax: import koffi from 'koffi';
442
+ const koffi = require('koffi');
443
+
444
+ const lib = koffi.load('./flexible.so');
445
+
446
+ const FlexibleArray = koffi.struct('FlexibleArray', {
447
+ count: 'size_t',
448
+ numbers: koffi.array('int', 'count', 256, 'Array')
449
+ });
450
+
451
+ const AppendValues = lib.func('void AppendValues(_Inout_ FlexibleArray *arr, int count, int start, int step)');
452
+
453
+ let array = { count: 0, numbers: [] };
454
+
455
+ AppendValues(array, 5, 1, 1);
456
+ console.log(array); // Prints { count: 5, numbers: [1, 2, 3, 4, 5] }
457
+
458
+ AppendValues(array, 3, 10, 2);
459
+ console.log(array); // Prints { count: 8, numbers: [1, 2, 3, 4, 5, 10, 12, 14] }
460
+ ```
461
+
462
+ > [!NOTE]
463
+ > This is frequently used in the Win32 API, an exemple of this is [AllocateAndInitializeSid()](https://learn.microsoft.com/windows/win32/api/securitybaseapi/nf-securitybaseapi-allocateandinitializesid).
464
+
399
465
  ## Dynamic arrays (pointers)
400
466
 
401
467
  In C, dynamically-sized arrays are usually passed around as pointers. Read more about [array pointers](pointers#dynamic-arrays) in the relevant section.
Binary file
Binary file
package/index.d.ts CHANGED
@@ -105,14 +105,14 @@ export class Union {
105
105
  }
106
106
 
107
107
  export function array(ref: TypeSpec, len: number, hint?: ArrayHint | null): IKoffiCType;
108
- export function array(ref: TypeSpec, countedBy: string, hint?: ArrayHint | null, len?: number | null): IKoffiCType;
108
+ export function array(ref: TypeSpec, countedBy: string, maxLen: number, hint?: ArrayHint | null): IKoffiCType;
109
109
 
110
110
  export function opaque(name: string | null | undefined): IKoffiCType;
111
111
  export function opaque(): IKoffiCType;
112
112
  /** @deprecated */ export function handle(name: string | null | undefined): IKoffiCType;
113
113
  /** @deprecated */ export function handle(): IKoffiCType;
114
114
 
115
- export function pointer(ref: TypeSpec, countedBy?: string | null): IKoffiCType;
115
+ export function pointer(ref: TypeSpec): IKoffiCType;
116
116
  export function pointer(ref: TypeSpec, count: number): IKoffiCType;
117
117
  export function pointer(name: string | null | undefined, ref: TypeSpec, countedBy?: string | null): IKoffiCType;
118
118
  export function pointer(name: string | null | undefined, ref: TypeSpec, count: number): IKoffiCType;
package/index.js CHANGED
@@ -402,7 +402,7 @@ var require_package = __commonJS({
402
402
  "../../../bin/Koffi/package/src/koffi/package.json"(exports2, module2) {
403
403
  module2.exports = {
404
404
  name: "koffi",
405
- version: "2.14.0-beta.2",
405
+ version: "2.14.0",
406
406
  description: "Fast and simple C FFI (foreign function interface) for Node.js",
407
407
  keywords: [
408
408
  "foreign",
package/indirect.js CHANGED
@@ -402,7 +402,7 @@ var require_package = __commonJS({
402
402
  "../../../bin/Koffi/package/src/koffi/package.json"(exports2, module2) {
403
403
  module2.exports = {
404
404
  name: "koffi",
405
- version: "2.14.0-beta.2",
405
+ version: "2.14.0",
406
406
  description: "Fast and simple C FFI (foreign function interface) for Node.js",
407
407
  keywords: [
408
408
  "foreign",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koffi",
3
- "version": "2.14.0-beta.2",
3
+ "version": "2.14.0",
4
4
  "description": "Fast and simple C FFI (foreign function interface) for Node.js",
5
5
  "keywords": [
6
6
  "foreign",
@@ -854,6 +854,31 @@ TimeSpec DecomposeTimeLocal(int64_t time)
854
854
  return spec;
855
855
  }
856
856
 
857
+ int64_t ComposeTimeUTC(const TimeSpec &spec)
858
+ {
859
+ RG_ASSERT(!spec.offset);
860
+
861
+ struct tm ti = {};
862
+
863
+ ti.tm_year = spec.year - 1900;
864
+ ti.tm_mon = spec.month - 1;
865
+ ti.tm_mday = spec.day;
866
+ ti.tm_hour = spec.hour;
867
+ ti.tm_min = spec.min;
868
+ ti.tm_sec = spec.sec;
869
+
870
+ #if defined(_WIN32)
871
+ int64_t time = (int64_t)_mkgmtime64(&ti);
872
+ #else
873
+ int64_t time = (int64_t)timegm(&ti);
874
+ #endif
875
+
876
+ time *= 1000;
877
+ time += spec.msec;
878
+
879
+ return time;
880
+ }
881
+
857
882
  // ------------------------------------------------------------------------
858
883
  // Strings
859
884
  // ------------------------------------------------------------------------
@@ -3669,6 +3669,7 @@ struct TimeSpec {
3669
3669
 
3670
3670
  TimeSpec DecomposeTimeUTC(int64_t time);
3671
3671
  TimeSpec DecomposeTimeLocal(int64_t time);
3672
+ int64_t ComposeTimeUTC(const TimeSpec &spec);
3672
3673
 
3673
3674
  // ------------------------------------------------------------------------
3674
3675
  // Format
@@ -517,7 +517,7 @@ bool CallData::PushObject(Napi::Object obj, const TypeInfo *type, uint8_t *origi
517
517
  RG_UNREACHABLE();
518
518
  }
519
519
 
520
- memset(origin, 0, type->size);
520
+ MemSet(origin, 0, type->size);
521
521
 
522
522
  for (Size i = 0; i < members.len; i++) {
523
523
  const RecordMember &member = members[i];
@@ -715,13 +715,11 @@ bool CallData::PushObject(Napi::Object obj, const TypeInfo *type, uint8_t *origi
715
715
  case PrimitiveKind::Array: {
716
716
  if (value.IsArray()) {
717
717
  Napi::Array array = value.As<Napi::Array>();
718
- Size len = (Size)member.type->size / member.type->ref.type->size;
719
-
720
- if (!PushNormalArray(array, len, member.type, dest))
718
+ if (!PushNormalArray(array, member.type, member.type->size, dest))
721
719
  return false;
722
720
  } else if (IsRawBuffer(value)) {
723
721
  Span<const uint8_t> buffer = GetRawBuffer(value);
724
- PushBuffer(buffer, member.type->size, member.type, dest);
722
+ PushBuffer(buffer, member.type, dest);
725
723
  } else if (value.IsString()) {
726
724
  if (!PushStringArray(value, member.type, dest))
727
725
  return false;
@@ -763,15 +761,18 @@ bool CallData::PushObject(Napi::Object obj, const TypeInfo *type, uint8_t *origi
763
761
  return true;
764
762
  }
765
763
 
766
- bool CallData::PushNormalArray(Napi::Array array, Size len, const TypeInfo *type, uint8_t *origin)
764
+ bool CallData::PushNormalArray(Napi::Array array, const TypeInfo *type, Size size, uint8_t *origin)
767
765
  {
768
766
  RG_ASSERT(array.IsArray());
769
767
 
770
768
  const TypeInfo *ref = type->ref.type;
769
+ Size len = (Size)array.Length();
770
+ Size available = len * ref->size;
771
771
 
772
- if (array.Length() != (size_t)len) [[unlikely]] {
773
- ThrowError<Napi::Error>(env, "Expected array of length %1, got %2", len, array.Length());
774
- return false;
772
+ if (available > size) {
773
+ len = size / ref->size;
774
+ } else {
775
+ MemSet(origin + available, 0, size - available);
775
776
  }
776
777
 
777
778
  Size offset = 0;
@@ -945,13 +946,11 @@ bool CallData::PushNormalArray(Napi::Array array, Size len, const TypeInfo *type
945
946
 
946
947
  if (value.IsArray()) {
947
948
  Napi::Array array2 = value.As<Napi::Array>();
948
- Size len2 = (Size)ref->size / ref->ref.type->size;
949
-
950
- if (!PushNormalArray(array2, len2, ref, dest))
949
+ if (!PushNormalArray(array2, ref, (Size)ref->size, dest))
951
950
  return false;
952
951
  } else if (IsRawBuffer(value)) {
953
952
  Span<const uint8_t> buffer = GetRawBuffer(value);
954
- PushBuffer(buffer, ref->size, ref, dest);
953
+ PushBuffer(buffer, ref, dest);
955
954
  } else if (value.IsString()) {
956
955
  if (!PushStringArray(value, ref, dest))
957
956
  return false;
@@ -1001,13 +1000,13 @@ bool CallData::PushNormalArray(Napi::Array array, Size len, const TypeInfo *type
1001
1000
  return true;
1002
1001
  }
1003
1002
 
1004
- void CallData::PushBuffer(Span<const uint8_t> buffer, Size size, const TypeInfo *type, uint8_t *origin)
1003
+ void CallData::PushBuffer(Span<const uint8_t> buffer, const TypeInfo *type, uint8_t *origin)
1005
1004
  {
1006
- buffer.len = std::min(buffer.len, size);
1005
+ buffer.len = std::min(buffer.len, (Size)type->size);
1007
1006
 
1008
1007
  // Go fast brrrrrrr :)
1009
- MemCpy(origin, buffer.ptr, (size_t)buffer.len);
1010
- MemSet(origin + buffer.len, 0, (size_t)(size - buffer.len));
1008
+ MemCpy(origin, buffer.ptr, buffer.len);
1009
+ MemSet(origin + buffer.len, 0, (Size)type->size - buffer.len);
1011
1010
 
1012
1011
  #define SWAP(CType) \
1013
1012
  do { \
@@ -1131,7 +1130,7 @@ bool CallData::PushPointer(Napi::Value value, const TypeInfo *type, int directio
1131
1130
  ptr = AllocHeap(size, 16);
1132
1131
 
1133
1132
  if (directions & 1) {
1134
- if (!PushNormalArray(array, len, type, ptr))
1133
+ if (!PushNormalArray(array, type, size, ptr))
1135
1134
  return false;
1136
1135
  } else {
1137
1136
  MemSet(ptr, 0, size);
@@ -1369,9 +1368,9 @@ void CallData::DumpForward(const FunctionInfo *func) const
1369
1368
  bool CallData::CheckDynamicLength(Napi::Object obj, Size element, const char *countedby, Napi::Value value)
1370
1369
  {
1371
1370
  int64_t expected = -1;
1372
- int64_t len = -1;
1371
+ int64_t size = -1;
1373
1372
 
1374
- // Get expected length
1373
+ // Get expected size
1375
1374
  {
1376
1375
  Napi::Value by = obj.Get(countedby);
1377
1376
 
@@ -1380,24 +1379,28 @@ bool CallData::CheckDynamicLength(Napi::Object obj, Size element, const char *co
1380
1379
  return false;
1381
1380
  }
1382
1381
 
1383
- expected = GetNumber<int64_t>(by);
1382
+ // If we get anywhere near overflow there are other problems to worry about.
1383
+ // So let's not worry about that.
1384
+ expected = GetNumber<int64_t>(by) * element;
1384
1385
  }
1385
1386
 
1386
- // Get actual length
1387
+ // Get actual size
1387
1388
  if (value.IsArray()) {
1388
1389
  Napi::Array array = value.As<Napi::Array>();
1389
- len = array.Length();
1390
+ size = array.Length() * element;
1390
1391
  } else if (value.IsTypedArray()) {
1391
1392
  Napi::TypedArray typed = value.As<Napi::TypedArray>();
1392
- len = typed.ByteLength() / element;
1393
+ size = typed.ByteLength();
1393
1394
  } else if (value.IsArrayBuffer()) {
1394
1395
  Napi::ArrayBuffer buffer = value.As<Napi::ArrayBuffer>();
1395
- len = buffer.ByteLength() / element;
1396
+ size = buffer.ByteLength();
1397
+ } else if (!IsNullOrUndefined(value)) {
1398
+ size = element;
1396
1399
  } else {
1397
- len = !IsNullOrUndefined(value);
1400
+ size = 0;
1398
1401
  }
1399
1402
 
1400
- if (len != expected) {
1403
+ if (size != expected) {
1401
1404
  ThrowError<Napi::Error>(env, "Mismatched dynamic length between '%1' and actual array", countedby);
1402
1405
  return false;
1403
1406
  }
@@ -121,8 +121,8 @@ public:
121
121
  bool PushString32(Napi::Value value, int directions, const char32_t **out_str32);
122
122
  Size PushString32Value(Napi::Value value, const char32_t **out_str32);
123
123
  bool PushObject(Napi::Object obj, const TypeInfo *type, uint8_t *origin);
124
- bool PushNormalArray(Napi::Array array, Size len, const TypeInfo *type, uint8_t *origin);
125
- void PushBuffer(Span<const uint8_t> buffer, Size size, const TypeInfo *type, uint8_t *origin);
124
+ bool PushNormalArray(Napi::Array array, const TypeInfo *type, Size size, uint8_t *origin);
125
+ void PushBuffer(Span<const uint8_t> buffer, const TypeInfo *type, uint8_t *origin);
126
126
  bool PushStringArray(Napi::Value value, const TypeInfo *type, uint8_t *origin);
127
127
  bool PushPointer(Napi::Value value, const TypeInfo *type, int directions, void **out_ptr);
128
128
  bool PushCallback(Napi::Value value, const TypeInfo *type, void **out_ptr);
@@ -156,7 +156,7 @@ inline bool CallData::AllocStack(Size size, Size align, T **out_ptr)
156
156
  }
157
157
 
158
158
  #if defined(RG_DEBUG)
159
- memset(ptr, 0, delta);
159
+ MemSet(ptr, 0, delta);
160
160
  #endif
161
161
 
162
162
  mem->stack.len -= delta;
@@ -173,7 +173,7 @@ inline T *CallData::AllocHeap(Size size, Size align)
173
173
 
174
174
  if (size < 4096 && delta <= mem->heap.len) [[likely]] {
175
175
  #if defined(RG_DEBUG)
176
- memset(mem->heap.ptr, 0, (size_t)delta);
176
+ MemSet(mem->heap.ptr, 0, delta);
177
177
  #endif
178
178
 
179
179
  mem->heap.ptr += delta;
@@ -348,6 +348,8 @@ static Napi::Value CreateStructType(const Napi::CallbackInfo &info, bool pad)
348
348
  size = member.offset + member.type->size;
349
349
  type->align = std::max(type->align, align);
350
350
 
351
+ member.countedby = -1;
352
+
351
353
  if (size > instance->config.max_type_size) {
352
354
  ThrowError<Napi::Error>(env, "Struct '%1' size is too high (max = %2)", type->name, FmtMemSize(size));
353
355
  return env.Null();
@@ -372,8 +374,9 @@ static Napi::Value CreateStructType(const Napi::CallbackInfo &info, bool pad)
372
374
  type->members.Append(member);
373
375
  }
374
376
 
375
- for (RecordMember &member: type->members) {
376
- const char *countedby = member.type->countedby;
377
+ for (Size i = 0; i < type->members.len; i++) {
378
+ RecordMember *member = &type->members[i];
379
+ const char *countedby = member->type->countedby;
377
380
 
378
381
  if (countedby) {
379
382
  const RecordMember *by = std::find_if(type->members.begin(), type->members.end(),
@@ -387,10 +390,12 @@ static Napi::Value CreateStructType(const Napi::CallbackInfo &info, bool pad)
387
390
  ThrowError<Napi::Error>(env, "Dynamic length member %1 is not an integer", countedby);
388
391
  return env.Null();
389
392
  }
393
+ if (member->type->primitive == PrimitiveKind::Array && i < type->members.len - 1) {
394
+ ThrowError<Napi::Error>(env, "Flexible array '%1' is not the last member of struct", member->name);
395
+ return env.Null();
396
+ }
390
397
 
391
- member.countedby = by - type->members.ptr;
392
- } else {
393
- member.countedby = -1;
398
+ member->countedby = by - type->members.ptr;
394
399
  }
395
400
  }
396
401
 
@@ -966,7 +971,7 @@ static Napi::Value CreateArrayType(const Napi::CallbackInfo &info)
966
971
  InstanceData *instance = env.GetInstanceData<InstanceData>();
967
972
 
968
973
  if (info.Length() < 2) {
969
- ThrowError<Napi::TypeError>(env, "Expected 2 to 3 arguments, got %1", info.Length());
974
+ ThrowError<Napi::TypeError>(env, "Expected 2 to 4 arguments, got %1", info.Length());
970
975
  return env.Null();
971
976
  }
972
977
 
@@ -974,27 +979,26 @@ static Napi::Value CreateArrayType(const Napi::CallbackInfo &info)
974
979
  if (!ref)
975
980
  return env.Null();
976
981
 
977
- int64_t len = 0;
978
- Napi::Value countedby;
979
-
980
- if (info[1].IsNumber()) {
981
- len = info[1].As<Napi::Number>().Int64Value();
982
- } else if (info[1].IsString()) {
983
- countedby = info[1];
982
+ bool dynamic = (info.Length() >= 3) && info[1].IsString();
984
983
 
985
- if (info.Length() >= 4 && !IsNullOrUndefined(info[3])) {
986
- len = info[3].As<Napi::Number>().Int64Value();
984
+ if (dynamic && !info[1].IsString()) {
985
+ ThrowError<Napi::TypeError>(env, "Unexpected %1 value for countedBy, expected string", GetValueType(instance, info[1]));
986
+ return env.Null();
987
+ }
988
+ if (!info[1 + dynamic].IsNumber()) {
989
+ if (info.Length() == 2 && info[1].IsString()) {
990
+ ThrowError<Napi::TypeError>(env, "Missing maxLen argument");
991
+ } else {
992
+ ThrowError<Napi::TypeError>(env, "Unexpected %1 value for %2, expected integer", GetValueType(instance, info[1]), dynamic ? "maxLen" : "len");
987
993
  }
988
- } else {
989
- ThrowError<Napi::TypeError>(env, "Unexpected %1 value for length, expected integer or string", GetValueType(instance, info[1]));
994
+ return env.Null();
990
995
  }
991
996
 
992
- if (countedby.IsEmpty() && len <= 0) {
997
+ int64_t len = info[1 + dynamic].As<Napi::Number>().Int64Value();
998
+
999
+ if (len <= 0) {
993
1000
  ThrowError<Napi::TypeError>(env, "Array length must be positive and non-zero");
994
1001
  return env.Null();
995
- } else if (len < 0) {
996
- ThrowError<Napi::TypeError>(env, "Array length must be positive");
997
- return env.Null();
998
1002
  }
999
1003
  if (len > instance->config.max_type_size / ref->size) {
1000
1004
  ThrowError<Napi::TypeError>(env, "Array length is too high (max = %1)", instance->config.max_type_size / ref->size);
@@ -1003,25 +1007,25 @@ static Napi::Value CreateArrayType(const Napi::CallbackInfo &info)
1003
1007
 
1004
1008
  TypeInfo *type = nullptr;
1005
1009
 
1006
- if (info.Length() >= 3 && !IsNullOrUndefined(info[2])) {
1007
- if (!info[2].IsString()) {
1010
+ if (info.Length() >= 3u + dynamic && !IsNullOrUndefined(info[2 + dynamic])) {
1011
+ if (!info[2 + dynamic].IsString()) {
1008
1012
  ThrowError<Napi::TypeError>(env, "Unexpected %1 value for hint, expected string", GetValueType(instance, info[2]));
1009
1013
  return env.Null();
1010
1014
  }
1011
1015
 
1012
- std::string to = info[2].As<Napi::String>();
1016
+ std::string str = info[2 + dynamic].As<Napi::String>();
1013
1017
  ArrayHint hint = {};
1014
1018
 
1015
- if (to == "Typed" || to == "typed") {
1019
+ if (str == "Typed" || str == "typed") {
1016
1020
  if (!(ref->flags & (int)TypeFlag::HasTypedArray)) {
1017
1021
  ThrowError<Napi::Error>(env, "Array hint 'Typed' cannot be used with type %1", ref->name);
1018
1022
  return env.Null();
1019
1023
  }
1020
1024
 
1021
1025
  hint = ArrayHint::Typed;
1022
- } else if (to == "Array" || to == "array") {
1026
+ } else if (str == "Array" || str == "array") {
1023
1027
  hint = ArrayHint::Array;
1024
- } else if (to == "String" || to == "string") {
1028
+ } else if (str == "String" || str == "string") {
1025
1029
  if (ref->primitive != PrimitiveKind::Int8 && ref->primitive != PrimitiveKind::Int16) {
1026
1030
  ThrowError<Napi::Error>(env, "Array hint 'String' can only be used with 8 and 16-bit signed integer types");
1027
1031
  return env.Null();
@@ -1038,8 +1042,8 @@ static Napi::Value CreateArrayType(const Napi::CallbackInfo &info)
1038
1042
  type = MakeArrayType(instance, ref, len);
1039
1043
  }
1040
1044
 
1041
- if (!countedby.IsEmpty()) {
1042
- Napi::String str = countedby.As<Napi::String>();
1045
+ if (dynamic) {
1046
+ Napi::String str = info[1].As<Napi::String>();
1043
1047
  type->countedby = DuplicateString(str.Utf8Value().c_str(), &instance->str_alloc).ptr;
1044
1048
  }
1045
1049
 
@@ -2087,7 +2091,7 @@ static Napi::Value DecodeValue(const Napi::CallbackInfo &info)
2087
2091
  Napi::Env env = info.Env();
2088
2092
 
2089
2093
  bool has_offset = (info.Length() >= 2 && info[1].IsNumber());
2090
- bool has_len = (info.Length() >= 3u + has_offset && info[2u + has_offset].IsNumber());
2094
+ bool has_len = (info.Length() >= 3u + has_offset && info[2 + has_offset].IsNumber());
2091
2095
 
2092
2096
  if (info.Length() < 2u + has_offset) [[unlikely]] {
2093
2097
  ThrowError<Napi::TypeError>(env, "Expected %1 to 4 arguments, got %2", 2 + has_offset, info.Length());
@@ -2102,7 +2106,7 @@ static Napi::Value DecodeValue(const Napi::CallbackInfo &info)
2102
2106
  int64_t offset = has_offset ? info[1].As<Napi::Number>().Int64Value() : 0;
2103
2107
 
2104
2108
  if (has_len) {
2105
- Size len = info[2u + has_offset].As<Napi::Number>();
2109
+ Size len = info[2 + has_offset].As<Napi::Number>();
2106
2110
 
2107
2111
  Napi::Value ret = Decode(value, offset, type, &len);
2108
2112
  return ret;
@@ -2164,7 +2168,7 @@ static Napi::Value EncodeValue(const Napi::CallbackInfo &info)
2164
2168
  Napi::Env env = info.Env();
2165
2169
 
2166
2170
  bool has_offset = (info.Length() >= 2 && info[1].IsNumber());
2167
- bool has_len = (info.Length() >= 4u + has_offset && info[3u + has_offset].IsNumber());
2171
+ bool has_len = (info.Length() >= 4u + has_offset && info[3 + has_offset].IsNumber());
2168
2172
 
2169
2173
  if (info.Length() < 3u + has_offset) [[unlikely]] {
2170
2174
  ThrowError<Napi::TypeError>(env, "Expected %1 to 5 arguments, got %2", 3 + has_offset, info.Length());
@@ -2177,10 +2181,10 @@ static Napi::Value EncodeValue(const Napi::CallbackInfo &info)
2177
2181
 
2178
2182
  Napi::Value ref = info[0];
2179
2183
  int64_t offset = has_offset ? info[1].As<Napi::Number>().Int64Value() : 0;
2180
- Napi::Value value = info[2u + has_offset];
2184
+ Napi::Value value = info[2 + has_offset];
2181
2185
 
2182
2186
  if (has_len) {
2183
- Size len = info[3u + has_offset].As<Napi::Number>();
2187
+ Size len = info[3 + has_offset].As<Napi::Number>();
2184
2188
 
2185
2189
  if (!Encode(ref, offset, value, type, &len))
2186
2190
  return env.Null();
@@ -906,7 +906,12 @@ void DecodeObject(Napi::Object obj, const uint8_t *origin, const TypeInfo *type)
906
906
  case PrimitiveKind::Array: {
907
907
  if (member.countedby >= 0) {
908
908
  const RecordMember &by = type->members[member.countedby];
909
+
909
910
  uint32_t len = DecodeDynamicLength(origin, by);
911
+ uint32_t max = member.type->size / member.type->ref.type->size;
912
+
913
+ // Silently truncate result
914
+ len = std::min(len, max);
910
915
 
911
916
  Napi::Value value = DecodeArray(env, src, member.type, len);
912
917
  obj.Set(member.name, value);
@@ -1634,13 +1639,11 @@ bool Encode(Napi::Env env, uint8_t *origin, Napi::Value value, const TypeInfo *t
1634
1639
  case PrimitiveKind::Array: {
1635
1640
  if (value.IsArray()) {
1636
1641
  Napi::Array array = value.As<Napi::Array>();
1637
- Size len = (Size)type->size / type->ref.type->size;
1638
-
1639
- if (!call.PushNormalArray(array, len, type, origin))
1642
+ if (!call.PushNormalArray(array, type, type->size, origin))
1640
1643
  return false;
1641
1644
  } else if (IsRawBuffer(value)) {
1642
1645
  Span<const uint8_t> buffer = GetRawBuffer(value);
1643
- call.PushBuffer(buffer, type->size, type, origin);
1646
+ call.PushBuffer(buffer, type, origin);
1644
1647
  } else if (value.IsString()) {
1645
1648
  if (!call.PushStringArray(value, type, origin))
1646
1649
  return false;