Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compile C code into Swift Framework

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:

  • Geodesic/
    • Geodesic.h
    • Geodesic.c
    • Geodesic.swift

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.

like image 691
user1084447 Avatar asked Jul 08 '16 19:07

user1084447


People also ask

Can I use C code in Swift?

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 .

Can you mix Objective-C and Swift?

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.


2 Answers

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:

  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

Hope that helps!

like image 109
Jens Meder Avatar answered Sep 19 '22 20:09

Jens Meder


There is a solution, although it's not recommended. You can use @_silgen_name inside your .Swift file instead of using C header.

like image 26
bzz Avatar answered Sep 21 '22 20:09

bzz