Skip to content

feat(runtime): arrays WinRT hands back, String[]/object arrays, boxed-argument leak - #44

Merged
triniwiz merged 3 commits into
mainfrom
feat/winrt-received-arrays
Nov 6, 2026
Merged

triniwiz merged 3 commits into
mainfrom
feat/winrt-received-arrays

Conversation

@triniwiz

@triniwiz triniwiz commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Follows #41. Arrays now convert in both directions for every element type, on both engines, including arrays WinRT allocates and hands back. Before, those were refused when the call was built ("Unsupported array return type"). Three commits:

1. Arrays WinRT hands back

A received array takes two ABI slots, UINT32* length and T** data. MethodCall and PropertyCall lay these out for:

  • array return values and array-typed property getters, which land in the call's return buffer;
  • ByRef T[] parameters, as two out slots.

After the call, (length, data) is converted and the CoTaskMem array freed. The element type comes from classify_return, through the new ReturnKind::Array { element, element_type, size }:

element JS value
primitives (UInt8 … Double, Boolean, Char16, enums) typed array of the matching kind, the same kinds interop.Array.create returns
String array of strings
classes, interfaces, Object array of wrappers (null for null)
structs, Guid array of struct objects

String and object elements are taken over by the conversion, so nothing is left to release. Out parameters behave as they do for other types: classic accepts interop.out() or returns [result, ...outs]; napi needs interop.out().

const out = interop.out();
CryptographicBuffer.CopyToByteArray(buffer, out); // out.value: Uint8Array
Windows.Graphics.Display.DisplayServices.FindAll(); // [{ Value: 65537 }]
new ListBoxAutomationPeer(list).GetSelection(); // [wrapper, wrapper]
certificateExtension.Value; // Uint8Array

Boxed arrays: IReferenceArray<T> values (from PropertyValue.Create*Array, a PropertySet, settings, device properties) unbox the way boxed scalars already do, through IPropertyValue.Get*Array and the same conversion. In the other direction, an Object parameter boxes these with PropertyValue, so a returned array can be passed back:

  • typed arrays, which box as the matching IReferenceArray<T>;
  • plain arrays whose elements are all strings, all numbers (Int32[] when they're all 32-bit integers, Double[] otherwise), all booleans, or all WinRT objects (or null).

2. Boxed Object arguments no longer leak

Every JS value boxed for an Object parameter (strings, numbers, booleans, struct shapes, and now arrays) used to forget its reference, so each argument leaked its box. Each call now opens a BoxedArgs scope, and the boxes made while parsing its arguments are released when the call returns. A call made during argument parsing (e.g. from a getter) opens its own scope and releases only its own boxes.

Inserting the same values into a PropertySet 200k times (napi, memory after GC):

value before after
2 KB string +1087 MB +262 MB
Float64Array(256) +693 MB +262 MB
number +285 MB +262 MB
existing WinRT object (no boxing) +262 MB +261 MB

The ~262 MB the no-boxing row still shows isn't a leak. It's napi finalizers waiting for the event loop, which this synchronous loop never yields to. With the loop yielding between 50k-call batches, memory levels off after a few batches.

3. String[] and object arrays passed in or filled

  • Passed in (PropertyValue.CreateStringArray, IVector<T>.ReplaceAll, …): a JS array becomes HSTRINGs, or objects queried to the element's interface. They're released after the call.
  • Filled by the callee (IVector<T>.GetMany): the caller passes a JS array as long as the slots it wants, and the results are written back into that array. interop.Array.create('String', n), ('Object', n) or ('Windows.Data.Json.IJsonValue', n) returns an array of nulls for this.
  • Fill parameters are recognized by the [out] param attribute (ParameterDeclaration::is_fill_array).
  • GenericMethodCall, which is only used for open generic interfaces, refuses these arrays rather than passing them as bytes.

Testing

  • Rust unit tests:
    • array classification and layout
    • string/object element resolution
    • a box/unbox round trip through PropertyValue that frees the returned HSTRINGs
  • End-to-end, 24/24 on Node with windows-napi, 23/23 on classic V8 (the XAML check needs a XAML host):
    • out byte[], including an empty one
    • struct-array return
    • boxed arrays of every primitive kind
    • String[]/Object[] passed in, and round trips through PropertySet
    • IVector<String>.ReplaceAll/GetMany, and GetMany returning the filled count
    • IVector<IJsonValue>.ReplaceAll/GetMany
    • CertificateExtension.Value set and get
    • ListBoxAutomationPeer.GetSelection() in a live XamlWindow
  • Existing suites:

Not covered:

  • IReferenceArray<T>.Value called directly on an interface instance. It goes through the same property path, but boxed arrays now unbox before JS can reach that member, so I couldn't test it.
  • Struct arrays passed in (e.g. PropertyValue.CreatePointArray).

WinRT-allocated arrays now convert on both engines, where they were
refused when the call was built:

- T[] return values and received (out T[]) parameters. The callee's
  (length, data) pair is converted and the CoTaskMem array freed:
  primitive elements become a typed array of the matching kind (as
  interop.Array.create allocates), strings and objects a JS array, and
  structs an array of struct objects.
- Boxed arrays (IReferenceArray<T>, e.g. from PropertyValue.Create*Array
  or a PropertySet value) unbox the same way boxed scalars already do.
  In the other direction, a typed array, or a plain array of strings,
  numbers, booleans or WinRT objects, passed for an Object parameter is
  boxed with PropertyValue, so a returned array can be passed back.

Examples: CryptographicBuffer.CopyToByteArray(buffer, out) gives a
Uint8Array; DisplayServices.FindAll() gives an array of DisplayId;
ListBoxAutomationPeer.GetSelection() gives an array of wrappers.

Arrays of strings or objects passed in (String[] parameters) are still
unsupported.
@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1dd688ea-e360-46c5-99e5-14505ff67193

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A JS string, number, boolean, struct shape or (since this branch) array
passed for an Object parameter is boxed with PropertyValue, and the
box's reference was forgotten so the pointer stayed valid through the
call: every such argument leaked its box. Passing a 2 KB string 200k
times grew the process by ~0.8 GB more than passing an existing WinRT
object the same number of times.

Each call now opens a BoxedArgs scope; boxes made while its arguments
are parsed are released when the call returns. A call made during
argument parsing opens its own scope and releases only its own boxes.
Boxing outside any call keeps the reference as before.
- String[] and object-array parameters (Object[], IJsonValue[],
  Uri[], ...) the caller passes or the callee fills, on both engines.
  A passed JS array becomes HSTRINGs or objects queried to the element
  interface, released after the call; a fill array (IVector<T>.GetMany)
  takes a JS array as long as the slots wanted and gets the results
  written back into it. interop.Array.create('String', n), ('Object',
  n) or (TypeName, n) returns such an array.
- Array-typed property getters (e.g. CertificateExtension.Value)
  convert like array returns.

GenericMethodCall, used only for open generic interfaces, refuses
string and object arrays rather than passing them as bytes.
@triniwiz
triniwiz force-pushed the feat/winrt-received-arrays branch from 47a3e7c to 6038bfb Compare November 6, 2026 08:24
@triniwiz triniwiz changed the title feat(runtime): arrays WinRT hands back (out T[], T[] returns, boxed arrays) feat(runtime): arrays WinRT hands back, String[]/object arrays, boxed-argument leak Nov 6, 2026
@triniwiz
triniwiz merged commit f9f28cc into main Nov 6, 2026
3 checks passed
@triniwiz
triniwiz deleted the feat/winrt-received-arrays branch November 6, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant