/
DependentHandle.cs
263 lines (230 loc) · 12 KB
/
DependentHandle.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
namespace System.Runtime
{
/// <summary>
/// Represents a dependent GC handle, which will conditionally keep a dependent object instance alive as long as
/// a target object instance is alive as well, without representing a strong reference to the target instance.
/// </summary>
/// <remarks>
/// A <see cref="DependentHandle"/> value with a given object instance as target will not cause the target
/// to be kept alive if there are no other strong references to it, but it will do so for the dependent
/// object instance as long as the target is alive.
/// <para>
/// Using this type is conceptually equivalent to having a weak reference to a given target object instance A,
/// with that object having a field or property (or some other strong reference) to a dependent object instance B.
/// </para>
/// <para>
/// The <see cref="DependentHandle"/> type is not thread-safe, and consumers are responsible for ensuring that
/// <see cref="Dispose"/> is not called concurrently with other APIs. Not doing so results in undefined behavior.
/// </para>
/// <para>
/// The <see cref="IsAllocated"/>, <see cref="Target"/>, <see cref="Dependent"/> and <see cref="TargetAndDependent"/>
/// properties are instead thread-safe, and safe to use if <see cref="Dispose"/> is not concurrently invoked as well.
/// </para>
/// </remarks>
public struct DependentHandle : IDisposable
{
// =========================================================================================
// This struct collects all operations on native DependentHandles. The DependentHandle
// merely wraps an IntPtr so this struct serves mainly as a "managed typedef."
//
// DependentHandles exist in one of two states:
//
// IsAllocated == false
// No actual handle is allocated underneath. Illegal to get Target, Dependent
// or GetTargetAndDependent(). Ok to call Dispose().
//
// Initializing a DependentHandle using the nullary ctor creates a DependentHandle
// that's in the !IsAllocated state.
// (! Right now, we get this guarantee for free because (IntPtr)0 == NULL unmanaged handle.
// ! If that assertion ever becomes false, we'll have to add an _isAllocated field
// ! to compensate.)
//
//
// IsAllocated == true
// There's a handle allocated underneath. You must call Dispose() on this eventually
// or you cause a native handle table leak.
//
// This struct intentionally does no self-synchronization. It's up to the caller to
// to use DependentHandles in a thread-safe way.
// =========================================================================================
private IntPtr _handle;
/// <summary>
/// Initializes a new instance of the <see cref="DependentHandle"/> struct with the specified arguments.
/// </summary>
/// <param name="target">The target object instance to track.</param>
/// <param name="dependent">The dependent object instance to associate with <paramref name="target"/>.</param>
public DependentHandle(object? target, object? dependent)
{
// no need to check for null result: InternalInitialize expected to throw OOM.
_handle = InternalInitialize(target, dependent);
}
/// <summary>
/// Gets a value indicating whether this instance was constructed with
/// <see cref="DependentHandle(object?, object?)"/> and has not yet been disposed.
/// </summary>
/// <remarks>This property is thread-safe.</remarks>
public bool IsAllocated => (nint)_handle != 0;
/// <summary>
/// Gets or sets the target object instance for the current handle. The target can only be set to a <see langword="null"/> value
/// once the <see cref="DependentHandle"/> instance has been created. Doing so will cause <see cref="Dependent"/> to start
/// returning <see langword="null"/> as well, and to become eligible for collection even if the previous target is still alive.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if <see cref="IsAllocated"/> is <see langword="false"/> or if the input value is not <see langword="null"/>.</exception>
/// <remarks>This property is thread-safe.</remarks>
public object? Target
{
get
{
IntPtr handle = _handle;
if ((nint)handle == 0)
{
ThrowHelper.ThrowInvalidOperationException();
}
return InternalGetTarget(handle);
}
set
{
IntPtr handle = _handle;
if ((nint)handle == 0 || value is not null)
{
ThrowHelper.ThrowInvalidOperationException();
}
InternalSetTargetToNull(handle);
}
}
/// <summary>
/// Gets or sets the dependent object instance for the current handle.
/// </summary>
/// <remarks>
/// If it is needed to retrieve both <see cref="Target"/> and <see cref="Dependent"/>, it is necessary
/// to ensure that the returned instance from <see cref="Target"/> will be kept alive until <see cref="Dependent"/>
/// is retrieved as well, or it might be collected and result in unexpected behavior. This can be done by storing the
/// target in a local and calling <see cref="GC.KeepAlive(object)"/> on it after <see cref="Dependent"/> is accessed.
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown if <see cref="IsAllocated"/> is <see langword="false"/>.</exception>
/// <remarks>This property is thread-safe.</remarks>
public object? Dependent
{
get
{
IntPtr handle = _handle;
if ((nint)handle == 0)
{
ThrowHelper.ThrowInvalidOperationException();
}
return InternalGetDependent(handle);
}
set
{
IntPtr handle = _handle;
if ((nint)handle == 0)
{
ThrowHelper.ThrowInvalidOperationException();
}
InternalSetDependent(handle, value);
}
}
/// <summary>
/// Gets the values of both <see cref="Target"/> and <see cref="Dependent"/> (if available) as an atomic operation.
/// That is, even if <see cref="Target"/> is concurrently set to <see langword="null"/>, calling this method
/// will either return <see langword="null"/> for both target and dependent, or return both previous values.
/// If <see cref="Target"/> and <see cref="Dependent"/> were used sequentially in this scenario instead, it
/// would be possible to sometimes successfully retrieve the previous target, but then fail to get the dependent.
/// </summary>
/// <returns>The values of <see cref="Target"/> and <see cref="Dependent"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if <see cref="IsAllocated"/> is <see langword="false"/>.</exception>
/// <remarks>This property is thread-safe.</remarks>
public (object? Target, object? Dependent) TargetAndDependent
{
get
{
IntPtr handle = _handle;
if ((nint)handle == 0)
{
ThrowHelper.ThrowInvalidOperationException();
}
object? target = InternalGetTargetAndDependent(handle, out object? dependent);
return (target, dependent);
}
}
/// <summary>
/// Gets the target object instance for the current handle.
/// </summary>
/// <returns>The target object instance, if present.</returns>
/// <remarks>This method mirrors <see cref="Target"/>, but without the allocation check.</remarks>
internal object? UnsafeGetTarget()
{
return InternalGetTarget(_handle);
}
/// <summary>
/// Atomically retrieves the values of both <see cref="Target"/> and <see cref="Dependent"/>, if available.
/// </summary>
/// <param name="dependent">The dependent instance, if available.</param>
/// <returns>The values of <see cref="Target"/> and <see cref="Dependent"/>.</returns>
/// <remarks>
/// This method mirrors the <see cref="TargetAndDependent"/> property, but without the allocation check.
/// The signature is also kept the same as the one for the internal call, to improve the codegen.
/// Note that <paramref name="dependent"/> is required to be on the stack (or it might not be tracked).
/// </remarks>
internal object? UnsafeGetTargetAndDependent(out object? dependent)
{
return InternalGetTargetAndDependent(_handle, out dependent);
}
/// <summary>
/// Sets the dependent object instance for the current handle to <see langword="null"/>.
/// </summary>
/// <remarks>This method mirrors the <see cref="Target"/> setter, but without allocation and input checks.</remarks>
internal void UnsafeSetTargetToNull()
{
InternalSetTargetToNull(_handle);
}
/// <summary>
/// Sets the dependent object instance for the current handle.
/// </summary>
/// <remarks>This method mirrors <see cref="Dependent"/>, but without the allocation check.</remarks>
internal void UnsafeSetDependent(object? dependent)
{
InternalSetDependent(_handle, dependent);
}
/// <inheritdoc cref="IDisposable.Dispose"/>
/// <remarks>This method is not thread-safe.</remarks>
public void Dispose()
{
// Forces the DependentHandle back to non-allocated state
// (if not already there) and frees the handle if needed.
IntPtr handle = _handle;
if ((nint)handle != 0)
{
_handle = IntPtr.Zero;
InternalFree(handle);
}
}
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern IntPtr InternalInitialize(object? target, object? dependent);
#if DEBUG
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern object? InternalGetTarget(IntPtr dependentHandle);
#else
// This optimization is the same that is used in GCHandle in RELEASE mode.
// This is not used in DEBUG builds as the runtime performs additional checks.
// The logic below is the inlined copy of ObjectFromHandle in the unmanaged runtime.
#pragma warning disable 8500 // address of managed types
private static unsafe object? InternalGetTarget(IntPtr dependentHandle) => *(object*)dependentHandle;
#pragma warning restore 8500
#endif
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern object? InternalGetDependent(IntPtr dependentHandle);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern object? InternalGetTargetAndDependent(IntPtr dependentHandle, out object? dependent);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void InternalSetDependent(IntPtr dependentHandle, object? dependent);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void InternalSetTargetToNull(IntPtr dependentHandle);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void InternalFree(IntPtr dependentHandle);
}
}