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

  1. If needed, create a new Swift package with Package.swift, Sources directory, etc.
  2. Create a new module/directory under Sources for the C/C++ library. Let’s suppose it’s named CMyLib in the rest of this section.
    • One convention is to prefix the module name with C. For example, CDataStaxDriver.
  3. 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
    
  4. Modify Package.swift to add CMyLib 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 of cSettings. Additional options and parameters are available for a target definition. See SwiftPM API documentation for details.

  5. Try compiling the Swift package with swift build. Adjust Package.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:

  1. 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.
  2. Create a directory under Sources/CMyLib/ to house the required file(s). (e.g., Sources/CMyLib/extra)
  3. Copy the generated, required file(s) from C/C++ build outputs to the directory created in the previous step.
  4. 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:

  1. Create a directory under Sources/CMyLib to house the custom code file(s). (e.g., Sources/CMyLib/custom)
  2. Add the custom code files to the directory created in the previous step.
    • If needed, create separate sub-directories for source and header files.
  3. 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)

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.

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.

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:

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
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.