Skip to content

Latest commit

 

History

History
382 lines (277 loc) · 25.9 KB

StructMarshalling.md

File metadata and controls

382 lines (277 loc) · 25.9 KB

Struct Marshalling

As part of the new source-generated direction for .NET Interop, we are looking at various options for supporting marshaling user-defined struct types.

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 superseded by the designs in UserTypeMarshallingV2.md.

Problems

  • What types require marshalling and what types can be passed as-is to native code?
    • Unmanaged vs Blittable
      • The C# language (and Roslyn) do not have a concept of "blittable types". It only has the concept of "unmanaged types", which is similar to blittable, but differs for bools and chars. bool and char types are "unmanaged", but are never (in the case of bool), or only sometimes (in the case of char) blittable. As a result, we cannot use the "is this type unmanaged" check in Roslyn for structures without an additional mechanism provided by the runtime.
  • Limited type information in ref assemblies.
    • In the ref assemblies generated by dotnet/arcade's GenAPI (used in dotnet/runtime), we save space and prevent users from relying on private implementation details of structures by emitting limited information about their fields. Structures that have at least one non-object field are given a private int field, and structures that have at least one field that transitively contains an object are given one private object-typed field. As a result, we do not have full type information at code-generation time for any structures defined in the BCL when compiling a library that uses the ref assemblies.
  • Private reflection
    • Even when we do have information about all of the fields, we can't emit code that references them if they are private, so we would have to emit unsafe code and calculate offsets manually to support marshaling them.

Opt-in Interop

We've been working around another problem for a while in the runtime-integrated interop design: The user can use any type that is non-auto layout and has fields that can be marshaled in interop. This has lead to various issues where types that were not intended for interop usage became usable and then we couldn't update their behavior to be special-cased since users may have been relying on the generic behavior (Span<T>, Vector<T> come to mind).

I propose an opt-in design where the owner of a struct has to explicitly opt-in to usage for interop. This enables our team to add special support as desired for various types such as Span<T> while also avoiding the private reflection and limited type information issues mentioned above.

All design options would use these attributes:

[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)]
public sealed class GeneratedMarshallingAttribute : Attribute {}

[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)]
public sealed class NativeMarshallingAttribute : Attribute
{
     public NativeMarshallingAttribute(Type nativeType) {}
}

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Field)]
public sealed class MarshalUsingAttribute : Attribute
{
     public MarshalUsingAttribute(Type nativeType) {}
}

[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 enum CustomTypeMarshallerKind
{
     Value
}

[Flags]
public enum CustomTypeMarshallerFeatures
{
     None = 0,
     /// <summary>
     /// The marshaller owns unmanaged resources that must be freed
     /// </summary>
     UnmanagedResources = 0x1,
     /// <summary>
     /// The marshaller can use a caller-allocated buffer instead of allocating in some scenarios
     /// </summary>
     CallerAllocatedBuffer = 0x2,
     /// <summary>
     /// The marshaller uses the two-stage marshalling design for its <see cref="CustomTypeMarshallerKind"/> instead of the one-stage design.
     /// </summary>
     TwoStageMarshalling = 0x4
}
[Flags]
public enum CustomTypeMarshallerDirection
{
     /// <summary>
     /// No marshalling direction
     /// </summary>
     [EditorBrowsable(EditorBrowsableState.Never)]
     None = 0,
     /// <summary>
     /// Marshalling from a managed environment to an unmanaged environment
     /// </summary>
     In = 0x1,
     /// <summary>
     /// Marshalling from an unmanaged environment to a managed environment
     /// </summary>
     Out = 0x2,
     /// <summary>
     /// Marshalling to and from managed and unmanaged environments
     /// </summary>
     Ref = In | Out,
}

The NativeMarshallingAttribute and MarshalUsingAttribute attributes would require that the provided native type TNative is a struct that does not require any marshalling and has the CustomTypeMarshallerAttribute with the first parameter being a typeof() of the managed type (with the managed type named TManaged in this example), an optional CustomTypeMarshallerKind, CustomTypeMarshallerDirection, and optional CustomTypeMarshallerFeatures:

[CustomTypeMarshaller(typeof(TManaged), CustomTypeMarshallerKind.Value, Direction = CustomTypeMarshallerDirection.Ref, Features = CustomTypeMarshallerFeatures.None)]
partial struct TNative
{
     public TNative(TManaged managed) {}
     public TManaged ToManaged() {}
}

If the attribute specifies the Direction is either In or Ref, then the example constructor above must be provided. If the attribute specifies the Direction is Out or Ref, then the ToManaged method must be provided. If the Direction property is unspecified, then it will be treated as if the user provided CustomTypeMarshallerDirection.Ref. The analyzer will report an error if the attribute provides CustomTypeMarshallerDirection.None, as a marshaller that supports no direction is unusable.

If the attribute provides the CustomTypeMarshallerFeatures.UnmanagedResources flag to the Features property then a void-returning parameterless instance method name FreeNative must be provided. This method can be used to release any non-managed resources used during marshalling.

❓ Does this API surface and shape work for all marshalling scenarios we plan on supporting? It may have issues with the current "layout class" by-value [Out] parameter marshalling where the runtime updates a class typed object in place. We already recommend against using classes for interop for performance reasons and a struct value passed via ref or out with the same members would cover this scenario.

Performance features

Pinning

Since C# 7.3 added a feature to enable custom pinning logic for user types, we should also add support for custom pinning logic. If the user provides a GetPinnableReference method on the managed type that matches the requirements to be used in a fixed statement and the pointed-to type would not require any additional marshalling, then we will support using pinning to marshal the managed value when possible. The analyzer should issue a warning when the pointed-to type would not match the final native type, accounting for any optional features used by the custom marshaller type. Since MarshalUsingAttribute is applied at usage time instead of at type authoring time, we will not enable the pinning feature since the implementation of GetPinnableReference is likely designed to match the default marshalling rules provided by the type author, not the rules provided by the marshaller provided by the MarshalUsingAttribute.

Caller-allocated memory

Custom marshalers of collection-like types or custom string encodings (such as UTF-32) may want to use stack space for extra storage for additional performance when possible. If the [CustomTypeMarshaller] attribute sets the Features property to value with the CustomTypeMarshallerFeatures.CallerAllocatedBuffer flag, then TNative type must provide additional constructor with the following signature and set the BufferSize field on the CustomTypeMarshallerAttribute. It will then be opted-in to using a caller-allocated buffer:

[CustomTypeMarshaller(typeof(TManaged), BufferSize = /* */, Features = CustomTypeMarshallerFeatures.CallerAllocatedBuffer)]
partial struct TNative
{
     public TNative(TManaged managed, Span<byte> buffer) {}
}

When these CallerAllocatedBuffer feature flag is present, the source generator will call the two-parameter constructor with a possibly stack-allocated buffer of BufferSize bytes when a stack-allocated buffer is usable. Since a dynamically allocated buffer is not usable in all scenarios, for example Reverse P/Invoke and struct marshalling, a one-parameter constructor must also be provided for usage in those scenarios.

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 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.

Design 1: Introducing BlittableTypeAttribute

Traditionally, the concept of "does not require marshalling" is referred to as "blittable". The built-in runtime marshalling system has an issue as mentioned above that the concept of unmanaged is not the same as the concept of blittable. Additionally, due to the ref assembly issue above, we cannot rely on ref assemblies to have accurate information in terms of fields of a type. To solve these issues in combination with the desire to enable manual interop, we need to provide a way for users to signal that a given type should be blittable and that the source generator should not generate marshalling code. We'll introduce a new attribute, the BlittableTypeAttribute:

[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)]
public class BlittableTypeAttribute : Attribute {}

I'll give a specific example for where we need the BlittableTypeAttribute below. Let's take a scenario where we don't have BlittableTypeAttribute.

In Foo.csproj, we have the following types:

public struct Foo
{
     private bool b;
}

public struct Bar
{
     private short s;
}

We compile these types into an assembly Foo.dll and we want to publish a package. We decide to use infrastructure similar to dotnet/runtime and produce a ref assembly. The ref assembly will have the following types:

struct Foo
{
     private int dummy;
}
struct Bar
{
     private int dummy;
}

We package up the ref and impl assemblies and ship them in a NuGet package.

Someone else pulls down this package and writes their own struct type Baz1 and Baz2:

struct Baz1
{
     private Foo f;
}
struct Baz2
{
     private Bar b;
}

Since the source generator only sees ref assemblies, it would think that both Baz1 and Baz2 are blittable, when in reality only Baz2 is blittable. This is the ref assembly issue mentioned above. The source generator cannot trust the shape of structures in other assemblies since those types may have private implementation details hidden.

Now let's take this scenario again with BlittableTypeAttribute:

[BlittableType]
public struct Foo
{
     private bool b;
}

[BlittableType]
public struct Bar
{
     private short s;
}

This time, we produce an error since Foo is not blittable. We need to either apply the GeneratedMarshalling attribute (to generate marshalling code) or the NativeMarshallingAttribute attribute (so provide manually written marshalling code) to Foo. This is also why we require each type used in interop to have either a [BlittableType] attribute or a [NativeMarshallingAttribute] attribute; we can't validate the shape of a type not defined in the current assembly because its shape may be different between its reference assembly and the runtime type.

Now there's another question: Why we can't just say that a type with [GeneratedMarshalling] and not [NativeMarshallingAttribute] has been considered blittable?

We don't want to require usage of [GeneratedMarshalling] to mark that a type is blittable because then there is no way to enforce that the type is blittable. If we require usage of [GeneratedMarshalling], then we will automatically generate marshalling code if the type is not blittable. By also having the [BlittableType] attribute, we enable users to mark types that they want to ensure are blittable and an analyzer will validate the blittability.

Basically, the design of this feature is as follows:

At build time, the user can apply either [GeneratedMarshallling], [BlittableType], or [NativeMarshallingAttribute]. If they apply [GeneratedMarshalling], then the source generator will run and generate marshalling code as needed and apply either [BlittableType] or [NativeMarshallingAttribute]. If the user manually applies [BlittableType] or [NativeMarshallingAttribute] instead of [GeneratedMarshalling], then an analyzer validates that the type is blittable (for [BlitttableType]) or that the marshalling methods and types have the required shapes (for [NativeMarshallingAttribute]).

When the source generator (either Struct, P/Invoke, Reverse P/Invoke, etc.) encounters a struct type, it will look for either the [BlittableType] or the [NativeMarshallingAttribute] attributes to determine how to marshal the structure. If neither of these attributes are applied, then the struct cannot be passed by value.

If someone actively disables the analyzer or writes their types in IL, then they have stepped out of the supported scenarios and marshalling code generated for their types may be inaccurate.

Exception: Generics

Because the Roslyn compiler needs to be able to validate that there are not recursive struct definitions, reference assemblies have to contain a field of a type parameter type in the reference assembly if they do in the runtime assembly. As a result, we can inspect private generic fields reliably.

To enable blittable generics support in this struct marshalling model, we extend [BlittableType] as follows:

  • In a generic type definition, we consider all type parameters that can be value types as blittable for the purposes of validating that [BlittableType] is only applied to blittable types.
  • When the source generator discovers a generic type marked with [BlittableType] it will look through the fields on the type and validate that they are blittable.

Since all fields typed with non-parameterized types are validated to be blittable at type definition time, we know that they are all blittable at type usage time. So, we only need to validate that the generic fields are instantiated with blittable types.

As an alternative design, we can use a different definition of "does not require marshalling". This design proposes changing the definition from "the runtime's definition of blittable" to "types considered unmanaged in C#". The DisableRuntimeMarshallingAttribute attribute helps us solve this problem. When applied to an assembly, this attribute causes the runtime to not do any marshalling for any types that are unmanaged types and do not have any auto-layout fields for all P/Invokes in the assembly; this includes when the types do not fit the runtime's definition of "blittable". This definition of "does not require marshalling" will work for all of our scenarios, with one issue listed below.

For the auto-layout clause, we have one small issue; today, our ref-assemblies do not expose if a value type is marked as [StructLayout(LayoutKind.Auto)], so we'd still have some cases where we might have runtime failures. However, we can update the tooling used in dotnet/runtime, GenAPI, to expose this information if we so desire. Once that case is handled, we have a mechanism that we can safely use to determine, at compile time, which types will not require marshalling. If we decide to not cover this case (as cases where users mark types as LayoutKind.Auto manually are exceptionally rare), we still have a solid design as Roslyn will automatically determine for us if a type is unmanaged, so we don't need to do any additional work.

As unmanaged is a C# language concept, we can use Roslyn's APIs to determine if a type is unmanaged to determine if it does not require marshalling without needing to define any new attributes and reshape the ecosystem. However, to enable this work, the LibraryImportGenerator, as well as any other source generators that generate calls to native code using the interop team's infrastructure, will need to require that the user applies the DisableRuntimeMarshallingAttribute to their assembly when non-trivial custom user-defined types are used. To help support users in this case, the interop team will provide a code-fix that will generate the DisableRuntimeMarshallingAttribute for users when they use the source generator. New codebases that adopt the source-generated interop world should immediately apply DisableRuntimeMarshallingAttribute.

Applying DisableRuntimeMarshallingAttribute can impact existing codebases depending on what interop APIs are used. Along with the potential difficultly in tracking down these places there is also an argument to made around the UX of the source-generator. Users are permitted to rely upon the runtime's definition of blittable and not apply DisableRuntimeMarshallingAttribute if all the types to be marshalled adhere to the following constraints, termed "strictly blittable" in code:

  1. Is always blittable (for example, int or double, but not char).
  2. Is a value type defined in the source project the current source generator is running on.
  3. Is a type composed of types adhering to (1) and (2).

For example, the following value type would require the consumer of the source generator to apply DisableRuntimeMarshallingAttribute if the above was not permitted.

struct S
{
    public short A;
    public short B;
}

Usage

There are 2 usage mechanisms of these attributes.

Usage 1, Source-generated interop

The user can apply the GeneratedMarshallingAttribute to their structure S. The source generator will determine if the type requires marshalling. If it does, it will generate a representation of the struct that does not require marshalling with the aforementioned required shape and apply the NativeMarshallingAttribute and point it to that new type. This generated representation can either be generated as a separate top-level type or as a nested type on S.

Usage 2, Manual interop

The user may want to manually mark their types as marshalable with custom marshalling rules in this system due to specific restrictions in their code base around marshaling specific types that the source generator does not account for. We could also use this internally to support custom types in source instead of in the code generator. In this scenario, the user would apply either the NativeMarshallingAttribute attribute to their struct type. An analyzer would validate that the native struct type does not require marshalling and has marshalling methods of the required shape when the NativeMarshallingAttribute is applied.

The P/Invoke source generator (as well as the struct source generator when nested struct types are used) would use the NativeMarshallingAttribute to determine how to marshal a parameter or field with an unknown type.

If a structure type does not meet the requirements to not require marshalling or does not have the NativeMarshallingAttribute applied at the type definition, the user can supply a MarshalUsingAttribute at the marshalling location (field, parameter, or return value) with a native type matching the same requirements as NativeMarshallingAttribute's native type.

All generated stubs will be marked with SkipLocalsInitAttribute on supported frameworks. This does require attention when performing custom marshalling as the state of stub allocated memory will be in an undefined state.

Special case: Transparent Structures

There has been discussion about Transparent Structures, structure types that are treated as their underlying types when passed to native code. The source-generated model supports this design through the TwoStageMarshalling feature flag on the CustomTypeMarshaller attribute.

[NativeMarshalling(typeof(HRESULT))]
struct HResult
{
     public HResult(int result)
     {
          Result = result;
     }
     public readonly int Result;
}

[CustomTypeMarshaller(typeof(HResult), Features = CustomTypeMarshallerFeatures.TwoStageMarshalling)]
struct HRESULT
{
     private HResult managed;
     public HRESULT(HResult hr)
     {
          managed = hr;
     }

     public HResult ToManaged() => managed;
     public int ToNativeValue() => managed.Result;
}

For the more detailed specification, we will use the example below:

[NativeMarshalling(typeof(TMarshaller))]
public struct TManaged
{
     // ...
}

[CustomTypeMarshaller(typeof(TManaged))]
public struct TMarshaller
{
     public TMarshaller(TManaged managed) {}
     public TManaged ToManaged() {}

     public ref T GetPinnableReference() {}

     public unsafe TNative* ToNativeValue();
     public unsafe void FromNativeValue(TNative*);
}

In this case, the underlying native type would actually be an int, but the user could use the strongly-typed HResult type as the public surface area.

If a type TMarshaller with the CustomTypeMarshaller attribute specifies the TwoStageMarshalling feature, then it must provide the ToNativeValue feature if it supports the In direction, and the FromNativeValue method if it supports the Out direction. The return value of the ToNativeValue method will be passed to native code instead of the TMarshaller value itself. As a result, the type TMarshaller will be allowed to require marshalling and the return type of the ToNativeValue method, will be required be passable to native code without any additional marshalling. When marshalling in the native-to-managed direction, a default value of TMarshaller will have the FromNativeValue method called with the native value. If we are marshalling a scenario where a single frame covers the whole native call and we are marshalling in and out, then the same instance of the marshller will be reused.

If the TwoStageMarshalling feature is specified, the developer may also provide a ref-returning or readonly-ref-returning GetPinnableReference method. The GetPinnableReference method will be called before the ToNativeValue method is called. The ref returned by GetPinnableReference will be pinned with a fixed statement, but the pinned value will not be used (it acts exclusively as a side-effect). As a result, GetPinnableReference can return a ref to any T that can be used in a fixed statement (a C# unmanaged type).

A ref or ref readonly typed ToNativeValue method is unsupported. If a ref-return is required, the type author can supply a GetPinnableReference method on the native type to pin the desired ref to return and then use System.Runtime.CompilerServices.Unsafe.AsPointer to get a pointer from the ref that will have already been pinned by the time the ToNativeValue method is called.

❓ Should we support transparent structures on manually annotated types that wouldn't need marshalling otherwise? If we do, we should do so in an opt-in manner to make it possible to have a ToNativeValue method on the type without assuming that it is for interop in all cases.

Example: ComWrappers marshalling with Transparent Structures

Building on this Transparent Structures support, we can also support ComWrappers marshalling with this proposal via the manually-decorated types approach:

[NativeMarshalling(typeof(ComWrappersMarshaler<Foo, FooComWrappers>), Features = CustomTypeMarshallerFeatures.UnmanagedResources | CustomTypeMarshallerFeatures.TwoStageMarshalling)]
class Foo
{}

struct FooComWrappersMarshaler
{
     private static readonly FooComWrappers ComWrappers = new FooComWrappers();

     private IntPtr nativeObj;

     public ComWrappersMarshaler(Foo obj)
     {
          nativeObj = ComWrappers.GetOrCreateComInterfaceForObject(obj, CreateComInterfaceFlags.None);
     }

     public IntPtr ToNativeValue() => nativeObj;

     public void FromNativeValue(IntPtr value) => nativeObj = value;

     public Foo ToManaged() => (Foo)ComWrappers.GetOrCreateObjectForComInstance(nativeObj, CreateObjectFlags.None);

     public unsafe void FreeNative()
     {
          ((delegate* unmanaged[Stdcall]<IntPtr, uint>)((*(void***)nativeObj)[2 /* Release */]))(nativeObj);
     }
}

This ComWrappers-based marshaller works with all ComWrappers-derived types that have a parameterless constructor and correctly passes down a native integer (not a structure) to native code to match the expected ABI.