Skip to content
Skip
3k

Blog

Sharing C code between Swift and Kotlin for iPhone and Android apps

Swift — true to its name — is a fast language. It is just about as close-to-the-metal as any higher level programming language can be. Stack-allocated value types make efficient use of resources, and the lack of garbage collection means that memory allocation and deallocation is predictable, with controllable memory watermarks and minimal overhead from reference counting. Swift code is compiled down to architecture-specific machine instructions before being packaged into the app.ipa archive that is ultimately distributed to end users for installation on their iPhone.

Java (and, therefore, Kotlin) is not quite as fast or as efficient with memory as Swift. When a Kotlin Android app is built using gradle or an IDE, it processes the source code into Java bytecode, which is an intermediate representation of the program’s instructions. Next, the Java bytecode is re-processed into Dalvik Executable (DEX) bytecode for packaging into an app.apk for distribution to Android devices. Finally, when a user installs the app, the DEX is converted on the device into architecture-specific instructions, which is what is ultimately run during the program’s execution. The app’s code is run within Android’s managed runtime (ART), which provides automatic memory management using garbage collection (GC).

Skip is a tool that transpiles Swift into Kotlin, enabling the creation of SwiftUI apps for both iOS and Android. The transpiled Kotlin code maintains Swift’s semantics and behavior. But since Java lacks true value types, Swift struct and enumeration types can only be translated into Kotlin classes; this means that they will always be allocated on the heap and subject to indeterministic garbage collection, just like any other Java object. These, and other factors, contribute to the difference in performance and efficiency between the two languages.

Despite the difference in performance characteristics, both these languages are generally “fast enough” for app development. Day-to-day app development in the real world1 typically involves building and tweaking screens of controls and implementing designs, shuffling data between local storage and network services, and generally managing the interaction between a human user and some backend business logic. Rarely do language-level performance limitations become a consideration to a developer who is building an app at this level.

There are, however, many cases where the absolute optimal performance is critical for the app experience to be acceptable. Compression and encryption algorithms, database and format encoders and decoders, and anything to do with real-time video or audio: these are the sorts of things that need the barest-of-metal, lowest-level language there is: C. Nothing, short of hand-written assembly, beats C in terms of capacity for raw performance. And so it is the choice for nearly all the low-level components that are used in mobile operating systems today: the embedded SQLite database, the cryptography libraries, video and audio processing, are all implemented in C on both Android and iOS. Higher-lever wrappers are then built around these C interfaces to make them easy to consume by framework and app developers.

Swift integrates wonderfully with C. Many popular Swift projects use this C integration to great effect, such as the Yams parser embedding the libYAML C library, and the swift-cmark parser wrapping the cmark C library. Parsing YAML and markdown from Swift is so blazing fast because it isn’t implemented in Swift.

Adding your own C code to a Swift project is simple2. Add a new LibCLibrary Swift module and add a Sources/LibCLibrary/include/demo.h header file:

double demo_compute(int n, double a, double b);

and a Sources/LibCLibrary/src/demo.c source file:

#include "demo.h"
double demo_compute(int n, double a, double b) {
double result = 0.0;
for (int i = 0; i < n; i++) {
result += (a * b) / (a + b + i);
}
return result;
}

then add a Sources/LibCLibrary/CMakeLists.txt3 file to bring it all together:

cmake_minimum_required(VERSION 3.1)
project(cproject, LANGUAGES C)
file(GLOB SOURCES src/*.c)
add_library(clibrary SHARED ${SOURCES})
include_directories(clibrary PUBLIC include)

And you now have a C project that can be built with the Swift Package manager.

Next link to the library from a SkipCDemo Swift module, and you can call the C code directly from your Swift code, such as this test case:

XCTAssertEqual(105.95723590826853, demo_compute(n: 1_000_000, a: 2.5, b: 3.5))

Voilà! You’ve now unlocked the full potential for high-performance C libraries on the Swift side of your app. More information on Swift’s C integration can be found at https://developer.apple.com/documentation/swift/using-imported-c-functions-in-swift.

Xcode has built-in support for C. It provides syntax highlighting, auto-completion of function and type names, doc-comment lookup, and inline error reporting. If you are comfortable writing in a memory-unsafe language, Xcode is a productive way to develop cross-platform C code. And developing it alongside a higher-level Swift wrapper with fast unit testing cycles is an effective way to iterate on library development.

Screenshot of Xcode with C project

Smooth Swift/C interoperability is all well and good, but Skip is a tool for making dual-platform apps for both iOS and Android. What of the Kotlin/Java side? Swift’s automatic C interoperability is obviously never going to just work from Java-derived bytecode in the same way as it does from compiled Swift.

However, with runtime support from the SkipFFI framework, the C interface that developers enjoy from the Swift side can be — with some caveats — automatically converted into Kotlin code that interfaces the same way with the native C libraries on Android. It gets close to Swift-level C integration by leveraging the Java Native Access library (JNA), which is a venerable open-source Java library that enables C integration using foreign-function interface (FFI) techniques4. JNA runs atop the standard Java Native Interface (JNI) that provide the ability to pass data and invoke functions between Java and native-compiled languages.

The SkipFFI package uses JNA to provide support for creating native-aware wrapper libraries. The sample project’s wrapper SkipCDemo.swift looks like this:

import Foundation
import SkipFFI
#if !SKIP
import LibCLibrary
#endif
/// `DemoLibrary` is a Swift encapsulation of the embedded C library's functions and structures.
internal final class DemoLibrary {
/* SKIP EXTERN */ public func demo_compute(n: Int32, a: Double, b: Double) -> Double {
return LibCLibrary.demo_compute(n, a, b)
}
/// Singleton library instance
static let instance = registerNatives(DemoLibrary(), frameworkName: "SkipCDemo", libraryName: "clibrary")
}

On the Swift side the DemoLibrary functions are merely passed directly through to the same-named C functions. For the Kotlin side, Skip sees the /* SKIP EXTERN */ statements, and elides the function bodies from the transpiled Kotlin, resulting in a SkipCDemo.kt like:

package skip.cdemo
import skip.lib.*
import skip.foundation.*
import skip.ffi.*
/// `DemoLibrary` is a Swift encapsulation of the embedded C library's functions and structures.
internal class DemoLibrary {
external fun demo_compute(n: Int, a: Double, b: Double): Double
companion object {
/// Singleton library instance
internal val instance = registerNatives(DemoLibrary(), frameworkName = "SkipCDemo", libraryName = "clibrary")
}
}

These extern functions (which need to be public) are what JNA uses to match the functions with the corresponding C functions that it loads from the compiled dynamic library. The result is that the same interface is presented to both the Swift and Kotlin sides of your apps, using identical calling conventions.

In addition to handling the expected primitives arguments and return types, JNA also handles converting between C strings and Java strings. Since Swift strings are treated as Kotlin strings by Skip, and since Kotlin strings are just aliases to Java strings, string arguments and return types generally work transparently when zero-terminated C string conventions are followed.

On top of all this, the SkipFFI package provides some stand-in mappings for the Swift C interoperability types. For example, the Swift.OpaquePointer5 type will be represented by a typealias to the com.sun.jna.Pointer6 type. This is generally enough to create a portable Swift wrapper around a C type that is represented by a pointer.

Beyond functions, JNA has support for mapping structs and union types, as well as working with low-level shared memory. These are not yet directly supported by Skip (see “Future Work” below), but they can all be overcome by embedding Kotlin directly in #if SKIP / #endif blocks.

SkipFFI is how the the SkipSQL framework interfaces with the vendored SQLite API on Android and iOS. It is also how SkipScript works with JavaScriptCore, which is included with iOS but needs to be shipped as an additional library with Android apps. Regardless of whether the dependent library is included with the operating system or shipped separately, the mechanism for interacting with it will be the same. So If you already have pre-built native libraries for iOS and/or Android that you can include in your project, then SkipFFI is all you need to be able to access them from the Kotlin side.

However, if you want to include C code directly in your project, you’ll need to build that code for the Android side and package it in a way that makes it accessible at runtime, in the same way as SwiftPM handles building and packing it for iOS.

Compiling C into Android apps with the NDK

Section titled “Compiling C into Android apps with the NDK”

As of release 0.7.44, the Skip transpiler will generate a gradle project that will cross-compile your C project to each of the four Android native architectures and embed it in the resulting app.apk. Skip detects native support by checking for the following declaration in the target module:

cSettings: [.define("SKIP_BUILD_NDK")]

This causes Skip to create a build.gradle.kts project that uses the externalNativeBuild clause, directing it to use the CMakeLists.txt to build the native libraries for each of the supported Android NDK targets.

The generated build.gradle.kts project will look something like this:

plugins {
id("com.android.library") version "8.1.0"
}
android {
namespace = group as String
compileSdk = 34
defaultConfig {
minSdk = 29
}
externalNativeBuild {
cmake {
path = file("ext/CMakeLists.txt")
}
}
}

When building and testing the project, the Gradle project will be generated for the native library, which will instruct gradle to build the project with each of the four separate Android Native Development Kit (NDK) toolchains. The first time you do this, the toolchains will be automatically downloaded and installed as part of the build process, so you should expect the initial build to take a much longer time than subsequent builds.

As you can see in the Xcode Console area, when running the skip-c-demo test cases (and thereby running the transpiled Kotlin tests via gradle), the native compilation steps look like:

GRADLE> > Task :LibCLibrary:buildCMakeDebug[arm64-v8a]
GRADLE> > Task :LibCLibrary:buildCMakeDebug[armeabi-v7a]
GRADLE> > Task :LibCLibrary:buildCMakeDebug[x86]
GRADLE> > Task :LibCLibrary:buildCMakeDebug[x86_64]
GRADLE> > Task :LibCLibrary:mergeDebugJniLibFolders

The intermediate LibCLibrary.aar archive will correspondingly contain the embedded compiled shared object files:

classes.dex
lib/arm64-v8a/libclibrary.so
lib/armeabi-v7a/libclibrary.so
lib/x86/libclibrary.so
lib/x86_64/libclibrary.so
AndroidManifest.xml
resources.arsc

Each of these shared object files will be distributed as part of the final app.apk, although note that if you distribute your app as part of the an Android Bundle, then the app storefront may be able to thin out unused architectures before it delivers your app bundle to the end user for installation.

At runtime, the SkipFFI library will handle loading the shared object file that you specify by name in your wrapper class, and you’ll be able to interact with the C library via the wrapper. JNA will link up the function names (or throw an exception for any external functions that cannot be found), and the library will thus be ready for consumption by the Kotlin side of the calling code.

The ability to invoke C directly from both the iOS and Android side enables the prospect of unbeatably-fast dual-platform libraries that can be consumed by app and framework developers alike. We’ve only scratched the surface of what is possible, but to delve deeper, start with the https://github.com/skiptools/skip-c-demo sample repository and the SkipFFI documentation. The SkipTidy repository shows a non-trivial example of an existing C library being embedded in a dual-platform Swift framework in source form. And for examples of using SkipFFI with pre-compiled libraries (either included with the OS or bundled with the app), see the SkipSQL and SkipScript frameworks. The SkipFFI test cases also show some examples of integrating with OS-embedded libraries like zlib and libxml, as well as manually mapping C structures to Swift types.

We’re very excited to see the capabilities that will be unlocked by developers using this feature! If you have questions or suggestions for improvements, please reach out to us on Mastodon @skiptools@mas.to, via chat skiptools.slack.com, or in our discussion forums.


  • structs: automatic C integration using SkipFFI only works for function invocations. C structs, which are nicely mapped to Swift structs, do not benefit from any automatic conversion on the Kotlin side. It is possible to manually implement them using the JNA Structure interface, but it is currently cumbersome. For an example, see the ZlibLibrary test case in the SkipFFI tests.
  • C++: Swift recently gained the ability to integrate with C++ code. SkipFFI and JNA lack any understanding of C++, so while you can build and embed your C libraries using SwiftPM and gradle, you’ll need to interface with it through a C API.
  • JNA Overhead: while there is essentially zero overhead when calling into C from Swift the side, on the Kotlin side the JNA layer does introduce some overhead. When a JNA wrapper class is first instantiated, the runtime needs to do some work to load the correct shared libraries and match up the extern functions with their corresponding C functions. This mostly affects the initial load time, and single a library wrapper instance is likely to be a shared singleton for an application, this overhead is usually tolerable.
  • Size overhead: the SkipFFI depends on the JNA library, which itself includes some native shared object files to facilitate the C side of the bridge. This adds about 1.2 MB to the size of the compiled app.apk, but note that the resulting app size delivered to end users might be smaller if the app storefront is able to thin out unused architectures.
  • Unsigned types: Neither the Java language nor the Java bytecode specification have any support for unsigned types. Kotlin’s support for unsigned types (e.g., UInt8) are wrappers around the equivalent signed type (e.g., Int8), and are not understood or handled by JNA. Attempting to map a function parameter or return type to an unsigned data type will raise a runtime error. In order to work around this limitation, unsigned types must be manually marshalled and unmarshalled, which can be cumbersome.
  • Project Panama: this recent project aims to ease the interaction between Java and foreign APIs written in C and C++. It is presented as an alternative for JNI and JNA, and eliminated most of the bridging overhead, making it the most high-performance call interface from Java into native code. However, Project Panama is an OpenJDK project, and is unlikely to make it into Android’s Java runtime (ART) anytime soon.
  • Mach-O/ELF: You may be wondering what the difference between the libSkipCDemo.dylib and arm64-v8a/libclibrary.so are. If you’re building on an ARM macOS machine, the architecture for both these files should be arm64-v8a, right? Yes, but they are not the same format. Darwin-based operating systems like iOS and macOS use the “Mach-O” executable format7, whereas Linux-based operating systems like Android use the “ELF” format8. You can see this after running swift test in the sample project from the Terminal:
skiptools/skip-c-demo % file ./.build/arm64-apple-macosx/debug/libSkipCDemo.dylib ./.build/plugins/outputs/skip-c-demo/LibCLibrary/skipstone/LibCLibrary/.build/SkipCDemo/intermediates/merged_native_libs/debug/out/lib/*/*.so
…/libSkipCDemo.dylib: Mach-O 64-bit dynamically linked shared library arm64
…/arm64-v8a/libclibrary.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked
…/armeabi-v7a/libclibrary.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked
…/x86/libclibrary.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked
…/x86_64/libclibrary.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked
  1. Non-game development, that is.

  2. More information on Swift’s C integration can be found at https://developer.apple.com/documentation/swift/using-imported-c-functions-in-swift.

  3. The CMakeLists.txt is used by the cmake tool (https://cmake.org), a flexible and popular native build tool that is used by both Swift Package Manager and the Gradle Android plugin (see https://developer.android.com/ndk/guides/cmake) for integration with native build systems.

  4. The details of how this works can be found at the JNA project’s homepage at https://github.com/java-native-access/jna. JNA adds about 1.1 MB to the side of your app.apk, which is why it is included in a separate SkipFFI package rather than included directly with SkipLib or SkipFoundation.

  5. Swift C interoperability types5 (https://developer.apple.com/documentation/swift/c-interoperability) 2

  6. Javadoc for com.sun.jna.Pointer: https://java-native-access.github.io/jna/5.14.0/javadoc/com/sun/jna/Pointer.html

  7. Mach-O file format: https://en.wikipedia.org/wiki/Mach-O

  8. Executable and Linkable Format (ELF): https://en.wikipedia.org/wiki/Executable_and_Linkable_Format

Add a Custom Shadow to Any Content in Compose

Drop shadow on complex content

Skip’s open-source SkipUI library implements the SwiftUI API for Android. To do so, SkipUI leverages Compose, Android’s own modern, declarative UI framework.

The SwiftUI shadow(color:radius:x:y:) modifier adds a drop shadow with a customizable color, blur radius, and offset to any SwiftUI content. Implementing this in Compose posed a problem, because Compose’s own shadow(elevation:shape:clip:ambientColor:spotColor:) modifier works very differently. The most critical difference is readily apparent from the modifier’s signature: you have to supply the shadow’s shape (or be satisfied with the rectangular default). SwiftUI’s shadow, on the other hand, is more akin to a real shadow, automatically mirroring the outline of its target content.

Luckily, Compose is a powerful UI framework. We were able to create a composable function that adds a drop shadow to any content, without affecting your layout and while mirroring the content’s shape (as defined by its non-transparent pixels) exactly. To do so, we used a combination of techniques:

  • Modifier.drawWithContent to re-render the given content as its own shadow
  • A custom ColorMatrix to paint the content in the specified shadow color
  • Layout to place the shadow behind the content with the specified offset, without affecting your layout

The resulting code is below. Note: SkipUI’s implementation is tied to SwiftUI internals, so this is an untested and simplified port of the actual code.

// Compose the given content with a drop shadow on all
// non-transparent pixels
@Composable fun Shadowed(modifier: Modifier, color: Color, offsetX: Dp, offsetY: Dp, blurRadius: Dp, content: @Composable () -> Unit) {
val density = LocalDensity.current
val offsetXPx = with(density) { offsetX.toPx() }.toInt()
val offsetYPx = with(density) { offsetY.toPx() }.toInt()
val blurRadiusPx = ceil(with(density) {
blurRadius.toPx()
}).toInt()
// Modifier to render the content in the shadow color, then
// blur it by blurRadius
val shadowModifier = Modifier
.drawWithContent {
val matrix = shadowColorMatrix(color)
val filter = ColorFilter.colorMatrix(matrix)
val paint = Paint().apply {
colorFilter = filter
}
drawIntoCanvas { canvas ->
canvas.saveLayer(Rect(0f, 0f, size.width, size.height), paint)
drawContent()
canvas.restore()
}
}
.blur(radius = blurRadius, BlurredEdgeTreatment.Unbounded)
.padding(all = blurRadius) // Pad to prevent clipping blur
// Layout based solely on the content, placing shadow behind it
Layout(modifier = modifier, content = {
// measurables[0] = content, measurables[1] = shadow
content()
Box(modifier = shadowModifier) { content() }
}) { measurables, constraints ->
// Allow shadow to go beyond bounds without affecting layout
val contentPlaceable = measurables[0].measure(constraints)
val shadowPlaceable = measurables[1].measure(Constraints(maxWidth = contentPlaceable.width + blurRadiusPx * 2, maxHeight = contentPlaceable.height + blurRadiusPx * 2))
layout(width = contentPlaceable.width, height = contentPlaceable.height) {
shadowPlaceable.placeRelative(x = offsetXPx - blurRadiusPx, y = offsetYPx - blurRadiusPx)
contentPlaceable.placeRelative(x = 0, y = 0)
}
}
}
// Return a color matrix with which to paint our content
// as a shadow of the given color
private fun shadowColorMatrix(color: Color): ColorMatrix {
return ColorMatrix().apply {
set(0, 0, 0f) // Do not preserve original R
set(1, 1, 0f) // Do not preserve original G
set(2, 2, 0f) // Do not preserve original B
set(0, 4, color.red * 255) // Use given color's R
set(1, 4, color.green * 255) // Use given color's G
set(2, 4, color.blue * 255) // Use given color's B
set(3, 3, color.alpha) // Multiply by given color's alpha
}
}

You can see this in action in the Skip Showcase app’s shadow playground, and in the image at the top of this article.

We hope that you find this useful! If you have questions or suggestions for improvements, please reach out to us on Mastodon @skiptools@mas.to, via chat skiptools.slack.com, or in our discussion forums.

Nested Dropdown Menus in Compose

Skip’s open-source SkipUI library implements the SwiftUI API for Android. To do so, SkipUI leverages Compose, Android’s own modern, declarative UI framework.

While implementing SwiftUI’s Menu, we discovered that Compose doesn’t build in support for nested dropdown menus. Googling revealed that we weren’t the only devs wondering how to present a sub-menu from a Compose menu item, but the answers we found didn’t meet our needs. The code below represents our own simple, general solution to nested dropdown menus in Compose.

Note: SkipUI’s implementation is tied to SwiftUI internals, so this is an untested and simplified port of the actual code.

// Simple menu model. You could expand this for icons, section
// headings, dividers, etc
class MenuModel(title: String, val items: List<MenuItem>): MenuItem(title, {})
open class MenuItem(val title: String, val action: () -> Unit)
// Display a menu model, whose items can include nested menu models
@Composable fun DropdownMenu(menu: MenuModel) {
val isMenuExpanded = remember { mutableStateOf(false) }
val nestedMenu = remember { mutableStateOf<MenuModel?>(null) }
val coroutineScope = rememberCoroutineScope()
// Action to replace the currently-displayed menu with the
// given one on item selection. The hardcoded delays are
// unfortunate but improve the user experience
val replaceMenu: (MenuModel?) -> Unit = { menu ->
coroutineScope.launch {
// Allow selection animation before dismiss
delay(200)
isMenuExpanded.value = false
// Prevent flash of primary menu on nested dismiss
delay(100)
nestedMenu.value = null
if (menu != null) {
nestedMenu.value = menu
isMenuExpanded.value = true
}
}
}
DropdownMenu(expanded = isMenuExpanded.value, onDismissRequest = {
isMenuExpanded.value = false
coroutineScope.launch {
// Prevent flash of primary menu on nested dismiss
delay(100)
nestedMenu.value = null
}
}) {
for (item in menu.items) {
DropdownMenuItem(text = { Text(item.title) }, onClick = {
item.action()
replaceMenu(item as? MenuModel)
})
}
}
}

You can see this in action in the Skip Showcase app’s menu playground:

Nested dropdown menus in the Skip Showcase app

We hope that you find this useful! If you have questions or suggestions for improvements, please reach out to us on Mastodon @skiptools@mas.to, via chat skiptools.slack.com, or in our discussion forums.

Skip 2024 Roadmap

Skip enables you to build the best possible apps, for the widest possible audience, using a single codebase. Our goal with Skip is to enable individuals and small teams to create apps for both the iPhone and Android using the native first-party toolkits for those platforms: SwiftUI and Jetpack Compose. We believe this is the best possible experience for users of iPhone and Android devices.

We spent 2023 building the underlying technology to enable this project. The SkipStone transpiler takes your code written in Swift for iOS and converts it into Kotlin for Android. The open-source SkipStack frameworks provide runtime support: they transparently bridge the various Darwin and iOS software layers into their Android equivalents. This includes SkipUI, which takes your SwiftUI interface and turns it into the equivalent Jetpack Compose code, as well as SkipFoundation, which bridges the low-level Foundation framework to the Java APIs used by Android apps. Bringing it all together is the Skip Xcode plugin that automatically runs the transpiler when you build your project and converts your entire Swift package into a buildable Android gradle project.

The end result is a magical development experience: you develop in Xcode and run your SwiftUI app in the iOS Simulator, and Skip seamlessly transpiles and launches your Compose app in the Android emulator. Change some code and re-run, and within seconds both your iOS app and Android app are rebuilt and run side-by-side, ready for testing. The Skip Tour video provides a taste of this process.

In 2024 we will be expanding our ecosystems of Skip frameworks beyond SwiftUI and Foundation. Aside from user interface widgets and operating system integration, a modern mobile app needs a variety of capabilities: graphics and animation, SQL databases, push notifications, media players, cloud storage, payment integrations, parsers for common data formats, cryptography, game technologies, et cetera.

There are many existing libraries, both 1st and 3rd party, for both Android and iOS that fulfill these needs. Our goal is not to re-invent them, but rather to build common abstractions atop them. For example, the new SkipAV framework for playing music and videos is not created from scratch, but is rather implemented on top of iOS’s AVKit and Android’s ExoPlayer. This enables Skip apps to take advantage of each platform’s best-in-class libraries that have matured over the years, while at the same time maintaining a single dual-platform API for developer convenience.

These frameworks are all free and open-source software that Android and iOS app developers can use in their apps. We sell the Skip transpiler itself, but the ecosystem of Skip frameworks can be used – independently of the Skip transpiler – whether or not you are a customer. We choose to make them free software not merely as a value-add for our own customers, but also to grant you, the developer, the confidence that anything you build with Skip will remain under your purview, and that you retain the agency to continue to iterate on your app, with or without the Skip transpiler.

The realization of genuinely native apps for both major mobile platforms, created from a single codebase in a single language, has been a dream for a long time. 2024 will be an exciting year.

Skip is Free for Free Software

Skip brings your iPhone app to Android. With Skip, you can create a modern SwiftUI app with the standard iOS development tools, and Skip transforms it into a Kotlin app for Android. With Skip you can iteratively design, build, test, run, debug, and deploy a single app for both major mobile platforms using a single language (Swift) and a single IDE (Xcode). Watch our 12-minute tour for a glimpse of the magic.

Today we are pleased to announce that Skip will be free for all free open-source software.

There are two halves to the Skip project. The first is SkipStone, our integrated Xcode plugin that transpiles your Swift source code into Kotlin as part of the normal build process. SkipStone is commercial software. It is currently a public technology preview, with early adopter pricing to be announced soon1.

The other half is Skip’s ecosystem of libraries and frameworks, which is free and open-source software. These frameworks constitute the essential building blocks of any modern application, and include low-level adaptors from Darwin’s Foundation API to the equivalent Android Java API (skip-foundation), as well as the high-level SwiftUI user interface widgets that are manifested by Jetpack Compose views on Android (skip-ui). In addition, the growing constellation of community frameworks at github.com/skiptools provides essential functionality such as SQL database support, media player components, and more.

These frameworks are free and open-source software whose advancement will rely heavily on community contributions. And so we’ve made the SkipStone transpiler free, for free software. What this means is that Skip can be used – without cost – for building projects that consist exclusively of source code licensed under free and open source licenses.

This applies not just to framework development, but also to your own app projects: if your iOS app is free software, then Skip can be used to transpile it into an equivalent free Android app. In this way we hope to encourage and support the proliferation of genuinely native dual-platform apps created with the Skip transpiler and powered by the community-supported ecosystem of high-quality libraries and frameworks.

Skip is advancing by leaps and bounds, but it is still a technology preview. You can use it today to create a greenfield app for iOS and Android, provided you are willing to iterate carefully and to work around (or implement and contribute!) some missing pieces. Read the getting started guide to begin the adventure.

The reward will be well worth the effort. Your genuinely native app, created from a single modern Swift codebase, running on both Android and iOS, will be priceless.

More details can be found at our FAQ and documentation. Follow us on Mastodon at mas.to/@skiptools or with RSS.

  1. Commercial pricing will be announced soon, but you can qualify for an early adopter discount by registering during the tech preview period.