-
Notifications
You must be signed in to change notification settings - Fork 189
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
Review struct and enum projections in C# for CryptoKit dev templates #2533
Comments
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:
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,
Let's create classifications of the swift structs in order to map them cleanly:
Examples:
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:
The actual API in BTfS is:
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:
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? 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. |
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. |
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? |
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? |
Thanks, updated.
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.
Yes, good points. |
Good point. Maybe the proposed handling should be transitioned to the runtime layer to ensure uniform projections.
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. |
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 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 |
I think it would be useful to clearly separate:
|
Projection tooling public APIThis 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 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 ( 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 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. Public API implementationThis is the implementation of public API. Swift structs as C# structsThe 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);
} Swift structs as C# classesThe C# class allocates the same struct layout with explicit memory management by implementing 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 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. |
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.
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. Option 1: Runtime thunk wrappersThe 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 In this case, the projection tolling will have uniform mapping of structs. Option 2: Projections lowering logicThe runtime can support 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. |
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.
I think that we have to add this to |
Sorry, I didn't consider the library evolution mode at first.
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.
Yes, we need to add them to support non-frozen structs and
Primary concerns here are initializers and instance methods for Do you think this is something that should be implemented within the runtime or tooling? If we proceed with the runtime changes, then the |
@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 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 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 // 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 // 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 |
+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? |
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. Since "self" in Swift can be passed by value (does not match .NET) it seems like we need to expand 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.). If we do the lowering in the tooling then I do not think 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. |
I was referring to lowering bits proposed in the projection tooling, such as
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.
Thank you for bringing this up. |
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. CoreGraphics exampleStructures like 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) Date exampleStructs are allocated on the stack. let birthday = Date(timeIntervalSince1970: 0)
let today = Date()
let age = today.timeIntervalSince(birthday) SwiftUI exampleThese 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)
}
} Map exampleStack 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)") URL exampleStack allocated value types. let runtimeURL = URL(string: "https://github.com/dotnet/runtime")
applewe
if let url = runtimeURL {
print("URL: \(url.absoluteString)")
} Time exampleStack allocated value types. import AVFoundation
let time = CMTime(seconds: 1.5, preferredTimescale: 600) Audio exampleThis example involves explicit memory management for the buffer, using 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: In the experiment, structs are passed in public func doProxy(_ myStruct: Struct) {
proxy(myStruct)
} POD/Trivial structsA 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
Bitwise-takable structsThe 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
Non-trivial non-movable structsThe 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
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. Public APIThere are several options for projecting enums and structs into C#. Below are the options.
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.
This approach provides a clear distinction between projections and does not impose low-level implementation details. When using the library evolution mode, all
This approach is based on memory representation and maps After reviewing different options, the proposal is to combine struct memory representation with frozen attribute (as an implementation feature):
Projection implementationFor 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.
I think we need both
|
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.
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 |
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 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
The text was updated successfully, but these errors were encountered: