Prefix header for C targets

Is there any plan to support prefix header when building C targets? This is a pretty standard feature, and would be useful in many situations. I think this can be achieved by using unsafeFlags, but such packages can't be used as a dependency, even for local development.

What about adding it as CSetting.prefixHeader()?

3 Likes

I think it should be fine to add since it doesn't break the hermetic build model but IDK if there are other gotchas with prefix headers. Perhaps @Douglas_Gregor knows?

Not sure I agree, we could end up with a lot of build setting APIs this way. I think we need to be careful and evaluate which ones are important enough to add.

1 Like

It would also be helpful to know what the desired use case is.

"Prefix header" could mean a couple different things:

  • A file included textually at the beginning of each source file
  • A file that is precompiled, and that precompiled header is added at the beginning of each compilation

The first is just adding a compilation flag to the build, but less efficient. The second is more efficient, but more work because the build system needs to register a separate action to precompile the file and then add that as an input to subsequent compile actions.

Someone from Apple can correct me if I'm wrong, but the use of precompiled headers by default also seems to have fallen to the wayside in deference to modules—old Xcodes used to use precompiled headers to speed up imports of common frameworks, but modules are a cleaner representation of that. Modules don't replace all uses of prefix headers, but if the desired use case is "I want to avoid repeating a lot of common inclusions", then supporting modules is probably the better answer there.

1 Like

It often occurs that you need to include a set of common definitions that are supposed to be known to all files while building a library. For instance, I'd like to test against TARGET_OS_IPHONE everywhere in implementation files. The only alternative without a prefix header is to manually #import <TargetConditionals.h> in every single file, which is both error prone (test can silently fail if forgotten), and tedious (repetitive code). Like, adding #import "common_prefix.h" to tens or hundreds of .c files.

About the replacement with a module, I agree that, with caching, they efficiently replace precompiled headers when they are used as a speed optimization. But my use case is not about speed, and I don't see how modules could help solve the issue here (can you give an example of what you have in mind?).

Now it is not that I'd absolutely want a specific setting. I'd be fine with generic "safe" flags that would let me use this package as a dependency to other packages, as there's nothing unsafe about importing a file that resides in the package source tree. That being said, if it's very often used, a dedicated setting could well make sense.

2 Likes

+1 for the common definition case of injecting #import "x.h"

Any progress on this? It would definitely be useful. Running into the case of having to do a lot of gating on platform using TARGET_OS_something and as pointed out already these can fail silently if nothing imports TargetConditionals.

Instead of a prefix header, could you accomplish the same by doing something like this in the bottom of your package manifest?:

for target in package.targets {
  var settings = target.cSettings ?? []
  settings.append(contentsOf: [
    .define("TARGET_OS_MACOS", to: "YES", .when(platforms: [.macOS])),
    .define("TARGET_OS_LINUX", to: "YES", .when(platforms: [.linux])),
    .define("TARGET_OS_IOS", to: "YES", .when(platforms: [.iOS])),
    .define("TARGET_OS_WATCHOS", to: "YES", .when(platforms: [.watchOS])),
    .define("TARGET_OS_TVOS", to: "YES", .when(platforms: [.tvOS]))
  ])
  target.cSettings = settings
}

Needed to use "1" instead of "YES" but that seems to work. I'm a little hesitant to override these but also not clear on what could go wrong...

Yeah, I wasn’t sure precisely what your prefix header looked like, so I just gave an approximate example.

To be precise, adding this to a target in the manifest...

.define("TARGET_OS_MACOS", to: "1")

...invokes the C compiler with...

-D TARGET_OS_MACOS=1

...which is in turn equivalent to putting this at the top of every C file...

#define TARGET_OS_MACOS 1

...which is presumably what your prefix header would have been doing.

Those are not pre‐existing settings, so the only thing you could be “overriding” would be your own definitions if you had already used those names earlier in the manifest. And the only thing adjusting its behaviour according to those settings would be your own #if directives in your source code.

1 Like

please note that these constants have precise definitions in TargetConditionals.h on Apple platforms, and that these may not be "intuitive". For instance, on native iOS builds, TARGET_OS_MAC, TARGET_OS_IPHONE, TARGET_OS_IOS are all defined to 1. Also, this not great to duplicate and maintain this behavior separately when a reference definition exists (Linux needs one though).

About the original request (prefix header setting for C targets in SPM, or at least generic safe setting that allows to define a prefix), how should we proceed? I'm not familiar with things go (please let me know), but I think it would be good to either accept or reject the idea at some point.

Well @Aciid and @NeoNacho who commented upthread (disagreeing with one another) are probably the main two people you’d have to convince.

@NeoNacho said he would rather limit the build settings to really important ones. So with that in mind it would be worth elaborating what your specific use cases are.

So far only platform conditionals have been mentioned. If they are really the main impetus, maybe they could be targeted directly by adding to the current list of settings understood and managed automatically by SwiftPM. Right now we have DEBUG and SWIFT_PACKAGE (and Xcode). Platform conditionals are so important they are baked right into the Swift compiler, so maybe always supplying direct equivalents to C targets would be reasonable:

  • os(macOS)OS_MACOS
  • os(Linux)OS_LINUX
  • os(iOS)OS_IOS
  • os(watchOS)OS_WATCHOS
  • os(tvOS)OS_TVOS
  • os(Android)OS_ANDROID
  • os(Windows)OS_WINDOWS

As Swift evolves, the list would be kept in sync with the os(...) directive, with new additions and aliases as needed.

That would get you reliable platform conditionals for even less work than configuring a C setting, and it would not require evolving the manifest API. How well would that handle your use case? Even if it turns out to not be helpful at all, it illustrates the sort of questions that need to be sorted through at this point: Why do we want prefix headers? Are prefix headers really the best way to accomplish that goal (since we have a fresh slate and can design absolutely anything)? So for whatever your other use cases are, try to think about them the same way and report back what you come up with.

P.S. If we go this route we may as well just complete the entire list at once:

  • arch(i386)ARCH_I386
  • arch(x86_64)ARCH_X86_64
  • arch(arm)ARCH_ARM
  • arch(arm64)ARCH_ARM64
  • targetEnvironment(simulator)TARGET_ENVIRONMENT_SIMULATOR
  • targetEnvironment(macCatalyst)TARGET_ENVIRONMENT_MAC_CATALYST
3 Likes

Thank you for clarifying. Pros for prefix header support in SPM for C-based languages:

  1. Accessing code in the same target. With C-based languages, you are required to import headers to access code defined elsewhere in the same target. This is in contrast with Swift for which each file gets access to all other non-private definitions in files of the same module. This can be achieved to some extent with a prefix header.

  2. Define common behaviors and dependencies. It is also a common practice in C to define common functions / types / dependencies in prefix.h (or indirectly) expected to be known by all other files in a project. In the absence of prefix header mechanism, each file must explicitly include such a file manually (by typing #include "prefix.h"). Repetitive code shouldn't be forced by the tools.

  3. Automation. More generally, this can be seen as a powerful automation tool, that brings a lot of flexibility by avoiding the requirement of manually editing all the files of a project. This is a useful addition for C languages, for experimenting with global changes, but not only.

  4. Even for constant definition, sometimes it does make sense to have them defined in the build tool, but sometimes it is preferable to have them defined in header files. This is because the same code may be compiled with different tools on different platforms, and you typically don't want to maintain two (or more) sets of definitions.

  5. Transitioning existing projects. Many C projects use config.h or similar constructs (that may not be always suitable, agreed), and having support for that would enable easier transitioning to SPM. I guess many maintainer would be tempted to try it if it did not require editing every single source file in large repos with 100s of them.

I agree though with the general approach that new settings should be carefully hand-picked & best practices should be strongly encouraged / enforced. Lack of prefix support is one of the (few) things that required substantial work to port existing Xcode targets to SPM that I went through when trying to migrate our pre-existing stuff to it for an otherwise pretty smooth experience.

2 Likes

The main issue I'm running into is the inability to gate import statements in public headers. I'd like to be able to do something like:

#if defined COCOAPODS 
#import <SomeModule/SomeModule.h>
#elsif defined SWIFT_PACKAGE
@import SomeModule;
#endif

inb4 someone tells me to just negate the COCOAPODS check. This is just an example, the actual use case involves more build variants than this and sometimes they overlap. ex: COCOAPODS and __cplusplus checks.

Is there any progress on this?
Just asking, can we do this way,
cSettings: [.define("GCC_PREFIX_HEADER", to: "PrefixHeader.pch", nil)],

1 Like