How Xcode Recognizes Module Imports

In iOS development, a module is a self-contained unit of code offering a specific set of functionalities. Modules help break down complex apps into smaller components. Developers can incorporate a module into different parts of an app using the import keyword. Behind the scenes, the build system performs several tasks to recognize and integrate these imports seamlessly.

A module can be distributed in various formats such as collections of source files, or a pre-compiled binaries or bundles. For a framework bundle itself, the structure may differ depending on whether it’s built purely in Swift, Objective-C, or a mix of both. Xcode’s approach to identifying and processing import declarations varies accordingly.

This blog post delves into how Xcode manages module imports.

Build Tasks of a Target

When looking at Xcode build log, you might see some steps such as SwiftEmitModule, SwiftCompile, CompileC, Libtool, Ld, etc. They exhibits the compiler’s operations under the hood. The diagram below illustrates the compilation process of a target.

build_task.jpg

In short, the tasks primarily involve:

While compiling source files takes time, emitting module interfaces is relatively lightweight, as they are implementation-free. Essentially, importing a module only needs its interface. Their implementation, meaning the compiled object files, are later glued together at the very end, in the linking steps.

Thus, importing a module roughly means informing the build system how and where to locate its interface.

Importing a Module

Modules can be imported in several ways:

Via a .swiftmodule binary

When compiling a Swift target, you should see a .swiftmodule bundle among the build products. This bundle contains .swiftmodule binaries for certain slices, such as arm64-apple-ios-simulator.

swiftmodule.png

The .swiftmodule file, generated by the Swift compiler, represents interfaces of what symbols can be accessed from the outside. Though cannot be previewed in a trivial way, it’s equivalent to a .swiftinterface when “building libraries for distribution”.

For a module to be recognized, its .swiftmodule bundle must be accessible via search paths. This can be achieved in two ways:

Via a .modulemap file

C/C++/Objective-C modules can also be imported into Swift code, albeit differently. Imports in C-family languages are resolved with headers. And module maps define the linkage between modules and their headers.

module Logger {
  header "Logger.h"
}

To enable module detection, use the -fmodule-map-file argument (passed to Clang) to instruct the compiler to process the module map.

OTHER_SWIFT_FLAGS = $(inherited) -Xcc -fmodule-map-file=path/to/module.modulemap

Note: The -Xcc argument above is used to forward the -fmodule-map-file argument from the Swift compiler to Clang.

In case of Objective-C code that depends on this module, use the same argument with the OTHER_CFLAGS setting:

OTHER_CFLAGS = $(inherited) -fmodule-map-file=path/to/module.modulemap

Via a .pcm (pre-compiled module) binary

When seeing a .modulemap or a .swiftinterface, the compiler spawns a thread to compile this textual module into the pre-compiled module (.pcm) in binary format. The binary is then cached in the module cache directory and reused during compilation.

The .pcm of modules with a .modulemap can be found in the module cache directory (default: ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex).

modulecache_pcm.png

In the figure, DebugKit is an Objective-C module with a pre-defined .modulemap. We can even see the .pcm for system frameworks such as CoreGraphics, Foundation, etc. because those system frameworks are shipped with .swiftinterface.

foundation_swiftinterface.png

Like a .modulemap, we can import a module with a given .pcm by passing the -fmodule-file argument to Clang:

OTHER_SWIFT_FLAGS = $(inherited) -Xcc -fmodule-file=path/to/module.pcm
OTHER_CFLAGS = $(inherited) -fmodule-file=path/to/module.pcm

Via a .framework bundle

A framework contains pre-compiled code and related resources. Typically, a framework includes a .swiftmodule or/and a .modulemap inside its bundle.

A.framework / Modules / A.swiftmodule / arm64-apple-macos.swiftmodule
                      |
                      | module.modulemap

When the compiler detects such a framework, it automatically picks up the corresponding module files. This simplifies integrating 3rd-party code without worrying about handling Swift and Objective-C modules differently. All we need to do is to make sure the framework can be found under the framework search paths.

Given its seamless integration, frameworks are often the go-to choice for distributing modules. However, it is not always the case. Consider SPM packages, for instance. When building a package’s library that is not defined as dynamic, the build process generates both an .o object together with a .swiftmodule bundle. Of course when adding such a product to the “Link Binary With Libraries” section, Xcode handles the heavy lifting for you. In case you opt out of approach, you may want to configure build settings discussed in prior sections.

Example

Check out the demo at: trinhngocthuyen/ios-demos/module-import