uniffi-rs icon indicating copy to clipboard operation
uniffi-rs copied to clipboard

Async code doesn't conform to Swift 6 Sendable

Open mhammond opened this issue 8 months ago • 1 comments
trafficstars

Executing UNIFFI_TEST_SWIFT_VERSION=6 cargo test -p uniffi-fixture-futures will fail with various Sendable conformance issues.

mhammond avatar Feb 28 '25 15:02 mhammond

@Sendable Solution

I think I've come up with a preliminary solution to this problem. The error primarily lies in the fact that we are passing a closure to a Task that is not marked as @Sendable. This is clearly incorrect as Task takes a sending closure.

So, we simply mark the closures to be @Sendable.

// Async.swift
{%- if ci.has_async_callback_interface_definition() %}
private func uniffiTraitInterfaceCallAsync<T>(
    makeCall: @Sendable @escaping () async throws -> T,
    handleSuccess: @Sendable @escaping (T) -> (),
    handleError: @Sendable @escaping (Int8, RustBuffer) -> ()
) -> UniffiForeignFuture {
    ...
}
// CallbackInterfaceImpl.swift
static let vtable: ... (
       ...
            let makeCall = {
                {% if meth.is_async() %}@Sendable {% endif %}() {% if meth.is_async() %}async {% endif %}throws -> ... in
                ...
            }
)

Unfortunately, in the makeCall closure, we capture the parameters in the async function defined by the user. That means every parameter must be Sendable. All of the numerical primitives are Sendable, but all of the other types are not. Specifically:

  • RustArcPtr which is converted into UnsafeMutableRawPointer. This should be fine as Arc should imply Sendable
  • RustBuffer
  • RustCallStatus
  • ForeignBytes
  • potentially Callback
  • Reference which is converted to UnsafePointer
  • MutReference which is converted to UnsafeMutablePointer
  • Struct for Records as they are not marked as Sendable

Some of these are pretty simple. RustBuffer, RustCallStatus, ForeignBytes can all easily be marked as Sendable in the bridging header. This is assuming these types are actually guaranteed to be Send + Sync by UniFFI. (One weird thing is that the Docker container's clang test fails on the below because it sees the attribute is invalid. But, the clang test works on my system. I wonder if it needs to be updated)

typedef struct __attribute__((swift_attr("@Sendable"))) RustBuffer
{
    ...
} RustBuffer;

However, the raw pointers are a bit more complicated. Up until recently, these types were marked sendable. The question then becomes how to make those types Sendable. The naive solution would be to make a "newtype" in swift for these types which are @unchecked Sendable.

Additionally, the Callback closures should probably be marked as @Sendable. That however, limits the callback. I am unsure if it would break stuff.

sending Solution

There is one other option, which is to take a sending closure instead. This effectively allows you to send non-Sendable types if you do not use it later outside the closure, much like Rust ownership. This, however, I have not been able to get it to work. While it makes more sense in this situation and is probably the correct solution, I get compiler errors about data races.

Specifically, marking the closures as sending as below, I get the error in constructing the vtable: Sending 'unifyHandleError' risks causing data races. Task-isloated 'uniffiHandleError' is passed as a 'sending' parameter; Uses in callee may race with later task-isolated uses. and a similar error for uniffiHandleSuccess.

private func uniffiTraitInterfaceCallAsync<T>(
    makeCall: sending @escaping () async throws -> sending T,
    handleSuccess: sending @escaping (T) -> (),
    handleError: sending @escaping (Int8, RustBuffer) -> ()
) -> UniffiForeignFuture {
    ...
}

Honestly, I have no idea why this is the case from the error alone. I have some theories, but I wasn't able to confirm them.

TLDR

Apologies for the wall of text, but there's a lot to consider. I think there are two clear paths to integrating with Swift 6: either @Sendable or sending. @Sendable is a stronger claim than sending. However, I see no clear way to implement sending currently.

We can use @Sendable if all the types UniFFI accepts as parameters are Sendable. That would require some rejiggering to wrap pointers in a Sendable type, but is relatively trivial.

If some types are not Sendable, we should be able to use sending, but that imposes some other constraints elsewhere in the API that I was not immediately able to solve. If this is the case, let me know and I can dig deeper into this.

Also let me know if you have any questions.

chris-marrero avatar Apr 03 '25 22:04 chris-marrero

Hey, I'm really sorry this slipped under the radar, I entirely missed the comment - but thanks so much, that is amazing information and research. What a PITA :)

I've no idea of whether all these types are sendable, or tbh, what that means in this context. If the swift compiler doesn't think they are I guess we just need to assure ourselves they are safe to use in the way they are used by generated code - and there's certainly an assumption that is true 😅

I can see how that sendable approach would be easier if it worked, but I kinda suspect you are going to end up in the same place, just coming at it from the other direction?

mhammond avatar Aug 26 '25 01:08 mhammond

Well lets start with the definition of Sendable. According to the documentation:

All of the following can be marked as sendable:

  • Value types
  • Reference types with no mutable storage
  • Reference types that internally manage access to their state
  • Functions and closures (by marking them with @Sendable)

For functions and closures, "Any values that the function or closure captures must be sendable. In addition, sendable closures must use only by-value captures, and the captured values must be of a sendable type." There are a few more caveats, but I think those are all that apply to us.

With Swift 6.2, there is a new type called Span which represents contiguous memory and is sendable if the underlying type is sendable. I think that could be really useful for us and should be used for RustBuffer.

A sending closure is a closure where not all captured objects need to be Sendable. We just need to guarantee that the object is not used after the closure captures the object. We may need to take a dual approach if not all the types we use could be marked as Sendable. Ideally, we guarantee all types as Sendable.

It's been a little while since I've looked at this, so I need to go back into the code to see if Span would help with the references and raw pointers. I will take a look in the next few days and see which types, if any, have outstanding questions on their Sendable conformance.

chris-marrero avatar Aug 26 '25 04:08 chris-marrero

Thanks - I guess what I meant by "in this context" is that I could see the possibility that these types would not be strictly Sendable if they were user-defined types, but are Sendable in practice given they are internal only and their use follows very specific patterns via generated code. For example, their justification for making pointers non-sendable is something I only partially understand, but it seems that for our purposes, working out how to blindly treat them as Sendable in our generated code seems like it would be perfectly fine in practice as their example of how they are unsafe do not apply here?

mhammond avatar Aug 26 '25 15:08 mhammond

Sorry for that wall of words 😅 It was a long way of saying that

The naive solution would be to make a "newtype" in swift for these types which are @unchecked Sendable.

we should maybe just do that?

mhammond avatar Sep 15 '25 02:09 mhammond