-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Memory management doc #899
base: master
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# C#/WinRT Object Lifetime and Reference Tracking | ||
|
||
## Overview | ||
|
||
C#/WinRT is a WinRT projection for C# which at a high level generates wrapper C# types to represent | ||
WinRT types. The lifetime of any of these instantiated C# types is managed by the .NET garbage collector | ||
as with any C# object. But as a WinRT projection, the lifetime of the WinRT objects it wraps is | ||
managed by COM reference tracking. The XAML runtime also manages the lifetime of XAML / WinUI objects | ||
and has its own reference tracking that interacts with .NET and its garbage collector. This document | ||
serves the purpose of documenting how C#/WinRT interacts with all 3 systems to correctly manage the | ||
lifetime of projected WinRT objects. | ||
|
||
### COM reference tracking | ||
|
||
Each WinRT object that we project is based on COM and implements a set of interfaces. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we -> C#/WinRT |
||
As per the COM design, every COM interface implements `IUnknown` which has an `AddRef` and `Release` | ||
function. C#/WinRT calls the `AddRef` function anytime it gets a new reference to a WinRT object | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mention CCWs below. Maybe introduce IObjectReference as the RCW here. |
||
which C#/WinRT holds onto using an `IObjectReference` instance. It also calls `AddRef` whenever it gives | ||
out a reference to one of these objects across the ABI as an out parameter. C#/WinRT calls the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should introduce this acronym too: Application Binary Interface (ABI) |
||
`Release` function whenever any of the `IObjectReference` instances holding onto the WinRT object is | ||
disposed or finalized by the .NET garbage collector. As long as there are still references to the | ||
native object, it stays alive even if the projected C# object gets finalized due to there being | ||
no more references to it from C# code. But if the C# reference was the last reference to it, the | ||
release will also end up cleaning up the native object. | ||
|
||
The above describes what typically happens for any natively implemented WinRT object that C#/WinRT | ||
projects. There are some differences to this when the object is instead a C# implemented object that is | ||
projected into WinRT via a COM callable wrapper (CCW). This is done via a C# class implementing a set of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe factor out these into 2 separate sections? E.g. C# class implementing WinRT interface |
||
WinRT interfaces or via a C# class extending (aggregating in COM) an unsealed WinRT type. | ||
|
||
In the former, the object is implemented purely in C# and its lifetime is managed by the .NET | ||
garbage collector. C#/WinRT only comes into play when this C# object is passed across the ABI to a | ||
WinRT function. When this happens, C#/WinRT creates a CCW for it using the .NET 5 ComWrappers API | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Link to the |
||
and that is passed across the ABI. Any references to that CCW from the native side are tracked by | ||
`AddRef` / `Release` calls on the `IUnknown` of the CCW which is implemented by ComWrappers. | ||
manodasanW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This means in addition to any references to the object from C# tracked by the garbage collector | ||
keeping it alive, any native reference which increases the CCW reference count would also keep the | ||
object alive and that is managed by the .NET runtime and its ComWrappers implementation. | ||
manodasanW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
In the latter scenario, extending an unsealed WinRT type is typically done via COM Aggregation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "latter" here is referring back a bit - may help to provide some structure to the doc (section titles) |
||
which C#/WinRT does behind the scenes when a C# class extends such a projected type. In COM aggregation, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd include links inline in the body of the text, rather than at the end of the doc, like COM Aggregation here (https://docs.microsoft.com/en-us/windows/win32/com/aggregation) |
||
there is 2 objects in play: the outer object which is the CCW for the C# object and the inner object | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are two |
||
which is the object being extended. Both these objects are made to look like one object known as the | ||
manodasanW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
composed object. To achieve that, the outer object would delegate calls for any of the inner object | ||
manodasanW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
interfaces that aren't overridden to the inner object. Any calls for interfaces that are only | ||
implemented on the outer object or is overridden by the outer object or is for the `IUnknown` interface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or are overridden ..., or are for .... |
||
would be handled by the outer object itself. The last part means that the lifetime and the COM reference | ||
counting of this aggregated object is maintained by the outer object and more specifically its `IUnknown` | ||
implementation on the CCW from ComWrappers. This is where the standard COM reference tracking | ||
convention described earlier starts to differ. As we know for CCWs, there is 2 things which | ||
manodasanW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
keep it alive: any references from C# to the managed object or any native references which had done | ||
an `AddRef` incrementing the COM ref count. But we also know that for projected aggregated types | ||
to make calls on interfaces provided by the inner object, they need to QueryInterface (QI) for them | ||
from the inner object which would result in the COM reference count on the outer (CCW) increasing. | ||
This means any QIs from C# on such objects will end up increasing the COM reference count on the CCW | ||
and thereby keeping it alive and leaking it as any C# reference to such objects are | ||
supposed to be tracked as managed references by the garbage collector and not as native references. | ||
To address this, for any QI calls done as part of the aggregated object's C# projection implementation, | ||
`Release` should be called right after the reference is obtained even if you plan to hold onto | ||
the obtained interface to avoid repeatedly retrieving it. This prevents C# QIs by the composed object | ||
from increasing the CCW reference count meant for tracking native references while allowing the | ||
garbage collector to manage the lifetime of managed objects from managed references via its own | ||
tracking. For any QI calls for which the result is handed out to the native side, `Release` should | ||
not be called right after as it is a native reference which needs to be tracked by the CCW. | ||
|
||
One notable caveat to this is tear off interfaces on aggregated objects. With tear off interfaces, | ||
the interfaces typically perform their own COM reference counting separate from the object itself | ||
allowing for the interfaces to manage their own lifetime. But this doesn't work well with aggregated | ||
objects if one of these interfaces need to be QIed for by the composed object as part of the | ||
projection implementation. This is because a `Release` would happen right after which would trigger | ||
the cleanup of the interface as its lifetime isn't tied to the outer. Given that tear off interfaces | ||
are rare and not typically used by C# consumers, C#/WinRT today doesn't address this | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "not typically used by C# consumers" ? the tear-off implementor has no idea what client code is using it |
||
other than facilitating QI calls for them from the native side where `Release` isn't called right after. | ||
The recommendation for tear off interfaces which do want to support such uses on aggregated objects | ||
is that they can continue to be constructed on demand upon the first QI for it, but the interface | ||
should not be cleaned up until the object is cleaned up even if there are no longer any reference | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. references There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we be more crisp about the recommendation - that a tear-off implementor should cache all instances to prevent premature destruction? |
||
to that interface. | ||
|
||
### XAML reference tracking | ||
|
||
As mentioned earlier, the XAML runtime also manages the lifetime of XAML objects and has its own | ||
supplemental reference tracking to COM reference tracking which it uses when interacting with .NET | ||
and its garbage collector. | ||
|
||
For native XAML objects that are being wrapped by C#/WinRT, the XAML runtime needs to | ||
know about all the references to it from another reference tracking system like the .NET | ||
garbage collector. This allows XAML to track scenarios where objects may have circular references | ||
or only have references to it from objects that are pending clean up. Specifically, when a C# wrapper | ||
is created for a XAML runtime tracked object (implements `IReferenceTracker`), the XAML runtime | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
needs to be informed of it by a call to `ConnectFromTrackerSource` on `IReferenceTracker`. This is | ||
done by the ComWrappers implementation when an RCW is created. After that, any references to that | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (introduce 'RCW' above) |
||
object that are tracked by the other reference tracking system (.NET garbage collector in this case) | ||
needs to be informed to XAML by a call to `AddRefFromTrackerSource`. This is done by both C#/WinRT and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need |
||
ComWrappers after any `AddRef` call to increment the COM reference count. Similarly, before the | ||
`Release` call, there would be a `ReleaseFromTrackerSource` to indicate a reference on the object | ||
was released. When the RCW is destructed, there would similarly be a call to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. destructed -> finalized |
||
`DisconnectFromTrackerSource` to indicate that the .NET garbage collector no longer tracks the object. | ||
|
||
For composed XAML objects where the lifetime is controlled by the .NET garbage collector rather than | ||
the XAML runtime, XAML requires the CCW to implement the `IReferenceTrackerTarget` interface and its | ||
respective methods. This allows XAML to inform the .NET garbage collector of any references the XAML | ||
runtime takes and to indicate that even though an object may not have any COM reference counts that | ||
it shouldn't be cleaned up as it is still in use. | ||
|
||
### Related documentation | ||
|
||
- [COM Reference Tracking](https://docs.microsoft.com/en-us/windows/win32/com/managing-object-lifetimes-through-reference-counting) | ||
manodasanW marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- [COM Aggregation](https://docs.microsoft.com/en-us/windows/win32/com/aggregation) | ||
manodasanW marked this conversation as resolved.
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be easier to augment this with a series of enumerated steps in the process.