[API Proposal]: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute.DisableRuntimeMarshalling · Issue #60639 · dotnet/runtime (original) (raw)

Background and motivation

As the interop team is working on a source-generated DllImport solution, we've decided to take an extra look at the built-in marshalling rules per feedback from @stephentoub.

In the built-in marshalling system today, the runtime looks at all member fields of a type at runtime and uses this runtime type information to determine if a type is blittable and how to marshal it. For compile-time source generators, this sort of design is untenable for performance and feasibility reasons (we need a static type in the final "native call" signature to get correct calling-convention behavior). See https://github.com/dotnet/runtime/blob/main/docs/design/libraries/DllImportGenerator/StructMarshalling.md for a more information.

A complicating factor in this scenario is that the default marshalling rules for bool and char are non-blittable. In particular, bool maps to a 4-byte Windows API BOOL, and char maps to an ANSI (based on the current code page) 1-byte character. As a result, the C# language concept of "unmanaged" is not the same as the runtime concept of "blittable". In the proposal above, we took the route of building a new attribute and analysis system to ensure that a type marked as blittable is actually blittable.

@stephentoub and @jkotas recommended that we could instead investigate changing the rules in source-generated P/Invokes to make the unmanaged and blittable concepts equivalent. To enable this mechanism at the interop boundary, we need to provide some mechanism to disable the built in marshalling rules. We've explored a few options in #59824, and I've selected one of the options to bring up for API review here (the other options are covered in Alternative Designs).

We propose adding a new property to RuntimeCompatibilityAttribute that disables the built-in marshalling system for all P/Invoke and Reverse P/Invoke scenarios defined in the assembly (all methods marked [DllImport], all delegates that are defined in the assembly when used in interop scenarios, and all UnmanagedCallersOnly-attributed methods). When this property is set to true, all unmanaged types are considered blittable, and all non-blittable types (in/ref/out parameters as well since they require pinning) are disallowed.

We like the idea of disabling across a whole assembly for a few reasons:

  1. We're looking at providing guidance to developers to move all of their P/Invokes to use source-generated stubs. By making one of the gestures for using source generated P/Invokes force this behavior change, we can better push/enforce this guidance.
  2. By making the decision at the assembly boundary, analyzing all assemblies to determine if any use the built-in marshalling system becomes much easier. This is useful for device scenarios where Mono would like to exclude the native-implemented built-in marshalling system entirely if it is unused.
  3. By requiring an explicit gesture for opt-in to this system, we can use the presence of the property assignment as a marker that the developer has acknowledged the change in the "what is blittable?" rules and not issue a diagnostic to notify the developer that source-generated P/Invokes have differing behavior.

API Proposal

namespace System.Runtime.CompilerServices { public class RuntimeCompatibilityAttribute {

Additionally, we propose a behavior change for GCHandle.Alloc to support all unmanaged types for allocating a pinned GC handle. There is no API change required for this portion of the request, as this only changes an exceptional scenario (non-blittable unmanaged types throw today in this case, and all other unmanaged types are blittable and as such succeed).

API Usage

[assembly:RuntimeCompatibilityAttribute(WrapNonExceptionThrows = true, DisableRuntimeMarshalling = true)]

Alternative Designs

Alternative Design 1: System.Runtime.InteropServices.UnmanagedType.UnmanagedBlittable

namespace System.Runtime.InteropServices { public enum UnmanagedType {

In this design, we'd either explicitly require or implicitly emit a [MarshalAs(UnmanagedType.UnmanagedBlittable)] attribute on every unmanaged parameter or return type that doesn't have any marshalling attributes already applied.

Pros:

Cons:

Alternative design 2: System.Runtime.CompilerServices.CallConvUnmanagedBlittable

namespace System.Runtime.CompilerServices {

}

In this design, we introduce a new "calling convention" type that specifies the "unmanaged == blittable" behavior for all parameters and return values, and we'd implicitly apply it in source-generated scenarios.

Pros:

Cons:

Alternative design 3: System.Runtime.CompilerServices.CallConvNoMarshalling

namespace System.Runtime.CompilerServices {

}

In this design, we introduce a new "calling convention" type that disables the built-in marshalling behavior for all parameters and return values, and we'd implicitly apply it in source-generated scenarios.

Pros:

Cons:

Risks

Today, Roslyn emits the equivalent to [assembly:RuntimeCompatibilityAttribute(WrapNonExceptionThrows = true)] into every assembly if the RuntimeCompatibilityAttribute is not defined by the user. If the user manually provides the RuntimeCompatibilityAttribute attribution themselves, they may forget to set WrapNonExceptionThrows=true. As scenarios where WrapNonExceptionThrows=true actually kicks in (requires manually written IL or C++/CLI to my knowledge), this shouldn't be much of an issue. If we view this as a problem, we can do one or both of the following things:

  1. We could provide a code-fix that emits a RuntimeCompatibilityAttribute attribute application in scenarios where we'd recommend applying it (ie. with the DllImportGenerator or other interop source-generators).
  2. We could provide an MSBuild property that Roslyn will read in so it can emit the RuntimeCompatibilityAttribute with the DisableRuntimeMarshalling = true property itself.