How to hide private module of iOS Framework?
Asked Answered
F

2

5

I have a code-base which consists of Swift top-layer which is meant to be public API and Objective-C "guts" and I need to wrap it up nicely into the iOS .framework so other developers can incorporate my solution without stealing Intellectual Property.

As I know, I need to use Modules system in order to make Obj-C and Swift work together.

So what I did:

1) I created module.modulemap

2) I created module.private.modulemap

3) I set DEFINES_MODULE to YES

4) I set Swift's Import Paths to the Framework folder where both modulemaps are located.

Here's how my module.modulemap looks like:

framework module CoolSDK {
  umbrella header "/full/path/to/file/MyUmbrella.h"

  export *
  module * { export * }
}

Here's how my module.private.modulemap looks like:

module CoolSDK_Private [extern_c] {
      header "/full/path/to/file/ObjC-Guts.h"

      export *
}

I have multiple questions:

1) Why do I need to set the full path to files in modules? When I try to set just the name like so header "file.h" I'm getting an error that file can not be found. I'm positive that files that I try to include lie within modules. Is there some kind of parameter I can set in Build settings?

2) As I said above, my plan is to build a framework with Obj-C guts and Swift top layer, and hence I do not really need an umbrella header, I just need to define a private module so Swift part can have an access to Obj-C part (kind of the same way we do it with Bridging-Header but when it comes to frameworks it's not an option). What should I do in this situation? If I delete module.modulemap completely, XCode generates it's own module.modulemap which looks like this:

module CoolSDK.Swift {
    header "CoolSDK-Swift.h"
    requires objc
}

And of course I have an error that CoolSDK-Swift.h can not be found, so I believe it's not an option. Should I include my Swift public API file as an umbrella header?

3) Although my framework is built successfully and there's no module.private.modulemap inside Modules folder of framework which is exactly what I need (to hide any sign of Obj-C guts via private module), I'm still able to access private module in my SDK tests like so import CoolSDK.Private. How can I make this module exceptional for framework scope and not allow user to get an access to my private module?

Fawnfawna answered 29/1, 2020 at 19:24 Comment(0)
F
1

Just so you know, I dropped an idea of mixing Swift and Objective-C code base inside the framework. I converted all my Swift parts to Objective-C and after that I was able to apply different practices for hiding private code.

Fawnfawna answered 13/5, 2020 at 15:32 Comment(0)
V
0

I was in the same exact scenario where I'm developing an iOS Framework that relies on a C++ library that I did not want to expose to client applications that would integrate my Framework. I spent a few hours trying to figure out a way to achieve this, and rewriting this C++ part of the code in Swift wasn't an option.

I'm sharing what worked for me in case someone else finds this SO question with the same needs.

1. Creating a .modulemap file

Before anything, I was using a Bridging Header to link my native code to the Swift part, but I realized when trying to build the XCFramework that Bridging Headers weren't allowed in Frameworks. So I had to find another way to link the two languages that would work in Frameworks, and that only way that I found was relying on Modules.

I've read articles describing how to write a modulemap with a private part meant to hide some code to client apps. But that did not achieve what I wanted as the private headers were still shipped in the Framework and were entirely accessible in the Private Headers folder of the Framework. I wanted to hide everything, including the function prototypes that I have in my C++ library, for security reasons.

Anyway, what worked for me was writing a simple module.modulemap file like this:

module MyPrivateModule {
    header "my-private-lib.h"
    export *
}

Where my-private-lib.h header is declared in "Project headers" in the Build Phases pane in Xcode, as to not expose it in the final Framework.

2. Set up correct Build Settings

As Eugene said in his question, you're going to need to set those settings in the Build Settings pane of Xcode:

  • Packaging > Defines Module: Yes
  • Swift Compiler - Search Paths > Import Paths: specify the directory where your native code is located. You have to specify the full absolute path, which can be done dynamically using $(SRCROOT) to start your path at the source directory. Do not forget to tick the recursive option if you have nested folders
  • Optional if like me you have an include folder with headers from other native dependencies, I had to add the path to this folder in Search Paths > Header Search Paths using $(SRCROOT) once again

At this point, you should be able to use your module in your Swift class by adding import MyPrivateModule, and the code shouldn't be accessible for client apps. The issue is that your Framework is going to throw a No such module MyPrivateModule error when integrated in an external app. Which brings us to step 3.

3. Limit the scope of your import

The issue is that your import MyPrivateModule is leaking to the external app. It's a known issue that's been detailed and solved in the SE-0409 Proposal of an evolution in the Swift language by Alexis Laferrière.

With Swift 5, you can already use this feature by simply writing internal import MyPrivateModule or even fileprivate import MyPrivateModule. But an error will pop up in Xcode as this feature is hidden behind an experimental feature flag. So head back to the Build Settings pane and edit Swift Compiler - Custom Flags > Other Swift Flags. You have to type into two different lines: -enable-experimental-feature and AccessLevelOnImport. The error in Xcode will then disappear.

In Swift 6, which shouldn't take long to release, this feature will be fully available without the experimental feature flag, and all imports will be internal by default (to be confirmed). If they're indeed internal by default, the step 3 shouldn't be necessary anymore.

If you try to build your Framework and integrate it as a dependency in an external app, everything should work fine now, no more "No such module" error.

4. BONUS, additional Build Settings for more security

This part could be skipped, but if you want further security and assurance that your native code is hidden from clients, here are some settings I've added to my project:

  • Apple Clang - Custom Compiler Flags > Other C Flags: -O3 -flto -fstack-protector-strong -fvisibility=hidden, write one flag per line. The last flag removes all symbols from your native library
  • Deployment > Deployment Postprocessing: Yes
  • Deployment > Strip Style: Non-Global Symbols

I found those two last Deployment settings in this SO answer.

Conclusion

With this approach, all your headers remain in the Project part in Build Phases, which ensures that no header is accessible in the final binary. The module isn't accessible either from external apps when depending on your Framework and it builds fine. And with step 4, there should be no native symbol visible.

Varick answered 12/7, 2024 at 10:18 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.