Skip to content

Introduce SwiftSelf<T> and SwiftIndirectResult to represent Swift structs and enums in C# #100543

Closed
@kotlarmilos

Description

@kotlarmilos

Background and Motivation

The Swift programming language handles the lifetime of structs and enums differently than .NET. When mapping Swift structs and enums to C# in library evolution mode, frozen Swift structs are directly mapped to C# structs with identical field layouts, while non-frozen Swift structs are mapped to C# classes. To support the projection tooling (#95638) for handling these cases, we need to introduce additional runtime support related to passing and returning structs.

The proposal is to update the built-in support for the Swift ABI and introduce a new Swift-specific type result buffer access mechanism.

Proposed API

According to the Swift calling convention, structs are lowered into a sequence of primitives. If the number of primitives is 4 or less, the struct is passed by value via registers. Otherwise, it is passed by reference in the call context register. The proposal would be to change SwiftSelf to be generic so that a user can pass SwiftSelf<T> for a frozen struct type T which then is either enregistered into multiple registers or passed by reference in the call context register.

Additionally, for Swift functions returning non-frozen structs that are projected into C# classes, it is necessary to load return buffer with a memory address. The proposal would be to add a new Swift type SwiftIndirectResult that would provide access to the return buffer register.

Based on the this, we want to update SwiftSelf to handle frozen structs and add a new Swift type to handle returning non-frozen structs.

namespace System.Runtime.InteropServices.Swift
{
-     public readonly unsafe struct SwiftSelf
+    public readonly struct SwiftSelf<T> where T: unmanaged
     {
-         public SwiftSelf(void* value)
+         public SwiftSelf<T>(T value)
         {
             Value = value;
         }

-         public void* Value { get; }
+         public T Value { get; }
     }
+
+     public readonly unsafe struct SwiftIndirectResult
+     {
+         public SwiftIndirectResult(void* value)
+         {
+             Value = value;
+         }
+
+         public void* Value { get; }
+     }
+ }

Usage Examples

This API would be used by the projection tooling. The SwiftSelf<T> is needed when calling non-mutating instance methods on frozen structs to ensure the correct passing of the value type.

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Swift;

public struct Point
{
    public double X { get; }
    public double Y { get; }

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double DistanceToOrigin()
    {
        var swiftSelf = new SwiftSelf<Point>(this);
        return distanceFromOrigin(swiftSelf);
    }

    [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
    [DllImport("libSwiftPoint.dylib")]
    private static extern double distanceFromOrigin(SwiftSelf<Point> point);
}

The SwiftIndirectResult is required to enable the tooling to invoke functions that return non-frozen structs. The runtime will load the return buffer prior to the call based on the SwiftIndirectResult value. For simplicity, the class does not implement the IDisposable interface.

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Swift;

public class Point
{
    private void* _payload;
    private static readonly int _payloadSize = /* Metadata information from value witness table */;

    public Point(double x, double y)
    {
        _payload = Marshal.AllocHGlobal(_payloadSize).ToPointer();
        try
        {
            PIfunc_init(x, y, new SwiftIndirectResult(_payload));
        }
        catch
        {
            Marshal.FreeHGlobal(new IntPtr(payload));
            throw;
        }
    }

    ~Point()
    {
        if (_payload != null)
        {
            Marshal.FreeHGlobal(_payload);
            _payload = IntPtr.Zero;
        }
    }

    private Point(void* payload)
    {
        _payload = payload;
    }

    public static Point CreateWithMagnitudeAndAngle(double magnitude, double angle)
    {
        void* payload = Marshal.AllocHGlobal(_payloadSize).ToPointer();
        try
        {
            PIfunc_createFromMagnitudeAndAngle(magnitude, angle, new SwiftIndirectResult(payload));
            return new Point(payload);
        }
        catch
        {
            Marshal.FreeHGlobal(new IntPtr(payload));
            throw;
        }
    }

    [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
    [DllImport("libSwiftPoint.dylib", EntryPoint = "PIfunc_init")]
    private static extern void PIfunc_init(double x, double y, SwiftIndirectResult payload);

    [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
    [DllImport("libSwiftPoint.dylib")]
    private static extern void PIfunc_createFromMagnitudeAndAngle(double magnitude, double angle, SwiftIndirectResult payload);
}

Please review the proposal and suggest any updates or changes.

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

Metadata

Metadata

Type

No type

Projects

Status

No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions