Hi everyone –– I'd like to present the following pitch for adding support for targets with mixed language sources (e.g. Swift + Objective-C). The corresponding pre-pitch can be found here.
Package Manager Mixed Language Target Support
Implementation: apple/swift-package-manager#5919
Introduction
This is a proposal for adding package manager support for targets containing both Swift and C based language sources (henceforth, referred to as mixed language sources). Currently, a target’s source can be either Swift or a C based language (SE-0038), but not both.
Motivation
Packages may need to contain mixed language sources for both legacy or technical reasons. For developers building or maintaining packages with mixed languages (e.g. Swift and Objective-C), there are two workarounds for doing so with Swift Package Manager, but they have drawbacks that degrade the developer experience, and sometimes are not even an option:
-
Distribute binary frameworks via binary targets. Drawbacks include that the package will be less portable as it can only support platforms that the binaries support, binary dependencies are only available on Apple platforms, customers cannot view or easily debug the source in their project workspace, and requires tooling to generate the binaries for release.
-
Separate a target’s implementation into sub-targets based on language type, adding dependencies where necessary. For example, a target
Foo
may have Swift-only sources that can call into an underlying targetFooObjc
that contains Clang-only sources. Drawbacks include needing to depend on the public API surfaces between the targets, increasing the complexity of the package’s manifest and organization for both maintainers and clients, and preventing package developers from incrementally migrating internal implementation from one language to another (e.g. Objective-C to Swift) since there is still a separation across targets based on language.
Package manager support for mixed language targets would address both of the above drawbacks by enabling developers to mix sources of supported languages within a single target without complicating their package’s structure or developer experience.
Proposed solution
This solution allows the package manager to determine if a target contains mixed language sources and build it as a single module. It's done automatically and doesn't require changes to the package manager's public API.
At a high level, it splits the package creation process into two parts based on the language of the sources. The Swift sources are built by the Swift compiler and the C Language sources are built by the Clang compiler. To achieve interoperability between the two halves of the package, a few things have to happen:
- The Swift compiler is made aware of the Clang half of the package when building the Swift sources into a
swiftmodule
. - The generated interoperability header emitted by the Swift compiler is added as a submodule to the Clang half of the package’s generated module map.
- The Clang half of the package is built with knowledge of the generated interoperability header.
The following example layout defines a package containing mixed language sources.
// Manifest
MixedPackage/Package.swift
// Clang sources
// - Public headers
MixedPackage/Sources/MixedPackage/include/MixedPackage.h
MixedPackage/Sources/MixedPackage/include/Sith.h
MixedPackage/Sources/MixedPackage/include/droid_debug.h
// - Implementations and internal headers
MixedPackage/Sources/MixedPackage/Sith.m
MixedPackage/Sources/MixedPackage/droid_debug.c
MixedPackage/Sources/MixedPackage/SithRegistry.h
MixedPackage/Sources/MixedPackage/SithRegistry.m
// Swift sources
MixedPackage/Sources/MixedPackage/Jedi.swift
MixedPackage/Sources/MixedPackage/Lightsaber.swift
// Resources
MixedPackage/Sources/MixedPackage/hello_there.txt
// Tests
MixedPackage/Tests/MixedPackageTests/JediTests.swift
MixedPackage/Tests/MixedPackageTests/SithTests.m
The proposed solution would enable the above package to have the following capabilities:
- Export the public API of the mixed language sources as a single module for use by clients of the package.
- Use any Objective-C compatible API exposed by the package’s Swift sources within the package’s Objective-C sources.
- Use any public API exposed by the package’s Objective-C or C sources within the package’s Swift sources.
- Use internal C based language sources within the Clang half of the module. Likewise for internal Swift types within the Swift half of the package.
- Access target resources from a Swift or Objective-C context.
Requirements
Initial support for targets containing mixed language sources will have the following requirements:
- The target must be either a library or test target. Support for other types of targets is deferred until the use cases become clear.
- The target must be built on a Mac. This is because the Swift compiler-generated Objective-C compatibility header is only generated on macOS.
- Including a custom module map is not supported. This is to reduce the possibility of an invalid module map since a custom module map will need to be modified by the package manager to add a submodule to expose the generated Objective-C compatibility header. Support for this is deferred until the use cases become clear.
Detailed design
Up until this proposal, when a package was loading, each target was represented programmatically as either a SwiftTarget
or ClangTarget
. Which of these types to use was informed by the sources found in the target. For targets with mixed language sources, an error was thrown and surfaced to the client. During the build process, each of those types mapped to another type (SwiftTargetBuildDescription
or ClangTargetBuildDescription
) that described how the target should be built.
This proposal adds two new types, MixedTarget
and MixedTargetDescription
, that represent targets with mixed language sources during the package loading and building phases, respectively.
While an implementation detail, it’s worth noting that in this approach, a MixedTarget
is a wrapper type around an underlying SwiftTarget
and ClangTarget
. Initializing a MixedTarget
will internally initialize a SwiftTarget
from the given Swift sources and a ClangTarget
from the given Clang sources. This extends to the MixedTargetDescription
type in that it wraps a SwiftTargetDescription
and ClangTargetDescription
and configures them accordingly to successfully build.
To compile both sources, the Clang Virtual File System overlay system is used to pass necessary metadata and file references to the Clang compiler. Specifically, a temporary module map is substituted when compiling the Swift part of the module because the real module map includes a reference to the Swift generated header that does not exist yet. This approach was, in part, informed by this Swift forums discussion.
The proposed solution adds the following behavior:
-
During the package loading phase, a target with mixed language sources will be represented by a
MixedTarget
. -
During the package building phase, the
MixedTarget
will be used to create aMixedTargetBuildDescription
to inform the build process. -
Initializing a
MixedTargetBuildDescription
will do a few things, both directly and indirectly. -
Generate a module map that exposes the public Clang sources and includes a submodule for the interoperability header that will generate during the build.
-
Generate a Clang VFS overlay of the module’s public headers and module map.
-
Generate a Clang VFS overlay of a modified module map that excludes the submodule with the generated interoperability header.
-
Pass the
--import-underlying-module
flag to the underlyingSwiftTargetDescription
’s build flags. -
Pass the two VFS overlays to the
SwiftTargetDescription
’s build flags. The Swift compiler will pass these to the underlying Clang compiler. -
Pass the directory of the generated interoperability header to the
ClangTargetDescription
’s build flags. -
When building a
MixedTarget
as a dependency of a parent target, pass the module map and/or public header directory to the parent target's build flags. This follows the exact behavior of when building aClangTarget
as a dependency of a parent target. -
Construct the llbuild manifest (found at
$(PackageName)/.debug/debug.yaml
) to include the Swift compiler command to built the Swift half of the target and the Clang compiler command to build the Clang half of the target. The Clang command is always executed after the Swift compile command as it depends on the interoperability header generated by the Swift compile command as an input.
One benefit of this design is that it offers a natural path to making all targets mixed source targets by default. This would greatly simplify logic in the package loading and building phases. With this approach, the implementation from ClangTarget
, SwiftTarget
, ClangTargetBuildDescription
and SwiftTargetBuildDescription
can be bubbled up to the mixed target types accordingly.
Security
This has no impact on security, safety, or privacy.
Impact on existing packages
This proposal will not affect the behavior of existing packages. In the proposed solution, the code path to build a mixed language package is separate from the existing code paths to build packages with Swift sources and C Language sources, respectively.
Alternatives considered
Provide custom implementations for MixedTarget
and MixedTargetBuildDescription
As explained in the Detailed Design section, these two types effectively wrap the Swift and Clang parts necessary to define or build the target. One alternative approach was to provide custom implementations that did not heavily rely on code reuse of existing types. The deciding drawback of this approach was that it would have resulted in a lot of duplicated code.
Future Directions
Some of these are informed by current constraints from the above Requirements section.
- Extend mixed language target support to other types of targets (e.g. executables).
- Extend support for mixed language targets with custom module maps.
- Expand the level of support when building on non-macOS machines.
- Extend this solution so that all targets are mixed language targets by default. This would simplify the current implementation of the package manager.
Open questions for this Pitch
Please ask and discuss questions about any part of this pitch. I have identified the following questions while working on the solution that I’d like to resolve.
- The solution’s implementation at this point does omit support for custom module maps. While there is a path to support them, I’d like to hear from the community about whether that should be pursued. The reason I have flagged this is because the module map needs to include the generated Swift header, but that is only known at build time. Supporting custom module maps would mean taking the custom module map and programmatically adding to it during the build process. This would not be very intuitive if issues arise. What do folks think about supporting custom module maps for mixed language targets? What do folks currently like about defining custom module maps for targets with Clang-only resources?
- Supporting types of targets other than library or test targets is not straightforward, and will require some more investigation. Do folks have strong feelings about supporting other types of targets as well?
- Currently, the package manager will only generate the Swift interop header on macOS. As such, trying to build a mixed target on Linux, for example, would throw an error. There are cases though where this constraint may not limit a mixed language target. For example, building a Swift + C source target on Linux. The generated Swift interop header isn’t relevant here, so it may be able to build successfully. I wanted to flag this so folks in favor of such behavior could provide use cases to justify looking into this further.
Thank you for reading and discussing this pitch!
– Nick from the Firebase team