Skip to content

Link libdispatch for Swift Concurrency on Android - #1830

Merged
keith merged 2 commits into
bazelbuild:mainfrom
AttilaTheFun:android-concurrency-autolink
Jul 6, 2026
Merged

Link libdispatch for Swift Concurrency on Android#1830
keith merged 2 commits into
bazelbuild:mainfrom
AttilaTheFun:android-concurrency-autolink

Conversation

@AttilaTheFun

Copy link
Copy Markdown
Contributor

Follow-up to #1818. Swift Concurrency's global executor on Android lives on libdispatch, but the dependency comes from C++ objects inside libswift_Concurrency.a, so autolink extraction never sees it. Any async Swift linked with the Android Swift SDK currently fails with:

ld.lld: error: undefined symbol: dispatch_main
ld.lld: error: undefined symbol: _dispatch_main_q
ld.lld: error: undefined symbol: dispatch_assert_queue
...

unless the consumer adds -ldispatch -lBlocksRuntime to every binary themselves.

This adds both to the generated Android toolchain's linkopts, next to the existing -lswiftCore:

  • the archives ship in the SDK bundle's swift_static-<arch>/android directory, which is already on -L;
  • lld only extracts referenced archive members, so binaries that don't use concurrency are unaffected;
  • (they're also absent from the SDK's static-stdlib-args.lnk, so this isn't covered by the flags that file mirrors.)

Verified against a real async consumer (an async Swift module behind JNI): without this change it fails with the errors above; with --override_module=rules_swift pointed at this branch it links clean with no consumer-side flags. //examples/cross_compilation/android_app/... still builds.

Swift Concurrency's global executor on Android lives on libdispatch, but
the dependency comes from C++ objects inside libswift_Concurrency.a, so
autolink extraction never sees it: any async Swift linked with the Android
Swift SDK fails with

    ld.lld: error: undefined symbol: dispatch_main
    ld.lld: error: undefined symbol: _dispatch_main_q
    ...

unless the consumer adds -ldispatch -lBlocksRuntime themselves. Add both
to the generated Android toolchain's linkopts: the archives ship in the
SDK's swift_static-<arch>/android directory that is already on -L, and lld
only extracts referenced members, so binaries that don't use concurrency
are unaffected.
@keith

keith commented Jul 6, 2026

Copy link
Copy Markdown
Member

can you add a unit test for android that shows Swift that uses these deps links? also shouldn't this be coming from the autolinking behavior instead? how does this affect binary size otherwise?

greetingAsync + a Task in the JNI entry point make app_build_test a
link-time regression test for the concurrency linkopts: without
-ldispatch -lBlocksRuntime it fails with undefined dispatch symbols.
@AttilaTheFun

Copy link
Copy Markdown
Contributor Author

All three, in order:

Test: added — the android example's Greeter now has an async API and the JNI entry point spawns a Task, so the existing app_build_test is the link regression. On main it fails with undefined symbol: _dispatch_queue_attr_concurrent / dispatch_queue_create / …; with this change it links.

Why autolinking doesn't cover it: this toolchain does use autolink extraction (swift.use_autolink_extract), but that only collects the .swift1_autolink_entries Swift emits into its own object files — which is how -lswift_Concurrency itself gets pulled in. The dispatch dependency originates one level deeper, in the C++ objects inside libswift_Concurrency.a (the concurrency runtime's global executor), and C++ objects carry no autolink entries — an archive can't declare its own transitive link deps. It's also not in the SDK's static-stdlib-args.lnk that this linkopts list mirrors.

Binary size when unused: zero. I built the (pre-async) example APK with and without the flags — verified via aquery that the two link command lines genuinely differ — and the emitted libSwiftJNI.so is byte-identical (9,016,672 bytes, cmp clean). lld only extracts referenced archive members, so a binary with no concurrency pulls in nothing from either archive.

Given the zero cost I don't think this needs to be feature-gated — and anyone who does want different behavior can register their own toolchain, since root-module registrations take precedence.

@AttilaTheFun
AttilaTheFun marked this pull request as ready for review July 6, 2026 20:28
@AttilaTheFun

Copy link
Copy Markdown
Contributor Author

For completeness, the cost when concurrency is used (same example, async version): the .so goes 9,016,672 → 10,266,624 bytes, so adopting async costs ~1.2 MiB total. Of that, only ~233 KiB are symbols extracted from libdispatch.a/libBlocksRuntime.a (measured with llvm-nm --print-size over the final .so; lld takes roughly 40% of libdispatch.a) — the other ~1 MiB is libswift_Concurrency + libswiftDispatch, which autolinking already pulls in for any async code independent of this change. And without the ~233 KiB of dispatch the binary doesn't link at all, so it's the floor price of Swift Concurrency on Android rather than overhead added by this PR.

@keith
keith enabled auto-merge (squash) July 6, 2026 20:37
@keith
keith merged commit c6cad94 into bazelbuild:main Jul 6, 2026
15 checks passed
@AttilaTheFun
AttilaTheFun deleted the android-concurrency-autolink branch July 6, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants