Understanding Objective-C and Swift interoperability Sep 16 2019 Latest Update: Jun 28 2020

There are many reasons to use more than one programming language in a project. In some cases, a lot of work has gone into building a framework (years maybe). Rewriting the whole framework to have the codebase in the same programming language, might not be the best use of resources. You can see this in macOS between the two main programming languages,Swift and Objective-C (three counting C). In this post, I'll show how to use Swift code in Objective-C, and how to use Objective-C code in Swift. We are going to explore how the interoperability occurs by building the code manually to get a better understanding.

Let's begin by defining interoperability.

(You can find all the code in the GitHub repository: https://github.com/rderik/objc-swift-interoperability.)

Interoperability

The term interoperability is hard to define, especially if we worry about correctness. Abstract terms are hard to describe. Interoperability, in our context, is the ability of a programming language to work together with another programming language. But even in that simple "definition", we are not clearly defining what do we mean by working together.

A language might declare an FFI (Foreign Function Interface), where it'll define an interface to communicate and call functions or services from another programming language. For example, Ruby FFI provides an interface to access native C libraries and structures, making them accessible to Ruby by wrapping them in Ruby functions. That is one kind of interoperability.

Another possibility is to provide interoperability between programming languages that use the same runtime. Let's see what this means.

Runtime

The runtime defines an execution environment, this means, how programs are built, loaded and executed. And because of that it also describes the structure of executable files. The runtime defines how are routines, variables and structures mapped on the executable file. The runtime specifies where are these components located, so they are in a predictable "place" and "format".

Many runtimes can exist in an operating system, and different languages can implement their runtime (e.g. the JRE - Java Runtime Environment). But the operating system has only one native runtime, in the case of macOS, it is the Mach-O runtime.

In the end, if you want to run an executable in the OS, it would be a Mach-O executable. What this means is that even if you have something like the JRE, the JRE has a JVM(Java Virtual Machine) that runs as a Mach-O executable. Your Java executables "runs" on the JRE but the JVM runs as a Mach-O executable.

Something similar happens for the Objective-C runtime. The Objective-C runtime is a dynamic library (located in: /usr/lib/libobjc.A.dylib). It provides all the necessary methods and support for the dynamic properties of Objective-C. The Objective-C runtime is "nothing more" than a library.

Swift has its own runtime, but it also has access to Objective-C runtime, and also access to C. Having access to those runtimes makes the interoperability between Swift and Objective-C easier to bridge.

Because Swift already has access to Objective-C and because Swift can expose methods to the Objective-C runtime, there is no problem having a project that contains code in both languages.

But how do we do the bridging? Let's see an example:

Sharing code between Swift and Objective-C.

We are going to work on a simple example. The complexity of the application is not relevant. We want to focus on the interoperability. We are going to create a Greeter Class in Swift that displays a "greeting", and we are going to use that object in Objective-C.

Let's create a directory to put all of our files:

1
2
$ mkdir interoperability
$ cd interoperability

Create a new file Greeter.swift with the following content:

1
2
3
4
5
6
7
import Foundation

class Greeter {
  func greet() {
    print("Eh, hello?")
  }
}

Before we make our code interoperable with Objective-C, let's do everything in Swift.

Let's create a file main.swift:

1
2
3
4
import Foundation

let greeter = Greeter()
greeter.greet()

Now we can build and run it on the command-line:

1
2
3
4
$  swiftc -o runGreeter main.swift Greeter.swift
# This will create a file named: runGreeter
$ ./runGreeter
# Eh, hello?

Great, it works. Now let's do it for Objective-C.

Expose Swift code to Objective-C

Let's start with a simple Objective-C program. Create the file main.m with the following content:

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

int main (int argc, const char * argv[])
{
  @autoreleasepool {
    NSLog (@"Hello, World!");
  }
  return 0;
}

Now let's build and run it so we learn how to do it for Objective-C:

1
2
3
4
$  gcc -o runGreeterObjc main.m -framework Foundation
# This will create a file named: runGreeterObjc
$ ./runGreeterObjc
# 2019-09-14 05:44:11.781 runGreeterObjc[41628:12262014] Hello, World!

Ok, now, how do we link our Swift code to our Objective-C code?

Before we go into that, let's explore how would we use our Greeter (Swift object) in the Objective-C code:

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>
#import "Greeter-Swift.h"

int main (int argc, const char * argv[])
{
  @autoreleasepool {
    Greeter *greeter = [[Greeter alloc] init];
    [greeter greet];
  }
  return 0;
}

To begin with, we would have to import the object to our main.m file. That means that we need a header file. If you've worked with Objective-C (or C), you know that the header file defines the interface that our code provides, so if we want to expose some functionality from our Greeter object, we need to make our interface available to Objective-C. The question is: how do we generate the header file to expose our interface?

The good thing is that all of the work has already been made for us. The Swift compiler provides the flag -emit-objc-header or -emit-objc-header-path [Header filename]. If you are curious and want to see our objects interface, we can use the Swift compiler (swiftc) to view it. We can see our Greeter.swift's interface running the following command:

1
2
3
4
5
6
7
8
$ swiftc -print-ast Greeter.swift
#import Foundation
#
#internal class Greeter {
#  internal func greet()
#  internal init()
#  @objc deinit
#}

(This is the same as what you see in Xcode when you click Generated Interface on the Related Items toolbar. See image below.) Xcode show generated interface

By what we can see from the interface, only our deinit is being exposed to @objc. To make our Object accessible to Objective-C, we can use the @objc attribute. Let's update our Greeter.swift:

1
2
3
4
5
6
7
import Foundation

public class Greeter: NSObject {
  @objc public func greet() {
    print("Eh, hello?")
  }
}

We added @objc to our method, but we also made the class and method public, so it can be accessed from outside. Also, to make our Greeter accessible from Objective-C we made it a descendant of NSObject.

Le'ts check our interface:

1
2
3
4
5
6
7
8
$ swiftc -print-ast Greeter.swift
#import Foundation
#
#@objc public class Greeter : NSObject {
#  @objc public func greet()
#  @objc override dynamic public init()
#  @objc deinit
#}

That looks better, let's generate our header-file now that the interface is correct:

1
2
3
4
5
6
7
8
9
# We are going to be organized and create a directory for Headers and one for Modules
$ mkdir -p ./{Headers,Modules}
$ swiftc -emit-objc-header-path ./Headers/Greeter-Swift.h -emit-module-path ./Modules/ Greeter.swift
# We'll have the following structure:
./
├── Headers
│   └── Greeter-Swift.h
├── Modules
    └── Greeter.swiftmodule

Right, we have our header file, and we can add it to our main.m. Let's update the code to our main.m:

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>
#import "Headers/Greeter-Swift.h"

int main (int argc, const char * argv[])
{
  @autoreleasepool {
    Greeter *greeter = [[Greeter alloc] init];
    [greeter greet];
  }
  return 0;
}

If we run the previous command to build our main.m, we'll get the following error:

1
2
3
4
5
6
$ gcc -o runGreeterObjc main.m -framework Foundation
#Undefined symbols for architecture x86_64:
#  "_OBJC_CLASS_$__TtC7Greeter7Greeter", referenced from:
#      objc-class-ref in main-0ddc1a.o
#ld: symbol(s) not found for architecture x86_64
#clang: error: linker command failed with exit code 1 (use -v to see invocation)

The linker needs the library to "link" it to the executable. If you haven't spent much time in C world, this might not be intuitive. We could think that we only need to add the import line pointing to the header file and everything will get resolved. In reality, the header only provides the interface. We still need to build the library and link it to create our executable.

Again the swift compiler already provides a way to generate the library:

1
2
3
# let's create a Libraries directory
$ mkdir Libraries
$ swiftc -emit-objc-header-path ./Headers/Greeter-Swift.h -emit-module-path ./Modules/ -emit-library -o ./Libraries/libGreeter.dylib Greeter.swift

And now we can build and run our runGreeterObjc executable:

1
2
3
$ gcc -Wall -o runGreeterObjc  main.m -l Greeter -L ./Libraries/ -framework Foundation
$ ./runGreeterObjc
# Eh, hello?

If you haven't used gcc here is the meaning of the flags (But I encourage you to read the --help):

Congratulations now you know how to use a Swift object in Objective-C code.

Now, let's do the opposite, let's use an Objective-C Object in Swift.

Expose Objective-C code to Swift

First, let's make everything work on Objective-C.

We are going to create an object called SendOff that will say goodbye to our users.

Create a SendOff.h to define the interface. We are going to follow our previous structure putting all our header files inside the Headers directory.

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

@interface SendOff : NSObject

@property (assign)NSString *message;

- (instancetype)initWithMessage:(NSString*)message;
- (void)goodbye;
@end

Now on our working directory creat SendOff.m with the following implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import "Headers/SendOff.h"

@implementation SendOff

- (instancetype)initWithMessage:(NSString*)message {
  if (self = [super init]) {
    self.message = message;
  }
  return self;
}

- (void) goodbye {
  fprintf(stdout,"%s", [self.message UTF8String]);
}

@end

Let's use SendOff in our main.m. This way we make sure everything in Objective-C works before trying to bridge it to Swift. Our main.m will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h>
#import "Headers/Greeter-Swift.h"
#import "Headers/SendOff.h"

int main (int argc, const char * argv[])
{
  @autoreleasepool {
    Greeter *greeter = [[Greeter alloc] init];
    [greeter greet];
    SendOff *sendOff = [[SendOff alloc] initWithMessage: @"Good bye!"];
    [sendOff goodbye];
  }
  return 0;
}

I only added the SendOff code to our previous main. Now we can build and run it.

1
2
3
4
5
$ gcc -Wall -o main main.m SendOff.m -l Greeter -L ./Libraries/ -framework Foundation
# this generates the executable `main`
$ ./main
#Eh, hello?
#Good bye! 

Perfect, everything is working now. Now let's get to the exciting part, using our code in Swift.

Swift import directive uses modules not directly header files. So we will need to work with modules. I won't go into much detail, but you should read the clang documentation on modules to learn more.

Ok, so we will use a modulemap to tell Swift where to locate our module interfaces and the header files. We are going to follow the convention of Xcode and create a [ModuleName]-Bridging-Header.h that will include all the header files to include in the module.

In our Headers directory create the file SendOff-Bridging-Header.h with the following content:

1
#import "SendOff.h"

Simple enough, we are only using one header file. Now we have to create our modulemap. We already have a Modules directory that we created before. To keep everything clean we are going to use that directory to store our module.modulemap file. Create module.modulemap and add the following content:

1
2
3
4
module SendOff {
 header "../Headers/SendOff-Bridging-Header.h"
 export *
}

Ok, that makes sense. Now we can work on our main.swift to use the SendOff object we created in Objective-C:

1
2
3
4
5
6
7
8
import Foundation
import SendOff

let greeter = Greeter()
greeter.greet()

let sendOff = SendOff(message: "Good bye!")
sendOff?.goodbye()

We import the module SendOff and use the SendOff object. If we try to compile our swift main we will get an error:

1
2
3
$ swiftc main.swift Greeter.swift -I ./Modules -L ./Libraries -lGreeter -lSendOff
#ld: library not found for -lSendOff
#<unknown>:0: error: link command failed with exit code 1 (use -v to see invocation)

We haven't created the library so we can't use it in Swift. Let's build it so we can use it:

1
2
3
4
5
#only run the preprocess
$ gcc -c SendOff.m
#this will generate `SendOff.o` that we'll use to build the library
$ gcc -dynamiclib -o  ./Libraries/libSendOff.dylib -install_name ./Libraries/libSendOff.dylib SendOff.o -framework Foundation
# This generates the dynamic library: libSendOff.dlib inside our Libraries directory

Now that we have the library, we will be able to link to it, and we'll be able to run our swift code:

1
2
3
4
5
$ swiftc -o mainSwift main.swift Greeter.swift -I ./Modules -L ./Libraries -lGreeter -lSendOff
# this generates our `main` executable
$ ./mainSwift
#Eh, hello?
#Good bye!   

Congratulations, now we have used Objective-C code on Swift, and Swift code in Objective-C.

Final Thoughts

Xcode hides a lot of the complexity for us, making the interoperability seem seamless. In reality, a lot is going on. Without understanding how all this works, I would have a hard time linking Swift only code without using Xcode.

We get a lot of help from the developer tools. We don't need to know every detail on how everything is tied-up together. The tools abstract a lot of complexity for us. That is a good thing. We don't need to be worrying about linking libraries or other million more details. But deeply understanding how things work, gives us the capability of using those basic building blocks and develop innovative solutions. If we didn't understand how Swift, Objective-C and C work together, we wouldn't even think about sharing code between them. Our only option would be to rewrite everything in one language.

I tried to keep things simple in our small exercise. Also, I skimmed over a few topics (e.g. modules, dynamic libraries) for the sake of brevity. But I encourage you to explore more about these topics, and I hope you learned something new. As always, feedback is welcome.

(You can find all the code in the GitHub repository: https://github.com/rderik/objc-swift-interoperability.)

Related topics/notes of interest:


** If you want to check what else I'm currently doing, be sure to follow me on twitter @rderik or subscribe to the newsletter. If you want to send me a direct message, you can send it to derik@rderik.com.