Skip to content

Commit aee3122

Browse files
authored
jextract: introduce importedModuleStubs config (#656)
This is mostly a workaround for not handling cross module types yet, but it's good enough to unblock some use-cases so going with this for now. It allows to tell jextract that "pretend there's a type like that" in that other "known module" -- similar to like we teach it about Data in Foundation. add some tests
1 parent 98a6cba commit aee3122

6 files changed

Lines changed: 344 additions & 22 deletions

File tree

Sources/JExtractSwiftLib/Swift2JavaTranslator.swift

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ extension Swift2JavaTranslator {
7878
AnalysisResult(
7979
importedTypes: self.importedTypes,
8080
importedGlobalVariables: self.importedGlobalVariables,
81-
importedGlobalFuncs: self.importedGlobalFuncs
81+
importedGlobalFuncs: self.importedGlobalFuncs,
8282
)
8383
}
8484

@@ -117,12 +117,12 @@ extension Swift2JavaTranslator {
117117
visitor.visit(
118118
nominalDecl: dataDecl.syntax!.asNominal!,
119119
in: nil,
120-
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift"
120+
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift",
121121
)
122122
visitor.visit(
123123
nominalDecl: dataProtocolDecl.syntax!.asNominal!,
124124
in: nil,
125-
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATAPROTOCOL.swift"
125+
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATAPROTOCOL.swift",
126126
)
127127
}
128128
}
@@ -133,7 +133,7 @@ extension Swift2JavaTranslator {
133133
visitor.visit(
134134
nominalDecl: dateDecl.syntax!.asNominal!,
135135
in: nil,
136-
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATE.swift"
136+
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATE.swift",
137137
)
138138
}
139139
}
@@ -145,7 +145,8 @@ extension Swift2JavaTranslator {
145145
let symbolTable = SwiftSymbolTable.setup(
146146
moduleName: self.swiftModuleName,
147147
inputs + [dependenciesSource],
148-
log: self.log
148+
config: self.config,
149+
log: self.log,
149150
)
150151
self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable)
151152
}
@@ -225,7 +226,7 @@ extension Swift2JavaTranslator {
225226
/// Try to resolve the given nominal declaration node into its imported representation.
226227
func importedNominalType(
227228
_ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax,
228-
parent: ImportedNominalType?
229+
parent: ImportedNominalType?,
229230
) -> ImportedNominalType? {
230231
if !nominalNode.shouldExtract(config: config, log: log, in: parent) {
231232
return nil
@@ -249,9 +250,12 @@ extension Swift2JavaTranslator {
249250
}
250251

251252
// Whether to import this extension?
252-
guard swiftNominalDecl.moduleName == self.swiftModuleName else {
253+
let isFromThisModule = swiftNominalDecl.moduleName == self.swiftModuleName
254+
let isFromStubbedModule = config.hasImportedModuleStub(moduleOfNominal: swiftNominalDecl.moduleName)
255+
guard isFromThisModule || isFromStubbedModule else {
253256
return nil
254257
}
258+
255259
guard swiftNominalDecl.syntax!.shouldExtract(config: config, log: log, in: nil) else {
256260
return nil
257261
}
@@ -266,20 +270,14 @@ extension Swift2JavaTranslator {
266270
return alreadyImported
267271
}
268272

269-
// Apply type-name filters (patterns with `.`)
270-
guard shouldJExtractType(qualifiedName: fullName, config: config) else {
271-
log.info("Skipping type (filtered out): \(fullName)")
272-
return nil
273-
}
274-
275273
let importedNominal = try? ImportedNominalType(swiftNominal: nominal, lookupContext: lookupContext)
276274

277275
importedTypes[fullName] = importedNominal
278276
return importedNominal
279277
}
280278
}
281279

282-
// ==== ----------------------------------------------------------------------------------------------------------------
280+
// ==== -----------------------------------------------------------------------
283281
// MARK: Errors
284282

285283
public struct Swift2JavaTranslatorError: Error {

Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import CodePrinting
16+
import SwiftJavaConfigurationShared
17+
import SwiftParser
1618
import SwiftSyntax
1719

1820
package protocol SwiftSymbolTableProtocol {
@@ -66,7 +68,8 @@ extension SwiftSymbolTable {
6668
package static func setup(
6769
moduleName: String,
6870
_ inputFiles: some Collection<SwiftJavaInputFile>,
69-
log: Logger
71+
config: Configuration?,
72+
log: Logger,
7073
) -> SwiftSymbolTable {
7174

7275
// Prepare imported modules.
@@ -90,12 +93,36 @@ extension SwiftSymbolTable {
9093
}
9194
}
9295

96+
// Load stub type declarations for imported modules from config.
97+
// This enables types from external modules (e.g. extension targets) to be
98+
// resolved in the symbol table without scanning their actual source.
99+
if let stubs = config?.importedModuleStubs {
100+
for (stubModuleName, declarations) in stubs {
101+
if importedModules[stubModuleName] == nil {
102+
let source = declarations.joined(separator: "\n")
103+
let sourceFile = Parser.parse(source: source)
104+
var stubBuilder = SwiftParsedModuleSymbolTableBuilder(
105+
moduleName: stubModuleName,
106+
importedModules: ["Swift": importedModules["Swift"]!],
107+
)
108+
stubBuilder.handle(sourceFile: sourceFile, sourceFilePath: "\(stubModuleName)_stub.swift")
109+
let stubModule = stubBuilder.finalize()
110+
importedModules[stubModuleName] = stubModule
111+
log.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())")
112+
} else {
113+
log.info("Module '\(stubModuleName)' already known, skipping stub")
114+
}
115+
}
116+
} else {
117+
log.debug("No importedModuleStubs in config")
118+
}
119+
93120
// FIXME: Support granular lookup context (file, type context).
94121

95122
var builder = SwiftParsedModuleSymbolTableBuilder(
96123
moduleName: moduleName,
97124
importedModules: importedModules,
98-
log: log
125+
log: log,
99126
)
100127
// First, register top-level and nested nominal types to the symbol table.
101128
for sourceFile in inputFiles {

Sources/SwiftJavaConfigurationShared/Configuration.swift

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,44 @@ public struct Configuration: Codable {
8787
/// Same pattern syntax as swiftFilterInclude
8888
public var swiftFilterExclude: [String]?
8989

90+
/// Stub type declarations for imported modules whose source is not available
91+
/// to the jextract tool. Keyed by module name, values are arrays of Swift
92+
/// declaration strings that will be parsed as if they belonged to that module.
93+
///
94+
/// Example:
95+
/// ```json
96+
/// {
97+
/// "importedModuleStubs": {
98+
/// "ExternalModule": [
99+
/// "public enum Outer {}",
100+
/// "public struct Config {}"
101+
/// ]
102+
/// }
103+
/// }
104+
/// ```
105+
public var importedModuleStubs: [String: [String]]?
106+
107+
/// Whether the given module name has stub declarations configured
108+
public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool {
109+
importedModuleStubs?.keys.contains(moduleName) ?? false
110+
}
111+
112+
/// Monomorphization entries for generic types, mapping a qualified Swift type
113+
/// name to a concrete specialization with a custom Java-facing name.
114+
///
115+
/// Example:
116+
/// ```json
117+
/// {
118+
/// "monomorphize": {
119+
/// "Tank": {
120+
/// "javaName": "FishTank",
121+
/// "typeArgs": {"Element": "Fish"}
122+
/// }
123+
/// }
124+
/// }
125+
/// ```
126+
public var monomorphize: [String: MonomorphizeEntry]?
127+
90128
// ==== wrap-java ---------------------------------------------------------
91129

92130
/// The Java class path that should be passed along to the swift-java tool.
@@ -220,7 +258,7 @@ public enum MavenRepositoryDescriptor: Hashable, Codable {
220258
throw DecodingError.dataCorruptedError(
221259
forKey: .type,
222260
in: container,
223-
debugDescription: "Unknown repository type: '\(type)'. Supported: maven, mavenCentral, mavenLocal, google"
261+
debugDescription: "Unknown repository type: '\(type)'. Supported: maven, mavenCentral, mavenLocal, google",
224262
)
225263
}
226264
}
@@ -302,7 +340,7 @@ public func readConfiguration(
302340
string: String,
303341
configPath: URL?,
304342
file: String = #fileID,
305-
line: UInt = #line
343+
line: UInt = #line,
306344
) throws -> Configuration? {
307345
guard let configData = string.data(using: .utf8) else {
308346
return nil
@@ -319,7 +357,7 @@ public func readConfiguration(
319357
error: error,
320358
text: string,
321359
file: file,
322-
line: line
360+
line: line,
323361
)
324362
}
325363
}
@@ -426,6 +464,23 @@ public struct ConfigurationError: Error {
426464
}
427465
}
428466

467+
// ==== -----------------------------------------------------------------------
468+
// MARK: MonomorphizeEntry
469+
470+
/// Configuration entry for monomorphizing a generic type into a concrete Java class
471+
public struct MonomorphizeEntry: Codable, Sendable {
472+
/// Mapping from generic parameter name to concrete type (e.g. {"T": "Fish"})
473+
public var typeArgs: [String: String]
474+
475+
/// The Java-facing class name (e.g. "FishTank")
476+
public var javaName: String
477+
478+
public init(typeArgs: [String: String], javaName: String) {
479+
self.typeArgs = typeArgs
480+
self.javaName = javaName
481+
}
482+
}
483+
429484
public enum LogLevel: String, ExpressibleByStringLiteral, Codable, Hashable {
430485
case trace = "trace"
431486
case debug = "debug"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftSyntax
16+
import SwiftSyntaxMacros
17+
18+
/// Marker macro for jextract: forces a Swift declaration to be exported to Java.
19+
///
20+
/// When applied to a typealias, registers a monomorphization entry for generic types.
21+
/// When applied to a nominal type, force-includes it for export regardless of filters.
22+
///
23+
/// This macro produces no code — it is purely a marker read by the jextract tool.
24+
package enum JavaExportMacro {}
25+
26+
extension JavaExportMacro: PeerMacro {
27+
package static func expansion(
28+
of node: AttributeSyntax,
29+
providingPeersOf declaration: some DeclSyntaxProtocol,
30+
in context: some MacroExpansionContext,
31+
) throws -> [DeclSyntax] {
32+
// Marker-only macro — no code generation
33+
[]
34+
}
35+
}

0 commit comments

Comments
 (0)