15

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.

user1084447
  • 839
  • 3
  • 9
  • 21
  • What is your current setup? C code side by side with Swift code? – zneak Jul 08 '16 at 20:16
  • 1
    https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html. Does this help? C code is Objective-C, and it looks like one can write a framework in a mixture of C and Swift, and it will appear to users as if it was written in a single language. I haven't tried this myself yet. – Anatoli P Jul 13 '16 at 02:39
  • @OmniProg so that's what we're doing currently. The problem with that method is that it necessarily exposes all of those headers publicly, which is what we're trying to avoid. – user1084447 Jul 13 '16 at 17:58

2 Answers2

26

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!

Jens Meder
  • 4,237
  • 1
  • 25
  • 25
  • Works perfectly! Thank you! – user1084447 Jul 14 '16 at 19:34
  • this answer is incredible of course, @JensMeder, thanks. one thing - perhaps you could add a short addendum of the *simpler case*, where there is *no need at all* to prevent direct access. This may clarify the answer. I bet this would be helpful to many. (All the older articles on this on the www have disappeared with time! :) ) – Fattie May 29 '17 at 16:13
  • Under Xcode 11 I had a _Redefinition of module_ build error. Naming the modulemap file `MyFramework.modulemap` rather than `module.modulemap` overcame this. I also had to ensure my c headers were listed as Private and my umbrella header was listed as Public in my framework's Headers Build Phase to avoid a _Umbrella header not found_ build error and enable autocompletion. – followben Oct 11 '19 at 06:39
  • 2
    Thanks for you answer. One problem (at least for me) that I found is that whatever function is defined on the C headers it will appear when you cmd+click on the module name when using the framework. Any idea how to prevent that from happening? – Rui Rodrigues Feb 08 '20 at 22:12
2

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

Community
  • 1
  • 1
bzz
  • 5,556
  • 24
  • 26