Cross-Platform Swift

Swift is now not only available on all four Apple platforms, but also on Linux — and possibly soon elsewhere as well. Each platform has some peculiarities, but we want to write at least some of our code in a way that works on all of them. In this talk from AltConf 2016, Boris Bügling covers which APIs are available cross-platform, and how we can effectively build components that work everywhere. He also looks into how the Swift runtime differs between the Apple platforms and the ones supported only by open-source Swift.


Introduction (0:00)

Hey, I’m Boris. You might know me from GitHub repositories like CocoaPods and I also work at a startup called Contentful in Berlin. We do CMS as a software as a service. If you have an app that needs editorial content, or general content that needs to be edited by nontechnical people, you should check it out.

Why Cross-Platform Swift (0:41)

Why do we even want to write cross-platform Swift? In the introduction to Swift slide where Chris Lattner was saying this is basically a classification of languages in terms of developer productivity and app performance, and the goal for Swift is to be in the top right spot where nothing really is at the moment.

You either have things that are a little bit harder to use, but really performant or you have really easy-to-use languages which are rather on the slow side.

Which platforms can be targeted?

All the Apple platforms can be targeted using Swift: Mac OS, iOS, watchOS, tvOS, carOS. This is a list that’s shared between all four Apple platforms.

  • CFNetwork.framework
  • CoreData.framework
  • CoreFoundation.framework
  • CoreGraphics.framework
  • CoreLocation.framework
  • CoreText.framework
  • Foundation.framework
  • ImageIO.framework
  • Security.framework

And then there’s a lot more frameworks you can use if you target the iOS-ish platforms, so iOS, watchOS, and tvOS.

  • Accelerate.framework
  • AudioToolbox.framework
  • AudioUnit.framework
  • AVFoundation.framework
  • AVKit.framework
  • CloudKit.framework
  • CoreBluetooth.framework
  • CoreImage.framework
  • CoreMedia.framework
  • CoreVideo.framework
  • EventKit.framework
  • GameController.framework
  • GameKit.framework
  • GLKit.framework
  • MapKit.framework
  • MediaAccessibility.framework
  • Metal.framework
  • MobileCoreServices.framework
  • SceneKit.framework
  • SpriteKit.framework
  • StoreKit.framework
  • SystemConfiguration.framework

And you can also use UIKit and that kind of cross platform case when targeting iOS and tvOS. An unfortunate thing is you can actually not share nibs between tvOS and iOS. I made a little tool to help with that if you have an existing iOS app and you want to port it to the TV and you don’t want to start from scratch with your interface build the files. You can use this tool to convert your storyboard or your xib from iOS to tvOS. Of course the layout will need to be adjusted and stuff like this but you don’t have to start from scratch.

Open-Source Swift (3:04)

Open-source Swift targeted Linux at the beginning, but now there is a number of new platforms also which have been added, like freebsd. There’s Windows with cygwin port and there’s Android. A few more words about Android. What can you actually practically do with it? Unfortunately not that much at this point because there’s no frameworks, so you can’t access any of the Android build in things.

There’s not even foundation yet, as far as I know. A venue that might work for doing actually some more stuff there is using JNI in Swift. I played around a little bit with it with it on macOS, I haven’t yet done it on Android. So you can use module map for the JNI API. It’s a Java driven interface; an API for calling Java code from C normally, but because all C libraries work inside Swift, we can just create this module map for the JNI and then we can actually call the Java from Swift.

Importing JNI into Swift

module CJavaVM [system] {
  header "jni.h"
  link "jvm"
  export *
}

Calling Java using JNI from Swift

import CJavaVM

var vm: UnsafeMutablePointer<JavaVM> = nil
let env = create_vm(&vm)
let jni = env.pointee.pointee

let hello_class = jni.FindClass(env, "helloWorld")

let main_method = jni.GetStaticMethodID(env, hello_class,"main", "([Ljava/lang/String;)V")
jni.CallStaticVoidMethodA(env, hello_class, main_method, [])

It looks actually much better than the C version of this because we don’t have to write all the very long type names that JNI has. And then we can call methods and instantiate classes. You can basically do anything you can do with a JVM you can do with a JNI. It’s a little bit tedious interface, but it should be possible to use this for calling Android native functionality. But there’s a really long way to go, so someone needs to actually make it practical so that not every app needs to basically reimplement this bridging layer.

Foundation is Shared Across All Platforms (5:15)

One framework that is shared between all of the open source platforms is Foundation. Apple is rewriting it in the Swift calling Foundation project which was part of the Swift open source release. To note it’s incomplete and sometimes it’s different from macOS. Some things are even not available on non-Apple platforms, and that is because the Objective-C runtime doesn’t exist on anything but the Apple platforms and some Swift features depend on the Objective-C runtime.

Any functionality that uses auto release is not available because there’s no auto release in native Swift. An example that was surprising for me is the get b a list function. Where you actually get a pointer that is auto released and not available if you don’t have Objective-C runtime.

But even using the libc is problematic because libc is different. There is a common subset of libc that works everywhere, but there’s many extensions which only work on certain platforms. And we even have to do this build configuration thing to even import the right module because there’s no Darwin Lib C on Linux for example.

#if os(Linux)
import Glibc
#else
import Darwin.C
#endif

And there can be subtle differences like in this case, I was using the glob function from lib C and there’s a struct where you get the matches for the glob. And members of the struct differ between lib C implementation so I actually had to do this build configuration switch between them.

let flags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK
if system_glob(cPattern, flags, nil, &gt) == 0 {
#if os(Linux)
let matchc = gt.gl_pathc
#else
let matchc = gt.gl_matchc
#endif

So it’s complicated but only right now I think. Once Foundation is actually fully available on Linux, we can rely on that to work with a lower level OS and that should provide the same API everywhere.

How to Share Code Between Platforms (8:11)

There’s basically three methods we can use. We can share files between targets, we can use shared frameworks, or we can use shared packages.

Shared files

Shared files, that’s pretty simple, you probably all have done it. If you create a new file or if you look at the inspector of a file, we can check the targets where this file is available inside Xcode. And we can use the Swift build configurations like os and arch to select specific code path, which only run on one OS or even a specific architecture.

Shared frameworks

If we have a framework target inside Xcode, we can actually just change the supporter platforms, and let it build four more than one. For example, in this case for OS for iPhone and Apple TV. A big problem with shared frameworks is that they don’t work so well in Xcode still. So when you have one framework that uses the same product name, or cross different architectures you can have problems with, for example, archiving your build.

Shared packages libraries

There’s couple of options there you can use. There’s CocoaPods, Carthage, and now also the Swift package manager. In CocoaPods we have these platforms attribute, where you can actually specify which platform a pod should be built for, which is pretty simple. This is a way to, if you’re just prototyping something, I think a lot of pods are still not supporting tvOS or watchOS necessarily even they would work.

What’s nice about using CocoaPods is other tools can use and build on top of package manifests. For example, a friend of mine, Kyle Fuller, made a build tool called conche before Swift PM was announced to, as in preparation of Swift on the Nooks and it was able to build CocoaPods packages on the Nooks in a more limited fashion, but basically it worked and this was possible because we have a package manifest that is just a json file. And that can be used anywhere.

Carthage, is essentially a nicer way to do shared frameworks. We can version them so it doesn’t really differ too much from that approach. You may or may not still run into issues when you have the same framework name shared between different targets, but that depends on how you organize your project.

Swift Package Manager is the only way to target Linux. So it’s not optional if you want to do that. It doesn’t support any of the newer platforms, so no iOS, watchOS, or tvOS. And it doesn’t really have a platform syntax at the moment. If you run Swift Package Manager on macOS, you build for macOS, and if you build for Linux, you build for Linux, which means there’s no way to specify even if something works for a specific platform.

The Development Environment (14:06)

There are three development environments that we can use depending on which things we target.

You can use Xcode, and you can also use Xcode along with the Swift tool chain where we can select different tool chains. In Xcode 8, there’s the option to specify which Swift version your project uses. Because Xcode 8 ships with both Swift 2.3 and 3.0 you can use any editor and Swift Build which is the command for Swift Package Manager. The selection shows all the tool chains you have installed and you can select one and if you relaunch Xcode and use that tool chain and you can also use it for trying out one of the beta tool chains or development tool chains which are available on Swift.org. A way to switch between Swift versions when developing on the command line is either CH Swift or Swift End. They’re basically little tools you can install with brew.

How do we test and develop for Linux? The easiest thing, when you just want to try it out is the IBM Swift Sandbox. This is like a web app, where you can type some code, run it, nothing to install. Main problems is it’s not very versatile in terms of features, you can’t use any third-party dependencies. You could maybe paste them in, but then you also have the limit of 65,000 characters in a source file.

Personally, I’m using Docker with a tool called dlite, and if you install that you can just use Docker natively on macOS. With Docker you can run any kind of Linux in a container, which means it runs alongside your OS. It’s not a VM, so it’s much faster and also much smaller and it’s really easy to start because you just run it and you have a command line that runs on this virtualized Linux. And there’s Swift Docker files, for example, there’s a Swift Docker project, there’s one by IBM and you can use them to just run or build your own Docker container containing Swift.

Continuous Integration (18:03)

I’m using Travis CI for my packages and this is a Travis file you can basically use for anything because it uses swiftenv. The one that is basically specified in the Swift version file that I mentioned earlier, so if you are using that you can just always use the same Travis configuration and basically to get a sub up for testing Linux and macOS with one build.

Practical Example (18:37)

I wrote this little library called Electronic Moji, which has function that you give a string and it will look if there’s any unicode, character description that matches that string. So for example, if you would say, “sunglasses,” you get the sunglasses emoji.

The other thing we are going to use for this example is Frank, it’s a DSL for a quickly riding applications Swift with type safe path routing. And that enables us to make a really small web app in Swift. So we’re going to import the emoji library. We’re going to import Frank and then we have just two functions we have get which is what it’s called when you’re just getting the root, we’re getting the hello Golden Gate Bridge and in cases where we have action-arguments or path in our URL, we get that as a string.

import Emoji
import Frank

get { _ in
  return "Hello !\n"
}

get(*) {_, name: String) in
  return (EMOJI.findUnicodeName(name)
  .map { "\($0"}) }
  .first ?? "̄¯\\_()_/¯") + "\n"
}

We look for a matching unicode name, and a character with a matching unicode name if we have that, we return it, otherwise we just return this shruggy. Okay, let’s do a little demo of this. Okay as I said, we’re basically using two packages. We’re using Frank and we’re using the electronic moji package I made and we have the code that I just showed and now, I’ve already compiled it so we don’t need any internet and we can just run it as the example and then we have a web server. Gonna make another journal where we should curl our web server. So we get the hello Golden Gate Bridge and we can now also, for example, do hello nerd and we get the nerd emoji. And if we do something that doesn’t exist as an emoji, like altcomf, which is a shame, get the shruggy.

Q&A (23:57)

Q: Have you tried to build something in Swift Package Manager that contains C or C++ dependency?

So far only with existing libraries, as the Java example I showed, I was actually using Swift Package Manager for that. So it’s using the support for pre-build modules feature. You can just create the module map, so if you have a C library that you want interface with that already exists, you can use the module map approach. But I have not played around with the actual C target support which just landed a few weeks ago I think.


Boris Bügling

Boris Bügling

Boris is a Cocoa developer from Berlin, who currently works on the iOS SDK at Contentful. A Java developer in another life, with many iOS apps under his belt, he is also a strong open source contributor, building plugins to tame Xcode, and bashing bugs as the CocoaPods Senior VP of Evil