Skip to content

Commit

Permalink
Remove generic SwiftSelf option and add additional option for the err…
Browse files Browse the repository at this point in the history
…or register based on more feedback and experimentation.
  • Loading branch information
jkoritzinsky committed Oct 23, 2023
1 parent 818d620 commit 8e170e4
Showing 1 changed file with 17 additions and 12 deletions.
29 changes: 17 additions & 12 deletions proposed/swift-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,37 +53,42 @@ Swift also provides a strong Objective-C interop story through the `@objc` attri

##### Self register

We have two options for supporting the Self register in the calling convention:
We have a few options for supporting the Self register in the calling convention:

1. Use an attribute like `[SwiftSelf]` on a parameter to specify it should go in the self register.
2. Specify that combining the `Swift` calling convention with the `MemberFunction` calling convention means that the first argument is the `self` argument.
3. Use a `SwiftSelf<T>` argument to represent which parameter should go into the self register.
3. Use a `SwiftSelf` argument to represent which parameter should go into the self register.

The first option seems like a natural fit, but there is one significant limitation: Attributes cannot be used in function pointers. Function pointers are a vital scenario for us to support as we want to support virtual method tables as they are used in scenarios like protocol witness tables. The second option is a natural fit as the `MemberFunction` calling convention combined with the various C-based calling conventions specifies that there is a `this` argument as the first argument. Defining `Swift` + `MemberFunction` to imply/require the `self` argument is a great conceptual extension of the existing model.

In Swift, sometimes the `self` register is used for non-instance state. For example, in static functions, the type metadata is passed as the `self` argument. Since static functions are not member functions, we may want to not use the `MemberFunction` calling convention. Alternatively, we could provide a `SwiftSelf<T>` type to specify "this argument goes in the self register". Specifying the type twice in a signature would generate an `InvalidProgramException`. This would allow the `self` argument to be specified anywhere in the argument list.
In Swift, sometimes the `self` register is used for non-instance state. For example, in static functions, the type metadata is passed as the `self` argument. Since static functions are not member functions, we may want to not use the `MemberFunction` calling convention. Alternatively, we could provide a `SwiftSelf` type to specify "this argument goes in the self register". Specifying the type twice in a signature would generate an `InvalidProgramException`. This would allow the `self` argument to be specified anywhere in the argument list.

For reference, explicitly declaring a function with the Swift or SwiftAsync calling conventions in Clang requires the "context" argument, the value that goes in the "self" register, as the last parameter or the penultimate parameter followed by the error parameter.

The self register, like the error register and the async context register discussed later, is always a pointer-sized value. As a result, if we introduce any special intrinsic types for the calling convention, we don't need to make the type generic as we can always use a `void*` to represent the value at the lowest level.

###### Error register

We have many options for handling the error register in the Swift calling convention:

1. Use an attribute like `[SwiftError]` on a by-ref or `out` parameter to indicate the error parameter.
2. Use a special type like `SwiftError` on a by-ref or `out` parameter to indicate the error parameter.
3. Use a special return type `SwiftReturn<T>` to indicate the error parameter and combine it with the return value.
4. Use special helper functions `Marshal.Get/SetLastSwiftError` to get and set the last Swift error. Our various compilers will automatically emit a call to `SetLastSwiftError` from Swift functions and emit a call to `GetLastSwiftError` in the epilog of `UnmanagedCallersOnly` methods. The projection generation will emit calls to `Marshal.Get/SetLastSwiftError` to get and set these values to and from exceptions.
5. Implicitly transform the error register into an exception at the interop boundary.
1. Use an attribute like `[SwiftError]` on a `ref` or `out` parameter to indicate the error parameter.
2. Use a special type like `SwiftError` on a `ref` or `out` parameter to indicate the error parameter.
3. Use a special type like `SwiftError*` to indicate the error parameter;
4. Use a special return type `SwiftReturn<T>` to indicate the error parameter and combine it with the return value.
5. Use special helper functions `Marshal.Get/SetLastSwiftError` to get and set the last Swift error. Our various compilers will automatically emit a call to `SetLastSwiftError` from Swift functions and emit a call to `GetLastSwiftError` in the epilog of `UnmanagedCallersOnly` methods. The projection generation will emit calls to `Marshal.Get/SetLastSwiftError` to get and set these values to and from exceptions.
6. Implicitly transform the error register into an exception at the interop boundary.

We have a prototype that uses the first option; however, using an attribute has the same limitations as the attribute option for the self register. As a result, we should consider an alternative option.

Options 2 and 3 have similar characteristics: They both express that the to-be-called Swift function uses the Error Register in the signature and they both require signature manipulation in the JIT/AOT compilers. Option 2 is likely cheaper, as the argument with `ref/out SwiftError` type can easily be associated with the error register. Like with `SwiftSelf<T>`, we would throw an `InvalidProgramException` for a signature with multiple `SwiftError` parameters. Unlike `SwiftSelf<T>`, `SwiftError` is non-generic because the error is always passed as a pointer to the object, never by value.
Options 2 through 4 have similar characteristics: They both express that the to-be-called Swift function uses the Error Register in the signature and they both require signature manipulation in the JIT/AOT compilers. Option 2 is likely cheaper, as the argument with `ref/out SwiftError` type can easily be associated with the error register. Like with `SwiftSelf`, we would throw an `InvalidProgramException` for a signature with multiple `SwiftError` parameters.

Option 3 provides an alternative to Option 2 as `ref` and `out` parameters are not supported in `UnmanagedCallersOnly` methods. Option 2 likely provides more possible JIT optimizations as managed pointers are more easily reasoned about in some of our JIT/AOT intermediate representations, but supporting it in all of our goal scenarios (P/Invoke, UnmanagedCallersOnly, function pointers) would require some minor language changes to allow the managed pointers in the `UnmanagedCallersOnly` signature for these special types. Option 3 does not require any language changes.

Option 3 provides a more functional-programming-style API that is more intuitive, but likely more expensive to implement. As a result, Option 3 would be better as a higher-level option and not the option implemented by the JIT/AOT compilers.
Option 4 provides a more functional-programming-style API that is more intuitive, but likely more expensive to implement. As a result, Option 4 would be better as a higher-level option and not the option implemented by the JIT/AOT compilers.

Option 4 provides an alternative approach. As the "Error Register" is always register-sized, we can use runtime-supported helper functions to stash away and retrieve the error register. Unlike options 2 or 3, we don't need to do any signature manipulation as the concept of "this Swift call can return an error" is not represented in the signature. Responsibility to convert the returned error value into a .NET type, such as an exception, would fall to the projection tooling. Since option 4 does not express whether or not the target function throws at the signature level, the JIT/AOT compilers would always need to emit the calls to the helpers when compiling calls to and from Swift code. If the projection of Swift types into .NET will always use exception and not pass Swift errors as the error types directly, then Option 4 reduces the design burden on the runtime teams by removing the type that would only be used at the lowest level of the Swift/.NET interop. However, Option 4 would leave some performance on the table as it would effectively require us to store and read the error value into thread-local storage instead of reading the error value from the register directly.
Option 5 provides an alternative approach. As the "Error Register" is always register-sized, we can use runtime-supported helper functions to stash away and retrieve the error register. Unlike options 2 or 3, we don't need to do any signature manipulation as the concept of "this Swift call can return an error" is not represented in the signature. Responsibility to convert the returned error value into a .NET type, such as an exception, would fall to the projection tooling. Since option 4 does not express whether or not the target function throws at the signature level, the JIT/AOT compilers would always need to emit the calls to the helpers when compiling calls to and from Swift code. If the projection of Swift types into .NET will always use exception and not pass Swift errors as the error types directly, then Option 4 reduces the design burden on the runtime teams by removing the type that would only be used at the lowest level of the Swift/.NET interop. However, Option 4 would leave some performance on the table as it would effectively require us to store and read the error value into thread-local storage instead of reading the error value from the register directly.

Option 5 would be most similar to the Objective-C interop experience. However, this experience would require more work in the JIT/AOT compilers and would make the translation between .NET exception and Swift error codes inflexible. Modern .NET interop solutions generally push error-exception translation mechanisms to be controlled by higher-level interop code generators instead of the runtime for flexibility. Not selecting this option would require all `CallConvSwift` `UnmanagedCallersOnly` methods to wrap their contents in a `try-catch` to translate any exceptions. This is already done for the COM source generator and had to be done by Binding Tools for Swift, so the pattern has a lot of implementation expertise.
Option 6 would be most similar to the Objective-C interop experience. However, this experience would require more work in the JIT/AOT compilers and would make the translation between .NET exception and Swift error codes inflexible. Modern .NET interop solutions generally push error-exception translation mechanisms to be controlled by higher-level interop code generators instead of the runtime for flexibility. Not selecting this option would require all `CallConvSwift` `UnmanagedCallersOnly` methods to wrap their contents in a `try-catch` to translate any exceptions. This is already done for the COM source generator and had to be done by Binding Tools for Swift, so the pattern has a lot of implementation expertise.

##### Async Context Register

Expand Down

0 comments on commit 8e170e4

Please sign in to comment.