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

Introduce CallConvSwift to represent the Swift ABI calling convention #64215

Closed
AaronRobinsonMSFT opened this issue Jan 24, 2022 · 44 comments · Fixed by #95065
Closed

Introduce CallConvSwift to represent the Swift ABI calling convention #64215

AaronRobinsonMSFT opened this issue Jan 24, 2022 · 44 comments · Fixed by #95065
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.InteropServices
Milestone

Comments

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jan 24, 2022

Background and Motivation

The Swift programming language has a different ABI, runtime environment, and object model, making it challenging to call from the .NET without the runtime support. Ideally, we want to avoid generating extra wrappers in the objection tools and attempt to directly call all kinds of Swift functions.

For interop developers of managed APIs wrapping native Swift API layers, reconciling the Swift ABI with the existing P/Invoke options is close to intractable. The proposal would be to add built-in support for the Swift ABI via P/Invokes and Reverse P/Invokes through UnmanagedCallConvAttribute. This would directly help out binding efforts in the MAUI space.

Proposed API

According to the calling convention, the self context has dedicated registers, and it is always passed through them since it's heavily used. Methods calling other methods on the same object can share the self context.

Here are cases when self context is passed via register:

  • Instance methods on class types: pointer to self
  • Class methods: pointer to type metadata (which may be subclass metadata)
  • Mutating method on value types: pointer to the value (i.e. value is passed indirectly)
  • Non-mutating methods on value types: self may fit in one or more registers, else passed indirectly
  • Thick closures, i.e. closures requiring a context: the closure context

Error handling is also handled through registers, so the caller needs to check for error and throw if necessary. Implementing P/Invoke thunks should simplify registers juggling by using predefined set of registers in these scenarios.

namespace System.Runtime.CompilerServices
{
+    public class CallConvSwift
+    {
+        public CallConvSwift() { }
+    }
}

Based on the design doc we want to introduce types to represent each of the special registers.

+ namespace System.Runtime.InteropServices.Swift
+ {
+     public readonly struct SwiftSelf
+     {
+         public SwiftSelf(IntPtr value)
+         {
+             Value = value;
+         }
+
+         public IntPtr Value { get; }
+     }
+
+     public readonly struct SwiftError
+     {
+         public SwiftError(IntPtr value)
+         {
+             Value = value;
+         }
+
+         public IntPtr Value { get; }
+     }
+ }

Usage Examples

This API would be used in the P/Invoke and Reverse P/Invoke syntax as follows:

using System.Runtime.InteropServices.Swift;

[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("SwiftLibrary", EntryPoint = "export")]
public unsafe static extern nint conditionallyThrowError(bool willThrow, SwiftError* error);

[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("SwiftLibrary", EntryPoint = "export")]
public static extern nint getMagicNumber(SwiftSelf self);

Using the CallConv* approach would also permit use in C# unmanaged function pointers. This would follow the Swift ABI during dispatch. Documentation for the Swift ABI can be found here and its design philosophy here.

/cc @dotnet/jit-contrib @dotnet/interop-contrib @Redth

@AaronRobinsonMSFT AaronRobinsonMSFT added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.InteropServices labels Jan 24, 2022
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Jan 24, 2022
@AaronRobinsonMSFT AaronRobinsonMSFT changed the title Introduce CallConvSwift to represent the Swift ABI calling convention Introduce CallConvSwift to represent the Swift ABI calling convention Jan 24, 2022
@Redth
Copy link
Member

Redth commented Jan 24, 2022

/cc @dalexsoto @rolfbjarne

@jkotas
Copy link
Member

jkotas commented Jan 24, 2022

What would be the differences between regular C x64/arm64 Apple calling conventions and the CallConvSwift calling conventions? (I have glanced over the documents at https://github.com/apple/swift/tree/main/docs/ABI and I do not see any obvious differences.)

@mandel-macaque
Copy link
Member

@jkotas the register usage is different AFAIK but I'm not an expert, I know because of @stephen-hawley who can give a lot of details.

@AaronRobinsonMSFT
Copy link
Member Author

@jkotas There is definitely some overlap, but there are also novel semantics we would need to deal with if we were support the ABI in the runtime. For example, the error register would be something we would need to respect when calling. How we responded to a non-zero value could vary but we would need to respond in some way – fail fast, convert to .NET exception, etc.

@jkotas
Copy link
Member

jkotas commented Jan 24, 2022

or example, the error register would be something we would need to respect when calling. How we responded to a non-zero value could vary

Agree, the error register looks special. Do all Swift methods set the error register or just some of them? Do we need additional APIs to configure how to deal with the errors returned in the error register?

@stephen-hawley
Copy link

Will this handle generic types and especially with regards to passing protocol witness tables?

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Jan 24, 2022

Yay! Thanks for joining @stephen-hawley. I created this issue to lure someone who has far more knowledge about the Swift ABI to hopefully chime in. @jkotas brings up a great question about how we would handle the contents of the error register, this will require understanding our options. It would also be interesting to think can users suggest or take over that responsibility from the runtime.

Will this handle generic types and especially with regards to passing protocol witness tables?

This is another excellent question. If I understand correctly, Swift has a fair number of features that C# doesn't have obvious mappings for. I assume the passing protocol witness tables is one of them. The desire of this API would be to help core and performance sensitive APIs, for example the native UI stack supporting MAUI. If this couldn't be used to help improve the MAUI experience, then I think it would be less compelling, at least in the near term. I'd defer to your thoughts on how much of the Swift ABI we would need and if "all of it" is the answer, then the answer to the above question would need to be "yes" I think.

Once we get the list of what we need to support our next step would be to ensure that we could represent it in C# and if not, we can start that ask. This issue is really about pushing the conversation and getting clarity in the open on needs both first party and from the community.

@stephen-hawley
Copy link

With regards to the error register, if you have a method in swift that looks like this:

public func foo () throws -> SomeType
{
}

If the method throws it will set the error register to the error. If it does not then the error register is zero and the return value is valid.

In Binding Tools for Swift, I work around this by writing an adapter to the method. For the example, the adapter looks like this:

public func adapt_foo(return: UnsafeMutablePointer<SomeType, Error, Bool>) 
{
    do {
      var return = try foo ()
      setExceptionNotThrown(value:return, retval:retval);
   } catch let err {
     setExceptionThrown(err:err, retval: retval);
   }
}

In addition, how will this handle closures, which in addition to the calling conventions will also have a dedicated register (the self register) which is used for accessing data bound by the closure? Will it also handle closures that are escaping and non-escaping?

I understand that you're looking at Maui, but I'm trying to get as close to 100% coverage to the swift language from C# as I can.

Do you have plans for handling the ARC in value types? For example, since swift has semantics for what happens to an object when it gets copied and destroyed, I can't model value types as C# value types, so instead I model them as a class implementing IDisposable with a byte array to opaque data and accessors to get at the elements. When I need to marshal them to swift, I always pass by reference and make a copy of the payload into a stack allocated array using the method in the value type to copy the data in a way that follows ARC (and do the reverse on the way back).

I have so many questions. In a good way.

@stephen-hawley
Copy link

With regards to protocol witness tables, these come into play in a couple of different contexts.
If I have this function in swift:

public func foo<T:SomeProtocol>(a: Int) {
}

This actually gets implemented as under the hood like this:

public func foo<T>(T:SomeProtocol, tType: T.Self, pwt: ProtocolWitnessTableForSomeProtocolWithRespectToT) {
}

In other words, for every generic type, there is an extra argument added in that is the swift equivalent of the C# Type object then for each protocol constraint, there is a protocol witness table pointer that is effectively the vtable for the protocol with respect to the generic type.

Where this gets particularly interesting is when I need to model a class with a generic virtual method:

open class SomeClass {
    public init () { }
    open func someMethod<T: SomeProtocol>(value: T) -> T {
    }
}

To model this, I write an adapter class that overrides each of the virtual methods, then vectors into C# through a table of function pointers that receive the arguments. The problem is that I need to pass in the type of T (easy, I can get it through T.Self, but I also need the protocol witness table which is not accessible since it's implicit. I handle this through a set of C functions that are written to take arguments, but I surface them in swift as taking no arguments. So that for any protocol, I can write the following in swift:

public func protocolWitnessTableOf<T:SomeProtocol>(t: T) -> OpaquePointer {
    return swiftAsmArg2(); // returns pwt
   // void *swiftAsmArg2(void *arg0, void *arg1, void *arg2) { return arg2; }
}

With regards to the task of calling virtual methods in swift, I'm torn on this. Virtual methods use a self pointer. I have to wrap any virtual method like this:

public func virtual_someMethodOnFoo(this: Foo, arg0: SomeValueType) {
    this.someMethod(arg0);
}

The problem with this is that someMethod puts the instance pointer into the self register (R13 IIRC on x64) and then moves arg0 into rsi. In the case of the adapter, this requires maximal registerjuggling.
I'd like to avoid this and dispatch through the class' vtable directly. Unfortunately, the ordering of the vtable is undocumented and as of Swift 5, in order to maintain forward compatibility, Apple has said outright that vtables are fragile and they won't use them in any caller from outside the class.

And if it helps, I have a lot of this already documented here.

Feel free to reach out with questions. For sure I think we should get our stories straight in terms of how to ensure that marshalling value types works. For example, all my mappings of value types implement the interface ISwiftValueType which is defined like this:

	public interface ISwiftValueType : IDisposable {
		byte [] SwiftData { get; set; }
	}

And all swift class types implement the interface ISwiftObject:

	public interface ISwiftObject : IDisposable {
		IntPtr SwiftObject { get; }
	}

It would be super nice if I could write a pinvoke for the previous example of a method wrapper as:

[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("SwiftLibrary", EntryPoint = "export")]
internal static void virtual_someMethodOnFoo(ISwiftObject foo, ISwiftValueType value); // or maybe I use the real types here?

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Jan 25, 2022

Do you have plans for handling the ARC in value types?
For sure I think we should get our stories straight in terms of how to ensure that marshalling value types works.

The proposal wouldn't be about enabling marshalling support in the same way we support marshalling a value type to a C/C++ struct. This would be about ensuring we place arguments in the correct registers and following calling convention semantics. The actual marshalling logic would be source generated – see DllImport generator. However, if we couldn't source generate marshalling code that would be an indication the runtime needs to provide something, but only after ensuring there was no source generation solution.

I think the best example of this would be the error register above. Are there others?

@AaronRobinsonMSFT
Copy link
Member Author

Feel free to reach out with questions.

Yes! That needs to happen. It is on my list, be prepared :-)

@AaronRobinsonMSFT AaronRobinsonMSFT removed the untriaged New issue has not been triaged by the area owner label Jan 30, 2022
@AaronRobinsonMSFT AaronRobinsonMSFT modified the milestones: 7.0.0, Future Jan 30, 2022
@jkoritzinsky
Copy link
Member

After looking at the Swift calling convention docs again and in particular at the "library evolution" support in Swift 5+, I think we may need to introduce 2 types here depending on if we want to support the "non-stable" Swift ABI (which has some perf benefits).

We'd likely need one type for the "stable/evolution" ABI and one for the "non-stable" ABI. The "stable" ABI would be best-used with dynamically linked libraries (like the system libraries), and the "non-stable" ABI would be best for libraries that users would link into their final apps (as they would not version separately from the managed code).

@jkotas
Copy link
Member

jkotas commented Jun 22, 2023

"non-stable" Swift ABI (which has some perf benefits).

Could you please share link to details?

@jkoritzinsky
Copy link
Member

I'll try to find more info, but the basics are here:

https://www.swift.org/blog/library-evolution/

The main differences are captured here in the differences between the @frozen and non-@frozen types. When passing --enable-library-evolution, quite a few changes are made to the ABI, including the below items:

  • structures are passed by reference, not by value (so no engregistration of structures)
  • all structure property accesses go through accessor methods instead of directly accessing the properties by offset into the property (or from the register directly if possible)

If a type is marked as @frozen, then all usages of it in an exposed public API will use the "non-stable" ABI (it's like our NonVersionableAttribute).

There's also some cases around generics in Swift that allow reducing the number of indirections when using the "non-stable" ABI, mainly focused around being able to invoke specific specializations when types are known instead of always needing to go through the abstracted signature with all of the witness tables.

The exhastive list of rules around the "stable/evolution" ABI vs the "non-stable" ABI are here:

https://github.com/apple/swift/blob/main/docs/LibraryEvolution.rst

I still haven't read through the whole thing yet, but sharing it now so everyone can see it.

@jkoritzinsky
Copy link
Member

More information about the changes made in the Swift calling convention for the "stable" ABI are found here: https://github.com/apple/swift/blob/main/docs/ABIStabilityManifesto.md#calling-convention

@jkoritzinsky
Copy link
Member

Looking at LLVM and the swift compiler, there are two calling conventions: one is used for regular swift calls, the other for async/await swift calls. The stable vs unstable ABI handling is at a higher layer. We can likely use a similar layering in our design and only have CallConvSwift for the stable and unstable ABIs (and track adding async/await support for Swift as a separate work item from regular Swift calls).

@AaronRobinsonMSFT
Copy link
Member Author

stable and unstable ABIs

We probably don't need the unstable for now or even in the longer run in most cases. Adding support for the unstable would be premature for now, since my understanding the unstable is for the "static" linking scenarios (i.e., internal). As an interop scenario the priority and focus should be on targeting the stable ABI unless we have a concrete and business need scenario - not simply a "nice to have".

@jkoritzinsky
Copy link
Member

I agree, we likely don't need the unsable ABI. In any case, LLVM uses the same calling convention for both ABIs (the ABI handling is above the callconv layer), so we shouldn't need to worry about that anyway in the workt to implement CallConvSwift. We likely do need to handle the "tail call" calling convention if we decide to support interop with Swift's async/await, but I think that's a larger feature that we don't need to do immediately.

@stephen-hawley
Copy link

To be clear, we need to use the library evolution code. In determining the front-facing ABI, we needed reflection on the swift types and I had originally hacked the swift compiler to generate that for me. The problem is that we had to keep the reflection going lock-step with the compiler because Apple broke the serialized AST format routinely. With library evolution, they generate a .swiftinterface file which is now a "standard" format and we parse that to get the reflection information. But library evolution is a must to get that.

I've been thinking over what would help me the most for the swift ABI to simplify my code and to improve the performance. The biggest one would be to have an attribute like [SwiftSelf] and be able to write a pinvoke like this:

[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("SwiftLibrary", EntryPoint = "export")]
static extern void SomeSwiftMethod ([SwiftSelf] NativeHandle instance, ...);

and the instance handle would get put into the swift "self" register. Previously, this wouldn't help since method dispatch is done through a vtable, but in library evolution, they create adapter thunks that do the vtable call for you.

The win here is that as written today, my pinvokes with generate code that does worst case register juggling, this change will have 0 register juggling.

Other things that would make the biggest wins:
auto-handling of value types - I keep these as an opaque array of bytes. Would love if the pinvoke handling could understand that ISwiftValueType has a byte array and will fill registers if its small enough or will use the value witness table to copy the contents to the stack and pass a pointer instead (and clean up on return). This is needed because if the payload contains a reference counted type, the reference needs to go up by one on entry and down by one on return.

handling of swift errors - these also go in their own registers. If I could write a pinvoke that included and argument like this: [SwiftErrorReturn] out SwiftError _error, then it would make handling errors so much easier.

Auto-splatting of tuples that are arguments - tuples as arguments in swift get flattened and passed as individual arguments. So if you do public func foo (a: (Int, Float, Bool, String)) you are passing 4 arguments.

I'm sure I'll add more to this issue as I think of it.

@rolfbjarne
Copy link
Member

Do you have plans for handling the ARC in value types?
For sure I think we should get our stories straight in terms of how to ensure that marshalling value types works.

The proposal wouldn't be about enabling marshalling support in the same way we support marshalling a value type to a C/C++ struct. This would be about ensuring we place arguments in the correct registers and following calling convention semantics. The actual marshalling logic would be source generated – see DllImport generator. However, if we couldn't source generate marshalling code that would be an indication the runtime needs to provide something, but only after ensuring there was no source generation solution.

I think there's another criteria we can add: we shouldn't need to generate any wrapping code on the Swift side either.

I also have a question about the scope for source generators: could source generators be required to know about the target CPU, or should the runtime hide differences between CPUs? Made-up example: say I have a composite value type of 4 intptrs - on CPU A that's passed in 4 different registers, while on CPU B that's passed as a pointer in a single register. A source generator could easily generate two different P/Invoke methods and call the right one at runtime with the arguments in the correct locations, but IMHO this is best handled by the runtime, even though it can technically be handled by a source generator.

@jkotas
Copy link
Member

jkotas commented Sep 20, 2023

IMHO this is best handled by the runtime, even though it can technically be handled by a source generator.

Yes, these details should be handled by the runtime. You example should not need two different P/Invoke methods.

@jkotas
Copy link
Member

jkotas commented Oct 27, 2023

Should all the structs be readonly?

@jkoritzinsky
Copy link
Member

I'll update them to be readonly.

@AaronRobinsonMSFT
Copy link
Member Author

My vote would be to place them all under System.Runtime.InteropServices.Swift, but I like including the "Register" term present. These will be a low level interop concept and calling some "Self" or "AsyncContext" is going to get us into trouble. That means option (1) is my vote.

I prefer ContextRegister compared to the "Self" term, but I defer to what the Swift community uses most often. /cc @stephen-hawley.

@kotlarmilos
Copy link
Member

kotlarmilos commented Oct 30, 2023

Looks good! I agree with Aaron, option 1 provides the complete context. I think projection tools would rely on managed pointers like IntPtr instead of void*. Would there be an overhead when converting between managed and unmanaged pointers?

At the runtime layer, our focus is on the Mono runtime support; let us know if you would like us to proceed with the InteropServices types PoC introduced in https://github.com/kotlarmilos/runtime/blob/poc/swift-interop-direct-pinvoke/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/SwiftTypes.cs. Otherwise, we keep focusing on the Mono runtime support :)

@jkotas
Copy link
Member

jkotas commented Oct 30, 2023

the "Register" term present.

These special values may or may not be passed in a (processor) register depending on the target ABI - see https://github.com/llvm/llvm-project/blob/a1b2ace137385388bf9bd7ea4b6df3ff298900f6/clang/lib/CodeGen/ABIInfo.h#L142C1-L143 . Calling it a register can be misleading since it won't always match reality.

We have included ObjectiveC in both type name and namespace name in ObjectiveC types:

  • System.Runtime.InteropServices.ObjectiveC.ObjectiveCTrackedTypeAttribute
  • System.Runtime.InteropServices.ObjectiveC.ObjectiveCMarshal

It was intentional change made during API review. #44659 (comment) says "We renamed TrackedNativeReferenceAttribute to ObjectiveCTrackedTypeAttribute".

Should we follow the established pattern?

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Oct 30, 2023

Calling it a register can be misleading since it won't always match reality.

I'm in favor of deferring to the Swift community and documentation rather than any implementation detail of LLVM. Compilers are notorious for naming things. See the two below from the Swift repo - we should chose one and reference that as the stated term. Perhaps looking at C++'s work in this area.

From the manifesto, it is "Call Context Register" - https://github.com/apple/swift/blob/main/docs/ABIStabilityManifesto.md#call-context-register

But from the calling convention doc it is simply "self" - https://github.com/apple/swift/blob/main/docs/ABI/CallingConvention.rst#self

We have included ObjectiveC in both type name and namespace name in ObjectiveC types:
Should we follow the established pattern?

I think that is appropriate. I will say it was considered such narrow, mostly backwards looking, support that prepending ObjectiveC wasn't too bad. Since Swift is forward looking perhaps erring on the side of brevity is warranted.

@jkotas
Copy link
Member

jkotas commented Oct 30, 2023

Perhaps looking at C++'s work in this area.

The low-level C/C++ - Swift interop looks like this: https://godbolt.org/z/W7YhcqhrM .

__attribute__((swiftcall)) void noop(__attribute__((swift_context)) void *unused, __attribute__((swift_error_result)) void **error)
{
    *error = 0;
}

They do not call it register. Also, notice that it is swift_error_result and not swift_error, and swift_context instead of swift_self. Should we match these names?

The original swift ABI manifesto calls it register to communicate the design idea. It is not up to date spec. It was not updated to match what was actually implemented.

brevity

The .NET Framework design guidelines say:

  • DO NOT be afraid to use verbose identifier names when doing so makes the API self-documenting.

@AaronRobinsonMSFT
Copy link
Member Author

They do not call it register. Also, notice that it is swift_error_result and not swift_error, and swift_context instead of swift_self. Should we match these names?

Yes, that would be my preference.

@AaronRobinsonMSFT
Copy link
Member Author

The low-level C/C++

To be fair these are arbitrary gcc/clang attributes. I'm good matching them but these are really specific to gcc/clang.

@stephen-hawley
Copy link

stephen-hawley commented Oct 30, 2023 via email

@kotlarmilos
Copy link
Member

kotlarmilos commented Nov 21, 2023

I have included the additional details specifying the types to represent each of the special registers into this issue. Please review it and update it accordingly before marking it as api-ready-for-review. Here is the PR that introduces the public API surface to the ref assemblies and to System.Private.CoreLib: #95065.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Nov 21, 2023
@AaronRobinsonMSFT AaronRobinsonMSFT added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Nov 21, 2023
@jkoritzinsky
Copy link
Member

I just realized that this issue is still marked as Future. Moving it to 9.0.

@jkoritzinsky jkoritzinsky modified the milestones: Future, 9.0.0 Dec 3, 2023
@kotlarmilos
Copy link
Member

In order to push forward the runtime support for the Swift calling convention we need approval for this issue and #95065. Does anyone need to be explicitly informed?

@jkotas jkotas added the blocking Marks issues that we want to fast track in order to unblock other important work label Dec 4, 2023
@jkotas
Copy link
Member

jkotas commented Dec 4, 2023

I have marked this issue as blocking. It should give it a priority for API review. Also, you can email @terrajobst to find out when it will be scheduled for API review.

@bartonjs
Copy link
Member

bartonjs commented Jan 9, 2024

Video

  • SwiftSelf and SwiftError were proposed as IntPtr Value, but we accepted changing them to void* Value.
namespace System.Runtime.CompilerServices
{
    public class CallConvSwift
    {
        public CallConvSwift() { }
    }
}

namespace System.Runtime.InteropServices.Swift
{
    public readonly unsafe struct SwiftSelf
    {
        public SwiftSelf(void* value) {
            Value = value;
        }

        public void* Value { get; }
    }

    public readonly unsafe struct SwiftError
    {
        public SwiftError(void* value) {
            Value = value;
        }

        public void* Value { get; }
    }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jan 9, 2024
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jan 10, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Feb 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.InteropServices
Projects
Archived in project
9 participants