Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions Sources/JExtractSwiftLib/Swift2JavaTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ extension Swift2JavaTranslator {
AnalysisResult(
importedTypes: self.importedTypes,
importedGlobalVariables: self.importedGlobalVariables,
importedGlobalFuncs: self.importedGlobalFuncs
importedGlobalFuncs: self.importedGlobalFuncs,
)
}

Expand Down Expand Up @@ -117,12 +117,12 @@ extension Swift2JavaTranslator {
visitor.visit(
nominalDecl: dataDecl.syntax!.asNominal!,
in: nil,
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift"
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift",
)
visitor.visit(
nominalDecl: dataProtocolDecl.syntax!.asNominal!,
in: nil,
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATAPROTOCOL.swift"
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATAPROTOCOL.swift",
)
}
}
Expand All @@ -133,7 +133,7 @@ extension Swift2JavaTranslator {
visitor.visit(
nominalDecl: dateDecl.syntax!.asNominal!,
in: nil,
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATE.swift"
sourceFilePath: "Foundation/FAKE_FOUNDATION_DATE.swift",
)
}
}
Expand All @@ -145,7 +145,8 @@ extension Swift2JavaTranslator {
let symbolTable = SwiftSymbolTable.setup(
moduleName: self.swiftModuleName,
inputs + [dependenciesSource],
log: self.log
config: self.config,
log: self.log,
)
self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable)
}
Expand Down Expand Up @@ -225,7 +226,7 @@ extension Swift2JavaTranslator {
/// Try to resolve the given nominal declaration node into its imported representation.
func importedNominalType(
_ nominalNode: some DeclGroupSyntax & NamedDeclSyntax & WithModifiersSyntax & WithAttributesSyntax,
parent: ImportedNominalType?
parent: ImportedNominalType?,
) -> ImportedNominalType? {
if !nominalNode.shouldExtract(config: config, log: log, in: parent) {
return nil
Expand All @@ -249,9 +250,12 @@ extension Swift2JavaTranslator {
}

// Whether to import this extension?
guard swiftNominalDecl.moduleName == self.swiftModuleName else {
let isFromThisModule = swiftNominalDecl.moduleName == self.swiftModuleName
let isFromStubbedModule = config.hasImportedModuleStub(moduleOfNominal: swiftNominalDecl.moduleName)
guard isFromThisModule || isFromStubbedModule else {
return nil
}

guard swiftNominalDecl.syntax!.shouldExtract(config: config, log: log, in: nil) else {
return nil
}
Expand All @@ -266,20 +270,14 @@ extension Swift2JavaTranslator {
return alreadyImported
}

// Apply type-name filters (patterns with `.`)
guard shouldJExtractType(qualifiedName: fullName, config: config) else {
log.info("Skipping type (filtered out): \(fullName)")
return nil
}

let importedNominal = try? ImportedNominalType(swiftNominal: nominal, lookupContext: lookupContext)

importedTypes[fullName] = importedNominal
return importedNominal
}
}

// ==== ----------------------------------------------------------------------------------------------------------------
// ==== -----------------------------------------------------------------------
// MARK: Errors

public struct Swift2JavaTranslatorError: Error {
Expand Down
31 changes: 29 additions & 2 deletions Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
//===----------------------------------------------------------------------===//

import CodePrinting
import SwiftJavaConfigurationShared
import SwiftParser
import SwiftSyntax

package protocol SwiftSymbolTableProtocol {
Expand Down Expand Up @@ -66,7 +68,8 @@ extension SwiftSymbolTable {
package static func setup(
moduleName: String,
_ inputFiles: some Collection<SwiftJavaInputFile>,
log: Logger
config: Configuration?,
log: Logger,
) -> SwiftSymbolTable {

// Prepare imported modules.
Expand All @@ -90,12 +93,36 @@ extension SwiftSymbolTable {
}
}

// Load stub type declarations for imported modules from config.
// This enables types from external modules (e.g. extension targets) to be
// resolved in the symbol table without scanning their actual source.
if let stubs = config?.importedModuleStubs {
for (stubModuleName, declarations) in stubs {
if importedModules[stubModuleName] == nil {
let source = declarations.joined(separator: "\n")
let sourceFile = Parser.parse(source: source)
var stubBuilder = SwiftParsedModuleSymbolTableBuilder(
moduleName: stubModuleName,
importedModules: ["Swift": importedModules["Swift"]!],
)
stubBuilder.handle(sourceFile: sourceFile, sourceFilePath: "\(stubModuleName)_stub.swift")
let stubModule = stubBuilder.finalize()
importedModules[stubModuleName] = stubModule
log.info("Loaded module stub for '\(stubModuleName)' with \(declarations.count) declaration(s), top-level types: \(stubModule.topLevelTypes.keys.sorted())")
} else {
log.info("Module '\(stubModuleName)' already known, skipping stub")
}
}
} else {
log.debug("No importedModuleStubs in config")
}

// FIXME: Support granular lookup context (file, type context).

var builder = SwiftParsedModuleSymbolTableBuilder(
moduleName: moduleName,
importedModules: importedModules,
log: log
log: log,
)
// First, register top-level and nested nominal types to the symbol table.
for sourceFile in inputFiles {
Expand Down
61 changes: 58 additions & 3 deletions Sources/SwiftJavaConfigurationShared/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,44 @@ public struct Configuration: Codable {
/// Same pattern syntax as swiftFilterInclude
public var swiftFilterExclude: [String]?

/// Stub type declarations for imported modules whose source is not available
/// to the jextract tool. Keyed by module name, values are arrays of Swift
/// declaration strings that will be parsed as if they belonged to that module.
///
/// Example:
/// ```json
/// {
/// "importedModuleStubs": {
/// "ExternalModule": [
/// "public enum Outer {}",
/// "public struct Config {}"
/// ]
/// }
/// }
/// ```
public var importedModuleStubs: [String: [String]]?

/// Whether the given module name has stub declarations configured
public func hasImportedModuleStub(moduleOfNominal moduleName: String) -> Bool {
importedModuleStubs?.keys.contains(moduleName) ?? false
}

/// Monomorphization entries for generic types, mapping a qualified Swift type
/// name to a concrete specialization with a custom Java-facing name.
///
/// Example:
/// ```json
/// {
/// "monomorphize": {
/// "Tank": {
/// "javaName": "FishTank",
/// "typeArgs": {"Element": "Fish"}
/// }
/// }
/// }
/// ```
public var monomorphize: [String: MonomorphizeEntry]?

// ==== wrap-java ---------------------------------------------------------

/// The Java class path that should be passed along to the swift-java tool.
Expand Down Expand Up @@ -220,7 +258,7 @@ public enum MavenRepositoryDescriptor: Hashable, Codable {
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown repository type: '\(type)'. Supported: maven, mavenCentral, mavenLocal, google"
debugDescription: "Unknown repository type: '\(type)'. Supported: maven, mavenCentral, mavenLocal, google",
)
}
}
Expand Down Expand Up @@ -302,7 +340,7 @@ public func readConfiguration(
string: String,
configPath: URL?,
file: String = #fileID,
line: UInt = #line
line: UInt = #line,
) throws -> Configuration? {
guard let configData = string.data(using: .utf8) else {
return nil
Expand All @@ -319,7 +357,7 @@ public func readConfiguration(
error: error,
text: string,
file: file,
line: line
line: line,
)
}
}
Expand Down Expand Up @@ -426,6 +464,23 @@ public struct ConfigurationError: Error {
}
}

// ==== -----------------------------------------------------------------------
// MARK: MonomorphizeEntry

/// Configuration entry for monomorphizing a generic type into a concrete Java class
public struct MonomorphizeEntry: Codable, Sendable {
/// Mapping from generic parameter name to concrete type (e.g. {"T": "Fish"})
public var typeArgs: [String: String]

/// The Java-facing class name (e.g. "FishTank")
public var javaName: String

public init(typeArgs: [String: String], javaName: String) {
self.typeArgs = typeArgs
self.javaName = javaName
}
}

public enum LogLevel: String, ExpressibleByStringLiteral, Codable, Hashable {
case trace = "trace"
case debug = "debug"
Expand Down
35 changes: 35 additions & 0 deletions Sources/SwiftJavaMacros/JavaExportMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import SwiftSyntax
import SwiftSyntaxMacros

/// Marker macro for jextract: forces a Swift declaration to be exported to Java.
///
/// When applied to a typealias, registers a monomorphization entry for generic types.
/// When applied to a nominal type, force-includes it for export regardless of filters.
///
/// This macro produces no code — it is purely a marker read by the jextract tool.
package enum JavaExportMacro {}

extension JavaExportMacro: PeerMacro {
package static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext,
) throws -> [DeclSyntax] {
// Marker-only macro — no code generation
[]
}
}
Loading
Loading