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 language features] Improve Swift async by generating single Swift function #2950

Open
9 tasks
kotlarmilos opened this issue Jan 22, 2025 · 0 comments
Open
9 tasks
Assignees
Labels
area-SwiftBindings Swift bindings for .NET User Story A single user-facing feature. Can be grouped under an epic.

Comments

@kotlarmilos
Copy link
Member

Description

The existing approach generate a wrapper Swift function for each async function. The idea is to improve this by implementing a single Swift wrapper function with action parameter to limit the size of Swift code. This will include a single method for all Swift async functions in a module.

Let’s consider a simple Swift async function with suspension point:

void func1() async {
    call void @print()

    // suspension point
    await call void @func2()

    call void @print2()
}

The Swift compiler splits the function into fragments at suspension (await) points:

void func1() async {
    call void @print()

    %async_ctx = call @swift_task_alloc()
    %async_ctx.ResumeInParent = @func1_fragment1

    await call void @func2(%async_ctx)
}

void func1_fragment1(%async_ctx){
    // dealloc task
    // if pending
    //     call void swift_task_switch()
    // if success
    //     call void func1_fragment2()
    //     dealloc task
    // if error
    //     call void func1_fragment3()
    //     dealloc task
}

void func1_fragment2(%async_ctx){
    // handle error
}

void func1_fragment3(%async_ctx){
    call void @print2()
}

To run the function asynchronously, the runtime uses swift_task_create to create a task instance and async context.

To expose a Swift async function as C# Task, we can create a TaskCompletionSource with a callback.

[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvSwift) })]
public unsafe static void callback(IntPtr result)
{
    tcs.SetResult(IntPtr);
}

delegate* unmanaged[Swift]<IntPtr, void> tcsCallback = &callback;
tcs = new TaskCompletionSource<IntPtr>();
swift_task_create(..., tcsCallback);
tcs.Task.Wait();

Since projection tooling doesn’t have control over Swift generated fragments, a Swift wrapper can ensure the callback is invoked at the end of async execution:

func performTask(action: @escaping () async -> Void, onComplete: @escaping (UnsafeRawPointer) -> Void) {
    Task{
        let result = await action()
        onComplete(result)
    }
}

As async function signatures can differ, we aim to minimize Swift wrappers by using a “template” wrapper to ensure the callback is invoked when fragments are executed. The tooling can generate closure contexts for the “template” wrapper that are heap-allocated at runtime. This approach relies on the template that implements the correct control flow logic, while actual arguments are generated and passed via context at runtime.

To do this, each time Swift async function is encountered, projection tooling can generate different closure context and invoke swift_task_create:

AsyncTaskAndContext swift_task_create(
    size_t taskCreateFlags,
    TaskOptionRecord *options,
    const Metadata *futureResultType,
    void *closureEntry, HeapObject *closureContext);

The required parameters for swift_task_create are:

  • taskCreateFlags
  • options (null)
  • Metadata of the result type (void in this example)
  • Async function pointer (to a partial apply forwarder for the reabstraction thunk helper, required for tailcall of fragments, and available in the Swift wrapper)
  • Closure context

The closure context is heap-allocated and structured as:

|------------------|--------|------------|------------------------|-----------------|
| IntPtr           | IntPtr | IntPtr     | IntPtr                 | IntPtr          |
|------------------|--------|------------|------------------------|-----------------|
| closure metadata | null   | result     | async function pointer | closure context |
|                  |        | metadata   | for action closure     | for action      |
|------------------|--------|------------|------------------------|-----------------|

Closure context for action closure:

|------------------|--------|---------|------------------------|-----------------|------------------|-----------------|
| IntPtr           | IntPtr | IntPtr  | IntPtr                 | IntPtr          | IntPtr           | IntPtr          |
|------------------|--------|---------|------------------------|-----------------|------------------|-----------------|
| closure metadata | null   | null    | async function pointer | closure context | C# callback      | closure context |
|                  |        |         | for async function     | (optional)      | pointer          | (optional)      |
|------------------|--------|---------|------------------------|-----------------|------------------|-----------------|

Closures metadata:

|---------------|--------|------|------|--------|
| IntPtr        | IntPtr | i64  | i32  | IntPtr |
|---------------|--------|------|------|--------|
| objectdestroy | null   | 1024 | 16   | null   |
|---------------|--------|------|------|--------|

The objectdestroy is C# function that ensures proper cleanup/release of closure contexts using ARC APIs.

This approach allows us to use a single Swift “template” function for handling async calls, minimizing generated Swift code. Swift Task type will be projected as a separate type which utilize similar logic.

Tasks

  • [Swift language features] Implement Swift wrapper function with a callback parameter
  • [Swift language features] Add swift_task_create and swift_task_alloc to the Swift.Runtime namespace
  • [Swift language features] Construct/read anonymous Swift metadata based on parameters and return type of the function
  • [Swift language features] Construct/read anonymous Swift objectdestroy function to deallocate context's
  • [Swift language features] Construct/read async function pointer
  • [Swift language features] Emit code for context allocation and initialization
  • [Swift language features] Emit TaskCompletionSource callback function that resolves the C# Task
  • [Swift language features] Emit C# async signature that returns a Task
  • [Swift language features] Add functional tests
@kotlarmilos kotlarmilos self-assigned this Jan 22, 2025
@kotlarmilos kotlarmilos added the area-SwiftBindings Swift bindings for .NET label Jan 22, 2025
@kotlarmilos kotlarmilos changed the title [Swift langague features] Improve Swift async by generating single Swift function [Swift language features] Improve Swift async by generating single Swift function Jan 22, 2025
@kotlarmilos kotlarmilos added the User Story A single user-facing feature. Can be grouped under an epic. label Jan 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-SwiftBindings Swift bindings for .NET User Story A single user-facing feature. Can be grouped under an epic.
Projects
None yet
Development

No branches or pull requests

1 participant