Building apps with Skip's native Swift Android toolchain integration (tech preview)

We are excited to share a technology preview of Skip's native Android toolchain integration. Since we announced Skip 1.0 here in August, Skip's transpiler-based approach has enabled the creation of platform-native apps for iOS and Android. Now we've enhanced that with the ability to also integrate compiled Swift into your Android app as well.

You can read about it at Native Swift on Android, Part 2: Your First Swift Android App, which is a follow-up to our earlier introduction to the Android toolchain in Native Swift on Android, Part 1: Setup, Compiling, Running, and Testing. Follow along with the tutorial, and you can experience your compiled Swift code powering both your iOS and your Android app side-by-side, all without leaving the comfort of Xcode.

Screenshot of the Hello Swift native app

This is an early technology preview of the native Android toolchain integration, and despite the documentation covering many of the bases, there are likely to be some gaps in the coverage. We would love to hear from the community – either on this forum, or in our Slack or discussion forums – about your experience with the tooling.

We know that members of this community have been enthusiastic about Swift on Android for a very long time. We are delighted to be able to offer a solution that facilitates the creation of real, shippable apps powered by Swift for both major mobile operating systems – one more step towards a "Swift everywhere" world!

8 Likes

Great, glad to see this! :smiley:

I took a look at your well-written doc and have a couple questions:

  • You say "Building and deploying native Swift using the Android toolchain is slower than building using transpilation, due to overhead with the native compilation and packaging of the shared object files into the .apk." Have you looked into where that is slower and do you have any plans to speed it up?

  • Further, "Note that Skip transpiles your XCTest unit tests into JUnit tests for Android, regardless of whether your module is native or transpiled. This means that for now, you can only perform Android tests on native Swift that has been bridged to Kotlin/Java. Unit tests involving unbridged types should be excluded from Android testing. We will offer Android unit testing of unbridged native code in a future release." What is holding this back? I've been cross-compiling and running native Swift XCTest tests on Android for several years now.

Congratulations to the Skip team on all the work you've done to get this far. :confetti_ball:

1 Like

You say "Building and deploying native Swift using the Android toolchain is slower than building using transpilation, due to overhead with the native compilation and packaging of the shared object files into the .apk." Have you looked into where that is slower and do you have any plans to speed it up?

The template app created with the skip init --appid=… command mentioned in the blog post can be run in either --native mode or the (default) non-native mode, so the exact same template app can be used to provide an apples-to-apples comparison of the two build-launch scenarios.

On my 2021 M1 MacBook pro, the non-native version (where everything is purely transpiled Kotlin) builds and launches on both iOS simulator and Android emulator in 9.9 seconds, of which 7 seconds is the forked Gradle process where the Kotlin is compiled, the APK is repackaged, and the app is transmitted to the emulator and launched.

The native version takes 21.4 seconds to launch the two apps, of which Gradle is 15 seconds. The rebuild of the Swift package using the Android toolchain takes 8.19 seconds of that build time, some of which is due to the SkipStone plugin re-generating the bridging interfaces between Swift and Kotlin. The rest of it is likely due to the need to package all the native Swift dependencies (libSwiftCore.so, libSwiftFoundation.so, etc.) into the APK, and simply shuttling those bytes onto the emulator.

Reducing the size of the APK would likely help quite a bit. One avenue might be static compilation (which I notice you had been looking into), which ought to reduce the size of the bundled native libraries that need to be packaged with the APK. Another option I've been considering could be to perform strategic thinning of shared object files that are unused by the Swift code. E.g., if the Swift code doesn't use any internationalization, then we could drop the 40M lib_FoundationICU.so that is currently always being bundled. This would likely involve peeking into the DT_NEEDED sections of the ELF or some other mechanism to figure out what is actually used.

So there are some potential options for future research. But in the end, the addition of the native toolchain necessitates that the same Swift needs to be cross-compiled twice (once for iOS and once for Android), and on the Android side it additionally needs to be packaged in a way that includes all the dependencies that are available "for free" on iOS.

I'm also curious about how much these times would be affected by a newer development machine with an M4 processor. Maybe Santa will bring me one.

Further, "Note that Skip transpiles your XCTest unit tests into JUnit tests for Android, regardless of whether your module is native or transpiled. This means that for now, you can only perform Android tests on native Swift that has been bridged to Kotlin/Java. Unit tests involving unbridged types should be excluded from Android testing. We will offer Android unit testing of unbridged native code in a future release." What is holding this back? I've been cross-compiling and running native Swift XCTest tests on Android for several years now.

Yes, for pure Swift packages, running XCTest works fine against Android. We even have special tooling for this that we discuss in part 1 of the blog series: running skip android test will build a package's test cases, copy them over to the connected device/simulator, run them, and report the results. This is what is used by our swift-android-action GitHub action to facilitate testing packages as part of a cross-platform CI workflow (swift-sqlite, for example).

Where we don't yet have tooling support is testing the Swift package within the context of a Java environment, so that the bridging from Swift to Kotlin can be exercised and validated easily. Running the raw Swift in an XCTest is useful for testing the pure Swift functionality, but when you need to do testing against the Android Java SDK or Kotlin libraries, it needs to happen within within the context of a full APK that is launched via a Zygote.

Testing within a Java context can be done using standard Android testing tools (or using the Robolectric shims when running locally), and we do support testing the Kotlin-to-Swift side of the SkipFuse bridge when running this way, because in these scenarios we run the transpiled test cases. But testing the Swift-to-Kotlin bridge is trickier, because we need the transpiled test cases to first bridge into some Swift test shims so that they can bridge back out to the Java/Kotlin APIs that we want to test. And we actually do this in the validation testing for SkipBridge (e.g., SkipBridgeToSwiftSamplesTestsSupport), but it requires a lot of manual convoluted setup and intermediate modules. This is the part that we hope to simplify in the future, ideally by running non-transpiled native tests as part of the testing process.

Congratulations to the Skip team on all the work you've done to get this far. :confetti_ball:

Thanks! We are standing on the shoulders of giants here.

2 Likes

Significantly, according to the benchmarks, :wink: so much so that I'm considering picking one up myself. Obviously, speeding up cross-compilation is a giant effort that many people work on, but it is good to hear you are looking into speeding up the subsequent packaging step also. As you say, static linking may really help there, as might telling Skip users to be mindful of the new FoundationEssentials/FoundationInternationalization split in Swift 6.

OK, I had not considered that additional usecase you are now enabling. If the existing tools for testing such mixed native/JVM code, which I've never used, require providing a native shared library, it should be pretty easy to modify SwiftPM to do that.

Otherwise, if you need to roll your own testing using adb, my Android CI currently installs an Android apk to the emulator and runs the NIO tests using the permissions for that app: you could probably do something similar to build your own testing workflow. I simply do that to work around a minor permissions issue in the Android emulator and invoke no Java or any other code from that app, but you could do something similar and extend it for your hybrid testing needs.

Looking forward to hearing how your work progresses, and I'm sure many people are now following you, considering how your HN Q&A blew up a couple months ago. :smiley: