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.
Thanks! We are standing on the shoulders of giants here.