Skip to content

Commit

Permalink
Support separate modules for static and dynamic exporting symbols for…
Browse files Browse the repository at this point in the history
… Windows (#8049)

On Windows, there is a limit of around 64K to the number of symbols a
DLL/EXE can export. We hit that regularly with large projects like
SwiftPM itself. This hides those symbols and only exposes the ones
requested for a DLL.

For Windows triples only, creates a parallel build graph for Swift
modules, one for static linking using -static, and one for exporting
linking which is the default. DLL products consume their direct target
dependencies as exporting. All other dependencies use the static
versions to eliminate unnecessary symbol exports.

The bulk of this is managed by the SwiftModuleBuildDescription which
will create a duplicate of itself for the exporting case and set its own
type to static linking. Both modules are fed to the planner to create
llbuild swift command tasks. Code is added for dynamic libraries to hook
up the correct inputs for exporting the symbols in the libraries
targets.

This is WIP as we need to do a lot of testing to ensure we didn't break
anything. Ensuring this only affects builds for Windows triples helps
mitigate that.
  • Loading branch information
dschaefer2 authored Nov 7, 2024
1 parent 7c6da12 commit f34907c
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 4 deletions.
64 changes: 63 additions & 1 deletion Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ public final class SwiftModuleBuildDescription {

var modulesPath: AbsolutePath {
let suffix = self.buildParameters.suffix
return self.buildParameters.buildPath.appending(component: "Modules\(suffix)")
var path = self.buildParameters.buildPath.appending(component: "Modules\(suffix)")
if self.windowsTargetType == .dynamic {
path = path.appending("dynamic")
}
return path
}

/// The path to the swiftmodule file after compilation.
Expand Down Expand Up @@ -264,6 +268,19 @@ public final class SwiftModuleBuildDescription {
/// Whether to disable sandboxing (e.g. for macros).
private let shouldDisableSandbox: Bool

/// For Windows, we default to static objects and but also create objects
/// that export symbols for DLLs. This allows library targets to be used
/// in both contexts
public enum WindowsTargetType {
case `static`
case dynamic
}
/// The target type. Leave nil for non-Windows behavior.
public let windowsTargetType: WindowsTargetType?

/// The corresponding target for dynamic library export (i.e., not -static)
public private(set) var windowsDynamicTarget: SwiftModuleBuildDescription? = nil

/// Create a new target description with target and build parameters.
init(
package: ResolvedPackage,
Expand Down Expand Up @@ -319,6 +336,14 @@ public final class SwiftModuleBuildDescription {
observabilityScope: observabilityScope
)

if buildParameters.triple.isWindows() {
// Default to static and add another target for DLLs
self.windowsTargetType = .static
self.windowsDynamicTarget = .init(windowsExportFor: self)
} else {
self.windowsTargetType = nil
}

if self.shouldEmitObjCCompatibilityHeader {
self.moduleMap = try self.generateModuleMap()
}
Expand All @@ -340,6 +365,31 @@ public final class SwiftModuleBuildDescription {
try self.generateTestObservation()
}

/// Private init to set up exporting version of this module
private init(windowsExportFor parent: SwiftModuleBuildDescription) {
self.windowsTargetType = .dynamic
self.windowsDynamicTarget = nil
self.tempsPath = parent.tempsPath.appending("dynamic")

// The rest of these are just copied from the parent
self.package = parent.package
self.target = parent.target
self.swiftTarget = parent.swiftTarget
self.toolsVersion = parent.toolsVersion
self.buildParameters = parent.buildParameters
self.macroBuildParameters = parent.macroBuildParameters
self.derivedSources = parent.derivedSources
self.pluginDerivedSources = parent.pluginDerivedSources
self.pluginDerivedResources = parent.pluginDerivedResources
self.testTargetRole = parent.testTargetRole
self.fileSystem = parent.fileSystem
self.buildToolPluginInvocationResults = parent.buildToolPluginInvocationResults
self.prebuildCommandResults = parent.prebuildCommandResults
self.observabilityScope = parent.observabilityScope
self.shouldGenerateTestObservation = parent.shouldGenerateTestObservation
self.shouldDisableSandbox = parent.shouldDisableSandbox
}

private func generateTestObservation() throws {
guard target.type == .test else {
return
Expand Down Expand Up @@ -519,6 +569,18 @@ public final class SwiftModuleBuildDescription {
args += ["-parse-as-library"]
}

switch self.windowsTargetType {
case .static:
// Static on Windows
args += ["-static"]
case .dynamic:
// Add the static versions to the include path
// FIXME: need to be much more deliberate about what we're including
args += ["-I", self.modulesPath.parentDirectory.pathString]
case .none:
break
}

// Only add the build path to the framework search path if there are binary frameworks to link against.
if !self.libraryBinaryPaths.isEmpty {
args += ["-F", self.buildParameters.buildPath.pathString]
Expand Down
19 changes: 17 additions & 2 deletions Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ extension LLBuildManifestBuilder {
)
} else {
try self.addCmdWithBuiltinSwiftTool(target, inputs: inputs, cmdOutputs: cmdOutputs)
if let dynamicTarget = target.windowsDynamicTarget {
// Generate dynamic module for Windows
let inputs = try self.computeSwiftCompileCmdInputs(dynamicTarget)
let objectNodes = dynamicTarget.buildParameters.prepareForIndexing == .off ? try dynamicTarget.objects.map(Node.file) : []
let moduleNode = Node.file(dynamicTarget.moduleOutputPath)
let cmdOutputs = objectNodes + [moduleNode]
try self.addCmdWithBuiltinSwiftTool(dynamicTarget, inputs: inputs, cmdOutputs: cmdOutputs)
self.addTargetCmd(dynamicTarget, cmdOutputs: cmdOutputs)
try self.addModuleWrapCmd(dynamicTarget)
}
}

self.addTargetCmd(target, cmdOutputs: cmdOutputs)
Expand Down Expand Up @@ -532,7 +542,7 @@ extension LLBuildManifestBuilder {
inputs: cmdOutputs,
outputs: [targetOutput]
)
if self.plan.graph.isInRootPackages(target.target, satisfying: target.buildParameters.buildEnvironment) {
if self.plan.graph.isInRootPackages(target.target, satisfying: target.buildParameters.buildEnvironment), target.windowsTargetType != .dynamic {
if !target.isTestTarget {
self.addNode(targetOutput, toTarget: .main)
}
Expand Down Expand Up @@ -636,6 +646,11 @@ extension SwiftModuleBuildDescription {
}

public func getLLBuildTargetName() -> String {
self.target.getLLBuildTargetName(buildParameters: self.buildParameters)
let name = self.target.getLLBuildTargetName(buildParameters: self.buildParameters)
if self.windowsTargetType == .dynamic {
return "dynamic." + name
} else {
return name
}
}
}
13 changes: 12 additions & 1 deletion Sources/Build/BuildPlan/BuildPlan+Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,18 @@ extension BuildPlan {

buildProduct.staticTargets = dependencies.staticTargets.map(\.module)
buildProduct.dylibs = dependencies.dylibs
buildProduct.objects += try dependencies.staticTargets.flatMap { try $0.objects }
buildProduct.objects += try dependencies.staticTargets.flatMap {
if buildProduct.product.type == .library(.dynamic),
case let .swift(swiftModule) = $0,
let dynamic = swiftModule.windowsDynamicTarget,
buildProduct.product.modules.contains(id: swiftModule.target.id)
{
// On Windows, export symbols from the direct swift targets of the DLL product
return try dynamic.objects
} else {
return try $0.objects
}
}
buildProduct.libraryBinaryPaths = dependencies.libraryBinaryPaths
buildProduct.availableTools = dependencies.availableTools
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Build/BuildPlan/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,9 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
switch buildTarget {
case .swift(let target):
try self.plan(swiftTarget: target)
if let dynamicTarget = target.windowsDynamicTarget {
try self.plan(swiftTarget: dynamicTarget)
}
case .clang(let target):
try self.plan(clangTarget: target)
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/_InternalTestSupport/MockBuildTestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ extension Basics.Triple {
public static let arm64Linux = try! Self("aarch64-unknown-linux-gnu")
public static let arm64Android = try! Self("aarch64-unknown-linux-android")
public static let windows = try! Self("x86_64-unknown-windows-msvc")
public static let x86_64Windows = try! Self("x86_64-unknown-windows-msvc")
public static let arm64Windows = try! Self("aarch64-unknown-windows-msvc")
public static let wasi = try! Self("wasm32-unknown-wasi")
public static let arm64iOS = try! Self("arm64-apple-ios")
}
Expand Down
165 changes: 165 additions & 0 deletions Tests/BuildTests/WindowsBuildPlanTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import XCTest

import Basics
@testable import Build
import LLBuildManifest
import _InternalTestSupport

@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
import PackageGraph

final class WindowsBuildPlanTests: XCTestCase {
// Tests that our build plan is build correctly to handle separation
// of object files that export symbols and ones that don't and to ensure
// DLL products pick up the right ones.

func doTest(triple: Triple) async throws {
let fs = InMemoryFileSystem(emptyFiles: [
"/libPkg/Sources/coreLib/coreLib.swift",
"/libPkg/Sources/dllLib/dllLib.swift",
"/libPkg/Sources/staticLib/staticLib.swift",
"/libPkg/Sources/objectLib/objectLib.swift",
"/exePkg/Sources/exe/main.swift",
])

let observability = ObservabilitySystem.makeForTesting()

let graph = try loadModulesGraph(
fileSystem: fs,
manifests: [
.createFileSystemManifest(
displayName: "libPkg",
path: "/libPkg",
products: [
.init(name: "DLLProduct", type: .library(.dynamic), targets: ["dllLib"]),
.init(name: "StaticProduct", type: .library(.static), targets: ["staticLib"]),
.init(name: "ObjectProduct", type: .library(.automatic), targets: ["objectLib"]),
],
targets: [
.init(name: "coreLib", dependencies: []),
.init(name: "dllLib", dependencies: ["coreLib"]),
.init(name: "staticLib", dependencies: ["coreLib"]),
.init(name: "objectLib", dependencies: ["coreLib"]),
]
),
.createRootManifest(
displayName: "exePkg",
path: "/exePkg",
dependencies: [.fileSystem(path: "/libPkg")],
targets: [
.init(name: "exe", dependencies: [
.product(name: "DLLProduct", package: "libPkg"),
.product(name: "StaticProduct", package: "libPkg"),
.product(name: "ObjectProduct", package: "libPkg"),
]),
]
)
],
observabilityScope: observability.topScope
)

let label: String
let dylibPrefix: String
let dylibExtension: String
let dynamic: String
switch triple {
case Triple.x86_64Windows:
label = "x86_64-unknown-windows-msvc"
dylibPrefix = ""
dylibExtension = "dll"
dynamic = "/dynamic"
case Triple.x86_64MacOS:
label = "x86_64-apple-macosx"
dylibPrefix = "lib"
dylibExtension = "dylib"
dynamic = ""
case Triple.x86_64Linux:
label = "x86_64-unknown-linux-gnu"
dylibPrefix = "lib"
dylibExtension = "so"
dynamic = ""
default:
label = "fixme"
dylibPrefix = ""
dylibExtension = ""
dynamic = ""
}

let tools: [String: [String]] = [
"C.exe-\(label)-debug.exe": [
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swift.o",
"/path/to/build/\(label)/debug/exe.build/main.swift.o",
"/path/to/build/\(label)/debug/objectLib.build/objectLib.swift.o",
"/path/to/build/\(label)/debug/staticLib.build/staticLib.swift.o",
"/path/to/build/\(label)/debug/\(dylibPrefix)DLLProduct.\(dylibExtension)",
"/path/to/build/\(label)/debug/exe.product/Objects.LinkFileList",
] + (triple.isMacOSX ? [] : [
// modulewrap
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swiftmodule.o",
"/path/to/build/\(label)/debug/exe.build/exe.swiftmodule.o",
"/path/to/build/\(label)/debug/objectLib.build/objectLib.swiftmodule.o",
"/path/to/build/\(label)/debug/staticLib.build/staticLib.swiftmodule.o",
]),
"C.DLLProduct-\(label)-debug.dylib": [
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swift.o",
"/path/to/build/\(label)/debug/dllLib.build\(dynamic)/dllLib.swift.o",
"/path/to/build/\(label)/debug/DLLProduct.product/Objects.LinkFileList",
] + (triple.isMacOSX ? [] : [
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swiftmodule.o",
"/path/to/build/\(label)/debug/dllLib.build/dllLib.swiftmodule.o",
])
]

let plan = try await BuildPlan(
destinationBuildParameters: mockBuildParameters(
destination: .target,
triple: triple
),
toolsBuildParameters: mockBuildParameters(
destination: .host,
triple: triple
),
graph: graph,
fileSystem: fs,
observabilityScope: observability.topScope
)

let llbuild = LLBuildManifestBuilder(
plan,
fileSystem: fs,
observabilityScope: observability.topScope
)
try llbuild.generateManifest(at: "/manifest")

for (name, inputNames) in tools {
let command = try XCTUnwrap(llbuild.manifest.commands[name])
XCTAssertEqual(Set(command.tool.inputs), Set(inputNames.map({ Node.file(.init($0)) })))
}
}

func testWindows() async throws {
try await doTest(triple: .x86_64Windows)
}

// Make sure we didn't mess up macOS
func testMacOS() async throws {
try await doTest(triple: .x86_64MacOS)
}

// Make sure we didn't mess up linux
func testLinux() async throws {
try await doTest(triple: .x86_64Linux)
}
}

0 comments on commit f34907c

Please sign in to comment.