Wrapping a C library in Swift (Part 1)
SPM, module maps, OpaquePointers and everything in between
This post is the first in a multi-part guide to wrapping a C library in Swift. Part 1 will walk step-by-step through the process of building a Swift project that can interact with a C library, libgraphqlparser, using Swift Package Manager (SPM). Further parts will cover how to wrap that C interface into a Swift API that feels more natural to use.
While this explores the example of a specific library, the same techniques described here can be applied to most other C libraries.
First, some history…
At Shopify, we’ve been using GraphQL in our mobile apps since 2016. One of the benefits that GraphQL provides over typical REST APIs is that it is backed by a well-defined schema.
The GraphQL schema can be leveraged to write strongly-typed Swift code representing the network responses of various queries and mutations. This code can be tedious and error-prone to write, so we decided to build a tool that automatically generates Swift models representing your GraphQL queries and mutations.
One of the first challenges of building a tool like this is finding a way to parse queries and mutations defined in GraphQL syntax and transform them into an Abstract Syntax Tree that can be understood by Swift code.
Building a grammar parser is no trivial task. Thankfully, the GraphQL org has published an open-source parser written in C++. Swift cannot interop with C++ code directly, but the libgraphqlparser project provides a pure C API that, with a little work, can be used in Swift.
The libgraphqlparser library has some functions defined in its headers that can do exactly what we need.
The first function will take a GraphQL query string and convert that into the AST representation of the query. For this tutorial, we will simply convert that AST to JSON using
graphql_ast_to_json(ast) and print the result.
Setting up the initial boilerplate
Setting up the base package
SPM requires that a Swift package be defined that wraps the system library, however this package cannot be built directly since it has no Swift code. To verify that the library can be imported into Swift code a base package must import the wrapper package as a dependency. Let’s build that base package.
$ mkdir GraphQLParser
$ cd GraphQLParser
GraphQLParser$ swift package init --type executable
Installing the library
Installing libgraphqlparser can be accomplished by either building from source or via homebrew with the following command.
$ brew install libgraphqlparser
Exposing the code to Swift
Once the initial project setup is complete, we need to expose the code to Swift. Since this is for a macOS command line application we can use Swift Package Manager (SPM) for this.
To expose a system library to Swift code, we need to tell the compiler where to find the code we wish to use. Typically this consists of informing the compiler of the location of the dynamic library and any necessary header files.
In this case, the dynamic library was likely installed to
/usr/local/lib/libgraphqlparser.dylib and the header files would have been written to
SPM approaches this problem by requiring the user to define a package that wraps the system library and to provide a module map for that library. The convention is to prefix these packages with a capitalized “C”, so this package will be called “Clibgraphqlparser”.
Initialize this package in a directory adjacent to the GraphQLParser folder.
GraphQLParser$ cd ..
$ mkdir Clibgraphqlparser
$ cd Clibgraphqlparser
Clibgraphqlparser$ swift package init --type system-module
Defining a module map
SPM will automatically generate a module map, which describes how a collection of existing headers maps on to the structure of a module. The module map allows the code to be imported into Swift with a simple
import Clibgraphqlparser statement.
Modify the contents of the generated module map to have the following contents.
Let’s go over this line-by-line.
The first line declares a module named “Clibgraphqlparser” as a system module. According to the Clang docs on modules, the
[system] attribute indicates that “all of the module’s headers will be considered system headers, which suppresses warnings”.
The next line declares that the specified header is associated with this module. You can have multiple header declarations in your module map, but for clarity’s sake one custom header file is defined that imports all the other headers of this library.
The link declaration specifies the name of the library that should be linked against. In this case we want to link against “graphqlparser”.
export * declaration specifies that any module imported by the headers of this module should automatically be re-exported along with it and exposed as part of its API.
Module maps can be a bit confusing to a non-C developer (like me) but for the most part they will all follow this same structure.
The Import Header
This step may not be necessary for all packages, but for convenience define a header file which imports all of the headers of libgraphqlparser. Name this file
Clibgraphqlparser.h and place it in the root of the Clibgraphqlparser directory. The headers that we want to import are in the
c/ subdirectory of
/usr/local/include/graphqlparser. Our custom header will have the following import statements.
The Package Description
Now that we have a module map and our custom header file, we need to provide the package description. Open up Package.swift and change the contents to the following.
Let’s look at the declaration of the package variable line by line.
The first line provides the name for this package, Clibgraphqlparser.
Next is a pkgConfig entry. Often (but not always) when you install a system library on your computer, it will include a pkgConfig which describes where SPM can find the dynamic library file and the header files for this library. You can check if a pkg-config was installed for your library by running
$ pkg-config --list-all. If a pkg-config was not installed for your library, you have to explicitly link the library and include the headers. This will be discussed below.
Next we define a single product, whose name is Clibgraphqlparser with a traget of Clibgraphqlparser. The target defined below will have a similar format. This pattern is the same as for other non-system Swift packages so there is nothing new to explain here.
This should be all the necessary pieces for this package. Commit it to master and give it a version tag.
Clibgraphqlparser$ git init
Clibgraphqlparser$ git add .
Clibgraphqlparser$ git commit -m "initial commit"
Clibgraphqlparser$ git tag 1.0.0
Compiling the code
Now that the package describing this library has all of its necessary pieces. It’s time to build it. Clibgraphqlparser doesn’t have any Swift code itself, so running
swift run within it will throw an error. The way to properly build it is to build our GraphQLParser package which contains the Clibgraphqlparser as a dependency.
Package.swift of GraphQLParser to point to the latest version of Clibgraphqlparser.
Open up the main.swift for GraphQLParser and add an import statement for Clibgraphqlparser.
Now we can run
swift run within the directory of GraphQLParser, and it should correctly fetch and build the Clibgraphqlparser package. Depending on how you installed libgraphqlparser, this command could fail with an error such as “could not build Objective-C module Clibgraphqlparser” and “‘c/GraphQLAstNode.h’ file not found”. These errors are indicative that SPM cannot find the header files or the dynamic library to link to. This is usually a result of the pkg-config for libgraphqlparser not being installed.
To fix this we explicitly link the directory where libgraphqlparser.dylib is stored as well as include the directory containing its headers.
GraphQLParser$ swift run -Xcc -I/usr/local/include/graphqlparser -Xlinker -L/usr/local/lib
This should fix any compile errors you are facing related to libgraphqlparser. If you are still receiving build errors and a warning about linking libraries “compiled with older version of Swift language”, you’ll need to add an additional linker flag which ensures you link against the Swift standard library that is bundled with your active toolchain. Your revised build command should instead look like the following.
GraphQLParser$ swift run -Xcc -I/usr/local/include/graphqlparser -Xlinker -L$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx
Actually using libgraphqlparser in Swift
You can now call any of the public functions of libgraphqlparser within your Swift code. Let’s try to implement our use case described above to parse a query string into its AST representation, and then subsequently convert that AST to a JSON string. Update the contents of main.swift in the GraphQLParser package to the following.
Running this will print the JSON representation of the AST of the example query.
We’re finished! Or… are we? We’ve successfully linked and executed code from a C library with Swift, but the code is ugly. Dealing with
OpaquePointer instead of a typed object and
UnsafePointer<Int8> instead of a Swift string can be error-prone and cumbersome to work with. Manually freeing the memory is also a detail that would be better off encapsulated as an implementation detail.
We’ve successfully exposed the C code to Swift and called its functions. In part 2 of this post we’ll explore how to wrap this API in something more strongly typed and Swift friendly.
Thanks for reading! You can find me on twitter if you have any questions.