Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add struct marshalling design doc. #45

Merged

Conversation

jkoritzinsky
Copy link
Member

Add a design doc explaining the approach we've discussed for struct marshalling.

@jkoritzinsky jkoritzinsky added the area-DllImportGenerator Source Generated stubs for P/Invokes in C# label Aug 4, 2020

public class NativeStructMarshallingAttribute : Attribute
{
public NativeStructMarshallingAttribute(Type nativeStructType, string managedToNativeMarshalMethod, string nativeToManagedMarshalMethod, string nativeFreeMethod) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth the complexity to make the method names configurable? Can the method names follow a convention instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we make the convention well documented so people writing manual marshalling code can easily opt in to this system (and validate that they have it set up correctly via an analyzer), I am totally fine making them follow a convention.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have an interface that needs to be implemented here or perhaps some kind of registration of function pointer contract.

Is it worth the complexity to make the method names configurable?

What complexity do you foresee with this approach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have an interface that needs to be implemented here or perhaps some kind of registration of function pointer contract.

Both interfaces or function pointers are extra indirections. I think they would just get into way for generating efficient marshalers.

What complexity do you foresee with this approach?

The configurable names make the sources more verbose, but I do not see the value that they add.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra indirections are a concern for me as well. I have measured the ICustomMarshaler overheard before and am concerned about how we could improve that. I am also conflicted by success of use. I know this is an advanced feature so that argument is fair. The other side of that is longevity that I want to ensure provides some intuitive sense to adapt and grow without becoming an incrementally designed and free form mess (i.e. support matrix of sadness).

Agreed upon convention is fine but not for naming in my experience because it embeds current thinking and doesn't typically allow much room for change. I don't mind the above in the sense the supplied string represents a method to call that has a defined signature and we insert that string into a format: "{ret} = {USER_SUPPLIED}({args});". If it compiles great, otherwise the supplied function needs to be "fixed".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright I am becoming less concerned about this. Our support assembly could contain a collection of these "NativeType"s and this would allow interop library authors the flexibility to mix/compose how they want to marshal the managed type. This scales pretty well as we can fold popular "NativeType"s into the support assembly. Is that the path you are seeing here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this can be one of the paths that this enables.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think simply matching by name alone is good enough. Depending on the exact scenario you may have helper methods or other members available on a type that aren't necessarily meant for use in marshaling.

IAMDevMemoryAllocator in particular has a method named Free(), as do several other types

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it needs to be opt-in in some fashion, likely by matching some attribute + signature to ensure that the correct thing always happens.

Copy link
Member

@jkotas jkotas Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is opt-in by having dedicated NativeType that contains the marshalling methods. This type is different from your mainstream type, and so there should not be risk of collisions.


The P/Invoke source generator (as well as the struct source generator when nested struct types are used) would use the `BlittableTypeAttribute` and `NativeStructMarshallingAttribute` to determine how to marshal a value type parameter or field instead of looking at the fields of the struct directly.

The largest problem of this design is the switch to an opt-in model for structure types. We could augment this design by providing a fallback to the built-in struct marshaling system via an attribute (either `MarshalAsAttribute` or another one) if we believe providing an explicit escape hatch is needed.
Copy link
Member

@jkotas jkotas Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This design assumes that there is one and only right way to marshal everything.

Maybe we should allow specifying the native type to use for marshalling in the PInvoke signature. Something like:

[DllImport(...)]
static void foo([MarshalUsing(MyNativeType)] TypeThatIDoNotOwn a);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to me that it would make the associated functions disjoint from the alloc/free methods implied on the NativeStructMarshallingAttribute type above. Is that intentional?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best motivating scenario for this is to allow marshalling of non-blittable types that are owned by somebody else (I have edited my example to reflect it.)

I expect that associated functions would be found by convention on MyNativeType in this case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. With this argument the BlittableTypeAttribute type becomes less interesting because if the native type is the managed type that could be interpreted as "Blittable". Right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. BlittableTypeAttribute can be encoded as NativeStructMarshallingAttribute pointing to self.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not be a good idea given the method name collisions that it can introduce (see the other comment).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be ok with encoding blittability as a NativeStructMarshallingAttribute pointing to self since in the blittable case we wouldn't need to use the marshalling methods (and we can't for cases where we pin).

Copy link
Member

@AaronRobinsonMSFT AaronRobinsonMSFT Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not be a good idea given the method name collisions that it can introduce (see the other comment).

Name collisions on what? The supplied "NativeType" would be a new "transport" type simply for the moving between managed/unmanaged.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to #45 (comment).

For example, let's say that we want mark the existing GCHandle type as blittable.

If it is done via [BlittableType], it is clear what it means. No ambiguity. This type is always passed through without any marshaling.

If it is done via [NativeStructMarshalling(typeof(GCHandle)], it is less clear. For example, should GCHandle.Free participate in marshaling or not, assuming Free method is part of the convention.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is done via [NativeStructMarshalling(typeof(GCHandle)], it is less clear. For example, should GCHandle.Free participate in marshaling or not, assuming Free method is part of the convention.

Agreed that is a concern - that was part of my push back when talking about convention. However, it presupposes the need to use existing types as "Transfer types" - I like this term because it describes their purpose. For example, GCHandle isn't a transfer type because it already has a purpose and isn't something we would be likely to express on the unmanaged side of the boundary - we can debate that. If it is what we want to reflect on both side, then it is blittable so the collision wouldnt come into play.

Alternatively we could expand the concept mentioned above by merging it with @jkoritzinsky's original proposal.

struct MyNameType
{
  // Pre-existing functions on the type
  public static MyNameType Alloc() { ... }
  public static long Free() { ... }

  // Support for DllImport source generator
  // N.B. Signatures are for illustrative purposes only.
  public static IntPtr MyAlloc() { ... }
  public static void MyFree(IntPtr a) { ... }
}

static void foo([MarshalUsing(MyNativeType, Alloc = nameof(MyNameType.MyAlloc), Free = nameof(MyNameType.MyFree))] TypeThatIDoNotOwn a);

This allows us by default to have a strongly typed explicit convention or in the cases where it doesn't work the user can say "Use this instead". I genuinely like the merging of the two ideas.


The user may want to manually mark their types as marshalable 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 `BlittableTypeAttribute` or the `NativeStructMarshallingAttribute` attribute to their struct type. An analyzer would validate that the struct is blittable if the `BlittableTypeAttribute` is applied or validate that the native struct type is blittable and that the methods provided in the attribute have the correct signatures when the `NativeStructMarshallingAttribute` is applied.

The P/Invoke source generator (as well as the struct source generator when nested struct types are used) would use the `BlittableTypeAttribute` and `NativeStructMarshallingAttribute` to determine how to marshal a value type parameter or field instead of looking at the fields of the struct directly.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if the type was marked Blittable but the type contained a non-blittable field?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two main concerns here are:

  1. Users can write their own IL
  2. Users can disable analyzer warnings

In both scenarios, you could have struct S { int _field; } in the reference assembly but struct S { bool _x; } in the implementation.
There are also odd scenarios where the user changes the backing field without a dependent assembly recompiling (which would be a breaking change, but which is still something that may need consideration).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user explicitly marks it as blittable, then the analyzer would produce an error. Otherwise, if the type is marked with [GeneratedMarshalling], it will only have the BlittableTypeAttribute attribute when it is blittable.

If someone disables the analyzer and applies BlittableTypeAttribute manually (which is very much a "don't do this"/unsupported scenario), then we will not generate any marshalling code and behavior at native boundaries would fall into unspecified behavior. (The P/Invoke source generator would currently fall back to the built-in system based on it's implementation)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two main concerns here are:

  1. Users can write their own IL
  2. Users can disable analyzer warnings

I think these are fair concerns, but I don't think they change much.

Anyone writing their own IL is in a very advanced scenario and this isn't something I think we can solve or address. That being said, I am very much open for making an attempt additionally I could be misunderstanding the concern here.

Disabling analyzer warnings is perfectly acceptable and that is the prerogative of any developer. If the developer knows better then the compiler/analyzer they are okay going it on their own and if the runtime crashes in an interop call that is on them. Consider the case of "use prior to initialize" in C/C++; if the warning was on the error would have been flagged and could have been corrected. Disabling the warning is on the developer.

The rebuttal to that is the case where .NET should be designed to help users avoid those mistakes and in general I agree. However, this is already an advanced scenario so I would prefer to focus on making analyzers avoid false positives for incorrect use - not noisy - and enable experts to produce high quality AOT friendly interop libraries that users can consume so very few need to actually author P/Invokes - see dotnet/pinvoke.


The user may want to manually mark their types as marshalable 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 `BlittableTypeAttribute` or the `NativeStructMarshallingAttribute` attribute to their struct type. An analyzer would validate that the struct is blittable if the `BlittableTypeAttribute` is applied or validate that the native struct type is blittable and that the methods provided in the attribute have the correct signatures when the `NativeStructMarshallingAttribute` is applied.

The P/Invoke source generator (as well as the struct source generator when nested struct types are used) would use the `BlittableTypeAttribute` and `NativeStructMarshallingAttribute` to determine how to marshal a value type parameter or field instead of looking at the fields of the struct directly.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the expected extensibility model, if any, for things which can't currently be surfaced to the runtime today?

For example, the "TransparentStruct" discussion we had previously, where you want to say "I have a struct, but you should pass it like the underlying T instead" because I really just want a convenience API.

or as we start bringing onboard Java (Android) and Objective-C (iOS) support; specific ABI concepts that can't be represented today such as differentiating a C++03 POD type which may have different behavior than a non POD type.

Copy link
Member

@jkotas jkotas Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TransparentStruct can be implemented using optional Value property on NativeType. If the source generator detected presence of Value property on the NativeType, it would use the value of this property for the inner call instead of the NativeType itself. It only works if the type is constant (ie it would not work well for CLong).

The low level ABI concepts would have to be built into the runtime, or we would need to generate a helper C/C++/Objective-C library to implement them on the unmanaged side. I do not think that this proposal helps with that.


- Usage 1, Source-generated interop:

The user can apply the `GeneratedMarshallingAttribute` to their structure `S`. The source generator will determine if the type is blittable. If it is blittable, the source generator will generate a partial definition and apply the `BlittableTypeAttribute` to the struct type `S`. Otherwise, it will generate a blittable representation of the struct and apply the `NativeStructMarshallingAttribute` and point it to the blittable representation, a method that marshals the managed value to the native value, a method that marshals a value of the native struct to a managed value, and a method that frees any native resources that were allocated for the native value. The blittable representation can either be generated as a separate top-level type or as a nested type on `S`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the functions, would there be any "new APIs" for doing low overhead malloc/free like operations? We have AllocHGlobal today, but it is fairly expensive (comparatively) and it would be nice to be able to do a simple "allocate native mem", "marshal data", "free native mem"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like an independent proposal / issue to me. AllocHGlobal has some unnecessary overhead (in particular on Unix). It is something to fix first.

@jkoritzinsky
Copy link
Member Author

I've updated the proposal with a few attribute renames and removing the customizability of the native marshalling method names. I've also added a description and a few examples using the "transparent struct" support Jan suggested with the Value property, which actually ended up making this design extremely powerful and versatile while adding minimal complexity.

Signed-off-by: Jeremy Koritzinsky <[email protected]>
public TNative(TManaged managed)
{}

public TManaged ToManaged() {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: constructor + ToManaged method are asymmetric style-wise. Maybe it should be constructor + conversion operator; or FromManaged + ToManaged methods.

We can easily support multiple styles if needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if I like using a conversion operator because it makes the IDE experience slightly worse and is a little harder to follow. If we go the FromManaged/ToManaged route, we'd have to decide if we want to use mutable structs and instance methods or use static methods and don't use mutable structures.

@jkoritzinsky jkoritzinsky merged commit e50c8c2 into dotnet:DllImportGenerator Aug 17, 2020
@jkoritzinsky jkoritzinsky deleted the StructMarshallingDesign branch August 17, 2020 16:54
scalablecory pushed a commit that referenced this pull request Sep 22, 2020
jkoritzinsky added a commit to jkoritzinsky/runtime that referenced this pull request Sep 20, 2021
MichalStrehovsky pushed a commit to MichalStrehovsky/runtimelab that referenced this pull request Oct 15, 2021
Fully compare ldstr and also compare any tokens in an instruction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-DllImportGenerator Source Generated stubs for P/Invokes in C#
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants