When Apple announced support for clang modules a few years ago, I jumped right in. I was curious to see what the build-time improvements might be, among other promised advantages.
Unfortunately things didn’t go well at the beginning. I ran into at least one bug, and probably more if I scratch my memory a bit harder. As cool as modules might turn out to be, they were not ready, at least for me.
Time heals some wounds, so I decided to take another look recently. I was annoyed by mysterious Xcode build failures, and at least some of them had to do with precompiled headers. I remembered in the back of my mind that modules were meant to (at your discretion) completely supersede precompiled headers, so I decided to give them another try.
True to form, I ran into another bug. This time, though, I could see a workaround in breaking down my monolithic project structure into smaller pieces. It was something I wanted to do anyway, and it would work around the modules problem. Win, win?
When I say I wanted to “break down” a monolithic project, I mean that, for example, a single ABC.xcodeproj that contains targets for A.framework, B.framework, and C.framework should be broken up so that each framework has its own .xcodeproj file. The monolithic structure is not too problematic on the face of it, but when you end up harboring references to specific targets in the monolithic project from various levels of other projects, things get a little hairy. In the case of the modules bug I reported, it seemed that some amount of cyclical referencing was responsible for surfacing the issue.
Unfortunately, breaking an Xcode project up can be fraught with peril. There is no easy way to say “take this target from ABC.xcodeproj, and move it to A.xcodeproj.” On the face of it, you’re stuck making a new A.xcodeproj and then painstakingly recreating the target definitions, file references, build phases, etc., that make up the rules for generating the target’s products.
A trick I use in this situation is to literally duplicate the ABC.xcodeproj file, then go in and hack out everything that isn’t contributing to the target you care about preserving. In this case, to get A.xcodeproj, I make a copy, then delete from the project any files that are solely used for B.framework or C.framework. The result is a standalone project for building A.framework. Of course, to avoid harboring redundant build targets for A.framework, you also remove from ABC.xcodeproj any of the files that are solely meant for building it.
That solves part of the problem, and you end up with a shiny new A.xcodeproj. But now you have to go trawling through your source bases, looking for any references to ABC.xcodeproj that expect to link to A.framework through it. These builds will now fail, because ABC.xcodeproj doesn’t have a target for the desired build. Worse, the way Xcode copes with this failure (dependent target suddenly missing), is to simply remove the dependent framework from both the dependencies and linked libraries list for the higher-level target.
To solve the problem, you have to add a project reference to the new A.xcodeproj, and then reconstitute both the dependency and linkage rules for the target. Once you do this for all the possibly umpteen-million targets that previously referenced A.framework in ABC.xcodeproj, you will once again enjoy a working build.
Ah, but thanks to an insidiously helpful new feature of modules, you don’t strictly have to do all of this. Enabling modules for your Xcode targets also opens the door to a feature called automatic linking which will cause modules that are imported by a target’s sources to be automatically linked. So, in a nutshell as long as my source code still contains references like:
Then Xcode and clang will helpfully also add the required framework linkage when building my target.
Ugh, that is so frustrating!
This sounds so helpful, how could I possibly be against it? The problem with auto linking is it will quietly mask any absent-minded dependencies on frameworks that violate the separation of concerns you have diligently instilled on your codebase. Put more bluntly: auto linking encourages spaghetti code.
One of the great advantages of frameworks is that they put up firm put permeable walls between the various components of a source base. If a given framework in your source base is intended to operate independently of the user interface, it’s easy to maintain this discipline by, for example, abstaining from linking with UIKit, AppKit, or whatever other user-interface frameworks may be at your disposal. Then when you absent-mindedly add a dependency to NSImage in your Foundation-level code, you’ll be greeted by a compiler error when it goes to link.
Not so with auto linking. The build completes without error, and now your LowLevelStuff.framework depends on UI frameworks. The permeable walls afforded by frameworks are no longer firm. You might argue: “What’s the big deal? It’s one little NSImage reference.” True, but it’s one little chink in the dam that gives you any sense of ownership or sanity about the scope of your code. Let that dam erode and you have cross-references not just between UI and Foundation layers of your code, but among and between any and all frameworks of varying, allegedly precise purposes. I.e. spaghetti code.
Discipline is one of the greatest and most rewarding challenges of programming. To the extent we successfully impose effortless standards on ourselves, we make the task of maintaining discipline that much easier. The firm walls of framework dependency are one such standard: a gentle reminder in the course of our daily work that things are running, even if only slightly, off the rails. Without those gentle reminders we’re bound to discover only months or years from now just how far we’ve diverged from our intended course.
In summary: enable clang modules, they’re pretty good.
CLANG_ENABLE_MODULES = YES
But automatic framework linking leads to lack of insight about actual dependencies between targets.
CLANG_MODULES_AUTOLINK = NO
Now that my source base is completely converted to using modules and eschewing precompiled headers, I feel good. I haven’t noticed any particular speed improvements, but I feel on board for the future. Adopting them sets me up for easier interoperation with Swift, when the time comes, and will surely spare me any bugs that sprout up specifically with precompiled headers, which Apple is no longer strongly motivated to fix.