Mixed language compiling is one of the daunting tasks in the area of iOS building. Xcode usually handles it nicely behind the scenes, but most of us don't know how to troubleshoot if errors occur.
Although technically C, C++ and Objective-C are mixed language, here we specifically mean Objective-C and Swift. Compiling those two language are particularly challenging because they use different compilers (clang
and swiftc
) and have different interface formats (.h
and .swiftmodule
).
Besides invoking the compiler, the building process is mostly about dealing with interfaces - finding the interfaces of the dependencies and providing the interfaces for the downstream modules. Here are some common types of interface.
- header file (
.h
): The traditional interface for.c
,.cpp
or.m
files. - umbrella header (
.h
) A header file that only includes other headers files, grouping related header files together. - module map (
module.modulemap
): A file that includes an umbrella header and defines a module, which is an important concept in clang. - swift module (
.swiftmodule
): The interface of a Swift module. It's like a module map for Swift. Different than all above interfaces, this is compiler generated and in a binary format.
For the sake of simplicity, we will discuss the case between two libraries. The process is almost identical between executable and libraries. For frameworks, since they are just libraries in a predefined directory structure, the underlying mechanism is the same.
The Objc module needs to provide a module.modulemap
file. This module map can be either manually created or generated by a build system.
module MyObjcModule {
umbrella header "MyObjcModule.h"
export *
module * { export * }
}
In Swift code, we simply import the module like a Swift one.
// MySwiftModule.swift
@import MyObjcModule
To compile Swift code, we have to let the compiler know where to locate the module.modulemap
. This is done by passing -I
to swiftc
.
swiftc ... -I/directory/of/module.modulemap MySwiftModule.swift ...
For this scenario, we need to deal with Swift module first. In Swift code, we annotate the classes or methods with @objc
. The Swift-only features, e.g. struct, tuple, generic, cannot be used in Objc.
// MySwiftModule.swift
@objc public class MySwiftModule : NSObject {}
Then we need to let Swift compiler generate a header file that can be imported by a Objc module. This is done by using -emit-objc-header
. By convention, this header file has -Swift
suffix.
swfitc ... -emit-objc-header -emit-objc-header-path MySwiftModule-Swift.h MySwiftModule.swift ....
Now in the Objc module, we import this header file like a regular header from other modules.
// MyObjcModule.m
#import <MySwiftModule/MySwiftModule-Swift.h>
To compile the Objc module, simplely let clang
know where to find the -Swift.h
file.
clang ... -I/directory/of/MySwiftModule-Swift.h MyObjcModule.m ...
Intra-module compiling is more nuanced. Within the same module, dependencies still exist. Objc can call Swift code or the other way around. Moreover, they can call each other.
First, we still need to annotate Swift classes or functions with @objc
. If the class is only used within the module, the classes don't have to be public, but there is a caveat (see below).
// MySwiftClass.swift
@objc class MySwiftClass : NSObject {}
Then we still need to generate -Swift.h
header file. The compiler flag is also -emit-objc-header
. However, by default, the generated header contains interfaces for Swift declarations marked with the public or open modifier. Using -emit-objc-header
with -import-objc-header
(see below bridging header) make generated header also include interfaces marked with the internal modifier.
swiftc -c ... -emit-objc-header -emit-objc-header-path MixedModule-Swift.h ...
In .m file, import the generated Swift header file with double quotes, like other headers in the same module.
// MyObjcClass.m
#import "MixedModule-Swift.h"
If the Swift class is exposed in the Objc header file, use forward declaration.
// MyObjcClass.h
@class MySwiftClass;
There are two ways for Swift to use Objc, underlying module and bridging header.
Similar to inter-module compiling, the Objc code needs to have a module.modulemap
. This is called underlying module and it should have the same name as the Swift module. Then pass -import-underlying-module
and the search path to swift compiler.
swiftc ... -import-underlying-module -I /directory/of/module.modulemap ...
We can also use a bridging header, which usually has -Bridging-Header.h
suffix. A bridging header is basically an umbrella header. When compile Swift code, we use -import-objc-header
flag.
swiftc ... -import-objc-header MixedModule-Bridging-Header.h ...
Please note that -import-underlying-module
and -import-objc-header
cannot be used at the same time.
error: using bridging headers with framework targets is unsupported
In reality, it's pretty common that Swift and Objc code are entwined in the same module. Here is a general way to build it.
- Generate a
module.modulemap
file for Objc underlying module. It's easier to just include all.h
in the module. - Compile Swift code and generate objects (
.o
). Make sure to import the underlying module (-import-underlying-module
). In the mean time, let the compiler generate.swiftmodule
(-emit-module
) and-Swift.h
(-emit-objc-header
). - Compile Objc code and generate objects (
.o
). Make sure the generated-Swift.h
is in one of the search paths. - Make a library from all object files.
- Make a public
module.modulemap
. This public one should include the generated Swift header file (*-Swift.h
).
- For a Swift module that depends on this mixed module, provide the search path for
.swiftmodule
and the underlyingmodule.modulemap
. - For an Objc module that depends on this mixed module, provide the search path for the public
module.modulemap
.