From 37b1aae6997001bbaa0ad6b0034f6ccc4d4d5065 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 20 Jun 2022 11:01:56 -0700 Subject: [PATCH 1/5] Update design docs to reflect our new marshaller design from partner feedback --- .../LibraryImportGenerator/Compatibility.md | 9 + .../LibraryImportGenerator/Pipeline.md | 10 +- .../LibraryImportGenerator/SpanMarshallers.md | 2 + .../StructMarshalling.md | 2 + .../UserTypeMarshallingV2.md | 695 ++++++++++++++++++ 5 files changed, 717 insertions(+), 1 deletion(-) create mode 100644 docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md diff --git a/docs/design/libraries/LibraryImportGenerator/Compatibility.md b/docs/design/libraries/LibraryImportGenerator/Compatibility.md index 0e3cd08727fb9..27cf3fa45e0fd 100644 --- a/docs/design/libraries/LibraryImportGenerator/Compatibility.md +++ b/docs/design/libraries/LibraryImportGenerator/Compatibility.md @@ -2,6 +2,15 @@ Documentation on compatibility guidance and the current state. The version headings act as a rolling delta between the previous version. +## Version 2 + +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. + +### User defined type marshalling + +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). + + ## Version 1 The focus of version 1 is to support `NetCoreApp`. This implies that anything not needed by `NetCoreApp` is subject to change. diff --git a/docs/design/libraries/LibraryImportGenerator/Pipeline.md b/docs/design/libraries/LibraryImportGenerator/Pipeline.md index 325da5e3edb8d..4524eefd8180d 100644 --- a/docs/design/libraries/LibraryImportGenerator/Pipeline.md +++ b/docs/design/libraries/LibraryImportGenerator/Pipeline.md @@ -75,11 +75,17 @@ The stub code generator itself will handle some initial setup and variable decla 1. `Pin`: data pinning in preparation for calling the generated P/Invoke - Call `Generate` on the marshalling generator for every parameter - Ignore any statements that are not `fixed` statements +1. `PinnedMarshal`: conversion of managed to native data + - Call `Generate` on the marshalling generator for every parameter 1. `Invoke`: call to the generated P/Invoke - Call `AsArgument` on the marshalling generator for every parameter - Create invocation statement that calls the generated P/Invoke -1. `KeepAlive`: keep alive any objects who's native representation won't keep them alive across the call. +1. `NotifyForSuccessfulInvoke`: Notify a marshaller that all stages through the "Invoke" stage were successful. + - Used to keep alive any objects who's native representation won't keep them alive across the call. - Call `Generate` on the marshalling generator for every parameter. +1. `UnmarshalCapture`: capture any native out parameters to avoid memory leaks if exceptions are thrown during `Unmarshal`. + - If the method has a non-void return, call `Generate` on the marshalling generator for the return + - Call `Generate` on the marshalling generator for every parameter 1. `Unmarshal`: conversion of native to managed data - If the method has a non-void return, call `Generate` on the marshalling generator for the return - Call `Generate` on the marshalling generator for every parameter @@ -97,9 +103,11 @@ try << Marshal >> << Pin >> (fixed) { + << Pinned Marshal >> << Invoke >> } << Keep Alive >> + << Unmarshal Capture >> << Unmarshal >> } finally diff --git a/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md b/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md index 320c48a5d6574..b98c1f05e2eb3 100644 --- a/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md +++ b/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md @@ -2,6 +2,8 @@ As part of the exit criteria for the LibraryImportGenerator experiment, we have decided to introduce support for marshalling `System.Span` and `System.ReadOnlySpan` into the LibraryImportGenerator-generated stubs. This document describes design decisions made during the implementation of these marshallers. +> NOTE: These design docs are kept for historical purposes. The designs in this file are supersceded by the designs in [UserTypeMarshallingV2.md](UserTypeMarshallingV2.md). + ## Design 1: "Intrinsic" support for `(ReadOnly)Span` In this design, the default support for `(ReadOnly)Span` is emitted into the marshalling stub directly and builds on the pattern we enabled for arrays. diff --git a/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md b/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md index c865ae2bae932..72966d339d0d0 100644 --- a/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md +++ b/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md @@ -4,6 +4,8 @@ As part of the new source-generated direction for .NET Interop, we are looking a These types pose an interesting problem for a number of reasons listed below. With a few constraints, I believe we can create a system that will enable users to use their own user-defined types and pass them by-value to native code. +> NOTE: These design docs are kept for historical purposes. The designs in this file are supersceded by the designs in [UserTypeMarshallingV2.md](UserTypeMarshallingV2.md). + ## Problems - What types require marshalling and what types can be passed as-is to native code? diff --git a/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md new file mode 100644 index 0000000000000..f7db15659d2a3 --- /dev/null +++ b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md @@ -0,0 +1,695 @@ +# User Defined Type Marshalling for Source-Generated Interop + +For the V1 of our source-generator, we designed support for marshalling collection types and user-defined structure types based on the designs in [StructMarshalling.md](StructMarshalling.md) and [SpanMarshallers.md](SpanMarshallers.md). As the model was adopted throughout dotnet/runtime, ASP.NET Core, WinForms, and early adopters, we recieved substantial feedback that led us to reconsider some components of the design. + +Here are some of the main feedback points on the previous design: + +- State diagram was complex + - The particular order in which methods are called on the marshallers were not simple to define. + - Handling exception scenarios without causing memory leaks was difficult. + - Supporting state preservation in "element of a collection" scenarios was going to be extremely difficult. +- Overcomplicated for simple marshallers + - The support for "Transparent Structures" in the original design added additional overhead on the base design instead of making this scenario cheaper. +- Concept Overload + - The design with optional features and multiple shapes was getting to the point that introducing people to the design was going to be difficult as there were many small options to pick. +- Limited specialization capabilities + - The V1 design mapped a single managed type to a single marshaller type. As a result, if the marshaller type required specialized support for a particular scenario such as a stack-allocated buffer optimization, then every scenario had to pay the overhead to support that conditional optimization. + - A marshaller could only be the marshaller for one managed type. As a result, if two types (such as `string` and `char`) both wanted to use the same marshalling concept, the developer would need to use two different marshaller types. + +The new design tries to address many of these concerns. + +The new marshallers have a stateless shape and a stateful shape. Stateful shapes are (currently) not allowed in "element of a collection" scenarios as handling them is difficult today, but we may improve this in the future. The stateful shapes are described in the order in which the methods will be called. Additionally, by moving away from using construtors for part of the marshalling, we can simplify the exception-handling guidance as we will only ever have one marshaller instance per parameter and it will always be assigned to the local. + +Stateless shapes avoid the problems of maintaining state and will be the primarily used shapes (they cover 90+% of our scenarios). + +The new stateless shapes provide simple mechanisms to implement marshalling for "Transparent Structures" without adding additional complexity. + +The new design has less "optional" members and each member in a shape is always used when provided. + +The new design uses a "marshaller entry-point" type to name a concept, which the user provides attributes on to point to the actual marshaller types per-scenario. This enables a marshaller entry-point type to provide specialized support for particular scenarios and support multiple managed types with one marshaller entry-point type. + +## API Diff for Supporting Attributes + +```diff +namespace System.Runtime.InteropServices.Marshalling; + +- [AttributeUsage(AttributeTargets.Struct)] +- public sealed class CustomTypeMarshallerAttribute : Attribute +- { +- public CustomTypeMarshallerAttribute(Type managedType, CustomTypeMarshallerKind marshallerKind = - CustomTypeMarshallerKind.Value) +- { +- ManagedType = managedType; +- MarshallerKind = marshallerKind; +- } +- +- public Type ManagedType { get; } +- public CustomTypeMarshallerKind MarshallerKind { get; } +- public int BufferSize { get; set; } +- public CustomTypeMarshallerDirection Direction { get; set; } = CustomTypeMarshallerDirection.Ref; +- public CustomTypeMarshallerFeatures Features { get; set; } +- public struct GenericPlaceholder +- { +- } +- } +- +- public enum CustomTypeMarshallerKind +- { +- Value, +- LinearCollection +- } +- +- [Flags] +- public enum CustomTypeMarshallerFeatures +- { +- None = 0, +- /// +- /// The marshaller owns unmanaged resources that must be freed +- /// +- UnmanagedResources = 0x1, +- /// +- /// The marshaller can use a caller-allocated buffer instead of allocating in some scenarios +- /// +- CallerAllocatedBuffer = 0x2, +- /// +- /// The marshaller uses the two-stage marshalling design for its instead of the - one-stage design. +- /// +- TwoStageMarshalling = 0x4 +- } +- [Flags] +- public enum CustomTypeMarshallerDirection +- { +- /// +- /// No marshalling direction +- /// +- [EditorBrowsable(EditorBrowsableState.Never)] +- None = 0, +- /// +- /// Marshalling from a managed environment to an unmanaged environment +- /// +- In = 0x1, +- /// +- /// Marshalling from an unmanaged environment to a managed environment +- /// +- Out = 0x2, +- /// +- /// Marshalling to and from managed and unmanaged environments +- /// +- Ref = In | Out, +- } + ++ /// ++ /// Define features for a custom type marshaller. ++ /// ++ [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)] ++ public sealed class CustomTypeMarshallerFeaturesAttribute : Attribute ++ { ++ /// ++ /// Desired caller buffer size for the marshaller. ++ /// ++ public int BufferSize { get; set; } ++ } ++ ++ ++ /// ++ /// Base class attribute for custom marshaller attributes. ++ /// ++ /// ++ /// Use a base class here to allow doing ManagedToUnmanagedMarshallersAttribute.GenericPlaceholder, etc. without having 3 + separate placeholder types. ++ /// For the following attribute types, any marshaller types that are provided will be validated by an analyzer to have the + correct members to prevent ++ /// developers from accidentally typoing a member like Free() and causing memory leaks. ++ /// ++ public abstract class CustomUnmanagedTypeMarshallersAttributeBase : Attribute ++ { ++ /// ++ /// Placeholder type for generic parameter ++ /// ++ public sealed class GenericPlaceholder { } ++ } ++ ++ /// ++ /// Specify marshallers used in the managed to unmanaged direction (that is, P/Invoke) ++ /// ++ [AttributeUsage(AttributeTargets.Class)] ++ public sealed class ManagedToUnmanagedMarshallersAttribute : CustomUnmanagedTypeMarshallersAttributeBase ++ { ++ /// ++ /// Create instance of . ++ /// ++ /// Managed type to marshal ++ public ManagedToUnmanagedMarshallersAttribute(Type managedType) { } ++ ++ /// ++ /// Marshaller to use when a parameter of the managed type is passed by-value or with the in keyword. ++ /// ++ public Type? InMarshaller { get; set; } ++ ++ /// ++ /// Marshaller to use when a parameter of the managed type is passed by-value or with the ref keyword. ++ /// ++ public Type? RefMarshaller { get; set; } ++ ++ /// ++ /// Marshaller to use when a parameter of the managed type is passed by-value or with the out keyword. ++ /// ++ public Type? OutMarshaller { get; set; } ++ } ++ ++ /// ++ /// Specify marshallers used in the unmanaged to managed direction (that is, Reverse P/Invoke) ++ /// ++ [AttributeUsage(AttributeTargets.Class)] ++ public sealed class UnmanagedToManagedMarshallersAttribute : CustomUnmanagedTypeMarshallersAttributeBase ++ { ++ /// ++ /// Create instance of . ++ /// ++ /// Managed type to marshal ++ public UnmanagedToManagedMarshallersAttribute(Type managedType) { } ++ ++ /// ++ /// Marshaller to use when a parameter of the managed type is passed by-value or with the in keyword. ++ /// ++ public Type? InMarshaller { get; set; } ++ ++ /// ++ /// Marshaller to use when a parameter of the managed type is passed by-value or with the ref keyword. ++ /// ++ public Type? RefMarshaller { get; set; } ++ ++ /// ++ /// Marshaller to use when a parameter of the managed type is passed by-value or with the out keyword. ++ /// ++ public Type? OutMarshaller { get; set; } ++ } ++ ++ /// ++ /// Specify marshaller for array-element marshalling and default struct field marshalling. ++ /// ++ [AttributeUsage(AttributeTargets.Class)] ++ public sealed class ElementMarshallerAttribute : CustomUnmanagedTypeMarshallersAttributeBase ++ { ++ /// ++ /// Create instance of . ++ /// ++ /// Managed type to marshal ++ /// Marshaller type to use for marshalling . ++ public ElementMarshallerAttribute(Type managedType, Type elementMarshaller) { } ++ } ++ ++ /// ++ /// Specifies that a particular generic parameter is the collection element's unmanaged type. ++ /// ++ /// ++ /// If this attribute is provided on a generic parameter of a marshaller, then the generator will assume ++ /// that it is a linear collection marshaller. ++ /// ++ [AttributeUsage(AttributeTargets.GenericParameter)] ++ public sealed class ElementUnmanagedTypeAttribute : Attribute ++ { ++ } +``` + +## Design Details + +First of all, this new design continues to use the existing policy for defining "blittable" types as described in the V1 design. The rest of this document will describe the custom user-defined marshalling rules. + +In the new design, the user will first define an "entry-point type" that represents a marshalling concept. For example, if we are marshalling a `string` to a native UTF-8 encoded string, we might call the marshaller `Utf8StringMarshaller`. This new type will be a `static class`. The developer will then use the `ManagedToUnmanagedMarshallersAttribute`, `UnmanagedToManagedMarshallersAttribute`, and `ElementMarshallerAttribute` to specify which "marshaller implementation type" will be used to actually provide the marshalling. If an attribute is missing or a property on the attribute is set to `null` or left unset, this marshaller will not support marshalling in that scenario. A single type can be specified multiple times if it provides the marshalling support for multiple scenarios. + +To avoid confusion around when each marshaller applies, we define when the marshallers apply based on the C# syntax used. This helps reduce the concept load as developers don't need to remember the mapping between the previous design's `CustomTypeMarshallerDirection` enum member and the C# keyword used for a parameter, which do not match in a Reverse P/Invoke-like scenario. + +We will recommend that the marshaller types that are supplied are nested types of the "entry-point type" or the "entry-point type" itself, but we will not require it. Each specified marshaller type will have to abide by one of the following shapes depending on the scenario is supports. + +The examples below will also show which properties in each attribute support each marshaller shape. + +## Value Marshaller Shapes + +We'll start with the value marshaller shapes. These marshaller shapes support marshalling a single self-contained value. + +Each of these shapes will support marshalling the following type: + +```csharp +// Any number of generic parameters is allowed, with any constraints +struct TManaged +{ + // ... +} +``` + +The type `TNative` can be any `unmanaged` type. It represents whatever unmanaged type the marshaller marshals the managed type to. + +### Stateless Managed->Unmanaged + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller +{ + public static class ManagedToNative + { + public static TNative ConvertToUnmanaged(TManaged managed); // Can throw exceptions + + public static ref TOther GetPinnableReference(TManaged managed); // Optional. Can throw exceptions. Result pinnned and passed to Invoke. + + public static void Free(TNative unmanaged); // Optional. Should not throw exceptions + } +} + +``` +### Stateless Managed->Unmanaged with Caller-Allocated Buffer + +The element type of the `Span` for the caller-allocated buffer can be any type that guarantees any alignment requirements. + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller +{ + [CustomTypeMarshallerFeatures(BufferSize = 0x200)] + public static class ManagedToNative + { + public static TNative ConvertToUnmanaged(TManaged managed, Span callerAllocatedBuffer); // Can throw exceptions + + public static void Free(TNative unmanaged); // Optional. Should not throw exceptions + } +} + +``` + +### Stateless Unmanaged->Managed + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller +{ + public static class NativeToManaged + { + public static TManaged ConvertToManaged(TNative unmanaged); // Can throw exceptions + + public static void Free(TNative unmanaged); // Optional. Should not throw exceptions + } +} + +``` + +### Stateless Unmanaged->Managed with Guaranteed Unmarshalling + +This shape directs the generator to emit the `ConvertToManagedGuaranteed` call in the "GuaranteedUnmarshal" phase of marshalling. + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller +{ + public static class NativeToManaged + { + public static TManaged ConvertToManagedGuaranteed(TNative unmanaged); // Should not throw exceptions + + public static void Free(TNative unmanaged); // Optional. Should not throw exceptions + } +} + +``` + +### Stateless Bidirectional +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), RefMarshaller = typeof(Bidirectional))] +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), RefMarshaller = typeof(Bidirectional))] +[ElementMarshaller(typeof(TManaged<,,,...>), typeof(Bidirectional))] +static class TMarshaller +{ + public static class Bidirectional + { + // Include members from each of the following: + // - One Stateless Managed->Unmanaged Value shape + // - One Stateless Unmanaged->Managed Value shape + } +} + +``` + +### Stateful Managed->Unmanaged + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller +{ + public struct ManagedToNative // Can be ref struct + { + public ManagedToNative(); // Optional, can throw exceptions. + + public void FromManaged(TManaged managed); // Can throw exceptions. + + public ref TIgnored GetPinnableReference(); // Result pinned for ToUnmanaged call and Invoke, but not used otherwise. + + public static ref TOther GetPinnableReference(TManaged managed); // Optional. Can throw exceptions. Result pinnned and passed to Invoke. + + public TNative ToUnmanaged(); // Can throw exceptions. + + public void NotifyInvokeSucceeded(); // Optional. Should not throw exceptions. + + public void Free(); // Should not throw exceptions. + } +} + +``` +### Stateful Managed->Unmanaged with Caller Allocated Buffer + +The element type of the `Span` for the caller-allocated buffer can be any type that guarantees any alignment requirements. + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller +{ + [CustomTypeMarshallerFeatures(BufferSize = 0x200)] + public struct ManagedToNative // Can be ref struct + { + public ManagedToNative(); // Optional, can throw exceptions. + + public void FromManaged(TManaged managed, Span buffer); // Can throw exceptions. + + public ref TIgnored GetPinnableReference(); // Result pinned for ToUnmanaged call and Invoke, but not used otherwise. + + public static ref TOther GetPinnableReference(TManaged managed); // Optional. Can throw exceptions. Result pinnned and passed to Invoke. + + public TNative ToUnmanaged(); // Can throw exceptions. + + public void NotifyInvokeSucceeded(); // Optional. Should not throw exceptions. + + public void Free(); // Should not throw exceptions. + } +} + +``` + +### Stateful Unmanaged->Managed + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller +{ + public struct NativeToManaged // Can be ref struct + { + public NativeToManaged(); // Optional, can throw exceptions. + + public void FromUnmanaged(TNative native); // Should not throw exceptions. + + public TManaged ToManaged(); // Can throw exceptions. + + public void Free(); // Should not throw exceptions. + } +} + +``` + +### Stateful Unmanaged->Managed with Guaranteed Unmarshalling + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TManaged<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller +{ + public struct NativeToManaged // Can be ref struct + { + public NativeToManaged(); // Optional, can throw exceptions. + + public void FromUnmanaged(TNative native); // Should not throw exceptions. + + public TManaged ToManagedGuaranteed(); // Should not throw exceptions. + + public void Free(); // Should not throw exceptions. + } +} + +``` + +### Stateful Bidirectional +```csharp +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), RefMarshaller = typeof(Bidirectional))] +[ManagedToUnmanagedMarshallers(typeof(TManaged<,,,...>), RefMarshaller = typeof(Bidirectional))] +static class TMarshaller +{ + public struct Bidirectional // Can be ref struct + { + // Include members from each of the following: + // - One Stateful Managed->Unmanaged Value shape + // - One Stateful Unmanaged->Managed Value shape + } +} +``` + +## Linear (Array-like) Collection Marshaller Shapes + +We'll continue with the collection marshaller shapes. These marshaller shapes support marshalling the structure of a collection of values, where the values themselves are marshalled with marshallers of their own (using the marshaller provided in the `ElementMarshallerAttribute`). This construction allows us to compose our marshallers and to easily support arrays of custom types without needing to implement a separate marshaller for each element type. + +Each of these shapes will support marshalling the following type: + +```csharp +// Any number of generic parameters is allowed, with any constraints +struct TCollection +{ + // ... +} +``` + +A collection marshaller for a managed type will have similar generics handling as the value marshaller case; however, there is one difference. A collection marshaller must have an additional generic parameter with the `ElementUnmanagedTypeAttribute`. This parameter can optionally be constrained to `: unmanaged` (but the system will not require this). The attributed parameter will be filled in with a generics-compatible representation of the unmanaged type for the collection's element type (`nint` will be used when the native type is a pointer type). + +The type `TNative` can be any `unmanaged` type. It represents whatever unmanaged type the marshaller marshals the managed type to. + + +### Stateless Managed->Unmanaged + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public static class ManagedToNative + { + public static TNative AllocateContainerForUnmanagedElements(TCollection managed, out int numElements); // Can throw exceptions + + public static ReadOnlySpan GetManagedValuesSource(TCollection managed); // Can throw exceptions + + public static Span GetUnmanagedValuesDestination(TNative nativeValue, int numElements); // Can throw exceptions + + public static ref TOther GetPinnableReference(TManaged managed); // Optional. Can throw exceptions. Result pinnned and passed to Invoke. + + public static void Free(TNative unmanaged); // Optional. Should not throw exceptions + } +} + +``` +### Stateless Managed->Unmanaged with Caller-Allocated Buffer + +The element type of the `Span` for the caller-allocated buffer can be any type that guarantees any alignment requirements, including `TUnmanagedElement`. + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + [CustomTypeMarshallerFeatures(BufferSize = 0x200)] + public static class ManagedToNative + { + public static TNative AllocateContainerForUnmanagedElements(TCollection managed, Span buffer, out int numElements); // Can throw exceptions + + public static ReadOnlySpan GetManagedValuesSource(TCollection managed); // Can throw exceptions + + public static Span GetUnmanagedValuesDestination(TNative nativeValue, int numElements); // Can throw exceptions + + public static ref TOther GetPinnableReference(TManaged managed); // Optional. Can throw exceptions. Result pinnned and passed to Invoke. + + public static void Free(TNative unmanaged); // Optional. Should not throw exceptions + } +} + +``` + +### Stateless Unmanaged->Managed + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public static class NativeToManaged + { + public static TCollection AllocateContainerForManagedElements(int length); // Can throw exceptions + + public static Span GetManagedValuesDestination(T[] managed) => managed; // Can throw exceptions + + public static ReadOnlySpan GetUnmanagedValuesSource(TNative nativeValue, int numElements); // Can throw exceptions + + public static void Free(TNative native); // Optional. Should not throw exceptions. + } +} + +``` + +### Stateless Unmanaged->Managed with Guaranteed Unmarshalling + +This shape directs the generator to emit the `ConvertToManagedGuaranteed` call in the "GuaranteedUnmarshal" phase of marshalling. + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public static class NativeToManaged + { + public static TCollection AllocateContainerForManagedElementsGuaranteed(int length); // Should not throw exceptions other than OutOfMemoryException. + + public static Span GetManagedValuesDestination(T[] managed) => managed; // Can throw exceptions + + public static ReadOnlySpan GetUnmanagedValuesSource(TNative nativeValue, int numElements); // Can throw exceptions + + public static void Free(TNative native); // Optional. Should not throw exceptions. + } +} + +``` + +### Stateless Bidirectional +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), RefMarshaller = typeof(Bidirectional))] +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), RefMarshaller = typeof(Bidirectional))] +[ElementMarshaller(typeof(TManaged<,,,...>), typeof(Bidirectional))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public static class Bidirectional + { + // Include members from each of the following: + // - One Stateless Managed->Unmanaged Linear Collection shape + // - One Stateless Unmanaged->Managed Linear Collection shape + } +} + +``` + +### Stateful Managed->Unmanaged + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public struct ManagedToNative // Can be ref struct + { + public ManagedToNative(); // Optional, can throw exceptions. + + public void FromManaged(TCollection collection); // Can throw exceptions. + + public ReadOnlySpan GetManagedValuesSource(); // Can throw exceptions. + + public Span GetNativeValuesDestination(); // Can throw exceptions. + + public ref TIgnored GetPinnableReference(); // Optional. Can throw exceptions. + + public TNative ToUnmanaged(); // Can throw exceptions. + + public static ref TOther GetPinnableReference(TCollection collection); // Optional. Can throw exceptions. Result pinnned and passed to Invoke. + + public void NotifyInvokeSucceeded(); // Optional. Should not throw exceptions. + } +} + +``` +### Stateful Managed->Unmanaged with Caller Allocated Buffer + +The element type of the `Span` for the caller-allocated buffer can be any type that guarantees any alignment requirements. + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(ManagedToNative))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(ManagedToNative))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + [CustomTypeMarshallerFeatures(BufferSize = 0x200)] + public struct ManagedToNative // Can be ref struct + { + public ManagedToNative(); // Optional, can throw exceptions. + + public void FromManaged(TCollection collection, Span buffer); // Can throw exceptions. + + public ReadOnlySpan GetManagedValuesSource(); // Can throw exceptions. + + public Span GetNativeValuesDestination(); // Can throw exceptions. + + public ref TIgnored GetPinnableReference(); // Optional. Can throw exceptions. + + public TNative ToUnmanaged(); // Can throw exceptions. + + public static ref TOther GetPinnableReference(TCollection collection); // Optional. Can throw exceptions. Result pinnned and passed to Invoke. + + public void NotifyInvokeSucceeded(); // Optional. Should not throw exceptions. + } +} + +``` + +### Stateful Unmanaged->Managed + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public struct NativeToManaged // Can be ref struct + { + public NativeToManaged(); // Optional, can throw exceptions. + + public void FromUnmanaged(TNative value); // Should not throw exceptions. + + public ReadOnlySpan GetNativeValuesSource(int length); // Can throw exceptions. + + public Span GetManagedValuesDestination(int length); // Can throw exceptions. + + public TCollection ToManaged(); // Can throw exceptions + + public void Free(); // Optional. Should not throw exceptions. + } +} + +``` + +### Stateful Unmanaged->Managed with Guaranteed Unmarshalling + +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), OutMarshaller = typeof(NativeToManaged))] +[UnmanagedToManagedMarshallers(typeof(TCollection<,,,...>), InMarshaller = typeof(NativeToManaged))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public struct NativeToManaged // Can be ref struct + { + public NativeToManaged(); // Optional, can throw exceptions. + + public void FromUnmanaged(TNative value); // Should not throw exceptions. + + public ReadOnlySpan GetNativeValuesSource(int length); // Can throw exceptions. + + public Span GetManagedValuesDestination(int length); // Can throw exceptions. + + public TCollection ToManagedGuaranteed(); // Can throw exceptions + + public void Free(); // Optional. Should not throw exceptions. + } +} + +``` + +### Stateful Bidirectional +```csharp +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), RefMarshaller = typeof(Bidirectional))] +[ManagedToUnmanagedMarshallers(typeof(TCollection<,,,...>), RefMarshaller = typeof(Bidirectional))] +static class TMarshaller where TUnmanagedElement : unmanaged +{ + public struct Bidirectional // Can be ref struct + { + // Include members from each of the following: + // - One Stateful Managed->Unmanaged Linear Collection shape + // - One Stateful Unmanaged->Managed Linear Collection shape + } +} +``` From 3753304c4443a10a96f508d20b2112c8d23a99ab Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 20 Jun 2022 12:03:04 -0700 Subject: [PATCH 2/5] Add comments about the optional methods --- .../UserTypeMarshallingV2.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md index f7db15659d2a3..e58c0c7cf0097 100644 --- a/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md +++ b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md @@ -693,3 +693,27 @@ static class TMarshaller w } } ``` + +## Optional Members In Shapes + +There's a few optional members in the above shapes. This section explains what these members do and why they're optional. + +### Free method + +The `Free` method on each shape supports releasing any unmanaged (or managed in the stateful shapes) resources. This method is optional as the `Free` method is required to be called in a `finally` clause and emitting a `try-finally` block with only method calls to empty methods puts a lot of stress on the JIT to inline all of the methods and realize that they are no-ops to remove the `finally` clause. Additionally, just having the `try-finally` block wrapping the main code can cause some de-optimizations. + +### NotifyInvokeSucceeded method + +This method is called after a stub successfully invokes the target code (unmanaged code in a P/Invoke scenario, managed code in a Reverse P/Invoke scenario). As this method would be called in a very large majority of cases in P/Invoke-style scenarios and has only limited utility (its main use is to provide a good place to call `GC.KeepAlive` that does not require a `try-finally` block), we decided to make it optional. + +### Instance GetPinnableReference method on stateful shapes + +The non-static `GetPinnableReference` method on stateful shapes is provided to enable pinning a managed value as part of the marshalling process. As some types don't have values that need to be pinned to help with marshalling and pinning has some overhead, this member is optional to make the overhead pay-for-play. + +### Static GetPinnableReference method + +The static GetPinnableReference method provides a mechanism to pin a managed value and pass down the pinned value directly to native code. This allows us to provide massive performance benefits and to match built-in interop semantics. Unlike the previous design that used the `GetPinnableReference` method on the managed type in some scenarios, this design allows the "interop" pinning rules to not match the easier-to-use `GetPinnableReference` instance method, which may have differing semantics (`Span` and arrays being a prime example here). As many types aren't marshallable via only pinning, the generator does not require this method on every marshaller. + +### `-Generated` method variants + +These method variants provide a mechanism for a marshaller to state that it needs to be called during the "Generated Unmarshal" phase in the `finally` block to ensure that resources are not leaked. This feature is required only by the SafeHandle marshaller, so it is an optional extension to the model instead of being a required feature. From 454107c446f81b36b8d24241282525d29f0e3a2c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 20 Jun 2022 22:17:58 -0700 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Aaron Robinson --- docs/design/libraries/LibraryImportGenerator/Pipeline.md | 2 +- docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md | 2 +- .../libraries/LibraryImportGenerator/StructMarshalling.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design/libraries/LibraryImportGenerator/Pipeline.md b/docs/design/libraries/LibraryImportGenerator/Pipeline.md index 4524eefd8180d..9533241c9ad29 100644 --- a/docs/design/libraries/LibraryImportGenerator/Pipeline.md +++ b/docs/design/libraries/LibraryImportGenerator/Pipeline.md @@ -106,7 +106,7 @@ try << Pinned Marshal >> << Invoke >> } - << Keep Alive >> + << Notify For Successful Invoke >> << Unmarshal Capture >> << Unmarshal >> } diff --git a/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md b/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md index b98c1f05e2eb3..d4dcae008de96 100644 --- a/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md +++ b/docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md @@ -2,7 +2,7 @@ As part of the exit criteria for the LibraryImportGenerator experiment, we have decided to introduce support for marshalling `System.Span` and `System.ReadOnlySpan` into the LibraryImportGenerator-generated stubs. This document describes design decisions made during the implementation of these marshallers. -> NOTE: These design docs are kept for historical purposes. The designs in this file are supersceded by the designs in [UserTypeMarshallingV2.md](UserTypeMarshallingV2.md). +> NOTE: These design docs are kept for historical purposes. The designs in this file are superseded by the designs in [UserTypeMarshallingV2.md](UserTypeMarshallingV2.md). ## Design 1: "Intrinsic" support for `(ReadOnly)Span` diff --git a/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md b/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md index 72966d339d0d0..bb0d151ebcbb6 100644 --- a/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md +++ b/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md @@ -4,7 +4,7 @@ As part of the new source-generated direction for .NET Interop, we are looking a These types pose an interesting problem for a number of reasons listed below. With a few constraints, I believe we can create a system that will enable users to use their own user-defined types and pass them by-value to native code. -> NOTE: These design docs are kept for historical purposes. The designs in this file are supersceded by the designs in [UserTypeMarshallingV2.md](UserTypeMarshallingV2.md). +> NOTE: These design docs are kept for historical purposes. The designs in this file are superseded by the designs in [UserTypeMarshallingV2.md](UserTypeMarshallingV2.md). ## Problems From 488b0e3e7fde89e8aebae607c3f9d66ee806f6cb Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 22 Jun 2022 16:48:59 -0700 Subject: [PATCH 4/5] Add a section showing usage of NativeMarshalling/MarshalUsing --- .../StructMarshalling.md | 2 +- .../UserTypeMarshallingV2.md | 53 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md b/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md index bb0d151ebcbb6..18fa309d3d3fe 100644 --- a/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md +++ b/docs/design/libraries/LibraryImportGenerator/StructMarshalling.md @@ -141,7 +141,7 @@ When these `CallerAllocatedBuffer` feature flag is present, the source generator Type authors can pass down the `buffer` pointer to native code by using the `TwoStageMarshalling` feature to provide a `ToNativeValue` method that returns a pointer to the first element, generally through code using `MemoryMarshal.GetReference()` and `Unsafe.AsPointer`. The `buffer` span must be pinned to be used safely. The `buffer` span can be pinned by defining a `GetPinnableReference()` method on the native type that returns a reference to the first element of the span. -### Determining if a type is doesn't need marshalling +### Determining if a type doesn't need marshalling For this design, we need to decide how to determine a type doesn't need to be marshalled and already has a representation we can pass directly to native code - that is, we need a definition for "does not require marshalling". We have two designs that we have experimented with below, and we have decided to go with design 2. diff --git a/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md index e58c0c7cf0097..c3ac59d011f05 100644 --- a/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md +++ b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md @@ -18,7 +18,7 @@ Here are some of the main feedback points on the previous design: The new design tries to address many of these concerns. -The new marshallers have a stateless shape and a stateful shape. Stateful shapes are (currently) not allowed in "element of a collection" scenarios as handling them is difficult today, but we may improve this in the future. The stateful shapes are described in the order in which the methods will be called. Additionally, by moving away from using construtors for part of the marshalling, we can simplify the exception-handling guidance as we will only ever have one marshaller instance per parameter and it will always be assigned to the local. +The new marshallers have a stateless shape and a stateful shape. Stateful shapes are (currently) not allowed in "element of a collection" scenarios as handling them is difficult today, but we may improve this in the future. The stateful shapes are described in the order in which the methods will be called. Additionally, by moving away from using constructors for part of the marshalling, we can simplify the exception-handling guidance as we will only ever have one marshaller instance per parameter and it will always be assigned to the local. Stateless shapes avoid the problems of maintaining state and will be the primarily used shapes (they cover 90+% of our scenarios). @@ -223,7 +223,7 @@ The examples below will also show which properties in each attribute support eac ## Value Marshaller Shapes -We'll start with the value marshaller shapes. These marshaller shapes support marshalling a single self-contained value. +We'll start with the value marshaller shapes. These marshaller shapes support marshalling a single value. Each of these shapes will support marshalling the following type: @@ -717,3 +717,52 @@ The static GetPinnableReference method provides a mechanism to pin a managed val ### `-Generated` method variants These method variants provide a mechanism for a marshaller to state that it needs to be called during the "Generated Unmarshal" phase in the `finally` block to ensure that resources are not leaked. This feature is required only by the SafeHandle marshaller, so it is an optional extension to the model instead of being a required feature. + +## Blittability + +To determine which types are blittable and which are not, we will be following [Design 2 in StructMarshalling.md](StructMarshalling.md#determining-if-a-type-doesnt-need-marshalling). + +## Using the marshallers + +To use these marshallers the user would apply either the `NativeMarshallingAttribute` attribute to their type or a `MarshalUsingAttribute` at the marshalling location (field, parameter, or return value) with a marshalling type matching the same requirements as `NativeMarshallingAttribute`'s marshalling type. + +The marshaller type must be an entry-point marshaller type as defined above and meet the following additional requirements: + +- The type must either be: + - Non-generic + - A closed generic + - An open generic with as many generic parameters with compatible constraints as the managed type (excluding up to one generic parameter with the `ElementUnmanagedTypeAttribute`) +- If used in `NativeMarshallingAttribute`, the type should be at least as visible as the managed type. + +Passing size info for parameters will be based to the [V1 design](SpanMarshallers.md#providing-additional-data-for-collection-marshalling) and the properties/fields on `MarshalUsingAttribute` will remain unchanged. + +Here are some examples of using these new marshaller shapes with the `NativeMarshallingAttribute` and the `MarshalUsingAttribute`. + +```csharp +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +[NativeMarshalling(typeof(HResultMarshaller))] +struct HResult +{ + private int hr; +} + +[ManagedToUnmanagedMarshallers(typeof(HResult), InMarshaller = typeof(HResultMarshaller), RefMarshaller = typeof(HResultMarshaller), OutMarshaller = typeof(HResultMarshaller))] +[UnmanagedToManagedMarshallers(typeof(HResult), InMarshaller = typeof(HResultMarshaller), RefMarshaller = typeof(HResultMarshaller), OutMarshaller = typeof(HResultMarshaller))] +[ElementMarshaller(typeof(HResult), typeof(HResultMarshaller))] +public static class HResultMarshaller +{ + public int ConvertToUnmanaged(HResult hr); + public HResult ConvertToManaged(int hr); +} + +public static class NativeLib +{ + [LibraryImport(nameof(NativeLib))] + public static partial HResult CountArrayElements( + [MarshalUsing(typeof(ArrayMarshaller<,>))] int[] array, // Unlike the V1 system, we'll allow open generics in the V2 system in MarshalUsing since there's an extra generic parameter that the user does not provide. + out int numElements); +} + +``` From 443fdc7cb297e7a270268c173b72b19cc9ad4647 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 22 Jun 2022 17:01:09 -0700 Subject: [PATCH 5/5] Update docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md Co-authored-by: Elinor Fung --- .../libraries/LibraryImportGenerator/UserTypeMarshallingV2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md index c3ac59d011f05..072a97ab18ee5 100644 --- a/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md +++ b/docs/design/libraries/LibraryImportGenerator/UserTypeMarshallingV2.md @@ -753,8 +753,8 @@ struct HResult [ElementMarshaller(typeof(HResult), typeof(HResultMarshaller))] public static class HResultMarshaller { - public int ConvertToUnmanaged(HResult hr); - public HResult ConvertToManaged(int hr); + public static int ConvertToUnmanaged(HResult hr); + public static HResult ConvertToManaged(int hr); } public static class NativeLib