Swift Concurrency MainActor does not execute when Swift DLL is called from .NET on Windows

Sorry for this AI summary but I am a real person.

I am building a Swift 6 dynamic library (DLL) for Windows that is consumed by a .NET 8 application through a C ABI/PInvoke interface.

The goal is to share a Swift business logic layer between macOS and Windows.

The basic architecture is:

.NET 8 application (C#)
|
| P/Invoke
v
Swift dynamic library (DLL)

The DLL loads successfully and synchronous exported C functions execute correctly.

The issue appears when using Swift concurrency.

A function called from the C# application receives data from a Windows callback and needs to update Swift state that is isolated to the MainActor.

A simplified example:

private func runOnMainActor(_ operation: @escaping @MainActor () -> Void)
{
Task { @MainActor in
operation()
}
}

The Task is created, but the MainActor closure does not execute.

I also tested:

Task.detached
{
await MainActor.run
{
operation()
}
}

The detached task starts, but the MainActor.run closure does not execute.

A normal Swift Task executes correctly. The issue appears specific to getting work scheduled onto the MainActor.

An additional observation:

During initial DLL entry from the calling thread, MainActor.assumeIsolated works. However, later calls originating from other .NET callback threads cannot successfully execute MainActor work.

Environment:

  • Swift 6.3.2
  • Windows ARM64
  • .NET 8 application host
  • Visual Studio 2022
  • Swift dynamic library built with Swift Package Manager

Question:

When Swift concurrency is used inside a Windows DLL hosted by another runtime (.NET in this case), is the MainActor expected to be available automatically?

Does MainActor require the host application to provide a main thread, event loop, or other runtime integration?

What is the recommended pattern for a Swift library that needs actor isolation when called from a foreign runtime such as .NET?

Should a Swift DLL avoid MainActor and provide its own synchronization/executor, or is there a supported way to initialize and use Swift concurrency in this scenario?

Adding, I have a SwiftUI app on macOS that successfully runs all of the core functionality.

Without much knowledge of Windows or .NET, I assume that the culprit here is that the .NET runtime expects the callbacks to be invoked on the main thread.

On non-Darwin platforms, the thread associated with the MainActor ends up not being the main thread because the main thread is effectively destroyed after the MainActor (MainExecutor) is fully initialized.

I believe that if you manually start the main thread's RunLoop, i.e., start it before the first real suspension point, everything should work as expected.

Edit: Since RunLoop is marked as noasync, you have to wrap the call to RunLoop in a synchronous context.

cc @compnerd

1 Like

Thanks, that sounds promising. Could you elaborate on what you mean by “manually start the main thread’s RunLoop” in a .NET-hosted Windows process?

Would this be done from the C# application’s Main() before calling into the Swift DLL, or should it be done from within the Swift DLL during initialization?

If you have a minimal example using RunLoop.current.run() (or similar), that would be very helpful.

Ed

I don't know enough about .NET, but the main thread's RunLoop should be formally started before any real suspension point is reached (since the first synchronous part of a program is executed inline). But since you mentioned that the first MainActor.assumeIsolated executes as expected (calling Thread.isMainThread should return true at this stage), I think starting the RunLoop at DLL entry should work.

Basically, you just need to call one of the run() methods:


import Foundation

@main
enum App {
  static func main() async throws {
    RunLoop.main.run() // Should Work
  }
}

@main
enum App {
  static func main() async throws {
    await Task.yield()

    RunLoop.main.run() // Does not work
  }
}

I’ve run into a very similar issue with Qt, so I’m going to guess it’s the same case here.

If .NET has its own event loop / runtime (I’m not familiar with .NET) then you’d need to make a bridge between Swift Concurrency’s MainActor and .NET’s event loop.

In my case, Qt has a way to post a function to its main event loop with QMetaObject::invokeMethod. In Swift I have a QtExecutor that will build a queue of jobs to run and calls invokeMethod to flush that queue. It means that instead of annotating with MainActor you would annotate it with QtMainActor (which would use the QtExecutor.

There are a couple open source projects that deal with similar issues, such as swift-android-sdk:

And swift-cross-ui:

The solve the problems slightly different ways but the core idea is to hook into the host event loop.

There is a proposal to allow custom global main executors, so AFAIK @MainActor would be able to use whatever bridge you use, but I’m not sure when it’s going to be shipped.

1 Like

As far as I understand, Swift concurrency does not make any guarantees about threads. Even its "main actor isolation" can easily switch to a different thread. This is why it is not compatible with e.g. Windows GUI, because Windows GUI controls only allowed to be accessed from the same thread where they were created. I think your problems with Qt have the same root cause.

We were told "Don't think about threads, think about isolation domains". The real systems like Windows or Qt don't buy this. They were not rewritten in Swift using Swift Concurrency.

I do not think this is true. You can use custom executors to precisely control which threads end up doing the execution of actors. I believe this more or less applies to the main actor as well.

However, there are some circumstances where this does not happen, and I believe they are all just bugs. If you think there is a situation where this does not happen and it is intentional behavior or some other kind of gap, I'd like to know.

1 Like

Not only Swift Concurrency is not easy to comprehend, you are suggesting that an ordinary programmer should modify its internals.

It looks like on non-Darwin platforms the dispatchMain() is called during initialization of Swift Concurrency, which terminates the main thread (no less!), which in turn destroys all things that depend on initial/main/GUI thread.

Probably. But those that require fixing ASAP.

This was absolutely not what I was trying to suggest. I was saying that Swift does, for the most part, ask you to stop thinking about threads. If you run into a situation where the programming model is insufficient, and that happens, it also comes with an API that gives you complete control over the execution behavior of actors. I think this is extremely rare and totally unnecessary for ordinary programmers.

I think it's important to separate the model from the implementation. Implementations can be fixed, but changing models requires way more effort. And I agree that there's a bad bug here, however it is not in my power to prioritize or address.

OK, but I really don't know how to write custom executors and why/if i need to use them.

This is what I already said, and this is gravely wrong when it comes to systems depending on threads.

Can you provide an example that does not depend on a bug?

This depends on what we treat as a "bug". Currently Swift Concurrency (but not Swift in general) is unusable in Windows GUI apps. I would be happy to use Swift on Windows to create GUI apps if those bugs were fixed.

Update:
Sorry for the vague reply. Let me be more clear: I cannot "provide an example that does not depend on a bug". Here's why:

  1. I don't understand very well how to correctly use Swift Concurrency, and any code snippet involving concurrency provided by me may well turn out to be incorrect.
  2. I don't understand your question. An example that "depend on a bug" will demonstrate the bug. What an example should demonstrate otherwise? A solution/workaround? There is no solution or workaround currently other than not to use Swift Concurrency in Windows GUI apps.

In the end, an example is Swift Concurrency + Windows GUI. They are not compatible currently. But this example "depends on a bug". Again, I would be happy if this bug would be fixed.

To be even more clear:

Windows GUI apps depend on GUI thread(s). Swift suggests to stop thinking about threads. Not only this suggestion can screw up Windows GUI in user code, it already screws it up in internal concurrency code.

Well, the bug in concurrency implementation on Windows and Linux is fixable. But the concept of "stop thinking about threads" is not. Real-life systems do depend on threads. And when you write Swift code that interacts with these systems, you have to think about threads.

Hmm, you suggested "custom executors", what they are in the end?

You are far beyond my skill level. What it sounds like is that a C# GUI on Windows calling swift code compiled as a DLL isn’t going to happen.

I’ve been going down the path of removing all @MainActor’s from my Bridge code, but it is sounding that won’t even help.

Ed

RunLoop didn't work?

What it's good for?

I cannot confirm this, waiting for your comments/experience.

Starting the main thread's run loop puts it into an event-processing loop. This prevents the program from immediately exiting and allows the main thread to process various types of events, including certain dispatch events.

For Swift concurrency in particular (where Dispatch is used), if the program's main thread neither has an active run loop nor is otherwise kept alive (for example, dispatchMain() "parks" the main thread), the program would immediately exit.

1 Like

As far as I know (based on a quick read of src/queue.c in libdispatch), dispatch_main() will unconditionally call pthread_exit/_endthreadex, so merely having a main runloop would not be sufficient (it would just destroy the thread out from under the runloop). There'd need to be a way of using a CFMainExecutor (or some Windows-specific executor) instead of a DispatchMainExecutor for mainExecutor inside the runtime. On Darwin that's controlled by the presence of CoreFoundation in the process. I'm not a Windows expert, so I couldn't say what the equivalently appropriate check would be there.

1 Like

For GUI apps, I think you need @MainActor annotations. This should in theory tell Swift that it should use initial/main/GUI thread. In practice though Swift fails to do so (when using concurrency on Linux and Windows[1]). In current state of affairs these annotations would not help. As I see it, the only way to support GUI apps on Windows currently is to not use Swift Concurrency (e.g. await).

Some say that Windows apps do not have main thread. While this is technically true, there is always an initial thread, in which the app starts executing. And for GUI apps there should always be a dedicated thread in which GUI controls are created and accessed. Usually this GUI thread and the initial thread are the same. Swift Concurrency unfortunately messes things up, leaving you in a random thread that is different from both initial and GUI thread.


  1. on Apple platforms this bug is fixed ↩︎

1 Like