I have a Swift Framework which we compile and distribute to 3rd party developers. I want to include some C code into the project, but the only way I've been able to make it work is by importing the C header into my framework's header and setting the C header as public. This exposes all of the C code to the 3rd party developers which is not what we want.
Everything I can find online for how to do this is a variation of this method: https://spin.atomicobject.com/2015/02/23/c-libraries-swift/
But the caveat of this method is "Note that consumers of any frameworks you build using this technique are also going to have to add the module to their Swift search paths." And we've tried this and sure enough our 3rd party developers get an error Missing required module 'CGeodesic'
which is the module we've defined pointing to the C header.
How can we just compile the C code right into our framework while keeping the code private, and not requiring 3rd party developers to change their build settings in order to make it work? I don't mind having to setup these submodules in our project, but at the end of the day I want it compiled directly into a single flat framework binary, no dynamic linking or search paths or anything like that.
Edit
The project is C code side-by-side with Swift code. I have a subdirectory in my project that looks like this:
The Swift file wraps the C code to make it cleaner to work with. I had a module.modulemap file in that directory as well, but like I said previously that didn't work. I would prefer to keep the C code side-by-side with my Swift code but I'm willing to give that up in order to solve this issue.
C function pointers are imported into Swift as closures with the C function pointer calling convention, denoted by the @convention(c) attribute. For example, a function pointer that has the type int (*)(void) in C is imported into Swift as @convention(c) () -> Int32 .
You can use Objective-C and Swift files together in a single project, no matter which language the project used originally. This makes creating mixed-language app and framework targets as straightforward as creating an app or framework target written in a single language.
Just to rephrase your problem: You basically want to wrap C code in Swift and prevent direct access to the encapsulated C code. That means to prevent access from both Swift as well as Objective-C.
That is possible but requires some adjustments to your build process and some post processing.
C and Swift Interop in Frameworks
Any C code that you want to wrap in Swift 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 which means you can apply post processing to them (e.g., create a fat binary framework with lipo
). And that's what we need to do here.
Preparing the build
Instead of putting all the C header dependencies into the umbrella header you can also put them into the module.modulemap
file. That has one advantage: You can make your headers private in 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
in xcconfig files), e.g., $(SRCROOT)/MyFramework/module.modulemap
.
Your module.modulemap
file should have the following content:
# 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:
PrivateHeaders
folder from the framework bundle.header "XYZ.h"
statements, e.g., name it public.modulemap
.Run Script
Build PhaseHere'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
Hope that helps!
There is a solution, although it's not recommended. You can use @_silgen_name
inside your .Swift
file instead of using C header.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With