-
Notifications
You must be signed in to change notification settings - Fork 162
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 .NET Swift interop tooling components and layout #312
Changes from 9 commits
8cf069f
9c21864
76d1433
ea4d636
0bb0e3c
659e44d
722a72c
4ccb501
b9325c6
84b2fa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -130,7 +130,11 @@ CoreCLR and NativeAOT currently block the `VectorX<T>` types from P/Invokes as t | |
|
||
##### Automatic Reference Counting and Lifetime Management | ||
|
||
Swift has a strongly-defined lifetime and ownership model. This model is specified in the Swift ABI and is similar to Objective-C's ARC (Automatic Reference Counting) system. When .NET calls into Swift, the .NET GC is responsible for managing all managed objects. Unmanaged objects from C# should either implement `IDisposable` or utilize a designated thin wrapper over the Swift memory allocator, currently accessible through the `NativeMemory` class, to explicitly release memory. It's important to ensure that when a Swift callee function allocates an "unsafe" or "raw" pointer types, such as UnsafeMutablePointer and UnsafeRawPointer, where explicit control over memory is needed, and the pointer is returned to .NET, the memory is not dereferenced after the call returns. Also, if a C# managed object is allocated in a callee function and returned to Swift, the .NET GC will eventually collect it, but Swift will keep track using ARC, which represents an invalid case and should be handled by projection tools. | ||
Swift has a strongly-defined lifetime and ownership model. This model is specified in the Swift ABI and is similar to Objective-C's ARC (Automatic Reference Counting) system. When .NET calls into Swift, the .NET GC is responsible for managing all managed objects. | ||
|
||
The `IDisposable` provides an explicit mechanism for releasing unmanaged resources. Destructors are managed by the GC and offer a way to release unmanaged resources when an object is collected by the GC. While destructors abstract away memory management from the user, the `Idisposable` pattern provides deterministic control over when resources and can lead to better performance as it prevents the need for GC collection cycles. The `IDisposable` pattern is the typical .NET approach for dealing with unmanaged resources and thus is selected as default option at initial stage. If it is determined that the `IDisposable` pattern introduces unnecessary overhead for users, and that destructors can adequately manage the release of unmanaged resources, appropriate updates to the memory management approach will be made. | ||
|
||
Unmanaged objects from C# should either implement `IDisposable` or utilize a designated thin wrapper over the Swift memory allocator, currently accessible through the `NativeMemory` class, to explicitly release memory. It's important to ensure that when a Swift callee function allocates an "unsafe" or "raw" pointer types, such as UnsafeMutablePointer and UnsafeRawPointer, where explicit control over memory is needed, and the pointer is returned to .NET, the memory is not dereferenced after the call returns. Also, if a C# managed object is allocated in a callee function and returned to Swift, the .NET GC will eventually collect it, but Swift will keep track using ARC, which represents an invalid case and should be handled by projection tools. | ||
|
||
The Binding Tools for Swift tooling handles these explicit lifetime semantics with generated Swift code. In the new Swift/.NET interop, management of these lifetime semantics will be done by the Swift projection layer and not by the raw calling-convention support. If any GC interaction is required to handle the lifetime semantics correctly, we should take an approach more similar to the `ComWrappers` support (higher-level, less complex interop interface) rather than the Objective-C interop support (lower-level, basically only usable by the ObjCRuntime implementation). | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's worse than this. Value types in swift have specific semantics for what happens when an instance goes out of scope. This is, of course, completely different than the semantics of C#, but can be approximated by making the type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think the best way to model structs is either with an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, updated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the purpose of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is for disposal or release of native resources. In this case that is the intent. The fact that it is a refcount on the native side seems like implementation detail of the taret platform we are interoping with. |
||
|
@@ -142,7 +146,13 @@ All designs in this section should be designed such that they are trimming and A | |
|
||
#### Swift to .NET Language Feature Projections | ||
|
||
##### Structs/Value Types | ||
The following subheadings describe projections of Swift types into C#. This section illustrates general mechanisms and practices applied in the tooling projection. The complete documentation of the projection tooling is available at https://github.com/dotnet/runtimelab/tree/feature/swift-bindings/docs. | ||
|
||
##### Primitive types | ||
|
||
Swift primitive types are implemented as frozen structs that conform to Swift-specific lowering processes handled by the runtime. However, most of these types are below the size limit for being passed by reference and can fit within the underlying calling convention. | ||
|
||
##### Structs/Enums | ||
|
||
Unlike .NET, Swift's struct types have strong lifetime semantics more similar to C++ types than .NET structs. At the Swift ABI layer, there are broadly three types of structs/enums: "POD/Trivial" structs, "Bitwise Takable/Movable" structs, and non-bitwise movable structs. The [Swift documentation](https://github.com/apple/swift/blob/main/docs/ABIStabilityManifesto.md#layout-and-properties-of-types) covers these different kinds of structs. Let's look at how we could map each of these categories of structs into .NET. | ||
|
||
|
@@ -152,11 +162,23 @@ Unlike .NET, Swift's struct types have strong lifetime semantics more similar to | |
|
||
Structs that are non-bitwise-movable are more difficult. They cannot be moved by copying their bits; their copy constructors must be used in all copy scenarios. When mapping these structs to C#, we must take care that we do not copy the underlying memory and to call the deallocate function when the C# usage of the struct falls out of scope. These use cases best match up to C# class semantics, not struct semantics. | ||
|
||
We plan to interop with Swift's Library Evolution mode, which brings an additional wrinkle into the Swift struct story. Swift's Library Evolution mode abstracts away all type layout and semantic information unless a type is explicitly marked as `@frozen`. In the Library Evolution case, all structs have "opaque" layout, meaning that their exact layout and category cannot be determined until runtime. As a result, we need to treat all "opaque" layout structs as possibly non-bitwise-movable at compile time as we will not know until runtime what the exact layout is. Swift/C++ interop is not required to use the Library Evolution mode in all cases as it can statically link against Swift libraries, so it is not limited by opaque struct layouts in every case. The size and layout information of a struct is available in its [Value Witness Table](https://github.com/apple/swift/blob/main/docs/ABIStabilityManifesto.md#value-witness-table), so we can look up this information at runtime for allocating struct instances and manipulating struct memory correctly. | ||
We plan to interop with Swift's Library Evolution mode, which brings an additional wrinkle into the Swift struct story. Swift's Library Evolution mode abstracts away all type layout and semantic information unless a type is explicitly marked as `@frozen`. In the Library Evolution case, all structs have "opaque" layout, meaning that their exact layout and category cannot be determined until runtime. The size and layout information of concrete types is available in its [Value Witness Table](https://github.com/apple/swift/blob/main/docs/ABIStabilityManifesto.md#value-witness-table), so we can look up this information at runtime for allocating struct instances and manipulating struct memory correctly. As a result, we need to treat all "opaque" layout structs as possibly non-bitwise-movable at compile time as we will not know until runtime what the exact layout is. Swift/C++ interop is not required to use the Library Evolution mode in all cases as it can statically link against Swift libraries, so it is not limited by opaque struct layouts in every case. Every concrete type in Swift has a structure that provides information about how to manipulate values of that type. | ||
|
||
##### Tuples | ||
Swift structs and enums have richer semantics than in .NET and are projected as C# classes which implement `IDisposable` interface to streamline handling of both simple blittable and more complex scenarios. These C# classes have a single property that holds the data payload for the type. They typically include two constructors: one that corresponds to the init method in the Swift class, and another internal constructor used to create uninitialized types invoked by the marshaler in cases when it is a return value from a function. | ||
|
||
##### Classes/Protocols | ||
|
||
A public Swift class is projected as a final C# class, while a virtual Swift class is projected as an internal C# class. A final class has a straightforward inheritance model, while a virtual class introduces more complexity, particularly related to subclassing and simulated vtable methods. Another important type in Swift is protocols. Swift allows any type to implement a protocol and supports retroactive modeling through extensions. Since the protocol's implementation can't be part of the object, Swift uses a [Protocol Witness Table](https://github.com/apple/swift/blob/main/docs/ABIStabilityManifesto.md#protocol-witness-tables), which functions like a vtable for each conformance. A protocol type in Swift is represented using an [existential container](https://github.com/apple/swift/blob/main/docs/ABIStabilityManifesto.md#existential-metadata) that includes payload, type metadata pointer, and a protocol witness table pointer. | ||
|
||
If possible, Swift tuples should be represented as `ValueTuple`s in .NET. If this is not possible, then they should be represented as types with a `Deconstruct` method similar to `ValueTuple` to allow a tuple-like experience in C#. | ||
Projections can utilize [`IUnmanagedVirtualMethodTableProvider` interface](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshalling.iunmanagedvirtualmethodtableprovider?view=net-8.0) to retrieve vtable for a given target type from an object. | ||
|
||
This subheading will be updated with more details on projection once simpler types are reviewed, like structs and enums. | ||
|
||
##### Tuples/Closures | ||
|
||
A Swift tuple can be projected as `ValueTuple` type in C#. Swift uses two types of closures: escaping and non-escaping. Escaping closures can exist beyond their original context, while non-escaping closures should not persist beyond their declaration context and cannot reference external data. The Swift compiler has a mechanism to convert a non-escaping closure into an escaping one within the scope of another closure. | ||
|
||
This subheading will be updated with more details on projection once simpler types are reviewed, like structs and enums. | ||
|
||
##### SIMD types | ||
|
||
|
@@ -171,18 +193,9 @@ As mentioned in the calling-convention section above, none of the libraries we a | |
|
||
#### Projection Tooling Components | ||
|
||
The projection tooling should be split into these components: | ||
The projection tooling is based on the [Binding Tools for Swift](https://github.com/xamarin/binding-tools-for-swift). The tooling contain components that can consume a compiled Apple Swift library interface and generate C# source code bindings that allow it to be surfaced as a .NET library. The tool will not generate any Swift wrappers and it's users responsibility to provide Swift wrappers for cases where direct binding is not possible. The projection tooling will utilize the runtime core interop source-gen infrastructure to implement marshalling codegen. | ||
|
||
##### Importing Swift into .NET | ||
|
||
1. A tool that takes in a `.swiftinterface` file or Swift sources and produces C# code. | ||
2. A library that provides the basic support for Swift interop that the generated code builds on. | ||
3. User tooling to easily generate Swift projections for a given set of `.framework`s. | ||
- This tooling would build a higher-level interface on top of the tool in item 1 that is more user-friendly and project-system-integrated. | ||
4. (optional) A NuGet package, possibly referencable by `FrameworkReference` or automatically included when targeting macOS, Mac Catalyst, iOS, or tvOS platforms that exposes the platform APIs for each `.framework` that is exposed from Swift to .NET. | ||
- This would be required to provide a single source of truth for Swift types so they can be exposed across an assembly boundary. | ||
|
||
##### Exporting .NET to Swift | ||
##### Exporting .NET to Swift | ||
|
||
There are two components to exporting .NET to Swift: Implementing existing Swift types in .NET and passing instances of those types to Swift, and exposing novel types from .NET code to Swift code to be created from Swift. Exposing novel types from .NET code to Swift code is considered out of scope at this time. | ||
|
||
|
@@ -191,13 +204,17 @@ For implementing existing Swift types in .NET, we will require one of the follow | |
1. A Roslyn source generator to generate any supporting code needed to produce any required metadata, such as type metadata and witness tables, to pass instances of Swift-type-implementing .NET types defined in the current project to Swift. | ||
2. An IL-post-processing tool to generate the supporting code and metadata from the compiled assembly. | ||
|
||
If we were to use an IL-post-processing tool here, we would break Hot Reload in assemblies that implement Swift types, even for .NET-only code, due to introducing new tokens that the Hot Reload "client" (aka Roslyn) does not know about. As a result, we should prefer the Roslyn source generator approach. | ||
If we want to use an IL-post-processing tool here, we would break Hot Reload in assemblies that implement Swift types, even for .NET-only code, due to introducing new tokens that the Hot Reload "client" (aka Roslyn) does not know about. | ||
|
||
### Distribution | ||
|
||
The calling convention work will be implemented by the .NET runtimes in dotnet/runtime. | ||
The projection tooling will be implemented as a .NET CLI tool and integrated into the Xamarin publishing infrastructure. It will be included in the macios workload for Apple platforms and also available as a standalone package independent of MAUI framework. | ||
|
||
The calling convention work will be implemented in the [dotnet/runtime](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Swift/SwiftTypes.cs) repository. | ||
|
||
### Validation | ||
|
||
The projection tooling will not ship as part of the runtime. It should be available as a separate NuGet package, possibly as a .NET CLI tool package. The projections should either be included automatically as part of the TPMs for macOS, iOS, and tvOS, or should be easily referenceable. | ||
The interop will be showcased through CryptoKit library in the runtime repository and MAUI libraries and samples: https://github.com/dotnet/runtime/issues/95636. | ||
|
||
## Q & A | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this relevant to interop? It sounds like it doesn’t make any semantic difference if the object is released early or late, it’s just a design difference. That is, it doesn’t sound like there are any non-memory resources being tracked. In that case I would expect us to stick with C# design, which is to use the GC for all memory resources. Is there something else being tracked here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Composition can be a concern here. For example, in COM Aggregation there are potentially two distinct memory models that are represented by a single object - WinRT/COM class is the base of some .NET class. In the COM scenario this is relatively simple because it is expected the user to handle circular references that strattle the interop boundary. For WinRT, the Jupiter runtime has a reference tracking mechansim that means more cooperation is needed to collect a single object that is managed by the two systems.
I don't know if our Swift interop scenarios will have need for that sort of mechanism, but from experience interop scenarios that are non-deterministic make tooling very complicated - memory tracking, code coverage, etc. If we expect existing Swift tooling to be usable when .NET is involved having an explicit option is beneficial - even if it is an opt-in like
CreateObjectFlags.UniqueInstance
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The primary reason for considering destructors is to utilize the GC and the C# design pattern. The current tooling (BTfS) implements the
IDisposable
strategy. Since we are at an early stage, it is challenging to make a decision.Opting for
IDisposable
for the sake of simplicity seems like a reasonable argument. @agocke are you open to this approach (starting withIDisposable
), or do you prefer starting with destructors initially and transitioning toIDisposable
if required?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern is mainly that we overload the use of
IDisposable
a lot in .NET, and that makes it confusing for customers. In some cases, like for files, it's pretty important that you callIDisposable
to prevent running out of handles. In this case, pretty much every Swift type would come withIDisposable
. Without knowing details of the type, it seems impossible for me, the caller, to understand whether I should or shouldn't dispose it.Moreover, I think the preferred behavior for "trivial" deinit calls would be to not call Dispose (or more accurately, not wrap the type in a
using
). Since adding ausing
in C# produces a try-finally and hurts codegen, it's probably better to avoid a pattern that causes an explosion of usings. Instead, I think it would be ideal if we only used IDisposable for custom deinit. In those cases, we can be relatively confident that something more than just memory is being freed.So overall I'm very supportive of some way to explicitly call deinit/dispose on Swift type, but making everything IDisposable feels like an anti-pattern that will just cause a lot of user confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for info. I've updated the section and the MVP accordingly.