From b417b8b11fe84351d1b4e3ea6620d6262f67bcdf Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Tue, 31 Mar 2026 18:33:18 +0900 Subject: [PATCH] Add single-type mode for jextract --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 38 +++++++++++-------- .../FFM/FFMSwift2JavaGenerator.swift | 32 ++++++++++------ .../Configuration.swift | 3 ++ .../Commands/JExtractCommand.swift | 16 +++++--- scripts/swiftkit-ffm-generate-bindings.sh | 8 ++-- 5 files changed, 61 insertions(+), 36 deletions(-) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 64a67266..fe737275 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -46,29 +46,37 @@ extension FFMSwift2JavaGenerator { } package func writeSwiftThunkSources(printer: inout CodePrinter) throws { - let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" - let moduleFilename = "\(moduleFilenameBase).swift" - do { - log.debug("Printing contents: \(moduleFilename)") + // Skip global thunks when generating for a single type + if config.singleType == nil { + let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" + let moduleFilename = "\(moduleFilenameBase).swift" + do { + log.debug("Printing contents: \(moduleFilename)") - try printGlobalSwiftThunkSources(&printer) + try printGlobalSwiftThunkSources(&printer) - if let outputFile = try printer.writeContents( - outputDirectory: self.swiftOutputDirectory, - javaPackagePath: nil, - filename: moduleFilename, - ) { - log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") - self.expectedOutputSwiftFileNames.remove(moduleFilename) + if let outputFile = try printer.writeContents( + outputDirectory: self.swiftOutputDirectory, + javaPackagePath: nil, + filename: moduleFilename, + ) { + log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") + self.expectedOutputSwiftFileNames.remove(moduleFilename) + } + } catch { + log.warning("Failed to write to Swift thunks: \(moduleFilename)") } - } catch { - log.warning("Failed to write to Swift thunks: \(moduleFilename)") } // === All types // We have to write all types to their corresponding output file that matches the file they were declared in, // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. - let filteredTypes = self.analysis.importedTypes + let filteredTypes: [String: ImportedNominalType] + if let singleType = config.singleType { + filteredTypes = self.analysis.importedTypes.filter { $0.key == singleType } + } else { + filteredTypes = self.analysis.importedTypes + } for group: (key: String, value: [Dictionary.Element]) in Dictionary( grouping: filteredTypes, diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 5d37c17a..18dc222e 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -168,8 +168,15 @@ extension FFMSwift2JavaGenerator { /// Every imported public type becomes a public class in its own file in Java. package func writeExportedJavaSources(printer: inout CodePrinter) throws { - let typesToExport = analysis.importedTypes - .sorted(by: { $0.key < $1.key }) + let typesToExport: [(key: String, value: ImportedNominalType)] + if let singleType = config.singleType { + typesToExport = analysis.importedTypes + .filter { $0.key == singleType } + .sorted(by: { $0.key < $1.key }) + } else { + typesToExport = analysis.importedTypes + .sorted(by: { $0.key < $1.key }) + } for (_, ty) in typesToExport { let javaName = javaClassName(for: ty) @@ -186,16 +193,19 @@ extension FFMSwift2JavaGenerator { } } - let filename = "\(self.swiftModuleName).java" - log.debug("Printing contents: \(filename)") - printModule(&printer) + // Skip the module-level .java file when generating for a single type + if config.singleType == nil { + let filename = "\(self.swiftModuleName).java" + log.debug("Printing contents: \(filename)") + printModule(&printer) - if let outputFile = try printer.writeContents( - outputDirectory: javaOutputDirectory, - javaPackagePath: javaPackagePath, - filename: filename, - ) { - log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))") + if let outputFile = try printer.writeContents( + outputDirectory: javaOutputDirectory, + javaPackagePath: javaPackagePath, + filename: filename, + ) { + log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))") + } } } } diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index b660c459..2f81934d 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -67,6 +67,9 @@ public struct Configuration: Codable { public var generatedJavaSourcesListFileOutput: String? + /// If set, only generate bindings for this single Swift type name + public var singleType: String? + /// If set, JExtract (JNI mode) will write a linker version script to this /// path, listing all generated JNI ``@_cdecl`` entry-point symbols as /// global exports and hiding everything else with `local: *`. Pass this diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index dc7d1bab..93dc860f 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -34,7 +34,7 @@ extension SwiftJava { struct JExtractCommand: SwiftJavaBaseAsyncParsableCommand, HasCommonOptions { static let configuration = CommandConfiguration( commandName: "jextract", // TODO: wrap-swift? - abstract: "Wrap Swift functions and types with Java bindings, making them available to be called from Java" + abstract: "Wrap Swift functions and types with Java bindings, making them available to be called from Java", ) @OptionGroup var commonOptions: SwiftJava.CommonOptions @@ -61,7 +61,7 @@ extension SwiftJava { @Flag( inversion: .prefixedNo, help: - "Some build systems require an output to be present when it was 'expected', even if empty. This is used by the JExtractSwiftPlugin build plugin, but otherwise should not be necessary." + "Some build systems require an output to be present when it was 'expected', even if empty. This is used by the JExtractSwiftPlugin build plugin, but otherwise should not be necessary.", ) var writeEmptyFiles: Bool? @@ -92,7 +92,7 @@ extension SwiftJava { @Flag( inversion: .prefixedNo, help: - "By enabling this mode, JExtract will generate Java code that allows you to implement Swift protocols using Java classes. This feature requires disabling the SwiftPM Sandbox (!). This feature is onl supported in 'jni' mode." + "By enabling this mode, JExtract will generate Java code that allows you to implement Swift protocols using Java classes. This feature requires disabling the SwiftPM Sandbox (!). This feature is onl supported in 'jni' mode.", ) var enableJavaCallbacks: Bool? @@ -117,7 +117,7 @@ extension SwiftJava { Patterns are matched against relative file paths (without .swift extension). \ Supports * (single-segment wildcard) and ** (recursive wildcard). \ Example: --filter-include 'Models/**' - """ + """, ) var filterInclude: [String] = [] @@ -127,9 +127,12 @@ extension SwiftJava { Exclude Swift source files matching these patterns during jextract. \ Same pattern syntax as --filter-include. \ Example: --filter-exclude 'Internal/*' - """ + """, ) var filterExclude: [String] = [] + + @Option(help: "If specified, only generate bindings for this single Swift type name") + var singleType: String? } } @@ -151,6 +154,7 @@ extension SwiftJava.JExtractCommand { configure(&config.linkerExportListOutput, overrideWith: self.linkerExportListOutput) configure(&config.swiftFilterInclude, append: self.filterInclude) configure(&config.swiftFilterExclude, append: self.filterExclude) + configure(&config.singleType, overrideWith: self.singleType) try checkModeCompatibility(config: config) @@ -196,7 +200,7 @@ struct IncompatibleModeError: Error { extension SwiftJava.JExtractCommand { func jextractSwift( config: Configuration, - dependentConfigs: [Configuration] + dependentConfigs: [Configuration], ) throws { try SwiftToJava(config: config, dependentConfigs: dependentConfigs).run() } diff --git a/scripts/swiftkit-ffm-generate-bindings.sh b/scripts/swiftkit-ffm-generate-bindings.sh index 9cc0317c..9f706a77 100755 --- a/scripts/swiftkit-ffm-generate-bindings.sh +++ b/scripts/swiftkit-ffm-generate-bindings.sh @@ -25,19 +25,19 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" JAVA_OUTPUT="${REPO_ROOT}/SwiftKitFFM/src/main/java" JAVA_PACKAGE="org.swift.swiftkit.ffm.generated" -# Declare types to generate: SWIFT_MODULE FILTER_INCLUDE INPUT_SWIFT_DIR OUTPUT_SWIFT_DIR +# Declare types to generate: SWIFT_MODULE SINGLE_TYPE INPUT_SWIFT_DIR OUTPUT_SWIFT_DIR TYPES=( "SwiftRuntimeFunctions SwiftJavaError Sources/SwiftRuntimeFunctions Sources/SwiftRuntimeFunctions/generated" ) for entry in "${TYPES[@]}"; do - read -r MODULE FILTER INPUT_SWIFT OUTPUT_SWIFT <<< "$entry" + read -r MODULE SINGLE_TYPE INPUT_SWIFT OUTPUT_SWIFT <<< "$entry" - echo "==> Generating ${FILTER} (module: ${MODULE})..." + echo "==> Generating ${SINGLE_TYPE} (module: ${MODULE})..." xcrun swift run swift-java jextract \ --mode ffm \ - --filter-include "$FILTER" \ + --single-type "$SINGLE_TYPE" \ --swift-module "$MODULE" \ --input-swift "${REPO_ROOT}/${INPUT_SWIFT}" \ --output-swift "${REPO_ROOT}/${OUTPUT_SWIFT}" \