Swift frameworks with C code

Frameworks are powerful tools to separate your codebase into smaller, more maintainable modules. Unfortunately, Xcode has some flaws if you want to mix and match Swift and C code in a framework without providing direct access to private functions. In this article we will create a Swift framework that prevents direct access to the encapsulated C code - both from Swift as well as Objective-C.

C and Swift Interop in Frameworks

Any C code that you want to wrap in a Swift framework needs to be public during compile time, either via the umbrella header or using module maps. The important part here: during compile time. Frameworks have no integrity checks that means we can apply post processing, e.g., to obfuscate private functions.

Preparing the build

Instead of putting all the C header dependencies into the umbrella header we will put them into a module.modulemap file. The advantage: We can move our C headers into the private section of the Headers Build Phase.

Therefore, create a module.modulemap file in your project and set the path to it in the Module Map File Build setting in the Packaging section (MODULEMAP_FILE if you are using xcconfig files), e.g., $(SRCROOT)/MyFramework/module.modulemap.

Your module.modulemap file should look like this:

# module.modulemap

framework module MyFramework {
    
    umbrella header "MyFramework.h"  

    export *
    module * {export *}
    
    # All C Files you want to wrap in your Swift code

    header "A.h"
    header "B.h"
}

So far so good. You should now be able to build your code and access it from both Swift and Objective-C. The only problem: You can still access your C header files in Swift and Objective-C.

Postprocessing

If you have a look at the final Framework bundle you will notice that all your private header files have been copied to the MyFramework.framework/PrivateHeaders folder. That means you can still access them via #import <MyFramework/A.h> in Objective-C.

In Swift you can still access the C code because we put the header files in the module.modulemap file that has been copied to MyFramework.framework/Modules/module.modulemap.

Fortunately, we can just get rid of those two problems with a bit of post processing:

  1. Remove the PrivateHeaders folder from the framework bundle.
  2. Create a separate modulemap and remove all header "XYZ.h" statements, e.g., name it public.modulemap.
  3. Put all of that in a Run Script Build Phase

Here’s the public modulemap:

# public.modulemap

framework module MyFramework {
    
    umbrella header "MyFramework.h"  
    
    export *
    module * {export *}

    # No more C Headers here
}

And the run script that you should add to the end of your framework’s build phases:

# Delete PrivateHeaders folder
rm -rf ${TARGET_BUILD_DIR}/${PRODUCT_NAME}${WRAPPER_SUFFIX}/PrivateHeaders
    
# Remove module.modulemap file
rm ${TARGET_BUILD_DIR}/${PRODUCT_NAME}${WRAPPER_SUFFIX}/Modules/module.modulemap

# Copy public.modulemap file and rename it to module.modulemap
cp ${SRCROOT}/test/public.modulemap ${TARGET_BUILD_DIR}/${PRODUCT_NAME}${WRAPPER_SUFFIX}/Modules/module.modulemap

# Append the Swift module so you can access you Swift code in Objective-C via @import MyFramework.Swift
echo "module ${PRODUCT_NAME}.Swift { header \"${PRODUCT_NAME}-Swift.h\" }" >> ${TARGET_BUILD_DIR}/${PRODUCT_NAME}${WRAPPER_SUFFIX}/Modules/module.modulemap

You should now be able to build Swift frameworks that encapsulate C code but do not expose any private headers to any user of the framework.