Closed kotlarmilos closed 3 months ago
How does this relate to frozen structs vs. non-frozen structs? My assumption was that we will map frozen Swift structs to C# structs and non-frozen Swift structs to C# classes.
Notes: I think you mean 4 machine words and not 4 bytes. I like the approach of having a cached metadata type - that will certainly improve performance. In either case, we need to think ahead for how the type metadata gets used (and it gets used often) and should have a dedicated type for this. In BTfS, we have something that looks like this:
public struct SwiftMetatype {
internal NativeHandle handle;
public MetatypeKind Kind { get { return MetatypeKindFromHandle (handle); } }
public bool IsValid { get { return handle != IntPtr.Zero; } }
public SwiftMetatype (NativeHandle handle)
{
this.handle = handle;
}
void ThrowOnInvalid ()
{
if (!IsValid)
throw new NotSupportedException ();
}
public IntPtr Handle { get { return handle; }}
}
This is a minimal definition. What's left out of it is the tooling that's necessary for runtime marshaling of types - for example, getting the length and element types for tuples. Yes, I know this is beyond the scope here, but this will all be needed.
Every swift type has a metadata accessor function. This is a static function which takes 0 or more arguments (0 for a non-generic type).
To that end, we might want all nominal types (in swift these are types that can have names: structs, classes, enums, actors) to implement an interface, say, ISwiftMetadata
:
public interface ISwiftMetadata {
// either this - have to pass in the generic specialization
static abstract SwiftMetadata MetadataOf (params SwiftMetadata [] types); // don't like the name because of possible name conflicts
// or this - the type handles the generic specialization, which I prefer
SwiftMetadata SwiftMetadata { get; }
}
Let's create classifications of the swift structs in order to map them cleanly:
Examples:
public struct ABlittableStruct { // blittable, lowerable
public var a: Int // careful - the size of this depends on the platform. Do we care about non-64 bit anymore?
}
public struct NonBlittableStruct { // non-blittable, lowerable, referencecountable
public var a: SomeClass // reference count issue
public var b: Bool // blitable issue
}
public struct BlittableNonLowerable {
public var a: Int, b: Int, c: Int, d: Int, e: Int
}
In addition to the issues with blitability etc, for lowerable structs we need to match, size, stride and alignment. In addition, we can only expose members in frozen structs.
I like to think about how we need to think of the overall type system in doing this and how we can make the task both easy and performant for us.
We will need functions to live in runtime support somewhere that includes this C# method:
public static bool TryGetSwiftMetadata (object o, out SwiftMetadata md) { }
public static bool TryGetSwiftMetadata (Type t, out SwiftMetadata md) { }
public static bool TryGetSwiftMetadata (object o, Type [] interfaceConstraints, out SwiftMetadata md) { }
public static bool TryGetSwiftMetadata (Type t, Type [] interfaceConstraints, out SwiftMetadata md) { }
The actual API in BTfS is:
public static SwiftMetatype Metatypeof (Type t) { }
public static SwiftMetatype Metatypeof (Type t, Type [] interfaceConstraints) { }
This works, but without the instance, we have to call use reflection to get the method to retrieve the type metadata which is, of course, not optimal.
And inside those methods, I would really like to see something like:
public static bool TryGetSwiftMetadata (object o, out SwiftMetadata md) {
if (o is ISwiftMetadata omd) {
md = omd.SwiftMetadata;
return true;
} else {
return TryGetSwiftMetadata (o.GetType (), out md);
}
}
Other notes - PInvokes need to absolutely live in a different class than the class/struct that implements the binding. When we do generics, this is a requirement of the type system.
It's useful to be able to handle types generically and to be able to ask questions about them. We can either do that with interfaces or attributes or a common base class. In the case of the second struct in your example, how do we know that it's a swift struct?
Using a common base class allows us to put the IDisposable
implementation in one place.
Things will also get challenging when we consider structs that contain enums. As mentioned before, because of the non-documented fragile layout of swift enums and their discriminators, those may also be technically blittable and lowerable but not practically so.
How does this relate to frozen structs vs. non-frozen structs? My assumption was that we will map frozen Swift structs to C# structs and non-frozen Swift structs to C# classes.
This proposal should be aligned with mapping frozen Swift struct to C# structs and non-frozen Swift structs to C# classes. Frozen structs and enums are not considered opaque and will be enregistered, while non-frozen structs and enums are considered opaque and will be passed via reference.
There are two distinct projection approaches for structs based on their sequence length: projecting as C# structs for sequences less than 4 machine words, and projecting as C# classes for sequences equal to or greater than 4 machine words.
It seems this is a split based on the CPU architecture, and not something inherent to the language: will this stay true for all future CPU architectures?
This proposal should be aligned with mapping frozen Swift struct to C# structs and non-frozen Swift structs to C# classes
The following sentence from above sems to contradict that "projecting as C# structs for sequences less than 4 machine words, and projecting as C# classes for sequences equal to or greater than 4 machine words.". Am I missing some nuance?
I think you mean 4 machine words and not 4 bytes.
Thanks, updated.
This is a minimal definition. What's left out of it is the tooling that's necessary for runtime marshaling of types - for example, getting the length and element types for tuples. Yes, I know this is beyond the scope here, but this will all be needed.
I think we need this to project Swift structs as we need layout information to support runtime lowering. I tried to focus on projections initially, and I suggest we review implementation of metadata in a subsequent PR.
Other notes - PInvokes need to absolutely live in a different class than the class/struct that implements the binding. When we do generics, this is a requirement of the type system.
It's useful to be able to handle types generically and to be able to ask questions about them. We can either do that with interfaces or attributes or a common base class. In the case of the second struct in your example, how do we know that it's a swift struct?
Yes, good points.
It seems this is a split based on the CPU architecture, and not something inherent to the language: will this stay true for all future CPU architectures?
Good point. Maybe the proposed handling should be transitioned to the runtime layer to ensure uniform projections.
The following sentence from above sems to contradict that "projecting as C# structs for sequences less than 4 machine words, and projecting as C# classes for sequences equal to or greater than 4 machine words.". Am I missing some nuance?
I overlooked this. I need to verify, but my understanding is that structs and enums annotated with the frozen attribute are not considered opaque and will be enregistered only if its sequence is of length 4 or less. Frozen structs and enums that cannot be broken down in this way are passed by-reference to their specified frozen layout.
private var number1: Int32
public Int32 number1;
Not sure if this is significant, but the fields are private in swift and public in C#.
public MyStruct(Int32 number1, Int32 number2)
{
this = PIfunc_MyStruct(number1, number2);
if (_metadata_payload == null)
_metadata_payload = PIfunc_GetMetadata();
Console.WriteLine("Metadata kind: " + Marshal.ReadIntPtr((IntPtr)_metadata_payload, 0));
}
Maybe the initialization of _metadata_payload
should be in a static constructor instead?
public void Dispose()
{
Marshal.FreeHGlobal((IntPtr)_payload);
}
This doesn't support calling Dispose more than once:
public void Dispose()
{
Marshal.FreeHGlobal((IntPtr)_payload);
_payload = null;
}
public Int32 getMagicNumber()
{
SwiftSelf swiftSelf = new SwiftSelf(_payload);
return PIfunc_getMagicNumber(swiftSelf);
}
What happens if someone calls Dispose
before calling getMagicNumber
? Maybe check for disposed and throw an ObjectDisposedException
.
I think it would be useful to clearly separate:
This API is exposed to end users.
The projection of Swift structs is determined based on their size and layout. Structs with a size less than or equal to 4 machine words are enregistered and passed as a sequence of primitives. Structs with a size greater than 4 machine words are passed by reference. The frozen
annotation defines that no stored properties will be added to or removed from the struct, but these structs must comply with struct lowering algorithm.
Let's consider the following scenarios: initializers, methods, static methods, and functions.
On both arm64 and x64 architectures, Swift struct initializers pass structs as enregistered if they fit within the lowering sequence. Otherwise, indirect return registers are used (x8
/rax
). For example:
define swiftcc { i64, i64 } @"$s12HelloLibrary8MyStructV7number17number2ACs5Int64V_AGtcfC"(i64 %0, i64 %1)
define swiftcc void @"$s12HelloLibrary8MyStructV7number17number2ACs5Int64V_AGtcfC"(ptr noalias nocapture sret(%T12HelloLibrary8MyStructV) %0, i64 %1, i64 %2)
Similarly, Swift struct methods expect structs as enregistered if they fit within the lowering sequence. If not, the swiftself
register is utilized. Example:
define swiftcc i64 @"$s12HelloLibrary8MyStructV14getMagicNumbers5Int64VyF"(i64 %0, i64 %1)
define swiftcc i64 @"$s12HelloLibrary8MyStructV14getMagicNumbers5Int64VyF"(ptr noalias nocapture swiftself dereferenceable(40) %0)
Static methods require metadata in the call context but do not introduce additional requirements in this context. Struct parameters in functions are implicitly lowered by the runtime when the struct's size is within the lowering sequence.
The proposal is:
Please let me know if this looks like a good direction.
This is the implementation of public API.
The projection tool generates identical struct layouts on the C# side. Memory management is implicit and handled by the GC.
Constructor:
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("libHelloLibrary.dylib", EntryPoint = "$s12HelloLibrary11MyStructV7number17number2ACs5Int32V_AGtcfC")]
internal static extern MyStruct PIfunc_MyStruct(Int32 number1, Int32 number2);
public MyStruct(Int32 number1, Int32 number2)
{
this = PIfunc_MyStruct(number1, number2);
}
Instance method:
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("libHelloLibrary.dylib", EntryPoint = "$s12HelloLibrary11MyStructV14getMagicNumbers5Int32VyF")]
internal static extern Int32 PIfunc_getMagicNumber(MyStruct self);
public Int32 getMagicNumber()
{
return PIfunc_getMagicNumber(this);
}
The C# class allocates the same struct layout with explicit memory management by implementing IDisposable
. This also requires runtime support for indirect return register (SwiftIndirectResult
).
Constructor:
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("libHelloLibrary.dylib", EntryPoint = "$s12HelloLibrary11MyStructV7number17number2ACs5Int64V_AGtcfC")]
internal static extern MyStruct PIfunc_MyStruct(Int32 number1, Int32 number2, SwiftIndirectResult payload);
public MyStruct(Int32 number1, Int32 number2)
{
// _payloadSize is retrieved from the type metadata
void* _payload = Marshal.AllocHGlobal(_payloadSize).ToPointer();
SwiftIndirectResult swiftIndirectResult = new SwiftIndirectResult(_payload);
PIfunc_MyStruct(number1, number2, swiftIndirectResult);
}
~MyStruct()
{
Dispose(false);
}
Instance method:
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
[DllImport("libHelloLibrary.dylib", EntryPoint = "$s12HelloLibrary11MyStructV14getMagicNumbers5Int32VyF")]
internal static extern Int32 PIfunc_getMagicNumber(SwiftSelf self);
public Int32 getMagicNumber()
{
// Before this, check for disposed and throw an ObjectDisposedException
SwiftSelf swiftSelf = new SwiftSelf(_payload);
return PIfunc_getMagicNumber(swiftSelf);
}
I suggest reviewing and aligning on the direction first before introducing additional details.
The proposal is: Project Swift structs with a size less than or equal to 4 machine words into C# structs Project Swift structs with size greater that 4 machine words into C# IDisposable classes
The size of the non-frozen structs can fluctuate. Would this proposal mean that the projected type can flip from struct to class (or vice versa) from version to version?
It does not look right to leak the 4 machine words threshold into the higher-level programming model.
The size of the non-frozen structs can fluctuate. Would this proposal mean that the projected type can flip from struct to class (or vice versa) from version to version?
Changing non-frozen struct sizes means the Swift runtime emits different code, and it can be expected to regenerate bindings in case of breaking changes. However, I agree that the tooling should provide uniform public API to the users.
It does not look right to leak the 4 machine words threshold into the higher-level programming model.
An alternative is to project Swift structs to C# structs with implicit memory management. In this case, the tooling will project identical struct layout and capture lowering sequence information. We can determine whether to integrate lowering logic into the tooling or runtime.
The runtime can implement a thin wrapper around constructors to ensure parameters and results are always passed via reference. For instance methods, when a struct cannot be enregistered, the runtime can utilize swiftself
.
In this case, the projection tolling will have uniform mapping of structs.
The runtime can support SwiftIndirectResult
parameter. The tooling can project Swift structs that are passed via reference into C# structs utilizing runtime Swift types.
In this case, the projection tolling will have the same public API, but different implementation for constructors and instance methods based on lowering sequence information.
Changing non-frozen struct sizes means the Swift runtime emits different code, and it can be expected to regenerate bindings in case of breaking changes.
Is changing non-frozen struct size consider to be a breaking change in Swift? My assumption has been that it is not breaking change, the whole point of non-frozen structs is to make it non-breaking. If my assumption is correct, taking a dependency on non-frozen struct layout would also mean that it may not be possible to build a single app binary that works on both iOS N and iOS N+1 if there was a non-frozen struct layout change between the two iOS versions. I do not think that it would be an acceptable experience.
SwiftIndirectResult
I think that we have to add this to CallConvSwift
to have proper for support returning non-frozen struct. I am not sure how we have missed this detail in https://github.com/dotnet/runtime/issues/64215.
Sorry, I didn't consider the library evolution mode at first.
Is changing non-frozen struct size consider to be a breaking change in Swift? My assumption has been that it is not breaking change, the whole point of non-frozen structs is to make it non-breaking.
You are right, it is not a breaking change in the library evolution mode. Non-frozen structs and enums in the library evolution mode are passed by reference.
I think that we have to add this to CallConvSwift to have proper for support returning non-frozen struct. I am not sure how we have missed this detail in https://github.com/dotnet/runtime/issues/64215.
Yes, we need to add them to support non-frozen structs and frozen
structs that exceed lowering sequence limit. Let's create a proposal separately.
It does not look right to leak the 4 machine words threshold into the higher-level programming model.
It seems this is a split based on the CPU architecture, and not something inherent to the language: will this stay true for all future CPU architectures?
Primary concerns here are initializers and instance methods for frozen
structs in library evolution mode. For initializers, If a struct exceeds lowering sequence limit, indirect result register is utilized. For instance methods, If a struct exceeds lowering sequence limit, the runtime will pass it by reference, but won't use the swiftself
as the arg register.
Do you think this is something that should be implemented within the runtime or tooling?
If we proceed with the runtime changes, then the SwiftIndirectResult
type may not be implemented as a public API as handling will be done within the runtime.
Primary concerns here are initializers and instance methods for frozen structs in library evolution mode. For initializers, If a struct exceeds lowering sequence limit, indirect result register is utilized. For instance methods, If a struct exceeds lowering sequence limit, the runtime will pass it by reference, but won't use the swiftself as the arg register.
Do you think this is something that should be implemented within the runtime or tooling?
@jakobbotsch Could you please advice on where this should live based on your current understanding of the responsibilities split between CallConvSwift and the higher-level projections?
My assumption so far has been that any struct we see in a CallConvSwift
signature is a frozen struct. Non-frozen structs should in turn be handled at the projection layer by turning them into pointers in the CallConvSwift
signature.
I agree that leaves a hole when returning non-frozen structs that need to go through ret buffer, where the tooling needs to be able to instruct the runtime what to pass as the ret buffer. I think it sounds reasonable to add SwiftIndirectResult
parameter that has intrinsic handling to handle the non frozen struct cases.
I do not see how it's possible to describe the frozen struct instance calls with the current design if we want to retain the lowering to happen in the runtime instead of the tooling. I think to do that we need to change SwiftSelf
to be generic so that you can pass SwiftSelf<T>
for a frozen struct type T
which then is either enregistered into multiple registers or passed by reference in the Swift self register.
It seems doable to do this within the runtime with these additional changes. But of course my personal opinion was always that we should do the lowering in the tooling :-)
Thanks for sharing your perspective! Let's try to frame this.
In library evolution mode, structs are passed by reference, except for frozen structs with a sequence less than or equal to 4 machine words.
When structs are passed by reference:
By mapping any Swift struct into C# struct, we can ensure the same public API for end-users with implicit memory management. An alternative would be to map Swift structs passed by reference into C# classes. In such cases, users would have explicit memory management but different public API.
Let's first align on the end-users public API, and then we can focus on the implementation and runtime integration.
Here is a proposal for mapping Swift structs passed by reference (non-frozen and frozen
with sequence length > 4) into C# structs:
// consider that the tooling projected the same layout
// constructor
public MyStruct(){
fixed (void* thisPtr = &this){
SwiftIndirectResult swiftResult = new SwiftIndirectResult(thisPtr);
PIfunc_MyStruct(swiftResult, args);
}
}
// instance method
public Int64 getMagicNumber()
{
fixed (void* thisPtr = &this) {
SwiftSelf self = new SwiftSelf(thisPtr);
return PIfunc_getMagicNumber(self);
}
}
Here is a proposal for mapping frozen
Swift structs passed by value:
// consider that the tooling projected the same layout
// constructor
public MyStruct(){
this = PIfunc_MyStruct(args);
}
// instance method
public Int64 getMagicNumber()
{
return PIfunc_getMagicNumber(this);
}
An alternative implementation is to perform struct lowering at the runtime layer. The swiftself
and swifterror
are designed to provide "stateless" handles into the runtime and are not designed for implementing lowering semantics within the runtime.
Let's first align on the end-users public API
+1. How does the code that end-user needs to write looks like with these proposals? Can we take a few examples of real-world Swift that calls APIs with structs and show the equivalent C# code written against the projections?
An alternative implementation is to perform struct lowering at the runtime layer. The
swiftself
andswifterror
are designed to provide "stateless" handles into the runtime and are not designed for implementing lowering semantics within the runtime.
I'm not sure I understand what this means. Can you elaborate? @jkoritzinsky already implemented the lowering in the runtimes in https://github.com/dotnet/runtime/pull/99438, https://github.com/dotnet/runtime/pull/98831, https://github.com/dotnet/runtime/pull/99439.
Since "self" in Swift can be passed by value (does not match .NET) it seems like we need to expand SwiftSelf
to allow describing the exact type of "self" being passed.
I do not think the user facing representation of the types should be related to the lowering at all, regardless of where the lowering happens. (Also, minor note: the lowering is not 4 machine words, but 4 primitives. Those primitives can be SIMD types as well that are larger than machine words.). I agree with the earlier comments in this issue that it would make sense to map frozen types to C# structs and other types to C# classes.
If we do the lowering in the tooling then I do not think SwiftSelf<T>
will be necessary. But if we do the lowering in the runtime, then we need it to describe instance calls on frozen structs.
Regardless of anything I do not think we can make the projection tooling generate wrappers that are pointer size agnostic with the way things are currently designed. That's because there are Swift types whose layout is not describable in a pointer size agnostic way in .NET. For example,
@frozen
public struct S0
{
let a : Int;
let b : Int32;
}
@frozen
public struct S1
{
let c : S0;
let d : Int32;
}
has the following layout in 64-bit:
[StructLayout(LayoutKind.Sequential, Size = 12)]
public struct S0
{
public nint a;
public int b;
}
[StructLayout(LayoutKind.Sequential, Size = 16)]
public struct S1
{
public S0 c;
public int d; // Starts at offset = 12
}
and the following layout on 32-bit:
[StructLayout(LayoutKind.Sequential, Size = 8)]
public struct S0
{
public nint a;
public int b;
}
[StructLayout(LayoutKind.Sequential, Size = 12)]
public struct S1
{
public S0 c;
public int d; // Starts at offset = 8
}
I do not think these types are describable in a pointer size agnostic way in .NET. This might be unrelated, but it's something to keep in mind when we are deciding whether to keep the lowering in the runtime or move it back to the tooling. If the goal is to have a fully agnostic way to generate the projections and keep all type layout and ABI handling in the runtime then it seems like we need a higher fidelity way to describe the Swift source of truth types in IL.
I'm not sure I understand what this means. Can you elaborate? @jkoritzinsky already implemented the lowering in the runtimes in dotnet/runtime#99438, dotnet/runtime#98831, dotnet/runtime#99439.
I was referring to lowering bits proposed in the projection tooling, such as SwiftIndirectResult
and passing SwiftSelf
by value (basically your comments below).
I do not see how it's possible to describe the frozen struct instance calls with the current design if we want to retain the lowering to happen in the runtime instead of the tooling. I think to do that we need to change SwiftSelf to be generic so that you can pass SwiftSelf
for a frozen struct type T which then is either enregistered into multiple registers or passed by reference in the Swift self register. Since "self" in Swift can be passed by value (does not match .NET) it seems like we need to expand SwiftSelf to allow describing the exact type of "self" being passed.
I agree with the earlier comments in this issue that it would make sense to map frozen types to C# structs and other types to C# classes.
What are the primary reasons for mapping non-frozen structs to C# classes? If one of the reasons is lowering, then we make the public API representation dependent on lowering semantics.
This might be unrelated, but it's something to keep in mind when we are deciding whether to keep the lowering in the runtime or move it back to the tooling.
Thank you for bringing this up.
What are the primary reasons for mapping non-frozen structs to C# classes? If one of the reasons is lowering, then we make the public API representation dependent on lowering semantics.
I think the main reason would be that non-frozen structs have associated lifetime management around allocating/releasing the dynamically sized memory. With structs it becomes very easy to have use-after-free bugs.
The indirect result register follows the ARM64 calling convention and is supported by the runtime. The LLVM IR made me think explicit handling was necessary, leading to the use of an incorrect initializer signature. The indirect result register is loaded with a value based on the invocation. Thanks to @janvorli for the assistance.
I will try to narrow down the discussion to the public API.
Here is a Swift code and equivalent C# code written against the projections for the CryptoKit scenario. In this scenario, Swift structs are mapped to C# structs. Mapping non-frozen structs to C# classes doesn't change the existing API, but introduces explicit memory management API.
let plaintext = "Hello, World!".data(using: .utf8)!
let aad = "Additional Authenticated Data".data(using: .utf8)!
let plaintextPointer = plaintext.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> UnsafeRawBufferPointer in
return bytes
}
let aadPointer = aad.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> UnsafeRawBufferPointer in
return bytes
}
let nonce = ChaChaPoly.Nonce()
let symmetricKey = SymmetricKey(size: SymmetricKeySize.bits256)
do {
let sealedBox = try ChaChaPoly.seal(plaintextPointer, using: symmetricKey, nonce: nonce, authenticating: aadPointer)
let result = try ChaChaPoly.open(sealedBox, using: symmetricKey, authenticating: aad)
return 1
}
catch {
return 0
}
// Prepare plaintext
byte[] plaintext = System.Text.Encoding.UTF8.GetBytes("Hello, World!");
byte[] aad = System.Text.Encoding.UTF8.GetBytes("Additional Authenticated Data");
// Generate nonce and symmetric key
var nonce = new ChaChaPoly.Nonce();
var symmetricKey = new SymmetricKey(new SymmetricKeySize(256));
fixed (byte* aadPtr = aad)
fixed (void* plaintextPtr = plaintext)
{
var aadBuffer = new UnsafeRawBufferPointer(aadPtr, aad.Length);
var plaintextBuffer = new UnsafeRawBufferPointer(plaintextPtr, plaintext.Length);
try
{
// public static SealedBox<A, B> seal<A, B> (A plaintext, SymmetricKey key, Nonce nonce, B aad)
var sealedBox = ChaChaPoly.seal<UnsafeRawBufferPointer, UnsafeRawBufferPointer>(plaintextBuffer, symmetricKey, nonce, aadBuffer);
// public static T open<T, A, B> (SealedBox<A, B> sealedBox, SymmetricKey key, T aad)
var result = ChaChaPoly.open<UnsafeRawBufferPointer, UnsafeRawBufferPointer, UnsafeRawBufferPointer>(sealedBox, symmetricKey, aadBuffer);
return 1;
}
catch
{
return 0;
}
}
Please note that this proposal doesn't implement Swift protocols.
This helps, but it does not look like typical Swift code to me - I would not expect Swift programmers to go through unmanaged pointers to encrypt a block of plaintext. We may want to look outside CryptoKit for motivating examples for how structs are used by Swift. What are good examples of structs used by public APIs in either Swift core libraries or in Apple SDKs? It would be useful to find examples of both structs that require explicit memory management and that do not require explicit memory management.
Below are additional examples, the most of them involve implicit memory management. The audio example requires explicit memory management, which is not handled internally within type, but externally. I will try to find more examples with explicit memory management.
Structures like CGPoint
, CGSize
, and CGRect
are allocated on the stack.
import CoreGraphics
let point = CGPoint(x: 10, y: 20)
let size = CGSize(width: 100, height: 200)
let rect = CGRect(origin: point, size: size)
let vector = CGVector(dx: 10, dy: 20)
Structs are allocated on the stack.
let birthday = Date(timeIntervalSince1970: 0)
let today = Date()
let age = today.timeIntervalSince(birthday)
These structs are "lightweight" with implicit memory management.
import SwiftUI
let color = Color.red
let font = Font.system(size: 14, weight: .bold, design: .default)
let padding = EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)
var text: some View {
Text("Hello, World!")
.foregroundColor(color)
.font(font)
.padding(padding)
}
let circle = Circle()
.fill(Color.blue)
var body: some View {
Image("link")
.overlay {
Rectangle()
.stroke(Color.green, lineWidth: 50)
.frame(width: 1179 / 3,
height: 2556 / 3)
}
}
Stack allocated value types.
import CoreLocation
import MapKit
let coordinate = CLLocationCoordinate2D(latitude: 50.0755, longitude: 14.4378)
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
print ("Latitude: \(coordinate.latitude), Longitude: \(coordinate.longitude)")
Stack allocated value types.
let runtimeURL = URL(string: "https://github.com/dotnet/runtime")
applewe
if let url = runtimeURL {
print("URL: \(url.absoluteString)")
}
Stack allocated value types.
import AVFoundation
let time = CMTime(seconds: 1.5, preferredTimescale: 600)
This example involves explicit memory management for the buffer, using allocate
and deallocate
. Often requires working with low-level APIs and handling buffers that can exceed the stack's size limits, thus heap allocated.
var numberBuffers = 1024
var bufferSize = 32
var bufferList = AudioBufferList()
bufferList.mNumberBuffers = numberBuffers
bufferList.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: Int(bufferSize), alignment: MemoryLayout<UInt8>.alignment)
bufferList.mBuffers.mDataByteSize = bufferSize
bufferList.mBuffers.mNumberChannels = 2
// processing
bufferList.mBuffers.mData?.deallocate()
This is a proposal on structs and enum projections from the design document: https://github.com/dotnet/designs/blob/main/proposed/swift-interop.md#structsvalue-types
I conducted an experiment to highlight the differences between outlined structs and to gain a deeper understanding of their semantics. The experiment below evaluated three types of struct: POD/Trivial
, bitwise-takable
, non-trivial non-movable
. It focused on their behavior when passed between functions in the library evolution mode, to determine if they contain "pass-by-value" semantics despite being passed by reference. The "pass-by-value" semantics here refers to creating a local copy on the stack.
In the experiment, structs are passed in doProxy
function:
public func doProxy(_ myStruct: Struct) {
proxy(myStruct)
}
A trivial struct with primitive types. It follows "pass-by-value" semantics, with a local copy stored on the stack when passed to a function.
public struct Point {
var x: Double
var y: Double
}
Assembly code for doProxy
with a trivial struct:
_$s6sample7doProxyyyAA5PointVF:
00000000000039b4 sub sp, sp, #0x30
00000000000039b8 stp x29, x30, [sp, #0x20]
00000000000039bc add x29, sp, #0x20
00000000000039c0 str xzr, [sp, #0x10] ; init struct layout
00000000000039c4 str xzr, [sp, #0x18]
00000000000039c8 ldr d1, [x0] ; load struct properties
00000000000039cc ldr d0, [x0, #0x8]
00000000000039d0 str d1, [sp, #0x10] ; local copy of struct on stack
00000000000039d4 str d0, [sp, #0x18]
00000000000039d8 mov x0, sp ; pass struct by reference as stack allocated value type
00000000000039dc str d1, [sp]
00000000000039e0 str d0, [sp, #0x8]
00000000000039e4 bl _$s6sample11proxyyyAA5PointVF
00000000000039e8 ldp x29, x30, [sp, #0x20]
00000000000039ec add sp, sp, #0x30
00000000000039f0 ret
The struct includes a reference type. It follows "pass-by-value" semantics, with a local copy stored on the stack when passed to a function.
class ReferenceType {
var value: Int
init(value: Int) {
self.value = value
}
}
public struct BitwiseMovableNonTrivial {
var reference: ReferenceType
var value: Int = 10
}
Assembly code for doProxy
with a bitwise-takable struct:
_$s6sample7doProxyyyAA24BitwiseMovableNonTrivialVF:
00000000000036e0 sub sp, sp, #0x30
00000000000036e4 stp x29, x30, [sp, #0x20]
00000000000036e8 add x29, sp, #0x20
00000000000036ec str xzr, [sp, #0x10] ; init struct layout
00000000000036f0 str xzr, [sp, #0x18]
00000000000036f4 ldr x9, [x0] ; load struct properties
00000000000036f8 ldr x8, [x0, #0x8]
00000000000036fc str x9, [sp, #0x10] ; local copy of struct on stack
0000000000003700 str x8, [sp, #0x18]
0000000000003704 mov x0, sp ; pass struct by reference as stack allocated value type
0000000000003708 str x9, [sp]
000000000000370c str x8, [sp, #0x8]
0000000000003710 bl _$s6sample11proxyyyAA24BitwiseMovableNonTrivialVF
0000000000003714 ldp x29, x30, [sp, #0x20]
0000000000003718 add sp, sp, #0x30
000000000000371c ret
The struct includes a weak reference and doesn't follow "pass-by-value" semantics. As such, it is not copied onto the stack, and its reference is copied onto the stack only.
class AnotherClass {
var value: Int
init(value: Int) {
self.value = value
}
}
public struct NonTrivialNonMovable {
weak var weakReference: AnotherClass?
var value: Int = 10
init(reference: AnotherClass) {
self.weakReference = reference
}
}
Assembly code for doProxy
with a non-trivial non-movable struct:
_$s6sample7doProxy8myStructyAA010NonTrivialF7MovableV_tF:
0000000000003734 sub sp, sp, #0x20
0000000000003738 stp x29, x30, [sp, #0x10]
000000000000373c add x29, sp, #0x10
0000000000003740 str xzr, [sp, #0x8]
0000000000003744 mov x8, x0
0000000000003748 str x8, [sp, #0x8] ; local copy of reference on stack
000000000000374c bl _$s6sample11finishProxyyyAA010NonTrivialD7MovableVF
0000000000003750 ldp x29, x30, [sp, #0x10]
0000000000003754 add sp, sp, #0x20
0000000000003758 ret
There is a public API in Swift for POD
and bitwise-takable
types:
/// Returns `true` if type is a POD type. A POD type is a type that does not
/// require any special handling on copying or destruction.
@_transparent
public // @testable
func _isPOD<T>(_ type: T.Type) -> Bool {
return Bool(Builtin.ispod(type))
}
/// Returns `true` if type is a bitwise takable. A bitwise takable type can
/// just be moved to a different address in memory.
@_transparent
public // @testable
func _isBitwiseTakable<T>(_ type: T.Type) -> Bool {
return Bool(Builtin.isbitwisetakable(type))
}
Here is a summary of the public API and projection implementation.
TL;DR: Align with the proposed approach from the design document.
There are several options for projecting enums and structs into C#. Below are the options.
Projecting as C# structs for sequences less than or equal to 4 machine words, and projecting as C# classes for sequences greater than 4 machine words.
This approach implies moving low-level logic into the projection tooling and may lead to flipping between C# structs and classes across different versions, resulting in behavior changes that may not be visible to end-users.
Map frozen Swift structs to C# structs and non-frozen Swift structs to C# classes.
This approach provides a clear distinction between projections and does not impose low-level implementation details. When using the library evolution mode, all non-frozen
structs are passed by reference.
POD/Trivial, bitwise-takable, and non-trivial non-movable structs
This approach is based on memory representation and maps POD/Trivial
and bitwise-takeable
structs to C# structs. The non-trivial non-movable
must ensure the explicit memory handling (see design document) and are mapped to C# classes.
After reviewing different options, the proposal is to combine struct memory representation with frozen attribute (as an implementation feature):
POD/Trivial
and bitwise-takeable
structs as C# structsnon-trivial non-movable
structs as C# classesFor frozen
structs that are POD/Trivial
or bitwise-takeable
, the proposal is to map these frozen
structs with each field treated as a separate local variable into C# structs. In library evolution mode, non-frozen
structs are treated as "opaque" value types. This means the tooling will treat all non-frozen
structs as potentially non-bitwise-movable
and will consult the value witness table during runtime for allocating layout and size. This means that all non-frozen
structs are mapped to C# classes in library evolution mode.
According to the proposed approach, no additional support is required within the runtime at this point.
Please share any comments or concerns regarding the proposed approach. Implementation details will be discussed in subsequent PRs.
Thanks for sharing those examples.
According to the proposed approach, no additional support is required within the runtime at this point.
I think we need both SwiftSelf<T>
and SwiftIndirectResult
to support these.
SwiftSelf<T>
is needed when calling non-mutating instance methods on frozen structs. From what I can see, we should require that such a parameter is the first in the signature. The runtime needs to lower it and pass it in the swift self register if it has more than 4 primitives, and otherwise pass it as if it was a normal struct (in multiple registers)SwiftIndirectResult
is needed to allow the tooling to call functions that return non-frozen structsSwiftSelf
is needed when calling non-mutating instance methods on frozen structs. From what I can see, we should require that such a parameter is the first in the signature. The runtime needs to lower it and pass it in the swift self register if it has more than 4 primitives, and otherwise pass it as if it was a normal struct (in multiple registers)
This is a good idea, but it will require an API review. Before proceeding with the review, I want to ensure the proposed direction is appropriate by implementing an MVP for CrytoKit and other libraries.
SwiftIndirectResult is needed to allow the tooling to call functions that return non-frozen structs
Could you provide an example? Initially, I had the same thought, but I confirmed that the indirect result register is used in assignment expressions and that this should be compatible with arm64/x64 calling conventions, not Swift-specific.
Could you provide an example? Initially, I had the same thought, but I confirmed that the indirect result register is used in assignment expressions and that this should be compatible with arm64/x64 calling conventions, not Swift-specific.
On x64 Swift passes the return buffer in rax
, which is not the same as the underlying calling convention. But furthermore there is no way for the projection tooling to generate a call to a function that returns a non-frozen struct. The tooling needs to allocate a dynamically sized buffer and pass a pointer to it as the return buffer, but there is no way to represent that without SwiftIndirectResult
.
You are correct. The arm64 callconv loads the indirect return result register for value types only. Since we want to project non-frozen structs as C# classes, the tooling needs direct access to the return buffer registers. We will create an API proposal for SwiftSelf<T>
and SwiftIndirectResult
and proceed with implementing the tooling support based on the confirmed public API and its implementation.
Thank you all for providing valuable feedback!
Description
This issue discusses the projection of structs and enums in C#. The primary goal is to validate the design of these projections and implement CryptoKit dev templates.
Here is a CryptoKit template in Swift that we want to implement:
ChaChaPoly
enum includesNonce
andSealedBox
structs and utilizes aSymmetricKey
struct. In Swift, structs and enums are value types allocated on the stack, with type metadata singletons accessible through metadata accessor functions ($Ma
).Structs and enums with a sequence of less than 4 machine words are lowered to a set of registers. Initializers follow the same rules, if the size of the return type is less than 4 machine words, it is passed via registers. For larger structs or enums, Swift utilizes an indirect result location register that contains a memory location where the valuetype is stored. Here is an example of a Swift struct initializer signature in LLVM IR:
The code snippet below illustrates handling of large structs when a direct register pass is not possible due to the size. Before the initializer is called, the
x8
register (indirect result register) is loaded with a memory reference to the struct's location. Within the initializer, the struct reference is saved onto the stack. A local copy of the struct is then initialized on the stack. After that, this local copy is transferred into the struct memory location stored on the stack.In Swift, enums have richer semantics compared to C#. When projecting Swift enums into C#, they can be represented in the same way as structs with a static enum field. This approach should support the expected lowering behavior, while preserving functionality of the enums.
Proposal
There are two distinct projection approaches for structs based on their sequence length: projecting as C# structs for sequences less than or equal to 4 machine words, and projecting as C# classes for sequences greater than 4 machine words.
Projecting Swift structs as C# structs
These structs are passed by value. The layout of the struct is retrieved by projection tooling from metadata nominal type descriptor. The memory management is implicit and handled by the GC. Here is an example is Swift:
And the corresponding projection in C#:
This approach enables calling the constructor and invoking both instance and static methods. It's important to note that instance methods require the self instance in the call context (self) register only when passed by reference, while static methods require metadata in the call context register.
Projecting Swift structs as C# classes
These structs are passed by reference. The layout of the struct is retrieved by projection tooling from metadata nominal type descriptor. The memory management is explicit and handled by implementing
IDIsposable
interface. Here is an example is Swift:And the corresponding projection in C#:
This approach enables calling the constructor and invoking both instance and static methods.
Runtime API
According to the calling convention, the indirect result location uses dedicated registers and is always passed through them when passing types by reference. When calling Swift constructors for types that do not transform to a primitive sequence, the result is returned via an indirect result location register.
This process can be handled either by the runtime itself or through projection tooling.
Implicit handling of indirect result location register
This approach proposes the runtime by handling the indirect result location register implicitly. For structs that cannot be directly passed, the runtime loads the dedicated register with a reference to the struct's memory.
Explicit handling of indirect result location register
This approach proposes introducing a new runtime API for explicit management of the indirect result location register.
This API would be used in the constructors loading passed memory reference to the indirect result location register.
Note: This discussion primarily focuses on struct projections, with the intention to apply similar constructs to enums. Generics are not covered in this current proposal.
/cc: @jkotas @jkoritzinsky @AaronRobinsonMSFT @stephen-hawley @rolfbjarne @vitek-karas