Fix the GetMany and IndexOf vtable signatures for IVector<T> and IVectorView<T> - #2519
Merged
Sergio Pedri (Sergio0694) merged 4 commits intoAug 13, 2026
Conversation
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>
Sergio Pedri (Sergio0694)
requested a review
from Manodasan Wignarajah (manodasanW)
August 12, 2026 21:04
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Sergio Pedri (Sergio0694)
enabled auto-merge (squash)
August 12, 2026 21:48
Manodasan Wignarajah (manodasanW)
approved these changes
Aug 12, 2026
Sergio Pedri (Sergio0694)
deleted the
user/sergiopedri/fix-getmany-vtable-signature
branch
August 13, 2026 00:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix two
IVector<T>/IVectorView<T>vtable slots that were declared with the wrong Windows Runtime ABI signature (GetManywas missing itscapacityparameter, andIVectorView<T>.IndexOfwas missing itsfoundparameter), and add a validation step to the interop generator so this class of bug can't silently reappear.The
GetManypart of this is extracted out of #2517, where it was needed to unblock theCopyTooptimization. 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>andIVectorView<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:
GetManywas missingcapacity, inIVectorVftbl,IVectorViewVftbl, and in the specialized vtable types the interop generator emits (viaIReadOnlyList1GetManyImpl, whichIList1GetManyImplalso reuses).IndexOfwas missingfound, inIVectorViewVftbland inIReadOnlyList1IndexOfImpl. TheIVector<T>counterpart (IList1IndexOfImpl) was already correct, which is whyIList<T>.IndexOfworks today: it is the only one of these slots that acalliis actually emitted for.Why no test caught this
Both were completely latent, for the same two reasons:
Nothing ever called through the slots. No
calliis emitted throughGetManyfor either interface (CopyTogoes through the indexer instead), andIReadOnlyList<T>has noIndexOfmember at all, so theIVectorView<T>.IndexOfslot 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/stfldpair, which the runtime does not type check, and the implementation methods stored into these slots already had the correct signatures (five parameters forGetMany, four forIndexOf). 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 missingcapacityparameter toGetMany.src/WinRT.Runtime2/InteropServices/Vtables/IVectorViewVftbl.cs: add the missingcapacityparameter toGetMany, and the missingfoundparameter toIndexOf.src/WinRT.Interop.Generator/Factories/WellKnownTypeSignatureFactory.cs: fixIReadOnlyList1GetManyImplandIReadOnlyList1IndexOfImpl. Now that both declarations agree,IList1IndexOfImpljust reuses the vector view one, as the other identicalIVector<T>slots already do.src/WinRT.Interop.Generator/Factories/WellKnownTypeDefinitionFactory.cs: update the vtable layout comment forIReadOnlyList1Vftblto 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 usevoid*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 inImpl, 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) andsrc/WinRT.Projection.Writer/: moveStripByRefAndCustomModifiersout of the projection writer and into the shared generator core (next to the other shared AsmResolver extensions,SignatureComparerExtensionsandRuntimeContextExtensions), 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>orIReadOnlyList<T>, instead of silently emitting a mismatched vtable.