Skip to content
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

Swift into .NET using CallConvSwift and UnmanagedCallersOnly calling convention #96059

Closed
kotlarmilos opened this issue Dec 15, 2023 · 6 comments

Comments

@kotlarmilos
Copy link
Member

kotlarmilos commented Dec 15, 2023

Background and Motivation

For interop developers calling from Swift into .NET, it is essential to have the ability to access the self register and set the error register. Projection tools will be responsible for ensuring the correct usage of these registers, but at the runtime layer, it should be possible to access them directly. The idea is to leverage the existing API proposal and treat UnmanagedCallersOnly method as a Swift-like method capable of handling self and error registers. This involves reading the self register value in the method prolog and storing it in the SwiftSelf argument. Additionally, in the method epilog, this involved loading the SwiftError* value into the error register.

Example

The proposed example code aims to test delegates and Swift calls into the .NET. The example utilizes the self and context registers as proxies to pass a value. Here's how it works:

  1. The C# code creates a SwiftSelf instance with an integer value
  2. It calls a Swift function, passing in a delegate, SwiftSelf, and Swifterror* arguments
    • The Swift function is not a method instance and does not throw any exceptions
    • The delegate represents an UnmanagedCallersOnly C# method
  3. The Swift function calls into .NET using the callback that accepts SwiftSelf and SwiftError* arguments
    • In the method prolog of the callback, the SwiftSelf context pointer is retrieved from the register and stored in the argument
  4. The callback sets the SwiftError* argument to point to the same value
    • In the method epilog of the callback, the SwiftError* is loaded into the error register
  5. When the Swift function returns to C#, the runtime will retrieve the error register and set the argument value accordingly
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Swift;
using Xunit;

public class UnmanagedCallersOnlyTests
{
    private const string SwiftLib = "libSwiftUnmanagedCallersOnly.dylib";

    [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
    [DllImport(SwiftLib, EntryPoint = "$s25SwiftUnmanagedCallersOnly26nativeFunctionWithCallbackyyyyXEF")]
    public static extern unsafe IntPtr NativeFunctionWithCallback(delegate* unmanaged[Swift]<SwiftSelf, SwiftError*, void> callback, SwiftSelf self, SwiftError* error);

    [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvSwift) })]
    private static unsafe void ProxyMethod(SwiftSelf self, SwiftError* error) {
        int value = *(int*)self.Value;
        Console.WriteLine ("ProxyMethod: {0}", value);
        *error = new SwiftError(self.Value);
    }

    [Fact]
    public static unsafe void TestUnmanagedCallersOnly()
    {
        int expectedValue = 42;
        SwiftSelf self = new SwiftSelf((IntPtr)(&expectedValue));
        SwiftError error;

        NativeFunctionWithCallback(&ProxyMethod, self, &error);

        int value = *(int*)error.Value;
        Assert.True(value == expectedValue, string.Format("The value retrieved does not match the expected value. Expected: {0}, Actual: {1}", expectedValue, value));
    }
}

Corresponding Swift code.

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

public func nativeFunctionWithCallback(_ callback: () -> Void) {
    callback()
}

The idea is to iterate on this proposal and receive feedback before updating the design document.

/cc @dotnet/jit-contrib @dotnet/interop-contrib @lambdageek @rolfbjarne @stephen-hawley @matouskozak @SamMonoRT

@kotlarmilos kotlarmilos added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.InteropServices labels Dec 15, 2023
@kotlarmilos kotlarmilos added this to the 9.0.0 milestone Dec 15, 2023
@kotlarmilos kotlarmilos self-assigned this Dec 15, 2023
@ghost
Copy link

ghost commented Dec 15, 2023

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and Motivation

For interop developers calling from Swift into .NET, it is essential to have the ability to access the self register and set the error register. Projection tools will be responsible for ensuring the correct usage of these registers, but at the runtime layer, it should be possible to access them directly. The idea is to leverage the existing API proposal and treat UnmanagedCallersOnly method as a Swift-like method capable of handling self and error registers. This involves reading the self register value in the method prolog and storing it in the SwiftSelf argument. Additionally, in the method epilog, this involved loading the SwiftError* value into the error register.

Proposed API

The proposed example code aims to test delegates and Swift calls into the .NET. The example utilizes the self and context registers as proxies to pass a value. Here's how it works:

  1. The C# code creates a SwiftSelf instance with an integer value
  2. It calls a Swift function, passing in a delegate, SwiftSelf, and Swifterror* arguments
    • The Swift function is not a method instance and does not throw any exceptions
    • The delegate represents an UnmanagedCallersOnly C# method
  3. The Swift function calls into .NET using the callback that accepts SwiftSelf and SwiftError* arguments
    • In the method prolog of the callback, the SwiftSelf context pointer is retrieved from the register and stored in the argument
  4. The callback sets the SwiftError* argument to point to the same value
    • In the method epilog of the callback, the SwiftError* is loaded into the error register
  5. When the Swift function returns to C#, the runtime will retrieve the error register and set the argument value accordingly
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Swift;
using Xunit;

public class UnmanagedCallersOnlyTests
{
    private const string SwiftLib = "libSwiftUnmanagedCallersOnly.dylib";

    [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSwift) })]
    [DllImport(SwiftLib, EntryPoint = "$s25SwiftUnmanagedCallersOnly26nativeFunctionWithCallbackyyyyXEF")]
    public static extern unsafe IntPtr NativeFunctionWithCallback(delegate* unmanaged[Swift]<SwiftSelf, SwiftError*, void> callback, SwiftSelf self, SwiftError* error);

    [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvSwift) })]
    private static unsafe void ProxyMethod(SwiftSelf self, SwiftError* error) {
        int value = *(int*)self.Value;
        Console.WriteLine ("ProxyMethod: {0}", value);
        *error = new SwiftError(self.Value);
    }

    [Fact]
    public static unsafe void TestUnmanagedCallersOnly()
    {
        int expectedValue = 42;
        SwiftSelf self = new SwiftSelf((IntPtr)(&expectedValue));
        SwiftError error;

        NativeFunctionWithCallback(&ProxyMethod, self, &error);

        int value = *(int*)error.Value;
        Assert.True(value == expectedValue, string.Format("The value retrieved does not match the expected value. Expected: {0}, Actual: {1}", expectedValue, value));
    }
}

Corresponding Swift code.

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

public func nativeFunctionWithCallback(_ callback: () -> Void) {
    callback()
}

The idea is to iterate on this proposal and receive feedback before updating the design document.

/cc @dotnet/jit-contrib @dotnet/interop-contrib @lambdageek @rolfbjarne @stephen-hawley @matouskozak @SamMonoRT

Author: kotlarmilos
Assignees: kotlarmilos
Labels:

api-suggestion, area-System.Runtime.InteropServices

Milestone: 9.0.0

@AaronRobinsonMSFT
Copy link
Member

@kotlarmilos I don't understand this proposal. The entire point of #64215 and the location of the new type means that it can be used with UnmanagedCallConvAttribute. What is the new API this issue is proposing?

@kotlarmilos
Copy link
Member Author

Correct. The purpose of this issue is to explore the possibilities of utilizing the existing Swift API for Swift into .NET scenarios. It has not been added to the design document yet, and the intention is to discuss it here before implementing any changes there.

@kotlarmilos kotlarmilos removed the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Dec 18, 2023
@jkotas
Copy link
Member

jkotas commented Dec 18, 2023

Do you see any problematic areas to discuss?

I think we just need to edit in the design doc to make it clear that both P/Invoke and reverse P/Invoke are in scope. It has been mentioned in #64215 already: "The proposal would be to add built-in support for the Swift ABI via P/Invokes and Reverse P/Invokes".

@kotlarmilos
Copy link
Member Author

kotlarmilos commented Dec 18, 2023

Not really, I wasn't sure at first if we would be able to achieve the intended behavior with the reverse P/Invoke. If the sample above is correct, I suggest to close this issue and create a PR that adds more context to the design document and expands the runtime tests.

@kotlarmilos
Copy link
Member Author

dotnet/designs#309

@github-actions github-actions bot locked and limited conversation to collaborators Jan 21, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
Archived in project
Development

No branches or pull requests

3 participants