Skip to content

Commit 5e0044f

Browse files
authored
Add new SafeHandleMarshaller type to provide out-of-generator marshalling support. (#85419)
Fixes #74035 We can't remove the built-in marshalling support from the generator yet, but once the out-of-band packages we ship don't support .NET 6. we can remove the built-in support that emits the marshalling code in the stub. I believe the .NET 9 packages won't support .NET 6, so once we snap for .NET 9 and update how we ship the packages, we can clean this up. This PR also adds a requested feature to the SafeHandle marshaller: If the call to native code fails, we'll call Dispose() on the pre-allocated handle to avoid leaking it to the finalizer queue.
1 parent f1fcba4 commit 5e0044f

File tree

10 files changed

+314
-5
lines changed

10 files changed

+314
-5
lines changed

docs/design/libraries/LibraryImportGenerator/Compatibility.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
Documentation on compatibility guidance and the current state. The version headings act as a rolling delta between the previous version.
44

5-
## Version 2
5+
## Version 3 (.NET 8)
6+
7+
### Safe Handles
8+
9+
Due to trimming issues with NativeAOT's implementation of `Activator.CreateInstance`, we have decided to change our recommendation of providing a public parameterless constructor for `ref`, `out`, and return scenarios to a requirement. We already required a parameterless constructor of some visibility, so changing to a requirement matches our design principles of taking breaking changes to make interop more understandable and enforce more of our best practices instead of going out of our way to provide backward compatibility at increasing costs.
10+
11+
## Version 2 (.NET 7 Release)
612

713
The focus of version 2 is to support all repos that make up the .NET Product, including ASP.NET Core and Windows Forms, as well as all packages in dotnet/runtime.
814

@@ -11,7 +17,7 @@ The focus of version 2 is to support all repos that make up the .NET Product, in
1117
Support for user-defined type marshalling in the source-generated marshalling is described in [UserTypeMarshallingV2.md](UserTypeMarshallingV2.md). This support replaces the designs specified in [StructMarshalling.md](StructMarshalling.md) and [SpanMarshallers.md](SpanMarshallers.md).
1218

1319

14-
## Version 1
20+
## Version 1 (.NET 6 Prototype and .NET 7 Previews)
1521

1622
The focus of version 1 is to support `NetCoreApp`. This implies that anything not needed by `NetCoreApp` is subject to change.
1723

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,7 @@
933933
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\Marshalling\NativeMarshallingAttribute.cs" />
934934
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\Marshalling\PointerArrayMarshaller.cs" />
935935
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\Marshalling\ReadOnlySpanMarshaller.cs" />
936+
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\Marshalling\SafeHandleMarshaller.cs" />
936937
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\Marshalling\SpanMarshaller.cs" />
937938
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\Marshalling\Utf16StringMarshaller.cs" />
938939
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\InteropServices\Marshalling\Utf8StringMarshaller.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace System.Runtime.InteropServices.Marshalling
7+
{
8+
/// <summary>
9+
/// A marshaller for <see cref="SafeHandle"/>-derived types that marshals the handle following the lifetime rules for <see cref="SafeHandle"/>s.
10+
/// </summary>
11+
/// <typeparam name="T">The <see cref="SafeHandle"/>-derived type.</typeparam>
12+
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder), MarshalMode.ManagedToUnmanagedIn, typeof(SafeHandleMarshaller<>.ManagedToUnmanagedIn))]
13+
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder), MarshalMode.ManagedToUnmanagedRef, typeof(SafeHandleMarshaller<>.ManagedToUnmanagedRef))]
14+
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder), MarshalMode.ManagedToUnmanagedOut, typeof(SafeHandleMarshaller<>.ManagedToUnmanagedOut))]
15+
public static class SafeHandleMarshaller<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T> where T : SafeHandle
16+
{
17+
/// <summary>
18+
/// Custom marshaller to marshal a <see cref="SafeHandle"/> as its underlying handle value.
19+
/// </summary>
20+
public struct ManagedToUnmanagedIn
21+
{
22+
private bool _addRefd;
23+
private T? _handle;
24+
25+
/// <summary>
26+
/// Initializes the marshaller from a managed handle.
27+
/// </summary>
28+
/// <param name="handle">The managed handle.</param>
29+
public void FromManaged(T handle)
30+
{
31+
_handle = handle;
32+
handle.DangerousAddRef(ref _addRefd);
33+
}
34+
35+
/// <summary>
36+
/// Get the unmanaged handle.
37+
/// </summary>
38+
/// <returns>The unmanaged handle.</returns>
39+
public IntPtr ToUnmanaged() => _handle!.DangerousGetHandle();
40+
41+
/// <summary>
42+
/// Release any references keeping the managed handle alive.
43+
/// </summary>
44+
public void Free()
45+
{
46+
if (_addRefd)
47+
{
48+
_handle!.DangerousRelease();
49+
}
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Custom marshaller to marshal a <see cref="SafeHandle"/> as its underlying handle value.
55+
/// </summary>
56+
public struct ManagedToUnmanagedRef
57+
{
58+
private bool _addRefd;
59+
private bool _callInvoked;
60+
private T? _handle;
61+
private IntPtr _originalHandleValue;
62+
private T _newHandle;
63+
private T? _handleToReturn;
64+
65+
/// <summary>
66+
/// Create the marshaller in a default state.
67+
/// </summary>
68+
public ManagedToUnmanagedRef()
69+
{
70+
_addRefd = false;
71+
_callInvoked = false;
72+
// SafeHandle ref marshalling has always required parameterless constructors,
73+
// but it has never required them to be public.
74+
// We construct the handle now to ensure we don't cause an exception
75+
// before we are able to capture the unmanaged handle after the call.
76+
_newHandle = Activator.CreateInstance<T>()!;
77+
}
78+
79+
/// <summary>
80+
/// Initialize the marshaller from a managed handle.
81+
/// </summary>
82+
/// <param name="handle">The managed handle</param>
83+
public void FromManaged(T handle)
84+
{
85+
_handle = handle;
86+
handle.DangerousAddRef(ref _addRefd);
87+
_originalHandleValue = handle.DangerousGetHandle();
88+
}
89+
90+
/// <summary>
91+
/// Retrieve the unmanaged handle.
92+
/// </summary>
93+
/// <returns>The unmanaged handle</returns>
94+
public IntPtr ToUnmanaged() => _originalHandleValue;
95+
96+
/// <summary>
97+
/// Initialize the marshaller from an unmanaged handle.
98+
/// </summary>
99+
/// <param name="value">The unmanaged handle.</param>
100+
public void FromUnmanaged(IntPtr value)
101+
{
102+
if (value == _originalHandleValue)
103+
{
104+
_handleToReturn = _handle;
105+
}
106+
else
107+
{
108+
Marshal.InitHandle(_newHandle, value);
109+
_handleToReturn = _newHandle;
110+
}
111+
}
112+
113+
/// <summary>
114+
/// Notify the marshaller that the native call has been invoked.
115+
/// </summary>
116+
public void OnInvoked()
117+
{
118+
_callInvoked = true;
119+
}
120+
121+
/// <summary>
122+
/// Retrieve the managed handle from the marshaller.
123+
/// </summary>
124+
/// <returns>The managed handle.</returns>
125+
public T ToManagedFinally() => _handleToReturn!;
126+
127+
/// <summary>
128+
/// Free any resources and reference counts owned by the marshaller.
129+
/// </summary>
130+
public void Free()
131+
{
132+
if (_addRefd)
133+
{
134+
_handle!.DangerousRelease();
135+
}
136+
137+
// If we never invoked the call, then we aren't going to use the
138+
// new handle. Dispose it now to avoid clogging up the finalizer queue
139+
// unnecessarily.
140+
if (!_callInvoked)
141+
{
142+
_newHandle.Dispose();
143+
}
144+
}
145+
}
146+
147+
/// <summary>
148+
/// Custom marshaller to marshal a <see cref="SafeHandle"/> as its underlying handle value.
149+
/// </summary>
150+
public struct ManagedToUnmanagedOut
151+
{
152+
private bool _initialized;
153+
private T _newHandle;
154+
155+
/// <summary>
156+
/// Create the marshaller in a default state.
157+
/// </summary>
158+
public ManagedToUnmanagedOut()
159+
{
160+
_initialized = false;
161+
// SafeHandle out marshalling has always required parameterless constructors,
162+
// but it has never required them to be public.
163+
// We construct the handle now to ensure we don't cause an exception
164+
// before we are able to capture the unmanaged handle after the call.
165+
_newHandle = Activator.CreateInstance<T>()!;
166+
}
167+
168+
/// <summary>
169+
/// Initialize the marshaller from an unmanaged handle.
170+
/// </summary>
171+
/// <param name="value">The unmanaged handle.</param>
172+
public void FromUnmanaged(IntPtr value)
173+
{
174+
_initialized = true;
175+
Marshal.InitHandle(_newHandle, value);
176+
}
177+
178+
/// <summary>
179+
/// Retrieve the managed handle from the marshaller.
180+
/// </summary>
181+
/// <returns>The managed handle.</returns>
182+
public T ToManaged() => _newHandle;
183+
184+
/// <summary>
185+
/// Free any resources and reference counts owned by the marshaller.
186+
/// </summary>
187+
public void Free()
188+
{
189+
// If we never captured the handle value, then we aren't going to use the
190+
// new handle. Dispose it now to avoid clogging up the finalizer queue
191+
// unnecessarily.
192+
if (!_initialized)
193+
{
194+
_newHandle!.Dispose();
195+
}
196+
}
197+
}
198+
}
199+
}

src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Microsoft.Interop.SourceGeneration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
</ItemGroup>
1616

1717
<ItemGroup>
18+
<Compile Include="$(CommonPath)Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
1819
<Compile Include="../../tests/Common/MarshalDirection.cs" />
1920
</ItemGroup>
2021

src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SafeHandleMarshallingInfoProvider.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Collections.Immutable;
67
using System.Text;
78
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
810

911
namespace Microsoft.Interop
1012
{
@@ -19,11 +21,13 @@ public sealed record SafeHandleMarshallingInfo(bool AccessibleDefaultConstructor
1921
public sealed class SafeHandleMarshallingInfoProvider : ITypeBasedMarshallingInfoProvider
2022
{
2123
private readonly Compilation _compilation;
24+
private readonly INamedTypeSymbol _safeHandleMarshallerType;
2225
private readonly ITypeSymbol _containingScope;
2326

2427
public SafeHandleMarshallingInfoProvider(Compilation compilation, ITypeSymbol containingScope)
2528
{
2629
_compilation = compilation;
30+
_safeHandleMarshallerType = compilation.GetBestTypeByMetadataName(TypeNames.System_Runtime_InteropServices_Marshalling_SafeHandleMarshaller_Metadata);
2731
_containingScope = containingScope;
2832
}
2933

@@ -47,19 +51,53 @@ public bool CanProvideMarshallingInfoForType(ITypeSymbol type)
4751

4852
public MarshallingInfo GetMarshallingInfo(ITypeSymbol type, int indirectionDepth, UseSiteAttributeProvider useSiteAttributes, GetMarshallingInfoCallback marshallingInfoCallback)
4953
{
54+
bool hasDefaultConstructor = false;
5055
bool hasAccessibleDefaultConstructor = false;
5156
if (type is INamedTypeSymbol named && !named.IsAbstract && named.InstanceConstructors.Length > 0)
5257
{
5358
foreach (IMethodSymbol ctor in named.InstanceConstructors)
5459
{
5560
if (ctor.Parameters.Length == 0)
5661
{
62+
hasDefaultConstructor = ctor.DeclaredAccessibility == Accessibility.Public;
5763
hasAccessibleDefaultConstructor = _compilation.IsSymbolAccessibleWithin(ctor, _containingScope);
5864
break;
5965
}
6066
}
6167
}
62-
return new SafeHandleMarshallingInfo(hasAccessibleDefaultConstructor, type.IsAbstract);
68+
69+
// If we don't have the SafeHandleMarshaller<T> type, then we'll use the built-in support in the generator.
70+
// This support will be removed when dotnet/runtime doesn't build any packages for platforms below .NET 8
71+
// as the downlevel support is dotnet/runtime specific.
72+
if (_safeHandleMarshallerType is null)
73+
{
74+
return new SafeHandleMarshallingInfo(hasAccessibleDefaultConstructor, type.IsAbstract);
75+
}
76+
77+
INamedTypeSymbol entryPointType = _safeHandleMarshallerType.Construct(type);
78+
if (!ManualTypeMarshallingHelper.TryGetValueMarshallersFromEntryType(
79+
entryPointType,
80+
type,
81+
_compilation,
82+
out CustomTypeMarshallers? marshallers))
83+
{
84+
return NoMarshallingInfo.Instance;
85+
}
86+
87+
// If the SafeHandle-derived type doesn't have a default constructor or is abstract,
88+
// we only support managed-to-unmanaged marshalling
89+
if (!hasDefaultConstructor || type.IsAbstract)
90+
{
91+
marshallers = marshallers.Value with
92+
{
93+
Modes = ImmutableDictionary<MarshalMode, CustomTypeMarshallerData>.Empty
94+
.Add(
95+
MarshalMode.ManagedToUnmanagedIn,
96+
marshallers.Value.GetModeOrDefault(MarshalMode.ManagedToUnmanagedIn))
97+
};
98+
}
99+
100+
return new NativeMarshallingAttributeInfo(ManagedTypeInfo.CreateTypeInfoForTypeSymbol(entryPointType), marshallers.Value);
63101
}
64102
}
65103
}

src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/TypeNames.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,7 @@ public static string MarshalEx(InteropGenerationOptions options)
136136
public const string GeneratedComClassAttribute = "System.Runtime.InteropServices.Marshalling.GeneratedComClassAttribute";
137137
public const string ComExposedClassAttribute = "System.Runtime.InteropServices.Marshalling.ComExposedClassAttribute";
138138
public const string IComExposedClass = "System.Runtime.InteropServices.Marshalling.IComExposedClass";
139+
140+
public const string System_Runtime_InteropServices_Marshalling_SafeHandleMarshaller_Metadata = "System.Runtime.InteropServices.Marshalling.SafeHandleMarshaller`1";
139141
}
140142
}

src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.Tests/SafeHandleTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ partial class NativeExportsNE
1212
{
1313
public partial class NativeExportsSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
1414
{
15-
private NativeExportsSafeHandle() : base(ownsHandle: true)
15+
public NativeExportsSafeHandle() : base(ownsHandle: true)
1616
{ }
1717

1818
protected override bool ReleaseHandle()

src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/CompileFails.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ public static IEnumerable<object[]> CodeSnippetsToCompile()
113113
// Abstract SafeHandle type by reference
114114
yield return new object[] { ID(), CodeSnippets.BasicParameterWithByRefModifier("ref", "System.Runtime.InteropServices.SafeHandle"), 1, 0 };
115115

116+
// SafeHandle array
117+
yield return new object[] { ID(), CodeSnippets.MarshalAsArrayParametersAndModifiers("Microsoft.Win32.SafeHandles.SafeFileHandle"), 5, 0 };
118+
119+
// SafeHandle with private constructor by ref or out
120+
yield return new object[] { ID(), CodeSnippets.SafeHandleWithCustomDefaultConstructorAccessibility(privateCtor: true), 3, 0 };
121+
116122
// Collection with constant and element size parameter
117123
yield return new object[] { ID(), CodeSnippets.MarshalUsingCollectionWithConstantAndElementCount, 2, 0 };
118124

src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/Compiles.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ public static IEnumerable<object[]> CodeSnippetsToCompile()
174174
yield return new[] { ID(), CodeSnippets.BasicParametersAndModifiers("Microsoft.Win32.SafeHandles.SafeFileHandle") };
175175
yield return new[] { ID(), CodeSnippets.BasicParameterByValue("System.Runtime.InteropServices.SafeHandle") };
176176
yield return new[] { ID(), CodeSnippets.SafeHandleWithCustomDefaultConstructorAccessibility(privateCtor: false) };
177-
yield return new[] { ID(), CodeSnippets.SafeHandleWithCustomDefaultConstructorAccessibility(privateCtor: true) };
178177

179178
// Custom type marshalling
180179
CustomStructMarshallingCodeSnippets customStructMarshallingCodeSnippets = new(new CodeSnippets());

0 commit comments

Comments
 (0)