Wrapping C/C++ Library in Swift
There are many great libraries out there that are written in C/C++. It is possible to make use of these libraries in your Swift code without having to rewrite any of them in Swift. This article will explain a couple of ways to acheive this and best practices when working with C/C++ in Swift.
Package
- If needed, create a new Swift package with
Package.swift
,Sources
directory, etc. - Create a new module/directory under
Sources
for the C/C++ library. Let’s suppose it’s namedCMyLib
in the rest of this section.- One convention is to prefix the module name with
C
. For example,CDataStaxDriver
.
- One convention is to prefix the module name with
- Add the C/C++ library’s source code directory as Git submodule under
Sources/CMyLib
.- If set up properly, there should be a
.gitmodules
file in the Swift package’s root directory with contents like this:
[submodule "my-lib"] path = Sources/CMyLib/my-lib url = http://222.178.203.72:19005/whst/63/=fhsgtazbnl//examples/my-lib.git
- If set up properly, there should be a
-
Modify
Package.swift
to addCMyLib
as a target, and specify locations for source and header files..target( name: "CMyLib", dependencies: [], exclude: [ // Relative paths under 'CMyLib' of the files // and/or directories to exclude. For example: // "./my-lib/src/CMakeLists.txt", // "./my-lib/tests", ], sources: [ // Relative paths under 'CMyLib' of the source // files and/or directories. For example: // "./my-lib/src/foo.c", // "./my-lib/src/baz", ], cSettings: [ // .headerSearchPath("./my-lib/src"), ] ),
For C++ library, use
cxxSettings
instead ofcSettings
. Additional options and parameters are available for a target definition. See SwiftPM API documentation for details. - Try compiling the Swift package with
swift build
. AdjustPackage.swift
as needed.
Module map
A module map is generated automatically for Clang targets (e.g., CMyLib
) unless a custom one is present. (i.e., module.modulemap
file exists in the header directory)
The rules for module map generation can be found here.
Include files generated by C/C++ library build
Some C/C++ libraries generate additional, required file(s) in their build (e.g., configuration file). To include these files in the Swift package:
cd
into the C/C++ library’s root directory (e.g.,Sources/CMyLib/my-lib
) then build it.- Recall from step 3 above that this is the directory for the Git submodule. No modifications are supposed to be made in this directory. Output files/directories generated by the C/C++ library build should be added to
.gitignore
.
- Recall from step 3 above that this is the directory for the Git submodule. No modifications are supposed to be made in this directory. Output files/directories generated by the C/C++ library build should be added to
- Create a directory under
Sources/CMyLib/
to house the required file(s). (e.g.,Sources/CMyLib/extra
) - Copy the generated, required file(s) from C/C++ build outputs to the directory created in the previous step.
- Update
Package.swift
by adding path of the directory created in step 2 (i.e.,extra
) or individual file path(s) (e.g.,./extra/config.h
) to either the target’s (i.e.,CMyLib
)sources
array for source file(s), or as.headerSearchPath
for header(s).
Overwrite files in C/C++ library
To use custom implementation instead of what comes with the C/C++ library:
- Create a directory under
Sources/CMyLib
to house the custom code file(s). (e.g.,Sources/CMyLib/custom
) - Add the custom code files to the directory created in the previous step.
- If needed, create separate sub-directories for source and header files.
- Update
Package.swift
:- Add path of the directory created in step 1 (i.e.,
custom
) or individual file path(s) (e.g.,./custom/my_impl.c
) to either the target’s (i.e.,CMyLib
)sources
array for source file(s), or as.headerSearchPath
for header(s). - Add file path(s) of the C/C++ library to the target’s (i.e.,
CMyLib
) exclude array. (e.g.,./my-lib/impl.c
)
- Add path of the directory created in step 1 (i.e.,
CMake
This example is geared toward importing a C library into Swift. You’ll need to obtain the library, provide a modulemap so that Swift can import it, and then link against it. The mechanics are largely the same for C++, and an example of how to bi-directionally interop with a C++ library built as part of a single project is available in the bidirectional cxx interop project in the Swift-CMake examples repository.
Obtaining the library
If you’re not building a C library alongside your Swift library, you’ll need to somehow obtain a copy of the library.
- ExternalProject
- Runs at build-time and has most limited configurability, but also isolates the build of the C/C++ library from your build.
- This is good when coupling between your project and the dependency is low, and the library is unlikely to be installed where your project is expected to run, or when you need some level of configurability over the dependency build.
- More details are available at External Project.
include(ExternalProject)
ExternalProject_Add(ZLIB
GIT_REPOSITORY "https://www.github.com/madler/zlib.git"
GIT_TAG "09155eaa2f9270dc4ed1fa13e2b4b2613e6e4851" # v1.3
GIT_SHALLOW TRUE
UPDATE_COMMAND ""
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
)
ExternalProject_Get_Property(ZLIB INSTALL_DIR)
add_library(zlib STATIC IMPORTED GLOBAL)
set_target_properties(zlib PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${INSTALL_DIR}/include"
IMPORTED_LOCATION "${INSTALL_DIR}/lib/libz.a"
)
add_executable(example example.c)
target_link_libraries(example PRIVATE zlib)
This example downloads zlib v1.3 from GitHub, and builds it. Since we have set a pinned tag, we don’t need CMake to attempt to update it, the contents of the commit hash will never change. The ZLIB
target created by the External Project is a CMake “utility” library, so we can’t link against it directly. Instead, we could use find_package
after setting the ZLIB_DIR
to the build directory to find it, or since we already know where it exists, we can create an imported static library. Setting the INTERFACE_INCLUDE_DIRECTORIES
to the location where we installed the zlib headers and the IMPORTED_LOCATION
to the static archive results in a target that we can link code against. CMake will then tell the compiler where to look for the headers and to link the static archive for any target that links against the imported zlib
target.
FetchContent
- Runs at configuration time and results in a merged build graph. This is best for pulling in external pieces that are implementation details of your library. Note that because the build graph is merged, variable names and targets need to be namespaced appropriately or they will collide and things may not build as expected.
- This is good when there is tight coupling between your project and the dependency. Since the build graphs are merged, your project can depend on individual build targets in the dependency, instead of on the project as a whole, which can improve build performance.
- More details are available at FetchContent.
find_package
- Finds the library and header from the sysroot. By default, CMake will look at the root of your OS as the sysroot, but can be isolated to other sysroots for cross-compilation.
- This option is good for picking up system dependencies from the base system or sysroot, or giving the distributor of your project the option of using a prebuilt project with the use of
<PackageName>_ROOT
. - More details are available at find_package.
The example of wrapping an existing C library in Swift using CMake will use find_package
, a custom module-map file, and a Virtual Filesystem (VFS) overlay, as well as a helper layer to migrate parts of the SQLite codebase to something that Swift can import.
Getting started
Start with a basic CMake setup:
cmake_minimum_required(VERSION 3.26)
project(SQLiteImportExample LANGUAGES Swift C)
This creates a CMake project called “SQLiteImportExample” that uses Swift and C, and requires CMake version 3.26 or newer.
We’re not going to build SQLite in this example, we’re going to pull it from the system, or from a provided sysroot.
find_package(SQLite3 REQUIRED)
This tells CMake to go find SQLite3 according to the FindSQLite3.cmake
package file. Since we’ve marked it as a required dependency, CMake will stop the configuration of the build if it can’t find parts of the package.
Once found, CMake defines the following variables:
SQLite3_INCLUDE_DIRS
— The file path wheresqlite3.h
is foundSQLite3_LIBRARIES
— The libraries consumers of sqlite will need to link againstSQLite3_VERSION
— The version of sqlite3 foundSQLite3_FOUND
— Used to tellfind_package
that SQLite was found. Note that if we did not mark it as aREQUIRED
package, we could later check this variable to see if it was found, and fall back onExternalProject
to build it separately if it was not.
CMake will also define the SQLite::SQLite3
build target, which we will use later to make it easier to propagate dependency and search location information through our build graph. Documentation on the SQLite3 package is available here: FindSQLite3.
Importing SQLite into Swift
Swift can’t import header files directly. Some tools like SwiftPM and Xcode can sometimes generate a modulemap for a bridging header, but others, like CMake, do not. Manually writing modulemaps can give you a bit more control over how a C library is imported into Swift. Details on how to write a module map file are available on the Module Map Language specification.
For our example, we only need to expose the sqlite3.h
header file to Swift.
The contents of our sqlite3.modulemap
file are as follows:
module CSQLite {
header "sqlite3.h"
}
The module name represents the name that we use to import this module into Swift. For our example, the corresponding Swift import statement will be import CSQLite
.
We can include additional directives, like link "sqlite3"
to indicate to the autolink mechanism that it should automatically link against the sqlite3 library, but this is unnecessary for our purposes as CMake will do this for us automatically when we tell our program to link against the sqlite library.
Now, we need to place the modulemap file in the right location. We expect the modulemap file to live next to the sqlite.h
file, but depending on where sqlite.h
lives, this may not be accessible to us. This is where virtual-filesystems come in. The virtual filesystem (or VFS) is the view of the filesystem from the perspective of the compiler. A VFS overlay file allows us to override that view so we can change filenames and place files anywhere in the filesystem from the view of the compiler, without actually placing them there on the physical drive.
The input format for a VFS overlay is YAML (note that JSON is a subset of YAML, so you can represent this as a JSON object if you prefer). The downside is that this file expects absolute paths to the roots, or the location that you’re overriding. Depending on where you’re writing to, that location may not be portable, so hard-coding these files may not work. We can, however, use CMake to generate the overlay that works for our system dynamically. We’ll add the following template to our project and call it sqlite-vfs-overlay.yaml
.
---
version: 0
case-sensitive: false
use-external-names: false
roots:
- name: "@SQLite3_INCLUDE_DIR@"
type: directory
contents:
- name: module.modulemap
type: file
external-contents: "@SQLite3_MODULEMAP_FILE@"
The file is incomplete though. We will pair the overlay template with the following CMake emit the final overlay file that matches our environment.
# Setup the VFS-overlay to inject the custom modulemap to import SQLite into Swift
set(SQLite3_MODULEMAP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/sqlite3.modulemap")
configure_file(sqlite-vfs-overlay.yaml "${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml")
target_compile_options(SQLite::SQLite3 INTERFACE
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-vfsoverlay ${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml>"
)
The result is a VFS overlay file that injects the custom modulemap file into the directory where sqlite.h
lives, while renaming sqlite.3.modulemap
to module.modulemap
. All Swift programs that use the SQLite3 library will need to use the associated VFS overlay file in order to find the modulemap. We use target_compile_options
to add it. Since SQLite::SQLite3
is an imported library, it can’t affect building SQLite itself, so we add it as an INTERFACE
option, ensuring that it gets propagated to all targets that depend on it.
Running CMake on this project now should either report that you’re missing SQLite, in which case you will need to install it in order to use it, or emit sqlite3-overlay.yaml
into the top of your build directory.
---
version: 0
case-sensitive: false
use-external-names: false
roots:
- name: "/usr/include"
type: directory
contents:
- name: module.modulemap
type: file
external-contents: "/home/ewilde/sqlite-import-example/sqlite3.modulemap"
This is what is emitted on my Linux system, where sqlite3.h
is located at /usr/include
and the project sources are in a directory in my home directory.
This should be sufficient to get things imported. Wrapping up, we have a total of four files in our project:
sqlite3.modulemap
tells Swift which C header files are associated with which imported module.sqlite-vfs-overlay.yaml
tells Swift to inject the sqlite3 modulemap file into the right place for importing without needing to change the actual system.CMakeLists.txt
organizes configuring the VFS overlay, and then building the project.hello.swift
calls into the C SQLite library.
// sqlite3.modulemap
module CSQLite {
header "sqlite3.h"
}
# sqlite-vfs-overlay.yaml
---
version: 0
case-sensitive: false
use-external-names: false
roots:
- name: "@SQLite3_INCLUDE_DIR@"
type: directory
contents:
- name: module.modulemap
type: file
external-contents: "@SQLite3_MODULEMAP_FILE@"
# CMakeLists.txt
cmake_minimum_required(VERSION 3.26)
project(SQLiteImportExample LANGUAGES Swift C)
find_package(SQLite3 REQUIRED)
# Setup the VFS-overlay to inject the custom modulemap file
set(SQLite3_MODULEMAP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/sqlite3.modulemap")
configure_file(sqlite-vfs-overlay.yaml
"${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml")
target_compile_options(SQLite::SQLite3 INTERFACE
"$<$<COMPILE_LANGUAGE:Swift>:SHELL:-vfsoverlay ${CMAKE_CURRENT_BINARY_DIR}/sqlite3-overlay.yaml>")
add_executable(Hello hello.swift)
target_link_libraries(Hello PRIVATE SQLite::SQLite3)
// hello.swift
import CSQLite
public class Database {
var dbCon: OpaquePointer!
public struct Flags: OptionSet {
public let rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let readonly = Flags(rawValue: SQLITE_OPEN_READONLY)
public static let readwrite = Flags(rawValue: SQLITE_OPEN_READWRITE)
public static let create = Flags(rawValue: SQLITE_OPEN_CREATE)
public static let deleteOnClose = Flags(rawValue: SQLITE_OPEN_DELETEONCLOSE)
}
public init?(filename: String, flags: Flags = [.create, .readwrite]) {
guard sqlite3_open_v2(filename, &dbCon, flags.rawValue, nil) == SQLITE_OK,
dbCon != nil else {
return nil
}
}
deinit {
sqlite3_close_v2(dbCon)
}
}
guard let database = Database(filename: ":memory:") else {
fatalError("Failed to load database for some reason")
}
Working with C/C++
Managing a wrapped C/C++ type’s lifetime
When wrapping a C/C++ type that has a specified lifetime such as outlined by some initialization and later on a “destroy” call of some form, there are two ways to approach this in Swift. This situation is especially common when wrapping C types which have some resource_init()
and resource_destroy(the_resource)
APIs.
The first approach is to use a Swift class to wrap the resource and manage its lifetime by the class’s init
/deinit
. Here is an example wrapping a C managed settings object from RocksDB:
public final class WriteOptions {
let underlying: OpaquePointer!
public init() {
underlying = rocksdb_writeoptions_create()
}
deinit {
rocksdb_writeoptions_destroy(underlying)
}
}
The second approach is to use “non-copyable” types (which you may know as move-only types from other languages). To declare a similar WriteOptions
wrapper using a noncopyable type you can do the following:
public struct WriteOptions: ~Copyable {
let underlying: OpaquePointer!
public init() {
underlying = rocksdb_writeoptions_create()
}
deinit {
rocksdb_writeoptions_destroy(underlying)
}
}
The downside of non-copyable types is that currently they cannot be used in all contexts. For example in Swift 5.9 it is not possible to store a non-copyable type as a field, or pass them through closures (as the closure could be used many times, which would break the uniqueness that a non-copyable type needs to guarantee). The upside is that, unlike classes, no reference counting is performed on non-copyable types.