Fix marshalling generators using 0 as cleanup length for ManagedToUnmanaged collections with NoCountInfo#126853
Fix marshalling generators using 0 as cleanup length for ManagedToUnmanaged collections with NoCountInfo#126853
Conversation
… arrays of jagged 2D arrays When the marshalling direction is ManagedToUnmanaged and there is no explicit count info (NoCountInfo), the cleanup code was setting numElements = 0, causing Free to never be called on inner array elements. This fix uses GetManagedValuesSource().Length instead to get the actual element count from the managed collection. Fixes #93423 Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3665c6a3-655d-4215-a791-d3a838f80bd4 Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
…esSource Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/3665c6a3-655d-4215-a791-d3a838f80bd4 Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes a source-generator bug in Microsoft.Interop.SourceGeneration where cleanup for ManagedToUnmanaged linear collections with NoCountInfo incorrectly set numElements = 0, preventing per-element Free from running and leaking unmanaged resources.
Changes:
- Update stateless linear-collection cleanup codegen to compute
numElementsfromGetManagedValuesSource(...).Lengthfor ManagedToUnmanaged +NoCountInfo. - Expose a reusable
ElementsMarshalling.GenerateNumElementsAssignmentFromManagedValuesSource(...)helper to enable the above codegen change. - Add new compilation snippets covering an element marshaller that defines
Freein both LibraryImportGenerator and ComInterfaceGenerator unit-test suites.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/StatelessMarshallingStrategy.cs | Fixes cleanup codegen for ManagedToUnmanaged + NoCountInfo to use managed source length instead of 0. |
| src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/ElementsMarshalling.cs | Adds an instance helper wrapping the existing file-local extension for assigning numElements from managed source length. |
| src/libraries/System.Runtime.InteropServices/tests/Common/CustomCollectionMarshallingCodeSnippets.cs | Adds ElementInWithFree and new snippet variants to exercise element marshallers with Free. |
| src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs | Includes new snippet variants in compilation-only generator tests. |
| src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/Compiles.cs | Includes new snippet variants in compilation-only generator tests for COM-related generators. |
| yield return new[] { ID(), customCollectionMarshallingCodeSnippets.Stateless.NativeToManagedFinallyOnlyReturnValue<int>() }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippets.Stateless.NestedMarshallerParametersAndModifiers<int>() }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippets.Stateless.NonBlittableElementByValue }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippets.Stateless.NonBlittableElementWithFreeByValue }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippets.Stateless.NonBlittableElementParametersAndModifiers }; |
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateless.NonBlittableElementByValue }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateless.NonBlittableElementWithFreeByValue }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateless.NonBlittableElementNativeToManagedOnlyOutParameter }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateless.NonBlittableElementNativeToManagedOnlyReturnValue }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateful.NativeToManagedOnlyOutParameter<int>() }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateful.NativeToManagedOnlyReturnValue<int>() }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateful.NonBlittableElementByValue }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateful.NonBlittableElementWithFreeByValue }; | ||
| yield return new[] { ID(), customCollectionMarshallingCodeSnippetsManagedToUnmanaged.Stateful.NonBlittableElementNativeToManagedOnlyOutParameter }; |
🤖 Copilot Code Review — PR #126853Note This review was generated by Copilot (Claude Opus 4.6), with additional analysis from Claude Sonnet 4.5. A GPT-5.3-codex sub-agent was launched but did not complete within the 10-minute timeout and is excluded from this synthesis. Holistic AssessmentMotivation: Justified. The previous code set Approach: Correct. Using Summary: ✅ LGTM. The fix correctly replaces the broken Detailed Findings✅ Core Fix — Correct cleanup element countThe change in
✅ Code Design — Well-placed wrapper method
✅ Test Coverage — Adequate for a source generator fixNew ✅
|
|
@copilot Can you add tests in the behavioral test projects to validate the marshalling works as expected? |
The previous fix only changed the inner numElements assignment but did not
declare the outer managedSpan in the cleanup scope, causing a compile error
in generated code for nested collections. This commit:
- Declares managedSpan in NonBlittableElementsMarshalling.GenerateElementCleanupStatement
when direction is ManagedToUnmanaged, so inner cleanup can reference it.
- Removes [ActiveIssue("#93423")] from MultidimensionalArray_CheckInnerArraysAreCleared
and updates expected free count to throwOn - throwOn % 10 (only fully
completed inner arrays are cleaned up; the remaining gap is tracked by #93431).
- Splits a separate MultidimensionalArray_CheckInnerArraysAreCleared_ProperCleanup
test marked with [ActiveIssue(#93431)] for the partial inner cleanup case.
- Uncomments the BoolStructInMarshaller asserts in MultiDimensionalOutArray_EnsureAllCleaned
which now correctly free all 100 inner elements on success and failure paths.
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/1d08ed99-2083-4c06-a7c1-15d6a4159e8e
Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com>
Added behavioral tests in 3ff90d5. Specifically:
The behavioral tests caught a real bug in the original code change — for nested collections the inner cleanup referenced |
Description
When marshalling collections ManagedToUnmanaged with no explicit count info (e.g., inner arrays of jagged 2D arrays with element marshallers that have
Free), the source generator emittednumElements = 0during cleanup. This caused the cleanup loop to never execute, leaking unmanaged memory on every successful call.The fix replaces the hardcoded
0withGetManagedValuesSource().Length, which is the correct element count for ManagedToUnmanaged cleanup — the managed collection is the source of truth.Before (generated cleanup code):
After:
For nested collections (jagged arrays), the inner element cleanup needs to reference the outer managed span via
<managedSpan>[<index>]. The outer cleanup scope previously only declared the unmanaged span, so this required also declaring the managed span when direction is ManagedToUnmanaged.Changes
ElementsMarshalling.cs:GenerateNumElementsAssignmentFromManagedValuesSource()on the base class, wrapping the existingfile staticextension method so it's callable fromStatelessMarshallingStrategy.cs.NonBlittableElementsMarshalling.GenerateElementCleanupStatementto also declaremanagedSpanin the outer cleanup scope when direction is ManagedToUnmanaged, so nested element cleanup can reference<managedSpan>[<index>].StatelessMarshallingStrategy.cs— ReplacenumElements = 0with the managed values source length inGenerateCleanupCallerAllocatedResourcesStatements.ElementInWithFreemarshaller andNonBlittableElementWithFreeByValuetest cases for both stateless/stateful paths in LibraryImportGenerator and ComInterfaceGenerator unit tests.LibraryImportGenerator.Tests/CollectionMarshallingFails.cs:[ActiveIssue("#93423")]fromMultidimensionalArray_CheckInnerArraysAreCleared— it now passes with expected free count ofthrowOn - throwOn % 10(only fully-completed inner arrays are cleaned up).MultidimensionalArray_CheckInnerArraysAreCleared_ProperCleanuptest marked[ActiveIssue("#93431")]covering the partial-inner-cleanup gap, which is a separate issue.BoolStructInMarshaller.Marshaller.AssertAllHaveBeenCleaned()checks inMultiDimensionalOutArray_EnsureAllCleaned, which now correctly verify all 100 inner elements are freed on both success and failure paths.Testing
[ActiveIssue]-marked tests now enabled)