C-Introp & Strict Concurrency

I have several C modules in my project. They use delegates and @convention(c) function pointers. I'm having a hard time migrating my project to Strict Concurrency because of these.

I started by just putting everything on the MainActor, as all that code is already on the main thread. But I can't seem to find a way to work with my C modules.

Are there any Swift attributes for C-Interop that allow specifying, safe or unsafely, that these C functions are MainActor? Adding @MainActor to a C function pointer seems to turn the function into a different type altogether, or the compiler is confused.

Some of these C function pointers are local package targets, but others are Windows and Linux system libraries.

@MainActor is part of the function type, making it a different type, yeah. I can pretty easily imagine that we're more restrictive with @MainActor conversions on @convention(c) functions than we need to be, though, or that we don't support @MainActor @convention(c) as well as we should in the importer. Can you give specific examples of what isn't working?

Sure, so one example is this:

// Swift C Function pointer that needs to be @MainActor
internal func unittestCallback(
    vm: OpaquePointer!,
    errorType: error_type_t,
    desc: UnsafePointer<CChar>?,
    note: UnsafePointer<CChar>?,
    value: gravity_value_t,
    row: Int32,
    column: Int32,
    xdata: UnsafeMutableRawPointer?
) {
    // Existing MainActor code that must run synchronously
    Gravity.unitTestExpected = Gravity.Testing(
        description: String(cString: desc!),
        errorType: errorType,
        row: row,
        column: column,
        value: value
    )
}

And that C Function Pointer gets assigned to a C variable:

delegate.unittest_callback = unittestCallback(vm:errorType:desc:note:value:row:column:xdata:)

So the problem I'm having is the C Function isn't MainActor but my Swift code needs to be MainActor to comply with strict concurrency in the rest of my project.

This is just the first example the compiler diagnostics pushed me toward, and that one might be rework-able, but I can think of a couple identical scenarios I'll run into that are not rework-able.
macOS DisplayLink API uses a similar method for VSync callbacks.
Windows uses a similar method for window events like user input, resizing, etc.

Do you control the declaration of unittest_callback?

For that particular example, yes; but I wouldn't be able to modify Win32 or Quartz(? I think is DisplayLink on macOS)

Are there Clang attributes for swift concurrency features?

Well, you should be able to use __attribute__((swift_attr("@MainActor"))) on the function type to get it to import as a @MainActor function type. I'm trying to figure out the exact details of that now. :)

1 Like

Oh that would be perfect! I might be able to use that to solve the other problems on the C side too.

This seems to work:

@protocol P
@property (assign) void (*__attribute__((swift_attr("@MainActor"))) callback)(void);
@end
@MainActor func foo() {}
func test(p: any P) {
  p.callback = { foo() }
}

You might have to toy around to find the right place to put the attribute on different kinds of declarations.

2 Likes

Awesome. I'll see how many problems I can solve this way.
Thank you :sunglasses:

1 Like

SourceKit does identify the C Function as MainActor with the Clang attribute, but the compiler continues to see it without.

I did do a Clean, "Clear All Issues", and Quit/Restart Xcode just to be sure.

Truncated for convenience:

@MainActor 
(Optional<OpaquePointer>, error_type_t, Optional<UnsafePointer<Int8>>, Optional<UnsafePointer<Int8>>, gravity_value_t, Int32, Int32, Optional<UnsafeMutableRawPointer>) -> ()
to 
@convention(c) 
(Optional<OpaquePointer>, error_type_t, Optional<UnsafePointer<Int8>>, Optional<UnsafePointer<Int8>>, gravity_value_t, Int32, Int32, Optional<UnsafeMutableRawPointer>) -> ()

I feel like this error is happening before the type check. Is it possible this diagnostic is not yet accounting for global actors?

A C function pointer can only be formed from a reference to a 'func' or a literal closure

Yeah, I saw that same diagnostic when I initialized the C function pointer with something other than a closure. I just figured I was missing something special in the invocation, but maybe it's a special problem. Definitely worth a bug.

The importer seems to honor imported @MainActor attributes when strict concurrency is enabled but not otherwise. I don't know if there's a more fine-grained option to control it. @hborla, does this sound familiar?

It does sound familiar! I don't think it's the importer, I think it's a consequence of how @preconcurrency works under minimal checking. Here's an example:

// -swift-version 5 -strict-concurrency=minimal

@preconcurrency typealias Callback = @MainActor () -> Void

func callback(_: Callback) {}

@MainActor func requiresMain() {}

func useCallback() {
  callback(requiresMain) // warning: Converting function value of type '@MainActor () -> ()' to '() -> Void' loses global actor 'MainActor'
}

The way that @preconcurrency works under minimal checking is the type checker will strip concurrency-related annotations off of types to accomplish diagnostic suppression (for example, see adjustTypeAliasTypeInContext in TypeCheckType.cpp to explain the example above).

This behavior is specified in SE-0337:

However, if we add @preconcurrency to the declaration of doSomethingThenFollowUp , its type is adjusted to remove both the @MainActor and the @Sendable , eliminating the errors and providing the same type inference from before concurrency was adopted by doSomethingThenFollowUp . The difference is visible in the type of doSomethingThenFollowUp in a minimal vs. a strict context

However, this can lead to confusing diagnostics in cases like the above, and I've seen other reports of @Sendable warnings that exist under minimal checking but not under complete checking:

// -swift-version 5 -strict-concurrency=minimal
struct S {
  @preconcurrency static let callback: (@Sendable () -> Void) = {}
}

func requiresSendableCallback(_ callback: @Sendable () -> ()) {}

func test() {
  requiresSendableCallback(S.callback) // warning: Converting non-sendable function value to '@Sendable () -> ()' may introduce data races
}

Personally, I think a better approach is to preserve @preconcurrency as a type attribute, which will then cause the actor isolation checker to downgrade errors to warnings or suppress them entirely.

EDIT: Note that this is related to the importer because all imported C-family declarations are treated as @preconcurrency.

1 Like