Skip to content

Fix the GetMany and IndexOf vtable signatures for IVector<T> and IVectorView<T> - #2519

Merged
Sergio Pedri (Sergio0694) merged 4 commits into
staging/3.0from
user/sergiopedri/fix-getmany-vtable-signature
Aug 13, 2026
Merged

Fix the GetMany and IndexOf vtable signatures for IVector<T> and IVectorView<T>#2519
Sergio Pedri (Sergio0694) merged 4 commits into
staging/3.0from
user/sergiopedri/fix-getmany-vtable-signature

Conversation

@Sergio0694

Copy link
Copy Markdown
Member

Summary

Fix two IVector<T>/IVectorView<T> vtable slots that were declared with the wrong Windows Runtime ABI signature (GetMany was missing its capacity parameter, and IVectorView<T>.IndexOf was missing its found parameter), and add a validation step to the interop generator so this class of bug can't silently reappear.

The GetMany part of this is extracted out of #2517, where it was needed to unblock the CopyTo optimization. It stands on its own as a correctness fix, so it is split out here to keep that PR focused on the optimization work.

Motivation

Both IVector<T> and IVectorView<T> declare these methods as:

  • HRESULT GetMany([in] UINT32 startIndex, [in] UINT32 capacity, [out] T* items, [out, retval] UINT32* actual)
  • HRESULT IndexOf([in] T value, [out] UINT32* index, [out, retval] boolean* found)

The declarations of those two vtable slots were missing a parameter each:

  • GetMany was missing capacity, in IVectorVftbl, IVectorViewVftbl, and in the specialized vtable types the interop generator emits (via IReadOnlyList1GetManyImpl, which IList1GetManyImpl also reuses).
  • IndexOf was missing found, in IVectorViewVftbl and in IReadOnlyList1IndexOfImpl. The IVector<T> counterpart (IList1IndexOfImpl) was already correct, which is why IList<T>.IndexOf works today: it is the only one of these slots that a calli is actually emitted for.

Why no test caught this

Both were completely latent, for the same two reasons:

  • Nothing ever called through the slots. No calli is emitted through GetMany for either interface (CopyTo goes through the indexer instead), and IReadOnlyList<T> has no IndexOf member at all, so the IVectorView<T>.IndexOf slot is never called from managed code either. The only slot in this group that is genuinely called through, IVector<T>.IndexOf, had the correct signature.

  • The CCW side never validates the slot. Populating a vtable slot is a plain ldftn/stfld pair, which the runtime does not type check, and the implementation methods stored into these slots already had the correct signatures (five parameters for GetMany, four for IndexOf). So native callers were always seeing the correct signature, and the incorrect declaration never influenced any executed code path.

In other words, the declarations were dead weight until something started calling through them, which is exactly what #2517 does. Since the bug is in metadata that no test could observe at run time, the guard added here (rather than a run time test) is what actually closes the gap: it compares the two signatures at the point where they are wired together, so a mismatch fails interop generation instead of silently producing a vtable that only breaks later.

Changes

  • src/WinRT.Runtime2/InteropServices/Vtables/IVectorVftbl.cs: add the missing capacity parameter to GetMany.

  • src/WinRT.Runtime2/InteropServices/Vtables/IVectorViewVftbl.cs: add the missing capacity parameter to GetMany, and the missing found parameter to IndexOf.

  • src/WinRT.Interop.Generator/Factories/WellKnownTypeSignatureFactory.cs: fix IReadOnlyList1GetManyImpl and IReadOnlyList1IndexOfImpl. Now that both declarations agree, IList1IndexOfImpl just reuses the vector view one, as the other identical IVector<T> slots already do.

  • src/WinRT.Interop.Generator/Factories/WellKnownTypeDefinitionFactory.cs: update the vtable layout comment for IReadOnlyList1Vftbl to match.

  • src/WinRT.Interop.Generator/Helpers/VtableSignatureValidator.cs (new): validates that a vtable slot is declared with the same signature as the method being stored into it. Only the arity and the return type are compared, since the two are allowed to spell an individual parameter differently: shared (i.e. non specialized) vtables use void* where the implementation method uses the exact ABI type of the element, which is ABI identical.

  • src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.cs: run that validation in Impl, which is the single place where every generated vtable slot is wired to its implementation method. This sits right next to the existing check that all vtable entries are initialized.

  • src/WinRT.Generator.Core/Extensions/TypeSignatureExtensions.cs (new) and src/WinRT.Projection.Writer/: move StripByRefAndCustomModifiers out of the projection writer and into the shared generator core (next to the other shared AsmResolver extensions, SignatureComparerExtensions and RuntimeContextExtensions), so the interop generator can use it too.

Validation

All affected projects build clean in Release (where warnings are errors): WinRT.Runtime, WinRT.Generator.Core, WinRT.Projection.Writer, and all five CLI build tools.

Reintroducing either signature bug now fails interop generation for any app using IList<T> or IReadOnlyList<T>, instead of silently emitting a mismatched vtable.

Both 'IVector<T>' and 'IVectorView<T>' declare 'GetMany' as:

  HRESULT GetMany([in] UINT32 startIndex, [in] UINT32 capacity, [out] T* items, [out, retval] UINT32* actual);

The 'capacity' parameter was missing from every declaration of that vtable
slot: the 'IVectorVftbl' and 'IVectorViewVftbl' binding types in the runtime,
and the specialized vtable types the interop generator emits (through
'IReadOnlyList1GetManyImpl', which 'IList1GetManyImpl' also reuses).

This was latent, which is why nothing caught it. The slot is only ever
populated on the CCW side, with an 'ldftn'/'stfld' pair that the runtime does
not type check, and the implementation methods stored into it already had the
correct five parameter signature. No 'calli' is ever emitted through the slot
either, as 'CopyTo' goes through the indexer instead, so the incorrect
declaration never influenced any executed code path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
'IVectorView<T>' declares 'IndexOf' as:

  HRESULT IndexOf([in] T value, [out] UINT32* index, [out, retval] boolean* found);

The 'found' parameter was missing from the 'IVectorViewVftbl' binding type in
the runtime and from 'IReadOnlyList1IndexOfImpl' in the interop generator. This
is the same class of bug as the 'GetMany' signature fixed in the previous
commit, and it was latent for the same reason: 'IReadOnlyList<T>' has no
'IndexOf' member, so the slot only exists to be populated on the CCW side (with
an implementation method that already had the correct four parameter
signature), and no 'calli' is ever emitted through it.

'IVector<T>' was unaffected: 'IList1IndexOfImpl' already declared 'found', and
that one is used to emit a real 'calli' for 'IList<T>.IndexOf'. Now that both
declarations agree, 'IList1IndexOfImpl' just reuses the vector view one, as the
other identical 'IVector<T>' slots already do.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The extension is generally useful to any tool working with AsmResolver
signatures, and the interop generator needs it as well (in the next commit), so
move it out of the projection writer and into 'WinRT.Generator.Core', next to
the other shared AsmResolver extensions ('SignatureComparerExtensions' and
'RuntimeContextExtensions').

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Populating a vtable slot is a plain 'ldftn'/'stfld' pair, which the runtime does
not type check, so the declared signature of a slot and the signature of the
method actually placed into it can silently disagree. That is what let the two
signature bugs fixed in the previous commits go unnoticed: both slots are only
ever populated (never called through), so the incorrect declarations never
affected any executed code path.

Validate the two in 'InteropTypeDefinitionBuilder.Impl', which is the single
place where every generated vtable slot is wired to its implementation method.
Only the arity and the return type are compared, since the two are allowed to
spell an individual parameter differently: shared (i.e. non specialized) vtables
use 'void*' where the implementation method uses the exact ABI type of the
element, which is ABI identical.

With this in place, reintroducing either bug fails interop generation for any
app using 'IList<T>' or 'IReadOnlyList<T>', instead of silently emitting a
vtable that only breaks once something calls through the slot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@Sergio0694
Sergio Pedri (Sergio0694) enabled auto-merge (squash) August 12, 2026 21:48
@Sergio0694
Sergio Pedri (Sergio0694) merged commit 14224c0 into staging/3.0 Aug 13, 2026
13 checks passed
@Sergio0694
Sergio Pedri (Sergio0694) deleted the user/sergiopedri/fix-getmany-vtable-signature branch August 13, 2026 00:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working CsWinRT 3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants