From effb93f7763785693c65d7d8427063a827e5b0b1 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 8 Jul 2026 13:19:10 +0900 Subject: [PATCH 1/6] Update structure of docs to use snippets --- .github/scripts/validate_docs.sh | 2 + .licenseignore | 1 + Package.swift | 24 +- .../Sources/JavaDependencySample/main.swift | 2 + .../JavaKitExample/JavaKitExample.swift | 35 +- .../com/example/swift/HelloJavaKitArrays.java | 2 + .../com/example/swift/HelloSubclass.java | 2 + .../com/example/swift/HelloSwift.java | 2 + .../com/example/swift/ThreadSafe.java | 2 + .../example/swift/ThreadSafeHelperClass.java | 2 + .../JavaKitArrayRuntimeTests.swift | 38 +- .../Sources/JavaProbablyPrime/prime.swift | 2 + .../JavaSieve/Sources/JavaSieve/main.swift | 2 + .../Sources/MySwiftLibrary/Arrays.swift | 20 + .../Sources/MySwiftLibrary/MySwiftClass.swift | 27 +- .../MySwiftLibrary/MySwiftLibrary.swift | 15 +- .../Sources/MySwiftLibrary/Optionals.swift | 23 + .../Sources/MySwiftLibrary/Throw.swift | 23 + .../Sources/MySwiftLibrary/Tuples.swift | 28 + .../UnsafeRawBufferPointer.swift | 31 + .../com/example/swift/DataImportTest.java | 35 + .../java/com/example/swift/FFMArraysTest.java | 44 +- .../java/com/example/swift/FFMTupleTest.java | 16 +- .../com/example/swift/MySwiftClassTest.java | 16 +- .../com/example/swift/MySwiftLibraryTest.java | 22 +- .../com/example/swift/OptionalImportTest.java | 8 + .../com/example/swift/WithBufferTest.java | 14 + .../swift/swiftkitffm/MySwiftStructTest.java | 16 + .../Sources/MySwiftLibrary/Alignment.swift | 2 + .../Sources/MySwiftLibrary/Arrays.swift | 28 +- .../Sources/MySwiftLibrary/Async.swift | 2 + .../MySwiftLibrary/BoxSpecialization.swift | 2 + .../Sources/MySwiftLibrary/Closures.swift | 2 + .../Sources/MySwiftLibrary/Data.swift | 2 + .../Sources/MySwiftLibrary/Dictionary.swift | 2 + .../MySwiftLibrary/EscapingClosures.swift | 2 + .../MySwiftLibrary/FoundationTypes.swift | 6 + .../Sources/MySwiftLibrary/GenericType.swift | 2 + .../Sources/MySwiftLibrary/MySwiftClass.swift | 8 + .../MySwiftLibrary/MySwiftLibrary.swift | 2 + .../MySwiftLibrary/MySwiftStruct.swift | 4 + .../Sources/MySwiftLibrary/Optionals.swift | 8 + .../Sources/MySwiftLibrary/ProtocolA.swift | 4 + .../MySwiftLibrary/ReturnProtocol.swift | 4 + .../Sources/MySwiftLibrary/Set.swift | 2 + .../Sources/MySwiftLibrary/Throw.swift | 2 + .../Sources/MySwiftLibrary/Tuples.swift | 12 + .../UnsafeRawBufferPointer.swift | 2 + .../Sources/MySwiftLibrary/Vehicle.swift | 2 + .../com/example/swift/AlignmentEnumTest.java | 2 + .../java/com/example/swift/ArraysTest.java | 60 +- .../java/com/example/swift/AsyncTest.java | 6 + .../example/swift/BoxSpecializationTest.java | 2 + .../java/com/example/swift/ClosuresTest.java | 6 + .../test/java/com/example/swift/DataTest.java | 2 + .../example/swift/EscapingClosuresTest.java | 14 +- .../com/example/swift/FoundationTypeTest.java | 6 + .../com/example/swift/GenericTypeTest.java | 2 + .../com/example/swift/MySwiftClassTest.java | 8 + .../com/example/swift/MySwiftLibraryTest.java | 2 + .../com/example/swift/MySwiftStructTest.java | 6 + .../java/com/example/swift/OptionalsTest.java | 10 + .../java/com/example/swift/ProtocolTest.java | 4 + .../com/example/swift/ReturnProtocolTest.java | 2 + .../example/swift/SwiftDictionaryMapTest.java | 2 + .../java/com/example/swift/SwiftSetTest.java | 2 + .../java/com/example/swift/ThrowTest.java | 2 + .../java/com/example/swift/TupleTest.java | 16 + .../swift/UnsafeRawBufferPointerTest.java | 2 + .../com/example/swift/VehicleEnumTest.java | 6 + Snippets/ArraysJavaFFM.java | 1 + Snippets/ArraysJavaJNI.java | 1 + Snippets/ArraysSwift.swift.symlink | 1 + Snippets/AsyncJavaJNI.java | 1 + Snippets/AsyncSwift.swift.symlink | 1 + Snippets/ClassesJavaFFM.java | 1 + Snippets/ClassesJavaJNI.java | 1 + Snippets/ClassesSwift.swift.symlink | 1 + Snippets/ClosuresJavaFFM.java | 1 + Snippets/ClosuresJavaJNI.java | 1 + Snippets/ClosuresSwift.swift.symlink | 1 + Snippets/DataJavaFFM.java | 1 + Snippets/DataJavaJNI.java | 1 + Snippets/DataSwift.swift.symlink | 1 + Snippets/DictionariesJavaJNI.java | 1 + Snippets/DictionariesSwift.swift.symlink | 1 + Snippets/EnumsJavaJNI.java | 1 + Snippets/EnumsSwift.swift.symlink | 1 + Snippets/EscapingClosuresJavaJNI.java | 1 + Snippets/EscapingClosuresSwift.swift.symlink | 1 + Snippets/FoundationTypesJavaJNI.java | 1 + Snippets/FoundationTypesSwift.swift.symlink | 1 + Snippets/GenericsJavaJNI.java | 1 + Snippets/GenericsSwift.swift.symlink | 1 + Snippets/JavaKitArraysJava.java | 1 + Snippets/JavaKitArraysSwift.swift.symlink | 1 + Snippets/JavaKitCastSwift.swift.symlink | 1 + Snippets/JavaKitClassJava.java | 1 + Snippets/JavaKitClassSwift.swift.symlink | 1 + Snippets/JavaKitEnumSwift.swift.symlink | 1 + Snippets/JavaKitImplementationJava.java | 1 + .../JavaKitImplementationSwift.swift.symlink | 1 + Snippets/JavaKitInheritanceJava.java | 1 + .../JavaKitInheritanceSwift.swift.symlink | 1 + Snippets/JavaKitOptionalsJava.java | 1 + Snippets/JavaKitOptionalsSwift.swift.symlink | 1 + Snippets/JavaKitReflectionSwift.swift.symlink | 1 + Snippets/JavaKitSendableAnnotationJava.java | 1 + Snippets/JavaKitSendableHelperJava.java | 1 + Snippets/JavaKitSendableSwift.swift.symlink | 1 + Snippets/JavaKitThrowsSwift.swift.symlink | 1 + Snippets/NotSupportedYetJavaFFM.java | 22 + Snippets/NotSupportedYetJavaJNI.java | 22 + Snippets/OptionalsJavaFFM.java | 1 + Snippets/OptionalsJavaJNI.java | 1 + Snippets/OptionalsSwift.swift.symlink | 1 + Snippets/ProtocolsJavaJNI.java | 1 + Snippets/ProtocolsSwift.swift.symlink | 1 + Snippets/RawBufferJavaFFM.java | 1 + Snippets/RawBufferJavaJNI.java | 1 + Snippets/RawBufferSwift.swift.symlink | 1 + Snippets/RawBufferSwiftFFM.swift.symlink | 1 + Snippets/RawRepresentableEnumsJavaJNI.java | 1 + .../RawRepresentableEnumsSwift.swift.symlink | 1 + Snippets/ReturnProtocolJavaJNI.java | 1 + Snippets/ReturnProtocolSwift.swift.symlink | 1 + Snippets/SetsJavaJNI.java | 1 + Snippets/SetsSwift.swift.symlink | 1 + Snippets/SpecializationJavaJNI.java | 1 + Snippets/SpecializationSwift.swift.symlink | 1 + Snippets/StringsJavaFFM.java | 1 + Snippets/StringsJavaJNI.java | 1 + Snippets/StringsSwift.swift.symlink | 1 + Snippets/StructsJavaFFM.java | 1 + Snippets/StructsJavaJNI.java | 1 + Snippets/StructsSwift.swift.symlink | 1 + Snippets/SubscriptsJavaJNI.java | 1 + Snippets/ThrowingInitJavaJNI.java | 1 + Snippets/ThrowingInitSwift.swift.symlink | 1 + Snippets/ThrowingJavaFFM.java | 1 + Snippets/ThrowingJavaJNI.java | 1 + Snippets/ThrowingSwift.swift.symlink | 1 + Snippets/TuplesJavaFFM.java | 1 + Snippets/TuplesJavaJNI.java | 1 + Snippets/TuplesSwift.swift.symlink | 1 + Snippets/WrapJavaDependencyConfig.json | 1 + .../WrapJavaDependencySwift.swift.symlink | 1 + Snippets/WrapJavaProbablyPrimeConfig.json | 1 + .../WrapJavaProbablyPrimeSwift.swift.symlink | 1 + Snippets/WrapJavaSieveConfig.json | 1 + Snippets/WrapJavaSieveSwift.swift.symlink | 1 + Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ | 7 + Sources/GenerateConfigDocs/ConfigParser.swift | 595 +++++++++++++ .../GenerateConfigDocs.swift | 124 +++ .../GenerateConfigDocs/MarkdownRenderer.swift | 198 +++++ .../GenerateConfigDocs/MarkerSplicer.swift | 35 + .../Configuration.swift | 178 +++- .../JExtract/JavaSourceLevel.swift | 1 + .../Documentation.docc/Android.md | 84 +- .../ExampleCommandLineProgram.md | 132 +++ .../FeaturesJavaKitMacros.md | 336 +++++++ .../Documentation.docc/FeaturesJextract.md | 830 ++++++++++++++++++ .../Documentation.docc/FeaturesOverview.md | 45 + .../Documentation.docc/ReducingBinarySize.md | 82 ++ .../Documentation.docc/SupportedFeatures.md | 632 ------------- .../SwiftJavaCommandLineTool.md | 173 +--- .../Documentation.docc/SwiftJavaConfigFile.md | 560 ++++++++++++ .../Documentation.docc/SwiftJavaJextract.md | 70 ++ .../Documentation.docc/SwiftJavaResolve.md | 45 + .../Documentation.docc/SwiftJavaWrapJava.md | 194 ++++ .../Documentation.docc/SwiftPMPlugin.md | 2 +- .../Documentation.docc/index.md | 16 +- scripts/generate-docs.sh | 58 ++ scripts/release.sh | 22 + 174 files changed, 4237 insertions(+), 1021 deletions(-) create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Arrays.swift create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Optionals.swift create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Throw.swift create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Tuples.swift create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift create mode 120000 Snippets/ArraysJavaFFM.java create mode 120000 Snippets/ArraysJavaJNI.java create mode 120000 Snippets/ArraysSwift.swift.symlink create mode 120000 Snippets/AsyncJavaJNI.java create mode 120000 Snippets/AsyncSwift.swift.symlink create mode 120000 Snippets/ClassesJavaFFM.java create mode 120000 Snippets/ClassesJavaJNI.java create mode 120000 Snippets/ClassesSwift.swift.symlink create mode 120000 Snippets/ClosuresJavaFFM.java create mode 120000 Snippets/ClosuresJavaJNI.java create mode 120000 Snippets/ClosuresSwift.swift.symlink create mode 120000 Snippets/DataJavaFFM.java create mode 120000 Snippets/DataJavaJNI.java create mode 120000 Snippets/DataSwift.swift.symlink create mode 120000 Snippets/DictionariesJavaJNI.java create mode 120000 Snippets/DictionariesSwift.swift.symlink create mode 120000 Snippets/EnumsJavaJNI.java create mode 120000 Snippets/EnumsSwift.swift.symlink create mode 120000 Snippets/EscapingClosuresJavaJNI.java create mode 120000 Snippets/EscapingClosuresSwift.swift.symlink create mode 120000 Snippets/FoundationTypesJavaJNI.java create mode 120000 Snippets/FoundationTypesSwift.swift.symlink create mode 120000 Snippets/GenericsJavaJNI.java create mode 120000 Snippets/GenericsSwift.swift.symlink create mode 120000 Snippets/JavaKitArraysJava.java create mode 120000 Snippets/JavaKitArraysSwift.swift.symlink create mode 120000 Snippets/JavaKitCastSwift.swift.symlink create mode 120000 Snippets/JavaKitClassJava.java create mode 120000 Snippets/JavaKitClassSwift.swift.symlink create mode 120000 Snippets/JavaKitEnumSwift.swift.symlink create mode 120000 Snippets/JavaKitImplementationJava.java create mode 120000 Snippets/JavaKitImplementationSwift.swift.symlink create mode 120000 Snippets/JavaKitInheritanceJava.java create mode 120000 Snippets/JavaKitInheritanceSwift.swift.symlink create mode 120000 Snippets/JavaKitOptionalsJava.java create mode 120000 Snippets/JavaKitOptionalsSwift.swift.symlink create mode 120000 Snippets/JavaKitReflectionSwift.swift.symlink create mode 120000 Snippets/JavaKitSendableAnnotationJava.java create mode 120000 Snippets/JavaKitSendableHelperJava.java create mode 120000 Snippets/JavaKitSendableSwift.swift.symlink create mode 120000 Snippets/JavaKitThrowsSwift.swift.symlink create mode 100644 Snippets/NotSupportedYetJavaFFM.java create mode 100644 Snippets/NotSupportedYetJavaJNI.java create mode 120000 Snippets/OptionalsJavaFFM.java create mode 120000 Snippets/OptionalsJavaJNI.java create mode 120000 Snippets/OptionalsSwift.swift.symlink create mode 120000 Snippets/ProtocolsJavaJNI.java create mode 120000 Snippets/ProtocolsSwift.swift.symlink create mode 120000 Snippets/RawBufferJavaFFM.java create mode 120000 Snippets/RawBufferJavaJNI.java create mode 120000 Snippets/RawBufferSwift.swift.symlink create mode 120000 Snippets/RawBufferSwiftFFM.swift.symlink create mode 120000 Snippets/RawRepresentableEnumsJavaJNI.java create mode 120000 Snippets/RawRepresentableEnumsSwift.swift.symlink create mode 120000 Snippets/ReturnProtocolJavaJNI.java create mode 120000 Snippets/ReturnProtocolSwift.swift.symlink create mode 120000 Snippets/SetsJavaJNI.java create mode 120000 Snippets/SetsSwift.swift.symlink create mode 120000 Snippets/SpecializationJavaJNI.java create mode 120000 Snippets/SpecializationSwift.swift.symlink create mode 120000 Snippets/StringsJavaFFM.java create mode 120000 Snippets/StringsJavaJNI.java create mode 120000 Snippets/StringsSwift.swift.symlink create mode 120000 Snippets/StructsJavaFFM.java create mode 120000 Snippets/StructsJavaJNI.java create mode 120000 Snippets/StructsSwift.swift.symlink create mode 120000 Snippets/SubscriptsJavaJNI.java create mode 120000 Snippets/ThrowingInitJavaJNI.java create mode 120000 Snippets/ThrowingInitSwift.swift.symlink create mode 120000 Snippets/ThrowingJavaFFM.java create mode 120000 Snippets/ThrowingJavaJNI.java create mode 120000 Snippets/ThrowingSwift.swift.symlink create mode 120000 Snippets/TuplesJavaFFM.java create mode 120000 Snippets/TuplesJavaJNI.java create mode 120000 Snippets/TuplesSwift.swift.symlink create mode 120000 Snippets/WrapJavaDependencyConfig.json create mode 120000 Snippets/WrapJavaDependencySwift.swift.symlink create mode 120000 Snippets/WrapJavaProbablyPrimeConfig.json create mode 120000 Snippets/WrapJavaProbablyPrimeSwift.swift.symlink create mode 120000 Snippets/WrapJavaSieveConfig.json create mode 120000 Snippets/WrapJavaSieveSwift.swift.symlink create mode 100644 Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ create mode 100644 Sources/GenerateConfigDocs/ConfigParser.swift create mode 100644 Sources/GenerateConfigDocs/GenerateConfigDocs.swift create mode 100644 Sources/GenerateConfigDocs/MarkdownRenderer.swift create mode 100644 Sources/GenerateConfigDocs/MarkerSplicer.swift create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md delete mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md create mode 100644 Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md create mode 100755 scripts/generate-docs.sh diff --git a/.github/scripts/validate_docs.sh b/.github/scripts/validate_docs.sh index 5e5539430..73e4edc6f 100755 --- a/.github/scripts/validate_docs.sh +++ b/.github/scripts/validate_docs.sh @@ -3,6 +3,8 @@ set -e set -x +swift run generate-config-docs --check + DEPENDENCY='.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0")' if grep -q "$DEPENDENCY" Package.swift; then diff --git a/.licenseignore b/.licenseignore index 40673100a..ca8600a84 100644 --- a/.licenseignore +++ b/.licenseignore @@ -45,6 +45,7 @@ gradlew.bat **/gradlew.bat **/ci-validate.sh **/DO_NOT_EDIT.txt +Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ Plugins/**/_PluginsShared Plugins/**/0_PLEASE_SYMLINK* Plugins/PluginsShared/JavaKitConfigurationShared diff --git a/Package.swift b/Package.swift index 14cc44f25..79e9de87c 100644 --- a/Package.swift +++ b/Package.swift @@ -11,12 +11,15 @@ if let localPath = Context.environment["SWIFT_JAVA_JNI_CORE_PATH"] { swiftJavaJNICoreDep = .package(url: "https://github.com/swiftlang/swift-java-jni-core", branch: "main") } -// Set SWIFTJAVA_DOCC_PLUGIN_INSTALL=1 to install the docc-plugin automatically. +// Set SWIFTJAVA_DOCC_PLUGIN_INSTALL=1 to install the docc-plugin automatically, +// or DOCC_PLUGIN_PATH= to point at a local checkout. // This is a workaround because swift-subprocess includes the plugin explicitly, // which breaks tools trying to add `swift package add-dependency` the plugin // to swift-java because it thinks the plugin was already added, but it is not. let extraDependencies: [Package.Dependency] -if Context.environment["SWIFTJAVA_DOCC_PLUGIN_INSTALL"] == "1" { +if let localPath = Context.environment["DOCC_PLUGIN_PATH"] { + extraDependencies = [.package(path: localPath)] +} else if Context.environment["SWIFTJAVA_DOCC_PLUGIN_INSTALL"] == "1" { extraDependencies = [ .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0") ] @@ -429,6 +432,23 @@ let package = Package( ] ), + // Dev-time tool: regenerates the "Supported configuration options" table in + // `Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md` + // straight from the doc comments on `Configuration.swift` and the enums it + // references, so the two never drift apart. + // + // Run manually via `swift run generate-config-docs`; CI validates freshness + // with `swift run generate-config-docs --check`. + .executableTarget( + name: "generate-config-docs", + dependencies: [ + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Sources/GenerateConfigDocs" + ), + .plugin( name: "_StaticBuildConfigPlugin", capability: .buildTool(), diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift index 64088d31a..01ac7ce6d 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift @@ -28,6 +28,7 @@ print("Start Sample app...") // TODO: locating the classpath is more complex, need to account for dependencies of our module let swiftJavaClasspath = findSwiftJavaClasspaths() // scans for .classpath files +// snippet.dependencyUsage // 1) Start a JVM with appropriate classpath let jvm = try JavaVirtualMachine.shared(classpath: swiftJavaClasspath) @@ -50,5 +51,6 @@ for record in try CSVFormatClass.RFC4180.parse(reader)!.getRecords()! { print("Field: \(field)") } } +// snippet.end print("Done.") diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift index 2c7ee67aa..b58c5c345 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -19,15 +19,24 @@ enum SwiftWrappedError: Error { case message(String) } +// snippet.implementation @JavaImplementation("com.example.swift.HelloSwift") extension HelloSwift: HelloSwiftNativeMethods { + @JavaMethod + func throwMessageFromSwift(_ message: String) throws -> String { + throw SwiftWrappedError.message(message) + } + // snippet.end + @JavaMethod func sayHello(_ i: Int32, _ j: Int32) -> Int32 { print("Hello from Swift!") let answer = self.sayHelloBack(i + j) print("Swift got back \(answer) from Java") + // snippet.staticFieldAccess print("We expect the above value to be the initial value, \(self.javaClass.initialValue)") + // snippet.end print("Updating Java field value to something different") self.value = 2.71828 @@ -35,12 +44,14 @@ extension HelloSwift: HelloSwiftNativeMethods { let newAnswer = self.sayHelloBack(17) print("Swift got back updated \(newAnswer) from Java") + // snippet.classDefinition let newHello = HelloSwift(environment: javaEnvironment) print("Swift created a new Java instance with the value \(newHello.value)") let name = newHello.name print("Hello to \(name)") newHello.greet("Swift 👋🏽 How's it going") + // snippet.end self.name = "a 🗑️-collected language" _ = self.sayHelloBack(42) @@ -49,9 +60,12 @@ extension HelloSwift: HelloSwiftNativeMethods { let value = predicate.test(JavaInteger(3)) print("Running a JavaPredicate from swift 3 < 10 = \(value)") + // snippet.arraysWrapper let strings = doublesToStrings([3.14159, 2.71828]) print("Converting doubles to strings: \(strings)") + // snippet.end + // snippet.castPattern // Try downcasting if let helloSub = self.as(HelloSubclass.self) { print("Hello from the subclass!") @@ -61,34 +75,45 @@ extension HelloSwift: HelloSwiftNativeMethods { } else { fatalError("Expected subclass here") } + // snippet.end // Check escaped name assert(self.`init`(42) == 42) assert(self._echo("Hello") == "Hello") + // snippet.isCheck // Check "is" behavior assert(newHello.is(HelloSwift.self)) assert(!newHello.is(HelloSubclass.self)) + // snippet.end - // Create a new instance. + // snippet.inheritance + // Create a new instance of the subclass; Swift mirrors the Java hierarchy. let helloSubFromSwift = HelloSubclass("Hello from Swift", environment: javaEnvironment) helloSubFromSwift.greetMe() + // snippet.end + // snippet.throwingMethods do { try throwMessage("I am an error") } catch { print("Caught Java error: \(error)") } + // snippet.end - // Make sure that the thread safe class is sendable + // snippet.sendableConformance + // Java classes annotated with @ThreadSafe surface as Sendable on the Swift side. let helper = ThreadSafeHelperClass(environment: javaEnvironment) let threadSafe: Sendable = helper + _ = threadSafe + // snippet.end checkOptionals(helper: helper) return i * j } + // snippet.optionalsWrapper func checkOptionals(helper: ThreadSafeHelperClass) { let text: JavaString? = helper.textOptional let value: String? = helper.getValueOptional(Optional.none) @@ -101,9 +126,5 @@ extension HelloSwift: HelloSwiftNativeMethods { print("Optional double function returned \(doubleOpt)") print("Optional long function returned \(longOpt)") } - - @JavaMethod - func throwMessageFromSwift(_ message: String) throws -> String { - throw SwiftWrappedError.message(message) - } + // snippet.end } diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloJavaKitArrays.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloJavaKitArrays.java index a0495fc71..57ee40466 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloJavaKitArrays.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloJavaKitArrays.java @@ -14,6 +14,7 @@ package com.example.swift; +// snippet.arrays public class HelloJavaKitArrays { public byte[] getFixedBytes() { @@ -57,3 +58,4 @@ public String[] getGreetings() { return new String[] { "hello", "world", "from", "java" }; } } +// snippet.end diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java index 2312d8f5a..b2a1fe1dd 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java @@ -14,6 +14,7 @@ package com.example.swift; +// snippet.helloSubclass public class HelloSubclass extends HelloSwift { private String greeting; @@ -25,3 +26,4 @@ public void greetMe() { super.greet(greeting); } } +// snippet.end diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java index 94f41d9a3..ca882da54 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java @@ -16,6 +16,7 @@ import java.util.function.Predicate; +// snippet.helloClass public class HelloSwift { public double value; public static double initialValue = 3.14159; @@ -71,3 +72,4 @@ public void throwMessage(String message) throws Exception { throw new Exception(message); } } +// snippet.end diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafe.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafe.java index 2b1b358d9..42c37ba50 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafe.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafe.java @@ -17,6 +17,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +// snippet.threadSafe @Retention(RetentionPolicy.RUNTIME) public @interface ThreadSafe { } +// snippet.end diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java index 38fe1a741..1912f2f0e 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java @@ -19,6 +19,7 @@ import java.util.OptionalInt; import java.util.OptionalDouble; +// snippet.threadSafeHelper @ThreadSafe public class ThreadSafeHelperClass { public ThreadSafeHelperClass() { } @@ -53,3 +54,4 @@ public OptionalLong from(OptionalInt value) { return OptionalLong.of(value.getAsInt()); } } +// snippet.end diff --git a/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitArrayRuntimeTests.swift b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitArrayRuntimeTests.swift index 9b186fc31..5af6329b2 100644 --- a/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitArrayRuntimeTests.swift +++ b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitArrayRuntimeTests.swift @@ -31,30 +31,41 @@ struct JavaKitArrayRuntimeTests { } @Test - func getEmptyBytes() throws { + func reverseBytes() throws { let env = try jvm.environment() let arrays = HelloJavaKitArrays(environment: env) - let bytes: [Int8] = arrays.getEmptyBytes() - #expect(bytes.isEmpty) + // snippet.arraysUsage + let reversed: [Int8] = arrays.reverseBytes([10, 20, 30]) + #expect(reversed == [30, 20, 10]) + // snippet.end } @Test - func filledBytes() throws { + func getGreetings() throws { let env = try jvm.environment() let arrays = HelloJavaKitArrays(environment: env) - let bytes: [Int8] = arrays.filledBytes(4, 42) - #expect(bytes == [42, 42, 42, 42]) + let greetings: [String] = arrays.getGreetings() + #expect(greetings == ["hello", "world", "from", "java"]) } @Test - func reverseBytes() throws { + func getEmptyBytes() throws { let env = try jvm.environment() let arrays = HelloJavaKitArrays(environment: env) - let reversed: [Int8] = arrays.reverseBytes([10, 20, 30]) - #expect(reversed == [30, 20, 10]) + let bytes: [Int8] = arrays.getEmptyBytes() + #expect(bytes.isEmpty) + } + + @Test + func filledBytes() throws { + let env = try jvm.environment() + let arrays = HelloJavaKitArrays(environment: env) + + let bytes: [Int8] = arrays.filledBytes(4, 42) + #expect(bytes == [42, 42, 42, 42]) } @Test @@ -84,13 +95,4 @@ struct JavaKitArrayRuntimeTests { // "Hi" in UTF-8 is [0x48, 0x69] #expect(bytes == [0x48, 0x69]) } - - @Test - func getGreetings() throws { - let env = try jvm.environment() - let arrays = HelloJavaKitArrays(environment: env) - - let greetings: [String] = arrays.getGreetings() - #expect(greetings == ["hello", "world", "from", "java"]) - } } diff --git a/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift b/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift index c070c87ad..f734d712f 100644 --- a/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift +++ b/Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift @@ -24,11 +24,13 @@ struct ProbablyPrime: ParsableCommand { var certainty: Int32 = 10 func run() throws { + // snippet.probablyPrime let bigInt = BigInteger(number) if bigInt.isProbablePrime(certainty) { print("\(number) is probably prime") } else { print("\(number) is definitely not prime") } + // snippet.end } } diff --git a/Samples/JavaSieve/Sources/JavaSieve/main.swift b/Samples/JavaSieve/Sources/JavaSieve/main.swift index 204c9153b..b18e74594 100644 --- a/Samples/JavaSieve/Sources/JavaSieve/main.swift +++ b/Samples/JavaSieve/Sources/JavaSieve/main.swift @@ -17,6 +17,7 @@ import SwiftJava let jvm = try JavaVirtualMachine.shared() +// snippet.sieveUsage do { let sieveClass = try JavaClass(environment: jvm.environment()) for prime in sieveClass.findPrimes(100)! { @@ -27,3 +28,4 @@ do { } catch { print("Failure: \(error)") } +// snippet.end diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Arrays.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Arrays.swift new file mode 100644 index 000000000..02ff3c281 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Arrays.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +// Mirrors `Arrays.swift` in the JNI sample app, so that documentation can show +// one Swift example alongside its usage in both jextract modes + +public func byteArray(array: [UInt8]) -> [UInt8] { + array +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index a9bd34d24..5a83a0c6c 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -15,12 +15,19 @@ public class MySwiftClass { public let byte: UInt8 = 0 - public var len: Int - public var cap: Int + public var x: Int + public var y: Int - public init(len: Int, cap: Int) { - self.len = len - self.cap = cap + // Mirrors `MySwiftClass` in the JNI sample app, so that documentation can + // show one Swift example alongside its usage in both jextract modes + public init(x: Int, y: Int) { + self.x = x + self.y = y + } + + public init() { + self.x = 10 + self.y = 5 } deinit { @@ -28,8 +35,12 @@ public class MySwiftClass { public var counter: Int32 = 0 - public static func factory(len: Int, cap: Int) -> MySwiftClass { - MySwiftClass(len: len, cap: cap) + public static func factory(x: Int, y: Int) -> MySwiftClass { + MySwiftClass(x: x, y: y) + } + + public func sum() -> Int { + x + y } public func voidMethod() { @@ -47,7 +58,7 @@ public class MySwiftClass { } public func describe() -> String { - "MySwiftClass(len: \(len), cap: \(cap))" + "MySwiftClass(x: \(x), y: \(y))" } public func makeRandomIntMethod() -> Int { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 305372eb4..b8991a6e5 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -64,27 +64,16 @@ public func globalReceiveReturnData(data: Data) -> Data { Data(data) } +// snippet.rawBufferDefinition public func withBuffer(body: (UnsafeRawBufferPointer) -> Void) { body(globalBuffer) } +// snippet.end public func getArray() -> [UInt8] { [1, 2, 3] } -// Tuple round-trips for jextract FFM (see `FFMTupleTest` in the sample app). -public func ffmTupleReturnPair() -> (Int32, Int64) { - (42, 43) -} - -public func ffmTupleSumPair(_ arg: (Int32, Int64)) -> Int64 { - Int64(arg.0) + arg.1 -} - -public func ffmTupleLabeledPair() -> (x: Int32, y: Int32) { - (x: 10, y: 20) -} - public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: Int) -> Int { let bufferPointer = UnsafeRawBufferPointer(start: actuallyAnArray, count: count) let array = Array(bufferPointer) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Optionals.swift new file mode 100644 index 000000000..94ce429d3 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +// Mirrors `Optionals.swift` in the JNI sample app, so that documentation can +// show one Swift example alongside its usage in both jextract modes +// +// Note that only optional *parameters* are supported in FFM mode, which is why +// this function returns a non-optional value + +public func optionalLongOrZero(input: Int64?) -> Int64 { + input ?? 0 +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Throw.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Throw.swift new file mode 100644 index 000000000..ffbc22085 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Throw.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +// Mirrors `Throw.swift` in the JNI sample app, so that documentation can show +// one Swift example alongside its usage in both jextract modes + +public func throwString(input: String) throws -> String { + if input.isEmpty { + throw SwiftExampleError(message: "swiftError") + } + return input +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Tuples.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Tuples.swift new file mode 100644 index 000000000..3af91ae1f --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Tuples.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +// Mirrors `Tuples.swift` in the JNI sample app, so that documentation can show +// one Swift example alongside its usage in both jextract modes + +public func returnIntPair() -> (Int32, Int64) { + (42, 43) +} + +public func sumIntPair(pair: (Int32, Int64)) -> Int64 { + Int64(pair.0) + pair.1 +} + +public func labeledTuple() -> (x: Int32, y: Int32) { + (x: 10, y: 20) +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift new file mode 100644 index 000000000..76ef5d9eb --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +// Mirrors `UnsafeRawBufferPointer.swift` in the JNI sample app, so that +// documentation can show one Swift example alongside its usage in both +// jextract modes + +/// Sum all bytes in the buffer +public func sumOfBytes(data: UnsafeRawBufferPointer) -> Int64 { + var sum: Int64 = 0 + for byte in data { + sum += Int64(byte) + } + return sum +} + +/// Return the count of bytes in the buffer +public func bufferCount(data: UnsafeRawBufferPointer) -> Int64 { + Int64(data.count) +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java index 870404640..488fbc702 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java @@ -23,6 +23,35 @@ import static org.junit.jupiter.api.Assertions.*; public class DataImportTest { + @Test + void data_echo() { + // snippet.dataUsageJava + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] bytes = new byte[] { 1, 2, 3, 4 }; + var data = Data.fromByteArray(bytes, arena); + + var echoed = MySwiftLibrary.echoData(data, arena); + assertArrayEquals(bytes, echoed.toByteArray()); + } + // snippet.end + } + + @Test + void data_withUnsafeBytes() { + // snippet.withUnsafeBytesUsageJava + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] bytes = new byte[] { 1, 2, 3, 4 }; + var data = Data.fromByteArray(bytes, arena); + + var echoed = MySwiftLibrary.echoData(data, arena); + echoed.withUnsafeBytes((segment) -> { + assertEquals(4, segment.byteSize()); + assertEquals(1, segment.get(ValueLayout.JAVA_BYTE, 0)); + }); + } + // snippet.end + } + @Test void test_Data_receiveAndReturn() { try (var arena = AllocatingSwiftArena.ofConfined()) { @@ -93,16 +122,19 @@ void test_Data_fromByteArray() { @Test void test_Data_fromByteBuffer() { + // snippet.byteBufferUsageJava try (var arena = AllocatingSwiftArena.ofConfined()) { byte[] original = new byte[] { 1, 2, 3, 4, 5 }; ByteBuffer buffer = ByteBuffer.wrap(original); var data = Data.fromByteBuffer(buffer, arena); assertEquals(5, data.getCount()); } + // snippet.end } @Test void test_Data_toMemorySegment() { + // snippet.memorySegmentUsageJava try (var arena = AllocatingSwiftArena.ofConfined()) { byte[] original = new byte[] { 10, 20, 30, 40 }; var data = Data.fromByteArray(original, arena); @@ -113,10 +145,12 @@ void test_Data_toMemorySegment() { assertEquals(original[i], segment.get(ValueLayout.JAVA_BYTE, i)); } } + // snippet.end } @Test void test_Data_toByteBuffer() { + // snippet.byteBufferToUsageJava try (var arena = AllocatingSwiftArena.ofConfined()) { byte[] original = new byte[] { 10, 20, 30, 40 }; var data = Data.fromByteArray(original, arena); @@ -127,6 +161,7 @@ void test_Data_toByteBuffer() { assertEquals(original[i], buffer.get(i)); } } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java index c195f11bf..e11d62a39 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java @@ -27,6 +27,31 @@ public class FFMArraysTest { + @Test + void test_byteArray() { + // snippet.primitiveArraysUsage + byte[] input = new byte[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.byteArray(input)); + // snippet.end + } + + @Test + void test_getArray() { + byte[] javaBytes = MySwiftLibrary.getArray(); // automatically converted [UInt8] to byte[] + assertArrayEquals(new byte[]{1, 2, 3}, javaBytes); + } + + @Test + void test_sumAllByteArrayElements_arrayCopy() { + byte[] bytes = new byte[124]; + Arrays.fill(bytes, (byte) 1); + + var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytes); + + int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum(); + assertEquals(javaSideSum, swiftSideSum); + } + @Test void test_sumAllByteArrayElements_throughMemorySegment() { byte[] bytes = new byte[124]; @@ -45,23 +70,4 @@ void test_sumAllByteArrayElements_throughMemorySegment() { assertEquals(javaSideSum, swiftSideSum); } } - - @Test - void test_sumAllByteArrayElements_arrayCopy() { - byte[] bytes = new byte[124]; - Arrays.fill(bytes, (byte) 1); - - var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytes); - - int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum(); - assertEquals(javaSideSum, swiftSideSum); - } - - @Test - void test_getArray() { - AtomicLong bufferSize = new AtomicLong(); - byte[] javaBytes = MySwiftLibrary.getArray(); // automatically converted [UInt8] to byte[] - - assertArrayEquals(new byte[]{1, 2, 3}, javaBytes); - } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java index 662dc846c..8e27c3241 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java @@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Runtime coverage for Swift tuples exported via jextract FFM (see {@code ffmTuple*} in {@link MySwiftLibrary}). + * Runtime coverage for Swift tuples exported via jextract FFM (see {@code Tuples.swift} in the sample library). */ public class FFMTupleTest { @@ -29,21 +29,23 @@ public class FFMTupleTest { } @Test - void ffmTupleReturnPair_roundTrip() { - Tuple2 result = MySwiftLibrary.ffmTupleReturnPair(); + void returnIntPair_roundTrip() { + // snippet.tupleUsageJava + Tuple2 result = MySwiftLibrary.returnIntPair(); assertEquals(42, result.$0); assertEquals(43L, result.$1); + // snippet.end } @Test - void ffmTupleSumPair_acceptsTupleFromJava() { - long sum = MySwiftLibrary.ffmTupleSumPair(new Tuple2<>(5, 7L)); + void sumIntPair_acceptsTupleFromJava() { + long sum = MySwiftLibrary.sumIntPair(new Tuple2<>(5, 7L)); assertEquals(12L, sum); } @Test - void ffmTupleLabeledPair_preservesElementOrder() { - Tuple2 result = MySwiftLibrary.ffmTupleLabeledPair(); + void labeledTuple_preservesElementOrder() { + Tuple2 result = MySwiftLibrary.labeledTuple(); assertEquals(10, result.$0); assertEquals(20, result.$1); } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index e6ac42b63..958df0ddc 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -49,6 +49,16 @@ void test_MySwiftClass_voidMethod() { } } + @Test + void test_MySwiftClass_sum() { + // snippet.classUsageJava + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(30, c.sum()); + } + // snippet.end + } + @Test void test_MySwiftClass_makeIntMethod() { try(var arena = AllocatingSwiftArena.ofConfined()) { @@ -63,16 +73,16 @@ void test_MySwiftClass_describe() { try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); var got = o.describe(); - assertEquals("MySwiftClass(len: 12, cap: 42)", got); + assertEquals("MySwiftClass(x: 12, y: 42)", got); } } @Test @Disabled // TODO: Need var mangled names in interfaces - void test_MySwiftClass_property_len() { + void test_MySwiftClass_property_x() { try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); - var got = o.getLen(); + var got = o.getX(); assertEquals(12, got); } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index 9b7373c27..15ba7ee8f 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -40,10 +40,12 @@ void call_globalTakeInt() { @Test void call_writeString_jextract() { + // snippet.stringUsageJava var string = "Hello Swift!"; long reply = MySwiftLibrary.globalWriteString(string); assertEquals(string.length(), reply); + // snippet.end } @Test @@ -109,6 +111,22 @@ void call_globalThrowingVoid_throws() { }); } + @Test + void call_throwString_throws() { + // snippet.throwUsageJava + SwiftJavaErrorException exception = assertThrows(SwiftJavaErrorException.class, () -> { + MySwiftLibrary.throwString(""); + }); + assertNotNull(exception.getMessage()); + assertTrue(exception.getMessage().contains("swiftError")); + // snippet.end + } + + @Test + void call_throwString_noThrow() throws SwiftJavaErrorException { + assertEquals("Hello!", MySwiftLibrary.throwString("Hello!")); + } + @Test void call_globalThrowingReturn_noThrow() throws SwiftJavaErrorException { long result = MySwiftLibrary.globalThrowingReturn(false); @@ -148,7 +166,9 @@ void call_globalThrowingString_throws_checkMessage() { @Test void call_globalCallMeBooleanSupplier_noThrow() { - boolean result = MySwiftLibrary.globalCallMeBooleanSupplier(() -> { return true; }); + // snippet.closureUsageJava + boolean result = MySwiftLibrary.globalCallMeBooleanSupplier(() -> true); assertEquals(true, result); + // snippet.end } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java index 57e8dba61..1c7754482 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java @@ -23,6 +23,14 @@ import static org.junit.jupiter.api.Assertions.*; public class OptionalImportTest { + @Test + void test_optionalLongOrZero() { + // snippet.optionalParameterUsageJava + assertEquals(0, MySwiftLibrary.optionalLongOrZero(OptionalLong.empty())); + assertEquals(999, MySwiftLibrary.optionalLongOrZero(OptionalLong.of(999))); + // snippet.end + } + @Test void test_Optional_receive() { try (var arena = AllocatingSwiftArena.ofConfined()) { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java index 9e0654767..6d6d435c6 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java @@ -28,8 +28,21 @@ public class WithBufferTest { + @Test + void test_sumOfBytes() { + // snippet.rawBufferUsageJava + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] input = new byte[] { 1, 2, 3, 4, 5 }; + MemorySegment buffer = arena.allocateFrom(ValueLayout.JAVA_BYTE, input); + + assertEquals(15, MySwiftLibrary.sumOfBytes(buffer)); + } + // snippet.end + } + @Test void test_withBuffer() { + // snippet.withBufferUsageJava AtomicLong bufferSize = new AtomicLong(); MySwiftLibrary.withBuffer((buf) -> { CallTraces.trace("withBuffer{$0.byteSize()}=" + buf.byteSize()); @@ -37,6 +50,7 @@ void test_withBuffer() { }); assertEquals(124, bufferSize.get()); + // snippet.end } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java index d904f7e82..9f77a3ab8 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java @@ -22,6 +22,20 @@ public class MySwiftStructTest { + @Test + void increaseCap() { + // snippet.structUsageJava + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + assertEquals(1337, s.getCapacity()); + + long newCap = s.increaseCap(10); + assertEquals(1347, newCap); + assertEquals(1347, s.getCapacity()); + } + // snippet.end + } + @Test void create_struct() { try (var arena = AllocatingSwiftArena.ofConfined()) { @@ -47,6 +61,7 @@ void testSubscript() { @Test void testSubscriptWithParams() { + // snippet.subscriptUsageJava try (var arena = AllocatingSwiftArena.ofConfined()) { MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); long currentValue = s.getSubscript(1); @@ -54,5 +69,6 @@ void testSubscriptWithParams() { assertEquals(20, currentValue); assertEquals(66, s.getSubscriptArrayValue(1)); } + // snippet.end } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift index 760c564b9..d23c62165 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift @@ -12,7 +12,9 @@ // //===----------------------------------------------------------------------===// +// snippet.rawRepresentableEnum public enum Alignment: String { case horizontal case vertical } +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift index 70f5afdc2..526faf8e2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift @@ -14,46 +14,49 @@ import SwiftJava -public func booleanArray(array: [Bool]) -> [Bool] { +// snippet.primitiveArrays +public func byteArray(array: [UInt8]) -> [UInt8] { array } -public func byteArray(array: [UInt8]) -> [UInt8] { +public func intArray(array: [Int32]) -> [Int32] { array } -public func byteArrayExplicit(array: [UInt8]) -> [UInt8] { +public func doubleArray(array: [Double]) -> [Double] { array } -public func charArray(array: [UInt16]) -> [UInt16] { +public func stringArray(array: [String]) -> [String] { array } +// snippet.end -public func shortArray(array: [Int16]) -> [Int16] { +public func booleanArray(array: [Bool]) -> [Bool] { array } -public func intArray(array: [Int32]) -> [Int32] { +public func byteArrayExplicit(array: [UInt8]) -> [UInt8] { array } -public func longArray(array: [Int64]) -> [Int64] { +public func charArray(array: [UInt16]) -> [UInt16] { array } -public func floatArray(array: [Float]) -> [Float] { +public func shortArray(array: [Int16]) -> [Int16] { array } -public func doubleArray(array: [Double]) -> [Double] { +public func longArray(array: [Int64]) -> [Int64] { array } -public func stringArray(array: [String]) -> [String] { +public func floatArray(array: [Float]) -> [Float] { array } +// snippet.customTypeArrays public func objectArray(array: [MySwiftClass]) -> [MySwiftClass] { array } @@ -62,10 +65,11 @@ public func nestedByteArray(array: [[UInt8]]) -> [[UInt8]] { array } -public func nestedLongArray(array: [[Int64]]) -> [[Int64]] { +public func nestedStringArray(array: [[String]]) -> [[String]] { array } +// snippet.end -public func nestedStringArray(array: [[String]]) -> [[String]] { +public func nestedLongArray(array: [[Int64]]) -> [[Int64]] { array } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift index 5152d147a..bb6e8aee6 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift @@ -14,6 +14,7 @@ import SwiftJava +// snippet.asyncDefinition public func asyncSum(i1: Int64, i2: Int64) async -> Int64 { i1 + i2 } @@ -21,6 +22,7 @@ public func asyncSum(i1: Int64, i2: Int64) async -> Int64 { public func asyncSleep() async throws { try await Task.sleep(for: .milliseconds(500)) } +// snippet.end public func asyncCopy(myClass: MySwiftClass) async throws -> MySwiftClass { let new = MySwiftClass(x: myClass.x, y: myClass.y) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift index f4164c8b4..0b3c72f99 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.boxSpecialization public struct Box: Hashable { public var count: Int64 @@ -39,3 +40,4 @@ extension Box where Element == Fish { } public typealias FishBox = Box +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift index a70566fab..06519586c 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.closureDefinition public func emptyClosure(closure: () -> Void) { closure() } @@ -31,3 +32,4 @@ public func closureMultipleArguments( ) -> Int64 { closure(input1, input2) } +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift index 3dcbc613b..93844e950 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift @@ -20,6 +20,7 @@ import FoundationEssentials import Foundation #endif +// snippet.dataDefinition public func echoData(_ data: Data) -> Data { data } @@ -27,6 +28,7 @@ public func echoData(_ data: Data) -> Data { public func makeData() -> Data { Data([0x01, 0x02, 0x03, 0x04]) } +// snippet.end public func getDataCount(_ data: Data) -> Int { data.count diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift index 78164e7e2..db5a4d465 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.dictionaryDefinition public func makeStringToLongDictionary() -> [String: Int64] { [ "hello": 1, @@ -22,6 +23,7 @@ public func makeStringToLongDictionary() -> [String: Int64] { public func stringToLongDictionary(dict: [String: Int64]) -> [String: Int64] { dict } +// snippet.end public func insertIntoStringToLongDictionary(dict: [String: Int64], key: String, value: Int64) -> [String: Int64] { var copy = dict diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EscapingClosures.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EscapingClosures.swift index dcc0d5762..ad69fb03c 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EscapingClosures.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EscapingClosures.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.escapingClosureDefinition public class CallbackManager { private var callback: (() -> Void)? private var intCallback: ((Int64) -> Int64)? @@ -38,6 +39,7 @@ public class CallbackManager { intCallback?(value) } } +// snippet.end public class ClosureStore { private var closures: [() -> Void] = [] diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationTypes.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationTypes.swift index 9d4ea6ff3..6cf4e7964 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationTypes.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationTypes.swift @@ -20,6 +20,7 @@ import FoundationEssentials import Foundation #endif +// snippet.dateDefinition public func compareDates(date1: Date, date2: Date) -> Bool { date1 == date2 } @@ -27,7 +28,9 @@ public func compareDates(date1: Date, date2: Date) -> Bool { public func dateFromSeconds(_ seconds: Double) -> Date { Date(timeIntervalSince1970: seconds) } +// snippet.end +// snippet.uuidDefinition public func echoUUID(_ uuid: UUID) -> UUID { uuid } @@ -35,7 +38,10 @@ public func echoUUID(_ uuid: UUID) -> UUID { public func makeUUID() -> UUID { UUID() } +// snippet.end +// snippet.foundationURLDefinition public func echoURL(_ url: URL) -> URL { url } +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift index a37904672..f44bc9020 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.genericTypeDefinition public struct MyID: Hashable { public var rawValue: T public init(_ rawValue: T) { @@ -21,6 +22,7 @@ public struct MyID: Hashable { "\(rawValue)" } } +// snippet.end public typealias MyIntID = MyID diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index df6d46036..5ca632526 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -21,6 +21,7 @@ public class MySwiftClass { public let byte: UInt8 = 0 public let constant: Int64 = 100 public var mutable: Int64 = 0 + // snippet.computedProperties public var product: Int64 { x * y } @@ -37,6 +38,7 @@ public class MySwiftClass { mutable = newValue * 2 } } + // snippet.end public let warm: Bool = false public var getAsync: Int64 { get async { @@ -46,11 +48,14 @@ public class MySwiftClass { /// `public private(set)` should expose only a getter to Java; the setter /// is unreachable so swift-java must not emit a setter thunk for it + // snippet.privateSetProperty public private(set) var privateSetCounter: Int64 = 7 + // snippet.end public static func method() { } + // snippet.initializers public init(x: Int64, y: Int64) { self.x = x self.y = y @@ -60,7 +65,9 @@ public class MySwiftClass { self.x = 10 self.y = 5 } + // snippet.end + // snippet.throwingInitDefinition convenience public init(throwing: Bool) throws { if throwing { throw MySwiftError.swiftError @@ -68,6 +75,7 @@ public class MySwiftClass { self.init() } } + // snippet.end deinit { } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index fc294b5a0..0335ff7e4 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -43,9 +43,11 @@ public func globalMakeInt() -> Int64 { 42 } +// snippet.stringFunction public func globalWriteString(string: String) -> Int64 { Int64(string.count) } +// snippet.end public func globalTakeIntInt(i: Int64, j: Int64) { p("i:\(i), j:\(j)") diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 4c7ce1bc3..c34cc4ad9 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -33,6 +33,7 @@ public struct MySwiftStruct { } } + // snippet.memberFunctions public func getCapacity() -> Int64 { self.cap } @@ -42,6 +43,7 @@ public struct MySwiftStruct { self.cap += value return self.cap } + // snippet.end public func getSubscriptValue() -> Int64 { self.subscriptValue @@ -51,6 +53,7 @@ public struct MySwiftStruct { self.subscriptArray[Int(index)] } + // snippet.subscriptDefinition public subscript() -> Int64 { get { subscriptValue } set { subscriptValue = newValue } @@ -60,6 +63,7 @@ public struct MySwiftStruct { get { subscriptArray[Int(index)] } set { subscriptArray[Int(index)] = newValue } } + // snippet.end // operator functions are ignored. public static func == (lhs: MySwiftStruct, rhs: MySwiftStruct) -> Bool { diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift index b58423013..e90eb1137 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -40,6 +40,13 @@ public func optionalInt(input: Int32?) -> Int32? { input } +// snippet.optionalParameterDefinition +public func optionalLongOrZero(input: Int64?) -> Int64 { + input ?? 0 +} +// snippet.end + +// snippet.optionalDefinition public func optionalLong(input: Int64?) -> Int64? { input } @@ -67,6 +74,7 @@ public func optionalDate(input: Date?) -> Date? { public func optionalData(input: Data?) -> Data? { input } +// snippet.end public func optionalJavaKitLong(input: JavaLong?) -> Int64? { if let input { diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift index 2ca862f0b..4bc632f62 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.protocolDefinition public protocol ProtocolA { var constantA: Int64 { get } var mutable: Int64 { get set } @@ -19,10 +20,13 @@ public protocol ProtocolA { func name() -> String func makeClass() -> MySwiftClass } +// snippet.end +// snippet.protocolUsage public func takeProtocol(_ proto1: any ProtocolA, _ proto2: some ProtocolA) -> Int64 { proto1.constantA + proto2.constantA } +// snippet.end /// A struct conformer to `ProtocolA`, used to prove that /// setter dispatch through a returned existential box diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift index f58b9a33e..d82574ed6 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift @@ -12,10 +12,12 @@ // //===----------------------------------------------------------------------===// +// snippet.returnProtocolDefinition public protocol Greeter { func greeting() -> String func repeated(count: Int64) -> String } +// snippet.end public struct EnglishGreeter: Greeter { public let name: String @@ -49,6 +51,7 @@ public struct DanishGreeter: Greeter { } } +// snippet.returnProtocolFunctions public func makeEnglishGreeter(name: String) -> any Greeter { EnglishGreeter(name: name) } @@ -60,6 +63,7 @@ public func makeDanishGreeter(name: String) -> any Greeter { public func makeOpaqueGreeter(name: String) -> some Greeter { EnglishGreeter(name: name) } +// snippet.end public func describeGreeter(_ greeter: any Greeter) -> String { greeter.greeting() diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift index 41c391867..6f16e3c54 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.setDefinition public func makeStringSet() -> Set { ["hello", "world"] } @@ -25,6 +26,7 @@ public func insertIntoStringSet(set: Set, element: String) -> Set Set { [1, 2, 3] diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Throw.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Throw.swift index 8eb18fcb6..b6c0184e6 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Throw.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Throw.swift @@ -14,9 +14,11 @@ import SwiftJava +// snippet.throwingFunction public func throwString(input: String) throws -> String { if input.isEmpty { throw MySwiftError.swiftError } return input } +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift index ff5bce585..fcdc5ac7f 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift @@ -12,6 +12,17 @@ // //===----------------------------------------------------------------------===// +// snippet.tupleDefinition +public func returnIntPair() -> (Int32, Int64) { + (42, 43) +} + +public func sumIntPair(pair: (Int32, Int64)) -> Int64 { + Int64(pair.0) + pair.1 +} +// snippet.end + +// snippet.stringTupleDefinition public func returnPair() -> (Int64, String) { (42, "hello") } @@ -23,6 +34,7 @@ public func takePair(pair: (Int64, String)) -> String { public func labeledTuple() -> (x: Int32, y: Int32) { (x: 10, y: 20) } +// snippet.end public func echoSingleTuple(input: (String)) -> (String) { input diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift index 962ec502c..a47052b68 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift @@ -14,6 +14,7 @@ import SwiftJava +// snippet.rawBufferDefinition /// Sum all bytes in the buffer public func sumOfBytes(data: UnsafeRawBufferPointer) -> Int64 { var sum: Int64 = 0 @@ -27,3 +28,4 @@ public func sumOfBytes(data: UnsafeRawBufferPointer) -> Int64 { public func bufferCount(data: UnsafeRawBufferPointer) -> Int64 { Int64(data.count) } +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift index 16ae2447a..3cecda30b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.enumDefinition public enum Vehicle { case bicycle case car(String, trailer: String?) @@ -61,3 +62,4 @@ public enum Vehicle { } } } +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java index 5bf059515..08a8f5ed3 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java @@ -26,6 +26,7 @@ public class AlignmentEnumTest { @Test void rawValue() { + // snippet.rawRepresentableEnumUsageJava try (var arena = SwiftArena.ofConfined()) { Optional invalid = Alignment.init("invalid", arena); assertFalse(invalid.isPresent()); @@ -38,5 +39,6 @@ void rawValue() { assertTrue(vertical.isPresent()); assertEquals("vertical", vertical.get().getRawValue()); } + // snippet.end } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java index 6efe1a0b3..9151ead56 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java @@ -24,16 +24,36 @@ import static org.junit.jupiter.api.Assertions.*; public class ArraysTest { - @Test - void booleanArray() { - boolean[] input = new boolean[] { true, false, false, true }; - assertArrayEquals(input, MySwiftLibrary.booleanArray(input)); - } - @Test void byteArray() { + // snippet.primitiveArraysUsage byte[] input = new byte[] { 10, 20, 30, 40 }; assertArrayEquals(input, MySwiftLibrary.byteArray(input)); + // snippet.end + } + + @Test + void intArray() { + int[] input = new int[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.intArray(input)); + } + + @Test + void doubleArray() { + double[] input = new double[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.doubleArray(input)); + } + + @Test + void stringArray() { + String[] input = new String[] { "hey", "there", "my", "friend" }; + assertArrayEquals(input, MySwiftLibrary.stringArray(input)); + } + + @Test + void booleanArray() { + boolean[] input = new boolean[] { true, false, false, true }; + assertArrayEquals(input, MySwiftLibrary.booleanArray(input)); } @Test @@ -65,46 +85,35 @@ void shortArray() { assertArrayEquals(input, MySwiftLibrary.shortArray(input)); } - @Test - void intArray() { - int[] input = new int[] { 10, 20, 30, 40 }; - assertArrayEquals(input, MySwiftLibrary.intArray(input)); - } - @Test void longArray() { long[] input = new long[] { 10, 20, 30, 40 }; assertArrayEquals(input, MySwiftLibrary.longArray(input)); } - @Test - void stringArray() { - String[] input = new String[] { "hey", "there", "my", "friend" }; - assertArrayEquals(input, MySwiftLibrary.stringArray(input)); - } - @Test void floatArray() { float[] input = new float[] { 10, 20, 30, 40 }; assertArrayEquals(input, MySwiftLibrary.floatArray(input)); } - @Test - void doubleArray() { - double[] input = new double[] { 10, 20, 30, 40 }; - assertArrayEquals(input, MySwiftLibrary.doubleArray(input)); - } - @Test void objectArray() { + // snippet.customTypeArraysUsage try (var arena = SwiftArena.ofConfined()) { - MySwiftClass[] input = new MySwiftClass[]{MySwiftClass.init(arena), MySwiftClass.init(arena), MySwiftClass.init(arena) }; + MySwiftClass[] input = new MySwiftClass[]{ + MySwiftClass.init(arena), + MySwiftClass.init(arena), + MySwiftClass.init(arena) + }; assertEquals(3, MySwiftLibrary.objectArray(input, arena).length); } + // snippet.end } @Test void nestedByteArray() { + // snippet.nestedArraysUsage byte[][] input = new byte[][] { { 1, 2, 3 }, { 4, 5 }, @@ -115,6 +124,7 @@ void nestedByteArray() { assertArrayEquals(input[0], result[0]); assertArrayEquals(input[1], result[1]); assertArrayEquals(input[2], result[2]); + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java index 400844fdc..1e9810954 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -33,20 +33,25 @@ public class AsyncTest { @Test void asyncSum() throws Exception { + // snippet.asyncUsageJava Future future = MySwiftLibrary.asyncSum(10, 12); Long result = future.get(); assertEquals(22, result); + // snippet.end } @Test void asyncSleep() throws Exception { + // snippet.asyncSleepUsageJava Future future = MySwiftLibrary.asyncSleep(); future.get(); + // snippet.end } @Test void asyncCopy() throws Exception { + // snippet.asyncCopyUsageJava try (var arena = SwiftArena.ofConfined()) { MySwiftClass obj = MySwiftClass.init(10, 5, arena); Future future = MySwiftLibrary.asyncCopy(obj, arena); @@ -56,6 +61,7 @@ void asyncCopy() throws Exception { assertEquals(10, result.getX()); assertEquals(5, result.getY()); } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java index 73134d97b..1c8baf8c5 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java @@ -64,6 +64,8 @@ void boxHasGenericTypeParameter() { @Test void callFishBoxStaticMethod() { + // snippet.specializationUsageJava assertEquals("Fish", FishBox.describeElement()); + // snippet.end } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java index e1d06f891..03aa53986 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java @@ -32,19 +32,25 @@ void emptyClosure() { @Test void closureWithInt() { + // snippet.closureWithIntUsageJava long result = MySwiftLibrary.closureWithInt(10, (value) -> value * 2); assertEquals(20, result); + // snippet.end } @Test void closureMultipleArguments() { + // snippet.closureMultipleArgumentsUsageJava long result = MySwiftLibrary.closureMultipleArguments(5, 10, (a, b) -> a + b); assertEquals(15, result); + // snippet.end } @Test void globalCallMeBooleanSupplier() { + // snippet.closureUsageJava boolean result = MySwiftLibrary.globalCallMeBooleanSupplier(() -> true); assertEquals(true, result); + // snippet.end } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index bc12a2322..87f732cce 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -22,6 +22,7 @@ public class DataTest { @Test void data_echo() { + // snippet.dataUsageJava try (var arena = SwiftArena.ofConfined()) { byte[] bytes = new byte[] { 1, 2, 3, 4 }; var data = Data.fromByteArray(bytes, arena); @@ -29,6 +30,7 @@ void data_echo() { var echoed = MySwiftLibrary.echoData(data, arena); assertArrayEquals(bytes, echoed.toByteArray()); } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EscapingClosuresTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EscapingClosuresTest.java index 2da95f297..e602e0ac2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EscapingClosuresTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EscapingClosuresTest.java @@ -26,31 +26,33 @@ public class EscapingClosuresTest { @Test void testCallbackManager_singleCallback() { + // snippet.escapingClosureUsageJava try (var arena = SwiftArena.ofConfined()) { CallbackManager manager = CallbackManager.init(arena); - + AtomicBoolean wasCalled = new AtomicBoolean(false); - + // Create an escaping closure (no try-with-resources needed - cleanup is automatic via Swift ARC) CallbackManager.setCallback.callback callback = () -> { wasCalled.set(true); }; - + // Set the callback manager.setCallback(callback); - + // Trigger it manager.triggerCallback(); assertTrue(wasCalled.get(), "Callback should have been called"); - + // Trigger again to ensure it's still stored wasCalled.set(false); manager.triggerCallback(); assertTrue(wasCalled.get(), "Callback should be called multiple times"); - + // Clear the callback - this releases the closure on Swift side, triggering GlobalRef cleanup manager.clearCallback(); } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java index a789bb206..f834c6f51 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java @@ -26,6 +26,7 @@ public class FoundationTypeTest { @Test void date_functions() { + // snippet.dateUsageJava try (var arena = SwiftArena.ofConfined()) { var date = MySwiftLibrary.dateFromSeconds(1000.50, arena); assertEquals(1000.5, date.getTimeIntervalSince1970()); @@ -36,6 +37,7 @@ void date_functions() { var date3 = Date.init(1000.49, arena); assertFalse(MySwiftLibrary.compareDates(date, date3)); } + // snippet.end } @Test @@ -63,8 +65,10 @@ void date_timeIntervalSince1970() { @Test void echoUUID() { + // snippet.uuidUsageJava var uuid = UUID.randomUUID(); assertEquals(uuid, MySwiftLibrary.echoUUID(uuid)); + // snippet.end } @Test @@ -75,6 +79,7 @@ void makeUUID() { @Test void echoURL() { + // snippet.foundationURLUsageJava try (var arena = SwiftArena.ofConfined()) { var url = URL.init("http://example.com", arena); assertDoesNotThrow(() -> { @@ -82,5 +87,6 @@ void echoURL() { assertEquals("http://example.com", unwrapped.getAbsoluteString()); }); } + // snippet.end } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java index e5c414ed5..e803c295b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java @@ -25,6 +25,7 @@ public class GenericTypeTest { @Test void genericTypeValueRoundtrip() { + // snippet.genericTypeUsageJava try (var arena = SwiftArena.ofConfined()) { MyID stringId = MyIDs.makeStringID("Java", arena); assertEquals("Java", stringId.getDescription()); @@ -49,6 +50,7 @@ void genericTypeValueRoundtrip() { assertEquals("Optional(\"Java\")", optionalStringId.getDescription()); assertEquals("Java", MyIDs.takeOptionalStringValue(optionalStringId).get()); } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 8e13c8d42..b2598602e 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -43,6 +43,7 @@ void init_withParameters() { @Test void init_throwing() { + // snippet.throwingInitUsageJava try (var arena = SwiftArena.ofConfined()) { Exception exception = assertThrows(Exception.class, () -> MySwiftClass.init(true, arena)); assertEquals("swiftError", exception.getMessage()); @@ -50,14 +51,17 @@ void init_throwing() { MySwiftClass c = assertDoesNotThrow(() -> MySwiftClass.init(false, arena)); assertNotNull(c); } + // snippet.end } @Test void sum() { + // snippet.classUsageJava try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(30, c.sum()); } + // snippet.end } @Test @@ -117,6 +121,7 @@ void throwingVariable() { @Test void mutableDividedByTwo() { + // snippet.computedPropertiesUsageJava try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(0, c.getMutableDividedByTwo()); @@ -125,6 +130,7 @@ void mutableDividedByTwo() { c.setMutableDividedByTwo(5); assertEquals(10, c.getMutable()); } + // snippet.end } @Test @@ -200,6 +206,7 @@ void toDebugStringTest() { @Test void privateSetCounter_getterOnly() throws Exception { + // snippet.privateSetUsageJava try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(7, c.getPrivateSetCounter()); @@ -209,5 +216,6 @@ void privateSetCounter_getterOnly() throws Exception { Method getter = MySwiftClass.class.getMethod("getPrivateSetCounter"); assertNotNull(getter); assertThrows(NoSuchMethodException.class, () -> MySwiftClass.class.getMethod("setPrivateSetCounter", long.class)); + // snippet.end } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index b6d6501d8..64aedc61d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -50,10 +50,12 @@ void call_globalTakeIntInt() { @Test void call_writeString_jextract() { + // snippet.stringUsageJava var string = "Hello Swift!"; long reply = MySwiftLibrary.globalWriteString(string); assertEquals(string.length(), reply); + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java index 24b1fdbf9..fc651dad0 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java @@ -54,12 +54,16 @@ void getAndSetLen() { @Test void increaseCap() { + // snippet.structUsageJava try (var arena = SwiftArena.ofConfined()) { MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + assertEquals(1337, s.getCapacity()); + long newCap = s.increaseCap(10); assertEquals(1347, newCap); assertEquals(1347, s.getCapacity()); } + // snippet.end } @Test @@ -75,6 +79,7 @@ void testSubscript() { @Test void testSubscriptWithParams() { + // snippet.subscriptUsageJava try (var arena = SwiftArena.ofConfined()) { MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); long currentValue = s.getSubscript(1); @@ -82,5 +87,6 @@ void testSubscriptWithParams() { assertEquals(20, currentValue); assertEquals(66, s.getSubscriptArrayValue(1)); } + // snippet.end } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java index 6db748496..de219f46a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -59,8 +59,18 @@ void optionalInt() { @Test void optionalLong() { + // snippet.optionalUsageJava assertEquals(OptionalLong.empty(), MySwiftLibrary.optionalLong(OptionalLong.empty())); assertEquals(OptionalLong.of(999), MySwiftLibrary.optionalLong(OptionalLong.of(999))); + // snippet.end + } + + @Test + void optionalLongOrZero() { + // snippet.optionalParameterUsageJava + assertEquals(0, MySwiftLibrary.optionalLongOrZero(OptionalLong.empty())); + assertEquals(999, MySwiftLibrary.optionalLongOrZero(OptionalLong.of(999))); + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java index 885dbf6f7..1257674ad 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -22,11 +22,13 @@ public class ProtocolTest { @Test void takeProtocol() { + // snippet.takeProtocolUsageJava try (var arena = SwiftArena.ofConfined()) { ConcreteProtocolAB proto1 = ConcreteProtocolAB.init(10, 5, arena); ConcreteProtocolAB proto2 = ConcreteProtocolAB.init(20, 1, arena); assertEquals(30, MySwiftLibrary.takeProtocol(proto1, proto2)); } + // snippet.end } @Test @@ -109,6 +111,7 @@ public void save(StorageItem item) { @Test void useStorage() { + // snippet.protocolUsageJava try (var arena = SwiftArena.ofConfined()) { JavaStorage storage = new JavaStorage(null); MySwiftLibrary.saveWithStorage(StorageItem.init(10, arena), storage); @@ -117,5 +120,6 @@ void useStorage() { MySwiftLibrary.saveWithStorage(StorageItem.init(5, arena), storage); assertEquals(5, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); } + // snippet.end } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java index 67d91aa80..2095e7c08 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java @@ -23,10 +23,12 @@ public class ReturnProtocolTest { @Test void returnExistentialAndCallMethod() { + // snippet.returnProtocolUsageJava try (var arena = SwiftArena.ofConfined()) { Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); assertEquals("Hello, World!", greeter.greeting()); } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java index a065c7460..3cc820e6e 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java @@ -26,6 +26,7 @@ public class SwiftDictionaryMapTest { @SuppressWarnings("SuspiciousMethodCalls") @Test void makeStringToLongDictionary() { + // snippet.dictionaryUsageJava try (var arena = SwiftArena.ofConfined()) { SwiftDictionaryMap dict = MySwiftLibrary.makeStringToLongDictionary(arena); assertEquals(2, dict.size()); @@ -37,6 +38,7 @@ void makeStringToLongDictionary() { assertNull(dict.get(99999L), "Java's Map accepts keys of different types"); assertFalse(dict.containsKey(99999L), "Java's Map accepts keys of different types"); } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java index 573af94f4..b7a1ed83a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java @@ -26,6 +26,7 @@ public class SwiftSetTest { @SuppressWarnings("SuspiciousMethodCalls") @Test void makeStringSet() { + // snippet.setUsageJava try (var arena = SwiftArena.ofConfined()) { SwiftSet set = MySwiftLibrary.makeStringSet(arena); assertEquals(2, set.size()); @@ -34,6 +35,7 @@ void makeStringSet() { assertFalse(set.contains("missing")); assertFalse(set.contains(99999L), "Java's Set accepts keys of different types"); } + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ThrowTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ThrowTest.java index e0d5440a6..8aec9f8ab 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ThrowTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ThrowTest.java @@ -28,10 +28,12 @@ void throwString() throws Exception { @Test void throwStringActuallyThrows() { + // snippet.throwUsageJava Exception exception = assertThrows(Exception.class, () -> { MySwiftLibrary.throwString(""); }); assertNotNull(exception.getMessage()); assertTrue(exception.getMessage().contains("swiftError")); + // snippet.end } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java index b58e12ab8..798276cf1 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java @@ -26,11 +26,27 @@ import java.util.OptionalLong; public class TupleTest { + @Test + void returnIntPair() { + // snippet.tupleUsageJava + Tuple2 result = MySwiftLibrary.returnIntPair(); + assertEquals(42, result.$0); + assertEquals(43L, result.$1); + // snippet.end + } + + @Test + void sumIntPair() { + assertEquals(12L, MySwiftLibrary.sumIntPair(new Tuple2<>(5, 7L))); + } + @Test void returnPair() { + // snippet.stringTupleUsageJava Tuple2 result = MySwiftLibrary.returnPair(); assertEquals(42L, result.$0); assertEquals("hello", result.$1); + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UnsafeRawBufferPointerTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UnsafeRawBufferPointerTest.java index 98542e26a..a7a823064 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UnsafeRawBufferPointerTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UnsafeRawBufferPointerTest.java @@ -21,8 +21,10 @@ public class UnsafeRawBufferPointerTest { @Test void sumOfBytes() { + // snippet.rawBufferUsageJava byte[] input = new byte[] { 1, 2, 3, 4, 5 }; assertEquals(15, MySwiftLibrary.sumOfBytes(input)); + // snippet.end } @Test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index 8fbe14c76..dbd9b47db 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -99,6 +99,7 @@ void getAsBicycle() { @Test void getAsCar() { + // snippet.enumUsageJava try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); Vehicle.Case.Car car = vehicle.getAsCar().orElseThrow(); @@ -108,6 +109,7 @@ void getAsCar() { car = vehicle.getAsCar().orElseThrow(); assertEquals("Long trailer", car.trailer().orElseThrow()); } + // snippet.end } @Test @@ -162,10 +164,12 @@ void associatedValuesAreCopied() { @Test void getDiscriminator() { try (var arena = SwiftArena.ofConfined()) { + // snippet.enumDiscriminatorUsageJava assertEquals(Vehicle.Discriminator.BICYCLE, Vehicle.bicycle(arena).getDiscriminator()); assertEquals(Vehicle.Discriminator.CAR, Vehicle.car("BMW", Optional.empty(), arena).getDiscriminator()); assertEquals(Vehicle.Discriminator.MOTORBIKE, Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena).getDiscriminator()); assertEquals(Vehicle.Discriminator.TRANSFORMER, Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.bicycle(arena), arena).getDiscriminator()); + // snippet.end } } @@ -180,6 +184,7 @@ void getCase() { @Test void switchGetCase() { + // snippet.enumSwitchUsageJava try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); switch (vehicle.getCase(arena)) { @@ -200,6 +205,7 @@ void switchGetCase() { break; } } + // snippet.end } } diff --git a/Snippets/ArraysJavaFFM.java b/Snippets/ArraysJavaFFM.java new file mode 120000 index 000000000..0e8304d17 --- /dev/null +++ b/Snippets/ArraysJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java \ No newline at end of file diff --git a/Snippets/ArraysJavaJNI.java b/Snippets/ArraysJavaJNI.java new file mode 120000 index 000000000..5630a73b4 --- /dev/null +++ b/Snippets/ArraysJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java \ No newline at end of file diff --git a/Snippets/ArraysSwift.swift.symlink b/Snippets/ArraysSwift.swift.symlink new file mode 120000 index 000000000..25d96f8c9 --- /dev/null +++ b/Snippets/ArraysSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift \ No newline at end of file diff --git a/Snippets/AsyncJavaJNI.java b/Snippets/AsyncJavaJNI.java new file mode 120000 index 000000000..de83521ed --- /dev/null +++ b/Snippets/AsyncJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java \ No newline at end of file diff --git a/Snippets/AsyncSwift.swift.symlink b/Snippets/AsyncSwift.swift.symlink new file mode 120000 index 000000000..cd1acd587 --- /dev/null +++ b/Snippets/AsyncSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift \ No newline at end of file diff --git a/Snippets/ClassesJavaFFM.java b/Snippets/ClassesJavaFFM.java new file mode 120000 index 000000000..da95d4143 --- /dev/null +++ b/Snippets/ClassesJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java \ No newline at end of file diff --git a/Snippets/ClassesJavaJNI.java b/Snippets/ClassesJavaJNI.java new file mode 120000 index 000000000..124bce8d8 --- /dev/null +++ b/Snippets/ClassesJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java \ No newline at end of file diff --git a/Snippets/ClassesSwift.swift.symlink b/Snippets/ClassesSwift.swift.symlink new file mode 120000 index 000000000..2a889c4d6 --- /dev/null +++ b/Snippets/ClassesSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift \ No newline at end of file diff --git a/Snippets/ClosuresJavaFFM.java b/Snippets/ClosuresJavaFFM.java new file mode 120000 index 000000000..eb9abb694 --- /dev/null +++ b/Snippets/ClosuresJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java \ No newline at end of file diff --git a/Snippets/ClosuresJavaJNI.java b/Snippets/ClosuresJavaJNI.java new file mode 120000 index 000000000..219aaf2e2 --- /dev/null +++ b/Snippets/ClosuresJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java \ No newline at end of file diff --git a/Snippets/ClosuresSwift.swift.symlink b/Snippets/ClosuresSwift.swift.symlink new file mode 120000 index 000000000..4446ac08f --- /dev/null +++ b/Snippets/ClosuresSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Closures.swift \ No newline at end of file diff --git a/Snippets/DataJavaFFM.java b/Snippets/DataJavaFFM.java new file mode 120000 index 000000000..19d6d0536 --- /dev/null +++ b/Snippets/DataJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java \ No newline at end of file diff --git a/Snippets/DataJavaJNI.java b/Snippets/DataJavaJNI.java new file mode 120000 index 000000000..72dd7e117 --- /dev/null +++ b/Snippets/DataJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java \ No newline at end of file diff --git a/Snippets/DataSwift.swift.symlink b/Snippets/DataSwift.swift.symlink new file mode 120000 index 000000000..2c2ea6be1 --- /dev/null +++ b/Snippets/DataSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift \ No newline at end of file diff --git a/Snippets/DictionariesJavaJNI.java b/Snippets/DictionariesJavaJNI.java new file mode 120000 index 000000000..90806582e --- /dev/null +++ b/Snippets/DictionariesJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java \ No newline at end of file diff --git a/Snippets/DictionariesSwift.swift.symlink b/Snippets/DictionariesSwift.swift.symlink new file mode 120000 index 000000000..8acbc139e --- /dev/null +++ b/Snippets/DictionariesSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift \ No newline at end of file diff --git a/Snippets/EnumsJavaJNI.java b/Snippets/EnumsJavaJNI.java new file mode 120000 index 000000000..bbd3957f4 --- /dev/null +++ b/Snippets/EnumsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java \ No newline at end of file diff --git a/Snippets/EnumsSwift.swift.symlink b/Snippets/EnumsSwift.swift.symlink new file mode 120000 index 000000000..108bfa67b --- /dev/null +++ b/Snippets/EnumsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift \ No newline at end of file diff --git a/Snippets/EscapingClosuresJavaJNI.java b/Snippets/EscapingClosuresJavaJNI.java new file mode 120000 index 000000000..d9fd7a8ea --- /dev/null +++ b/Snippets/EscapingClosuresJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EscapingClosuresTest.java \ No newline at end of file diff --git a/Snippets/EscapingClosuresSwift.swift.symlink b/Snippets/EscapingClosuresSwift.swift.symlink new file mode 120000 index 000000000..bfaf6216f --- /dev/null +++ b/Snippets/EscapingClosuresSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EscapingClosures.swift \ No newline at end of file diff --git a/Snippets/FoundationTypesJavaJNI.java b/Snippets/FoundationTypesJavaJNI.java new file mode 120000 index 000000000..61ca76218 --- /dev/null +++ b/Snippets/FoundationTypesJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java \ No newline at end of file diff --git a/Snippets/FoundationTypesSwift.swift.symlink b/Snippets/FoundationTypesSwift.swift.symlink new file mode 120000 index 000000000..8018d72f7 --- /dev/null +++ b/Snippets/FoundationTypesSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationTypes.swift \ No newline at end of file diff --git a/Snippets/GenericsJavaJNI.java b/Snippets/GenericsJavaJNI.java new file mode 120000 index 000000000..0d8a82ffc --- /dev/null +++ b/Snippets/GenericsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java \ No newline at end of file diff --git a/Snippets/GenericsSwift.swift.symlink b/Snippets/GenericsSwift.swift.symlink new file mode 120000 index 000000000..dc856cf60 --- /dev/null +++ b/Snippets/GenericsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift \ No newline at end of file diff --git a/Snippets/JavaKitArraysJava.java b/Snippets/JavaKitArraysJava.java new file mode 120000 index 000000000..fc38ee957 --- /dev/null +++ b/Snippets/JavaKitArraysJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloJavaKitArrays.java \ No newline at end of file diff --git a/Snippets/JavaKitArraysSwift.swift.symlink b/Snippets/JavaKitArraysSwift.swift.symlink new file mode 120000 index 000000000..4fc9e7977 --- /dev/null +++ b/Snippets/JavaKitArraysSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitArrayRuntimeTests.swift \ No newline at end of file diff --git a/Snippets/JavaKitCastSwift.swift.symlink b/Snippets/JavaKitCastSwift.swift.symlink new file mode 120000 index 000000000..79ed2061c --- /dev/null +++ b/Snippets/JavaKitCastSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift \ No newline at end of file diff --git a/Snippets/JavaKitClassJava.java b/Snippets/JavaKitClassJava.java new file mode 120000 index 000000000..eafb75afb --- /dev/null +++ b/Snippets/JavaKitClassJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java \ No newline at end of file diff --git a/Snippets/JavaKitClassSwift.swift.symlink b/Snippets/JavaKitClassSwift.swift.symlink new file mode 120000 index 000000000..79ed2061c --- /dev/null +++ b/Snippets/JavaKitClassSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift \ No newline at end of file diff --git a/Snippets/JavaKitEnumSwift.swift.symlink b/Snippets/JavaKitEnumSwift.swift.symlink new file mode 120000 index 000000000..a294d656c --- /dev/null +++ b/Snippets/JavaKitEnumSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaSieve/Sources/JavaSieve/main.swift \ No newline at end of file diff --git a/Snippets/JavaKitImplementationJava.java b/Snippets/JavaKitImplementationJava.java new file mode 120000 index 000000000..eafb75afb --- /dev/null +++ b/Snippets/JavaKitImplementationJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java \ No newline at end of file diff --git a/Snippets/JavaKitImplementationSwift.swift.symlink b/Snippets/JavaKitImplementationSwift.swift.symlink new file mode 120000 index 000000000..79ed2061c --- /dev/null +++ b/Snippets/JavaKitImplementationSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift \ No newline at end of file diff --git a/Snippets/JavaKitInheritanceJava.java b/Snippets/JavaKitInheritanceJava.java new file mode 120000 index 000000000..dfaa929b9 --- /dev/null +++ b/Snippets/JavaKitInheritanceJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java \ No newline at end of file diff --git a/Snippets/JavaKitInheritanceSwift.swift.symlink b/Snippets/JavaKitInheritanceSwift.swift.symlink new file mode 120000 index 000000000..79ed2061c --- /dev/null +++ b/Snippets/JavaKitInheritanceSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift \ No newline at end of file diff --git a/Snippets/JavaKitOptionalsJava.java b/Snippets/JavaKitOptionalsJava.java new file mode 120000 index 000000000..768b35bd4 --- /dev/null +++ b/Snippets/JavaKitOptionalsJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java \ No newline at end of file diff --git a/Snippets/JavaKitOptionalsSwift.swift.symlink b/Snippets/JavaKitOptionalsSwift.swift.symlink new file mode 120000 index 000000000..79ed2061c --- /dev/null +++ b/Snippets/JavaKitOptionalsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift \ No newline at end of file diff --git a/Snippets/JavaKitReflectionSwift.swift.symlink b/Snippets/JavaKitReflectionSwift.swift.symlink new file mode 120000 index 000000000..4534669e9 --- /dev/null +++ b/Snippets/JavaKitReflectionSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift \ No newline at end of file diff --git a/Snippets/JavaKitSendableAnnotationJava.java b/Snippets/JavaKitSendableAnnotationJava.java new file mode 120000 index 000000000..08d9dfc52 --- /dev/null +++ b/Snippets/JavaKitSendableAnnotationJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafe.java \ No newline at end of file diff --git a/Snippets/JavaKitSendableHelperJava.java b/Snippets/JavaKitSendableHelperJava.java new file mode 120000 index 000000000..768b35bd4 --- /dev/null +++ b/Snippets/JavaKitSendableHelperJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/ThreadSafeHelperClass.java \ No newline at end of file diff --git a/Snippets/JavaKitSendableSwift.swift.symlink b/Snippets/JavaKitSendableSwift.swift.symlink new file mode 120000 index 000000000..79ed2061c --- /dev/null +++ b/Snippets/JavaKitSendableSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift \ No newline at end of file diff --git a/Snippets/JavaKitThrowsSwift.swift.symlink b/Snippets/JavaKitThrowsSwift.swift.symlink new file mode 120000 index 000000000..79ed2061c --- /dev/null +++ b/Snippets/JavaKitThrowsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift \ No newline at end of file diff --git a/Snippets/NotSupportedYetJavaFFM.java b/Snippets/NotSupportedYetJavaFFM.java new file mode 100644 index 000000000..b0da78bfd --- /dev/null +++ b/Snippets/NotSupportedYetJavaFFM.java @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// This file is an intentional exception to the "Snippets/ is symlink-only" rule +// documented in Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ . It is a pedagogical +// placeholder for @Tab("Java (FFM) - not supported") blocks in the DocC pages. +// If FFM ever supports the feature in question, replace usages of this file +// with a symlink to a real sample. +// snippet.notSupportedYet +// not supported yet +// snippet.end diff --git a/Snippets/NotSupportedYetJavaJNI.java b/Snippets/NotSupportedYetJavaJNI.java new file mode 100644 index 000000000..7a5600880 --- /dev/null +++ b/Snippets/NotSupportedYetJavaJNI.java @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// This file is an intentional exception to the "Snippets/ is symlink-only" rule +// documented in Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ . It is a pedagogical +// placeholder for @Tab("Java (JNI) - not supported") blocks in the DocC pages. +// If JNI ever supports the feature in question, replace usages of this file +// with a symlink to a real sample. +// snippet.notSupportedYet +// not supported yet +// snippet.end diff --git a/Snippets/OptionalsJavaFFM.java b/Snippets/OptionalsJavaFFM.java new file mode 120000 index 000000000..4a99f7b85 --- /dev/null +++ b/Snippets/OptionalsJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java \ No newline at end of file diff --git a/Snippets/OptionalsJavaJNI.java b/Snippets/OptionalsJavaJNI.java new file mode 120000 index 000000000..6364fb8fb --- /dev/null +++ b/Snippets/OptionalsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java \ No newline at end of file diff --git a/Snippets/OptionalsSwift.swift.symlink b/Snippets/OptionalsSwift.swift.symlink new file mode 120000 index 000000000..727e40234 --- /dev/null +++ b/Snippets/OptionalsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift \ No newline at end of file diff --git a/Snippets/ProtocolsJavaJNI.java b/Snippets/ProtocolsJavaJNI.java new file mode 120000 index 000000000..6d38e6be5 --- /dev/null +++ b/Snippets/ProtocolsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java \ No newline at end of file diff --git a/Snippets/ProtocolsSwift.swift.symlink b/Snippets/ProtocolsSwift.swift.symlink new file mode 120000 index 000000000..80b9ef763 --- /dev/null +++ b/Snippets/ProtocolsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift \ No newline at end of file diff --git a/Snippets/RawBufferJavaFFM.java b/Snippets/RawBufferJavaFFM.java new file mode 120000 index 000000000..f09bc1bbd --- /dev/null +++ b/Snippets/RawBufferJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java \ No newline at end of file diff --git a/Snippets/RawBufferJavaJNI.java b/Snippets/RawBufferJavaJNI.java new file mode 120000 index 000000000..81c50b3a6 --- /dev/null +++ b/Snippets/RawBufferJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UnsafeRawBufferPointerTest.java \ No newline at end of file diff --git a/Snippets/RawBufferSwift.swift.symlink b/Snippets/RawBufferSwift.swift.symlink new file mode 120000 index 000000000..2f0920e94 --- /dev/null +++ b/Snippets/RawBufferSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UnsafeRawBufferPointer.swift \ No newline at end of file diff --git a/Snippets/RawBufferSwiftFFM.swift.symlink b/Snippets/RawBufferSwiftFFM.swift.symlink new file mode 120000 index 000000000..36c7aa00a --- /dev/null +++ b/Snippets/RawBufferSwiftFFM.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift \ No newline at end of file diff --git a/Snippets/RawRepresentableEnumsJavaJNI.java b/Snippets/RawRepresentableEnumsJavaJNI.java new file mode 120000 index 000000000..f85be64b8 --- /dev/null +++ b/Snippets/RawRepresentableEnumsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java \ No newline at end of file diff --git a/Snippets/RawRepresentableEnumsSwift.swift.symlink b/Snippets/RawRepresentableEnumsSwift.swift.symlink new file mode 120000 index 000000000..b1c048d4c --- /dev/null +++ b/Snippets/RawRepresentableEnumsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift \ No newline at end of file diff --git a/Snippets/ReturnProtocolJavaJNI.java b/Snippets/ReturnProtocolJavaJNI.java new file mode 120000 index 000000000..03328a0f6 --- /dev/null +++ b/Snippets/ReturnProtocolJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java \ No newline at end of file diff --git a/Snippets/ReturnProtocolSwift.swift.symlink b/Snippets/ReturnProtocolSwift.swift.symlink new file mode 120000 index 000000000..f42aa6ca2 --- /dev/null +++ b/Snippets/ReturnProtocolSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift \ No newline at end of file diff --git a/Snippets/SetsJavaJNI.java b/Snippets/SetsJavaJNI.java new file mode 120000 index 000000000..8dbb73c4e --- /dev/null +++ b/Snippets/SetsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java \ No newline at end of file diff --git a/Snippets/SetsSwift.swift.symlink b/Snippets/SetsSwift.swift.symlink new file mode 120000 index 000000000..b8f127159 --- /dev/null +++ b/Snippets/SetsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift \ No newline at end of file diff --git a/Snippets/SpecializationJavaJNI.java b/Snippets/SpecializationJavaJNI.java new file mode 120000 index 000000000..ec0c52ffb --- /dev/null +++ b/Snippets/SpecializationJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/BoxSpecializationTest.java \ No newline at end of file diff --git a/Snippets/SpecializationSwift.swift.symlink b/Snippets/SpecializationSwift.swift.symlink new file mode 120000 index 000000000..9f637fa53 --- /dev/null +++ b/Snippets/SpecializationSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift \ No newline at end of file diff --git a/Snippets/StringsJavaFFM.java b/Snippets/StringsJavaFFM.java new file mode 120000 index 000000000..eb9abb694 --- /dev/null +++ b/Snippets/StringsJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java \ No newline at end of file diff --git a/Snippets/StringsJavaJNI.java b/Snippets/StringsJavaJNI.java new file mode 120000 index 000000000..fff8d33a2 --- /dev/null +++ b/Snippets/StringsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java \ No newline at end of file diff --git a/Snippets/StringsSwift.swift.symlink b/Snippets/StringsSwift.swift.symlink new file mode 120000 index 000000000..8f0c789b6 --- /dev/null +++ b/Snippets/StringsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift \ No newline at end of file diff --git a/Snippets/StructsJavaFFM.java b/Snippets/StructsJavaFFM.java new file mode 120000 index 000000000..9c13bfa55 --- /dev/null +++ b/Snippets/StructsJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java \ No newline at end of file diff --git a/Snippets/StructsJavaJNI.java b/Snippets/StructsJavaJNI.java new file mode 120000 index 000000000..7dfc324fb --- /dev/null +++ b/Snippets/StructsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java \ No newline at end of file diff --git a/Snippets/StructsSwift.swift.symlink b/Snippets/StructsSwift.swift.symlink new file mode 120000 index 000000000..29863317c --- /dev/null +++ b/Snippets/StructsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift \ No newline at end of file diff --git a/Snippets/SubscriptsJavaJNI.java b/Snippets/SubscriptsJavaJNI.java new file mode 120000 index 000000000..7dfc324fb --- /dev/null +++ b/Snippets/SubscriptsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java \ No newline at end of file diff --git a/Snippets/ThrowingInitJavaJNI.java b/Snippets/ThrowingInitJavaJNI.java new file mode 120000 index 000000000..124bce8d8 --- /dev/null +++ b/Snippets/ThrowingInitJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java \ No newline at end of file diff --git a/Snippets/ThrowingInitSwift.swift.symlink b/Snippets/ThrowingInitSwift.swift.symlink new file mode 120000 index 000000000..2a889c4d6 --- /dev/null +++ b/Snippets/ThrowingInitSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift \ No newline at end of file diff --git a/Snippets/ThrowingJavaFFM.java b/Snippets/ThrowingJavaFFM.java new file mode 120000 index 000000000..eb9abb694 --- /dev/null +++ b/Snippets/ThrowingJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java \ No newline at end of file diff --git a/Snippets/ThrowingJavaJNI.java b/Snippets/ThrowingJavaJNI.java new file mode 120000 index 000000000..e18351e75 --- /dev/null +++ b/Snippets/ThrowingJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ThrowTest.java \ No newline at end of file diff --git a/Snippets/ThrowingSwift.swift.symlink b/Snippets/ThrowingSwift.swift.symlink new file mode 120000 index 000000000..1d6462573 --- /dev/null +++ b/Snippets/ThrowingSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Throw.swift \ No newline at end of file diff --git a/Snippets/TuplesJavaFFM.java b/Snippets/TuplesJavaFFM.java new file mode 120000 index 000000000..cf211d2b0 --- /dev/null +++ b/Snippets/TuplesJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java \ No newline at end of file diff --git a/Snippets/TuplesJavaJNI.java b/Snippets/TuplesJavaJNI.java new file mode 120000 index 000000000..9bae84ca7 --- /dev/null +++ b/Snippets/TuplesJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java \ No newline at end of file diff --git a/Snippets/TuplesSwift.swift.symlink b/Snippets/TuplesSwift.swift.symlink new file mode 120000 index 000000000..422ee244a --- /dev/null +++ b/Snippets/TuplesSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift \ No newline at end of file diff --git a/Snippets/WrapJavaDependencyConfig.json b/Snippets/WrapJavaDependencyConfig.json new file mode 120000 index 000000000..dda5b41e8 --- /dev/null +++ b/Snippets/WrapJavaDependencyConfig.json @@ -0,0 +1 @@ +../Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config \ No newline at end of file diff --git a/Snippets/WrapJavaDependencySwift.swift.symlink b/Snippets/WrapJavaDependencySwift.swift.symlink new file mode 120000 index 000000000..611a0ba91 --- /dev/null +++ b/Snippets/WrapJavaDependencySwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift \ No newline at end of file diff --git a/Snippets/WrapJavaProbablyPrimeConfig.json b/Snippets/WrapJavaProbablyPrimeConfig.json new file mode 120000 index 000000000..aef7939bd --- /dev/null +++ b/Snippets/WrapJavaProbablyPrimeConfig.json @@ -0,0 +1 @@ +../Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/swift-java.config \ No newline at end of file diff --git a/Snippets/WrapJavaProbablyPrimeSwift.swift.symlink b/Snippets/WrapJavaProbablyPrimeSwift.swift.symlink new file mode 120000 index 000000000..4534669e9 --- /dev/null +++ b/Snippets/WrapJavaProbablyPrimeSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaProbablyPrime/Sources/JavaProbablyPrime/prime.swift \ No newline at end of file diff --git a/Snippets/WrapJavaSieveConfig.json b/Snippets/WrapJavaSieveConfig.json new file mode 120000 index 000000000..f9ecec7d2 --- /dev/null +++ b/Snippets/WrapJavaSieveConfig.json @@ -0,0 +1 @@ +../Samples/JavaSieve/Sources/JavaMath/swift-java.config \ No newline at end of file diff --git a/Snippets/WrapJavaSieveSwift.swift.symlink b/Snippets/WrapJavaSieveSwift.swift.symlink new file mode 120000 index 000000000..a294d656c --- /dev/null +++ b/Snippets/WrapJavaSieveSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaSieve/Sources/JavaSieve/main.swift \ No newline at end of file diff --git a/Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ b/Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ new file mode 100644 index 000000000..4b2062fa7 --- /dev/null +++ b/Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ @@ -0,0 +1,7 @@ +DO NOT EDIT DIRECTLY IN THIS DIRECTORY. + +Files in here are not expected to compile as-is; they are symlinks to actual test +and sample files validated elsewhere. + +This way, the tests are actually compiled and executed, +and the snippet files are available for swift-docc which needs to resolve them in the Snippets/ directory. diff --git a/Sources/GenerateConfigDocs/ConfigParser.swift b/Sources/GenerateConfigDocs/ConfigParser.swift new file mode 100644 index 000000000..fa3d72b8b --- /dev/null +++ b/Sources/GenerateConfigDocs/ConfigParser.swift @@ -0,0 +1,595 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 Foundation +import SwiftParser +import SwiftSyntax + +// ==== ----------------------------------------------------------------------- +// MARK: Parsed model + +/// A single case on an enum we care about. +struct EnumCaseInfo { + var name: String + /// For raw-value / bare cases: the display string (raw value if present, else the case name). + var display: String? + /// For associated-value cases: the parameter clause verbatim (without parens). + var associatedSignature: String? + /// Doc-comment paragraph lines (already stripped of `///` prefix). + var docLines: [String] +} + +struct EnumInfo { + var name: String + var docLines: [String] + var cases: [EnumCaseInfo] + /// The name of the `.` value returned by `public static var \`default\``, if any. + var defaultCase: String? + + var hasAssociatedValues: Bool { + cases.contains { $0.associatedSignature != nil } + } + + /// First paragraph of doc-lines, joined with spaces (blank line ends the paragraph). + var firstParagraph: String { + var out: [String] = [] + for l in docLines { + if l.trimmingCharacters(in: .whitespaces).isEmpty { break } + out.append(l) + } + return out.joined(separator: " ").trimmingCharacters(in: .whitespaces) + } + + /// Plain / raw-value cases with their first-paragraph docs (associated-value cases skipped). + var caseDocs: [(display: String, doc: String)] { + cases.compactMap { c in + guard c.associatedSignature == nil, let display = c.display else { return nil } + var first: [String] = [] + for l in c.docLines { + if l.trimmingCharacters(in: .whitespaces).isEmpty { break } + first.append(l) + } + return (display, first.joined(separator: " ").trimmingCharacters(in: .whitespaces)) + } + } +} + +struct StructPropertyInfo { + var name: String + var type: String + var docLines: [String] +} + +struct StructInfo { + var name: String + var docLines: [String] + var properties: [StructPropertyInfo] +} + +/// One field on the `Configuration` struct. +struct ConfigField { + var name: String + var type: String + /// The literal RHS of `= ` on the stored-property line, if any (e.g. `nil`, `"[:]"`, `false`). + var defaultLiteral: String? + var docLines: [String] + var section: String +} + +// ==== ----------------------------------------------------------------------- +// MARK: Trivia helpers + +/// Doc-comment paragraph lines pulled off of leading trivia. +/// +/// Ignores section markers which start with `// ====`. +func docLines(from trivia: Trivia) -> [String] { + var out: [String] = [] + for piece in trivia.pieces { + switch piece { + case .docLineComment(let raw): + var text = raw + if text.hasPrefix("///") { text.removeFirst(3) } + if text.hasPrefix(" ") { text.removeFirst() } + out.append(text) + + case .lineComment(let raw): + // Skip section-divider comments; they're structural, not documentation. + if sectionName(fromLineComment: raw) != nil { continue } + var text = raw + if text.hasPrefix("//") { text.removeFirst(2) } + if text.hasPrefix(" ") { text.removeFirst() } + out.append(text) + + case .docBlockComment(let raw): + // `/** ... */`. Strip the delimiters and split into lines. + var body = raw + if body.hasPrefix("/**") { body.removeFirst(3) } + if body.hasSuffix("*/") { body.removeLast(2) } + for line in body.split(separator: "\n", omittingEmptySubsequences: false) { + var t = String(line).trimmingCharacters(in: .whitespaces) + if t.hasPrefix("*") { t.removeFirst() } + if t.hasPrefix(" ") { t.removeFirst() } + out.append(t) + } + + case .newlines(let n): + // A blank line (2+ newlines) between comment runs resets the buffer, + // matching the Python parser's `pending_doc = []` on empty lines. + if n >= 2 { out.removeAll() } + + case .blockComment: + // Non-doc block comments (e.g. copyright headers) reset the buffer. + out.removeAll() + + case _: + continue + + @unknown default: + continue + } + } + return out +} + +/// The `// ==== ---` divider convention used inside `Configuration`. +/// Returns the section name if the trivia piece is a divider line comment. +func sectionName(fromLineComment raw: String) -> String? { + // Line-comment form: "// ==== Foo Bar -----" + var body = raw + if body.hasPrefix("//") { body.removeFirst(2) } + body = body.trimmingCharacters(in: .whitespaces) + guard body.hasPrefix("====") else { return nil } + body.removeFirst(4) + // Trim trailing `---...`. + while body.hasSuffix("-") { body.removeLast() } + return body.trimmingCharacters(in: .whitespaces).isEmpty + ? nil + : body.trimmingCharacters(in: .whitespaces) +} + +// ==== ----------------------------------------------------------------------- +// MARK: Type shape helpers + +/// The inner named type for `T?`, `[T]`, `[K: T]`, or bare `T`; else nil. +func resolveContainerElementType(_ typeRaw: String) -> String? { + var core = typeRaw.trimmingCharacters(in: .whitespaces) + while core.hasSuffix("?") { core.removeLast() } + core = core.trimmingCharacters(in: .whitespaces) + + // [T] + if core.hasPrefix("["), core.hasSuffix("]"), !core.contains(":") { + let inner = String(core.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces) + return isBareIdentifier(inner) ? inner : nil + } + + // [K: V] + if core.hasPrefix("["), core.hasSuffix("]"), let colon = core.firstIndex(of: ":") { + var value = String(core[core.index(after: colon).. Bool { + guard !s.isEmpty else { return false } + return s.allSatisfy { $0.isLetter || $0.isNumber || $0 == "_" } +} + +// ==== ----------------------------------------------------------------------- +// MARK: The parser + +struct ConfigParser { + var enums: [String: EnumInfo] = [:] + var structs: [String: StructInfo] = [:] + + static func parse(rootDirs: [URL]) throws -> ConfigParser { + var parser = ConfigParser() + let fm = FileManager.default + var files: [URL] = [] + for dir in rootDirs { + guard let enumerator = fm.enumerator(at: dir, includingPropertiesForKeys: nil) else { continue } + for case let url as URL in enumerator { + if url.pathExtension == "swift" { + files.append(url) + } + } + } + files.sort { $0.path < $1.path } + for file in files { + let src = try String(contentsOf: file, encoding: .utf8) + let tree = Parser.parse(source: src) + parser.ingest(tree) + } + return parser + } + + mutating func ingest(_ tree: SourceFileSyntax) { + let visitor = TopLevelVisitor(viewMode: .sourceAccurate) + visitor.walk(tree) + for (name, info) in visitor.enums { + enums[name] = info + } + for (name, info) in visitor.structs { + structs[name] = info + } + // Apply any `public static var \`default\`: T { .caseName }` assignments now that + // enums/extensions from this file have been recorded (they may target enums + // declared in a different file we've already ingested, or in this one). + for (owner, caseName) in visitor.defaults { + if var info = enums[owner] { + info.defaultCase = caseName + enums[owner] = info + } + } + } +} + +// ==== ----------------------------------------------------------------------- +// MARK: SwiftSyntax visitor + +/// Walks a `SourceFileSyntax` collecting public enum / struct declarations and +/// any `public static var \`default\`` bindings on those types (whether declared +/// inline or in an `extension EnumName { ... }`). +private final class TopLevelVisitor: SyntaxVisitor { + var enums: [String: EnumInfo] = [:] + var structs: [String: StructInfo] = [:] + /// (owner type name, case name) pairs from `public static var \`default\`: T { .case }`. + var defaults: [(owner: String, caseName: String)] = [] + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + guard node.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + return .skipChildren + } + let name = node.name.text + var info = EnumInfo( + name: name, + docLines: docLines(from: node.leadingTrivia), + cases: [], + defaultCase: nil + ) + collectEnumMembers(node.memberBlock, into: &info, owner: name) + enums[name] = info + return .skipChildren + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + guard node.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + return .skipChildren + } + let name = node.name.text + var info = StructInfo( + name: name, + docLines: docLines(from: node.leadingTrivia), + properties: [] + ) + collectStructMembers(node.memberBlock, into: &info) + structs[name] = info + return .skipChildren + } + + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + // Only interested in extensions that add `public static var \`default\``. + let owner = node.extendedType.trimmedDescription + for member in node.memberBlock.members { + if let vd = member.decl.as(VariableDeclSyntax.self), + let caseName = defaultCaseName(from: vd) + { + defaults.append((owner: owner, caseName: caseName)) + } + } + return .skipChildren + } + + private func collectEnumMembers( + _ block: MemberBlockSyntax, + into info: inout EnumInfo, + owner: String + ) { + for member in block.members { + if let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) { + let docs = docLines(from: caseDecl.leadingTrivia) + // One `case` decl can list multiple elements (`case a, b`); doc lines + // apply to all of them. + for element in caseDecl.elements { + let caseName = element.name.text.trimmingBackticks() + if let params = element.parameterClause { + // Associated-value case. + let sig = params.parameters.map { $0.trimmedDescription }.joined(separator: ", ") + info.cases.append( + EnumCaseInfo( + name: caseName, + display: nil, + associatedSignature: sig, + docLines: docs + ) + ) + } else { + var display = caseName + if let raw = element.rawValue { + display = raw.value.trimmedDescription + } + info.cases.append( + EnumCaseInfo( + name: caseName, + display: display, + associatedSignature: nil, + docLines: docs + ) + ) + } + } + } else if let vd = member.decl.as(VariableDeclSyntax.self), + let caseName = defaultCaseName(from: vd) + { + info.defaultCase = caseName + } + } + _ = owner // silence unused-owner lint in some SwiftSyntax builds + } + + private func collectStructMembers( + _ block: MemberBlockSyntax, + into info: inout StructInfo + ) { + for member in block.members { + guard let vd = member.decl.as(VariableDeclSyntax.self) else { continue } + // Only `public var name: Type` stored properties (no accessor block). + guard vd.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + continue + } + guard vd.bindingSpecifier.tokenKind == .keyword(.var) else { continue } + guard let binding = vd.bindings.first, vd.bindings.count == 1 else { continue } + guard binding.accessorBlock == nil else { continue } + guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else { continue } + guard let typeAnn = binding.typeAnnotation else { continue } + let name = ident.identifier.text.trimmingBackticks() + let type = typeAnn.type.trimmedDescription + info.properties.append( + StructPropertyInfo( + name: name, + type: type, + docLines: docLines(from: vd.leadingTrivia) + ) + ) + } + } + + /// If `vd` is `public static var \`default\`: T { .caseName }` (or `{ return .caseName }`, + /// or `{ .caseName }` with the trailing whitespace), return `caseName`; else nil. + private func defaultCaseName(from vd: VariableDeclSyntax) -> String? { + guard vd.modifiers.contains(where: { $0.name.tokenKind == .keyword(.static) }) else { + return nil + } + guard vd.bindingSpecifier.tokenKind == .keyword(.var) else { return nil } + guard let binding = vd.bindings.first else { return nil } + guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else { return nil } + guard ident.identifier.text.trimmingBackticks() == "default" else { return nil } + guard let accessor = binding.accessorBlock else { return nil } + + // Body forms: + // `{ .caseName }` -> AccessorBlockSyntax.accessors == .getter(CodeBlockItemListSyntax) + // `{ get { .caseName } }` -> .accessors(AccessorDeclListSyntax) + var body: CodeBlockItemListSyntax? + switch accessor.accessors { + case .getter(let items): + body = items + case .accessors(let list): + for acc in list where acc.accessorSpecifier.tokenKind == .keyword(.get) { + body = acc.body?.statements + } + } + guard let statements = body, let first = statements.first else { return nil } + // Strip an optional leading `return`. + let expr: ExprSyntax + if let ret = first.item.as(ReturnStmtSyntax.self), let e = ret.expression { + expr = e + } else if let e = first.item.as(ExprSyntax.self) { + expr = e + } else { + return nil + } + // Expect `.caseName` (a MemberAccessExprSyntax with no base). + guard let member = expr.as(MemberAccessExprSyntax.self), member.base == nil else { + return nil + } + return member.declName.baseName.text.trimmingBackticks() + } +} + +/// Walks the `Configuration` struct's members in source order to produce the +/// ordered list of fields (with section headings from `// ==== Name -----` +/// dividers) and the `effective` -> fallback map. +struct ConfigurationBody { + var fields: [ConfigField] = [] + var effectiveFallbacks: [String: String] = [:] + + static func parse(source: String) throws -> ConfigurationBody { + let tree = Parser.parse(source: source) + let finder = ConfigStructFinder(viewMode: .sourceAccurate) + finder.walk(tree) + guard let decl = finder.decl else { + throw ConfigDocsError( + "Could not find 'public struct Configuration' in the config source" + ) + } + var body = ConfigurationBody() + var currentSection = "General" + var pendingDoc: [String] = [] + + for member in decl.memberBlock.members { + // Any `// ==== Name ----` line comment in the leading trivia of a member + // resets the current section. + for piece in member.leadingTrivia.pieces { + if case .lineComment(let raw) = piece, let name = sectionName(fromLineComment: raw) { + currentSection = name + pendingDoc = [] + } + } + + // Collect doc-lines from the member's leading trivia. + let docs = docLines(from: member.leadingTrivia) + if !docs.isEmpty { + pendingDoc = docs + } + + guard let vd = member.decl.as(VariableDeclSyntax.self) else { + // Functions and other members reset the pending doc buffer to mirror + // the Python parser (which drops docs on non-property members). + pendingDoc = [] + continue + } + guard vd.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + pendingDoc = [] + continue + } + guard vd.bindingSpecifier.tokenKind == .keyword(.var) else { + pendingDoc = [] + continue + } + guard let binding = vd.bindings.first, vd.bindings.count == 1 else { + pendingDoc = [] + continue + } + guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else { + pendingDoc = [] + continue + } + guard let typeAnn = binding.typeAnnotation else { + pendingDoc = [] + continue + } + let name = ident.identifier.text.trimmingBackticks() + let type = typeAnn.type.trimmedDescription + + if let accessor = binding.accessorBlock { + // Computed property. Detect `effective` and extract the `?? ` RHS. + if name.hasPrefix("effective"), + let fallback = nilCoalescingFallback(in: accessor) + { + let suffix = String(name.dropFirst("effective".count)) + guard let firstChar = suffix.first else { + pendingDoc = [] + continue + } + let underlying = String(firstChar).lowercased() + suffix.dropFirst() + body.effectiveFallbacks[underlying] = fallback + } + pendingDoc = [] + continue + } + + // Stored property. Record the field. + let defaultLit = binding.initializer.map { init_ in + init_.value.trimmedDescription.trimmingCharacters(in: .whitespaces) + } + body.fields.append( + ConfigField( + name: name, + type: type, + defaultLiteral: defaultLit, + docLines: pendingDoc, + section: currentSection + ) + ) + pendingDoc = [] + } + return body + } +} + +private final class ConfigStructFinder: SyntaxVisitor { + var decl: StructDeclSyntax? + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + if node.name.text == "Configuration" { + decl = node + return .skipChildren + } + return .visitChildren + } +} + +/// Given the accessor block of `effective: T { }`, if the body is +/// a single `?? ` expression (optionally with a `return`), return the +/// textual RHS (e.g. `.default`, `false`, `.public`). Otherwise nil. +private func nilCoalescingFallback(in accessor: AccessorBlockSyntax) -> String? { + let stmts: CodeBlockItemListSyntax? + switch accessor.accessors { + case .getter(let items): + stmts = items + case .accessors(let list): + stmts = + list.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) })? + .body?.statements + } + guard let statements = stmts, let first = statements.first else { return nil } + + var expr: ExprSyntax? + if let ret = first.item.as(ReturnStmtSyntax.self) { + expr = ret.expression + } else if let e = first.item.as(ExprSyntax.self) { + expr = e + } + guard let expression = expr else { return nil } + + // `a ?? b` parses as SequenceExpr(a, ??, b) *before* folding, or as + // InfixOperatorExpr(a, ??, b) after folding. + if let seq = expression.as(SequenceExprSyntax.self) { + let elements = Array(seq.elements) + for i in stride(from: 0, to: elements.count - 2, by: 2) { + if let op = elements[i + 1].as(BinaryOperatorExprSyntax.self), + op.operator.text == "??" + { + return elements[i + 2].trimmedDescription + } + } + } + if let infix = expression.as(InfixOperatorExprSyntax.self), + let op = infix.operator.as(BinaryOperatorExprSyntax.self), + op.operator.text == "??" + { + return infix.rightOperand.trimmedDescription + } + return nil +} + +// ==== ----------------------------------------------------------------------- +// MARK: Errors + +struct ConfigDocsError: Error, CustomStringConvertible { + let message: String + init(_ message: String) { self.message = message } + var description: String { message } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Small string helpers + +extension String { + fileprivate func trimmingBackticks() -> String { + var s = self + if s.hasPrefix("`") { s.removeFirst() } + if s.hasSuffix("`") { s.removeLast() } + return s + } +} + +extension Substring { + fileprivate func trimmingBackticks() -> String { + String(self).trimmingBackticks() + } +} diff --git a/Sources/GenerateConfigDocs/GenerateConfigDocs.swift b/Sources/GenerateConfigDocs/GenerateConfigDocs.swift new file mode 100644 index 000000000..b5c5b2bf9 --- /dev/null +++ b/Sources/GenerateConfigDocs/GenerateConfigDocs.swift @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 ArgumentParser +import Foundation + +/// Regenerates swift-java.config documentation in SwiftJavaConfigFile.md +/// straight from the doc comments on `SwiftJavaConfigurationShared.Configuration`. +/// +/// Usage: +/// swift run generate-config-docs regenerate the doc section in place +/// swift run generate-config-docs --check exit 1 if the doc section is stale, without writing +@main +struct GenerateConfigDocs: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "generate-config-docs", + abstract: + "Regenerate the Supported Configuration Options section of SwiftJavaConfigFile.md from Configuration.swift." + ) + + @Flag(name: .long, help: "Exit non-zero if the doc section is stale, without writing.") + var check: Bool = false + + func run() throws { + let repoRoot = try Self.detectRepoRoot() + + let configFile = + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftJavaConfigurationShared", isDirectory: true) + .appendingPathComponent("Configuration.swift", isDirectory: false) + + let docFile = + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftJavaDocumentation", isDirectory: true) + .appendingPathComponent("Documentation.docc", isDirectory: true) + .appendingPathComponent("SwiftJavaConfigFile.md", isDirectory: false) + + let scanDirs: [URL] = [ + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftJavaConfigurationShared", isDirectory: true), + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftExtractConfigurationShared", isDirectory: true), + ] + + // Parse enums and structs referenced by Configuration. + let parsed = try ConfigParser.parse(rootDirs: scanDirs) + + // Parse the Configuration struct itself for its fields, sections, and effective fallbacks. + let configSource = try String(contentsOf: configFile, encoding: .utf8) + let body = try ConfigurationBody.parse(source: configSource) + + // Render Markdown and splice into SwiftJavaConfigFile.md. + let renderer = MarkdownRenderer( + enums: parsed.enums, + structs: parsed.structs, + fields: body.fields, + effectiveFallbacks: body.effectiveFallbacks + ) + let generated = renderer.render() + + let docText = try String(contentsOf: docFile, encoding: .utf8) + let newText = try MarkerSplicer.splice(into: docText, generated: generated) + + let relDoc = docFile.path.replacingOccurrences(of: repoRoot.path + "/", with: "") + + if newText == docText { + print("\(relDoc) already up to date (\(body.fields.count) options documented)") + return + } + + if check { + FileHandle.standardError.write( + Data( + """ + error: \(relDoc) is stale. + Run 'swift run generate-config-docs' and commit the result. + + """.utf8 + ) + ) + throw ExitCode(1) + } + + try newText.write(to: docFile, atomically: true, encoding: .utf8) + print("Updated \(relDoc) (\(body.fields.count) options documented)") + } + + /// Walk up from the executable's file location (this source file, at build + /// time, and the invoking working directory otherwise) to find the repo root + /// (identified by `Package.swift`). + private static func detectRepoRoot() throws -> URL { + // Start from the current working directory - `swift run` always chdir's to + // the package root, which is exactly what we want. + let fm = FileManager.default + var dir = URL(fileURLWithPath: fm.currentDirectoryPath, isDirectory: true) + for _ in 0..<20 { + if fm.fileExists(atPath: dir.appendingPathComponent("Package.swift").path) { + return dir + } + let parent = dir.deletingLastPathComponent() + if parent.path == dir.path { break } + dir = parent + } + throw ConfigDocsError( + "Could not locate Package.swift from current directory \(fm.currentDirectoryPath)" + ) + } +} diff --git a/Sources/GenerateConfigDocs/MarkdownRenderer.swift b/Sources/GenerateConfigDocs/MarkdownRenderer.swift new file mode 100644 index 000000000..8aa93d35c --- /dev/null +++ b/Sources/GenerateConfigDocs/MarkdownRenderer.swift @@ -0,0 +1,198 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 Foundation + +/// Renders the parsed configuration model to the same Markdown shape the +/// previous Python generator produced, so the two can be diffed byte-for-byte +/// during the port and the DocC output stays unchanged. +struct MarkdownRenderer { + var enums: [String: EnumInfo] + var structs: [String: StructInfo] + var fields: [ConfigField] + var effectiveFallbacks: [String: String] + + func render() -> String { + var seen: [String: [ConfigField]] = [:] + var sectionOrder: [String] = [] + for field in fields { + if seen[field.section] == nil { sectionOrder.append(field.section) } + seen[field.section, default: []].append(field) + } + + var out: [String] = [] + for section in sectionOrder { + out.append("### \(section)") + out.append("") + for field in seen[section] ?? [] { + out.append(renderField(field)) + out.append("") + } + } + // Trim trailing whitespace-only lines, keep a single trailing newline. + var joined = out.joined(separator: "\n") + while joined.hasSuffix("\n") || joined.hasSuffix(" ") { + joined.removeLast() + } + return joined + "\n" + } + + private func renderField(_ field: ConfigField) -> String { + let typeClean = field.type.trimmingCharacters(in: .whitespaces).trimmingSuffix("?") + let isBareEnum = isBareIdentifier(typeClean) && enums[typeClean] != nil + let enumInfo = isBareEnum ? enums[typeClean] : nil + + var lines: [String] = [] + lines.append("#### \(field.name)") + lines.append("") + + lines.append("- **Type:** `\(field.type)`") + + var defaultDisplay = literalDisplay(field.defaultLiteral) + if defaultDisplay == nil, let fallback = effectiveFallbacks[field.name] { + if fallback == ".default", let ei = enumInfo, let defaultCase = ei.defaultCase { + defaultDisplay = "`\(resolveDefaultDisplay(ei, defaultCase))`" + } else if fallback.hasPrefix(".") { + defaultDisplay = "`\(fallback.dropFirst())`" + } else { + defaultDisplay = "`\(fallback)`" + } + } + if let display = defaultDisplay { + lines.append("- **Default:** \(display)") + } + + lines.append("") + + if !field.docLines.isEmpty { + lines.append(contentsOf: field.docLines) + } else if let ei = enumInfo, !ei.firstParagraph.isEmpty { + lines.append(ei.firstParagraph) + } else { + lines.append( + "_Undocumented - please add a doc comment on this property in `Configuration.swift`._" + ) + } + + if let ei = enumInfo, !ei.hasAssociatedValues { + let docs = ei.caseDocs + if !docs.isEmpty { + lines.append("") + lines.append("**Values:**") + lines.append("") + for (display, doc) in docs { + if doc.isEmpty { + lines.append("- `\(display)`") + } else { + lines.append("- `\(display)` - \(doc)") + } + } + } + } + + if let elementType = resolveContainerElementType(field.type), elementType != typeClean { + if structs[elementType] != nil + || (enums[elementType]?.hasAssociatedValues == true) + { + lines.append("") + lines.append(contentsOf: renderTypeSchemaLines(elementType)) + } + } + + lines.append("") + lines.append("---") + return lines.joined(separator: "\n") + } + + private func renderTypeSchemaLines(_ name: String) -> [String] { + var lines: [String] = ["**`\(name)`:**", ""] + if let s = structs[name] { + if !s.docLines.isEmpty { + lines.append(contentsOf: s.docLines) + lines.append("") + } + for prop in s.properties { + let doc = prop.docLines.joined(separator: " ").trimmingCharacters(in: .whitespaces) + if doc.isEmpty { + lines.append("- `\(prop.name)`: `\(prop.type)`") + } else { + lines.append("- `\(prop.name)`: `\(prop.type)` - \(doc)") + } + } + } else if let e = enums[name] { + if !e.docLines.isEmpty { + lines.append(contentsOf: e.docLines) + lines.append("") + } + if e.hasAssociatedValues { + var caseLines: [String] = [] + for c in e.cases { + let doc = c.docLines.joined(separator: " ").trimmingCharacters(in: .whitespaces) + if doc.isEmpty { continue } + let label: String + if let sig = c.associatedSignature { + label = "\(c.name)(\(sig))" + } else { + label = c.name + } + caseLines.append("- `\(label)` - \(doc)") + } + if caseLines.isEmpty && e.docLines.isEmpty { + for c in e.cases { + let label: String + if let sig = c.associatedSignature { + label = "\(c.name)(\(sig))" + } else { + label = c.name + } + caseLines.append("- `\(label)`") + } + } + lines.append(contentsOf: caseLines) + } else { + for (display, doc) in e.caseDocs { + if doc.isEmpty { + lines.append("- `\(display)`") + } else { + lines.append("- `\(display)` - \(doc)") + } + } + } + } + return lines + } + + private func resolveDefaultDisplay(_ enumInfo: EnumInfo, _ caseName: String) -> String { + for c in enumInfo.cases where c.name == caseName { + return c.display ?? caseName + } + return caseName + } + + private func literalDisplay(_ literal: String?) -> String? { + guard let raw = literal else { return nil } + if raw == "nil" { return nil } + if raw == "[:]" { return "empty dictionary (`[:]`)" } + if raw == "[]" { return "empty array (`[]`)" } + return "`\(raw)`" + } +} + +extension String { + fileprivate func trimmingSuffix(_ s: Character) -> String { + var r = self + while r.hasSuffix(String(s)) { r.removeLast() } + return r.trimmingCharacters(in: .whitespaces) + } +} diff --git a/Sources/GenerateConfigDocs/MarkerSplicer.swift b/Sources/GenerateConfigDocs/MarkerSplicer.swift new file mode 100644 index 000000000..cac01768a --- /dev/null +++ b/Sources/GenerateConfigDocs/MarkerSplicer.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 Foundation + +/// Splices generated content into a Markdown file between START/END markers. +struct MarkerSplicer { + static let startMarker = "" + static let endMarker = "" + + static func splice(into doc: String, generated: String) throws -> String { + guard let startRange = doc.range(of: startMarker) else { + throw ConfigDocsError("Could not find '\(startMarker)' marker in doc file") + } + guard let endRange = doc.range(of: endMarker), + endRange.lowerBound > startRange.upperBound + else { + throw ConfigDocsError("Could not find '\(endMarker)' marker (after start) in doc file") + } + let before = doc[..`` to enable + /// file to the linker via `-Xlinker --version-script=` to enable /// precise dead-code elimination of unused Swift code in the final shared /// library. public var linkerExportListOutput: String? @@ -181,10 +210,22 @@ public struct Configuration: Codable { importedModuleStubs?.keys.contains(moduleName) ?? false } - /// Specialization entries for generic types, mapping a Java-facing name - /// to its base Swift type and concrete type arguments. + /// Force specialization of generic types, mapping them to a specific generated Java-facing name. + /// This allows generating generic specializations that can be used only with some specific bound generic argument, + /// rather than using the usual generic machinery. Sometimes useful if a generic type is only reasonably usable with some specific type. + /// + /// Generating specializations takes into account Swift extensions where the generic is bound to that type, for example, a `Box` + /// type, would automatically gain `T == Fish` specific methods in the generated Java sources if there is an `extension ... where T == Fish` declared in Swift: + /// + /// ```swift + /// struct Box {} + /// extension Box where T == Fish { + /// func feedFish() + /// } + /// ``` + /// + /// When configured as follows: /// - /// Example: /// ```json /// { /// "specialize": { @@ -199,10 +240,60 @@ public struct Configuration: Codable { /// } /// } /// ``` + /// + /// Would result in Java code with the generated `feedFish()` method on the `FishBox` Java type: + /// + /// ```java + /// FishBox box = ...; + /// box.feedFish(); // type-safe generated specialized function + /// ``` + /// + /// You can also possible to cause such specialization to occurr by declaring a typealias in Swift sources: + /// + /// ```swift + /// typealias FishBox = Box + /// ``` + /// + /// So this configuration option is geared towards times when you do not control the sources that wrappers are being generated for. public var specialize: [String: SpecializationConfigEntry]? /// If set, use this JSON file as the static build configuration for jextract. /// This allows users to provide a custom StaticBuildConfiguration for #if resolution. + /// + /// You can generate one for a specific target triple using the Swift compiler itself: + /// + /// ``` + /// swift frontend -print-static-build-config -target > static-build-config.json + /// ``` + /// + /// Example: + /// + /// The configuration option is a path with a file generated like above, which will have a structure similar to this: + /// + /// ```json + /// { + /// "attributes": [], + /// "compilerVersion": { + /// "components": [6, 3] + /// }, + /// "customConditions": [ + /// "DEBUG" + /// ], + /// "endianness": "little", + /// "features": [], + /// "languageMode": { + /// "components": [5, 10] + /// }, + /// "targetArchitectures": [], + /// "targetAtomicBitWidths": [], + /// "targetEnvironments": [], + /// "targetOSs": [], + /// "targetObjectFileFormats": [], + /// "targetPointerAuthenticationSchemes": [], + /// "targetPointerBitWidth": 64, + /// "targetRuntimes": [] + /// } + /// ``` public var staticBuildConfigurationFile: String? // ==== wrap-java --------------------------------------------------------- @@ -215,14 +306,44 @@ public struct Configuration: Codable { } /// The Java classes that should be translated to Swift. The keys are - /// canonical Java class names (e.g., java.util.Vector) and the values are - /// the corresponding Swift names (e.g., JavaVector). + /// canonical Java class names (e.g., java.util.ArrayList) and the values are + /// the corresponding Swift names (e.g., JavaArrayList). + /// + /// Example: + /// ```json + /// { + /// "classes": { + /// "java.util.ArrayList": "JavaArrayList", + /// "java.util.HashMap": "JavaHashMap" + /// } + /// } + /// ``` public var classes: [String: String]? = [:] - // Compile for the specified Java SE release. + /// Compile for the specified Java SE release. + /// + /// `JavaVersion` is an integer identifying a Java SE release, in the same + /// shape as `javaSourceLevel`. Supported values: + /// + /// - `17` + /// - `18` + /// - `21` + /// - `22` + /// - `24` + /// - `25` public var sourceCompatibility: JavaVersion? - // Generate class files suitable for the specified Java SE release. + /// Generate class files suitable for the specified Java SE release. + /// + /// `JavaVersion` is an integer identifying a Java SE release, in the same + /// shape as `javaSourceLevel`. Supported values: + /// + /// - `17` + /// - `18` + /// - `21` + /// - `22` + /// - `24` + /// - `25` public var targetCompatibility: JavaVersion? /// Filter input Java types by their package prefix if set @@ -231,11 +352,12 @@ public struct Configuration: Codable { /// Exclude input Java types by their package prefix or exact match public var javaFilterExclude: [String]? + /// If set, place all generated code in this single Swift file instead of one file per class. public var singleSwiftFileOutput: String? // ==== dependencies --------------------------------------------------------- - // Java dependencies we need to fetch for this target. + /// Java dependencies we need to fetch for this target. public var dependencies: [JavaDependencyDescriptor]? /// Custom Maven repositories to use when resolving dependencies. @@ -248,6 +370,18 @@ public struct Configuration: Codable { } /// Represents a maven-style Java dependency. +/// +/// Encoded in JSON as a single `groupID:artifactID:version` coordinate string +/// (Gradle-style notation), not as a keyed object. +/// +/// Example: +/// ```json +/// { +/// "dependencies": [ +/// "com.google.code.gson:gson:2.10.1" +/// ] +/// } +/// ``` public struct JavaDependencyDescriptor: Hashable, Codable { public var groupID: String public var artifactID: String @@ -302,10 +436,22 @@ public struct JavaDependencyDescriptor: Hashable, Codable { /// Describes a Maven-style repository for dependency resolution. /// /// Supported types based on https://docs.gradle.org/current/userguide/supported_repository_types.html: -/// - `maven(url:artifactUrls:)` — A custom Maven repository at the given URL -/// - `mavenCentral` — Maven Central repository -/// - `mavenLocal(includeGroups:)` — Local Maven cache (~/.m2/repository) -/// - `google` — Google's Maven repository +/// - `maven(url:artifactUrls:)` - A custom Maven repository at the given URL +/// - `mavenCentral` - Maven Central repository +/// - `mavenLocal(includeGroups:)` - Local Maven cache (~/.m2/repository) +/// - `google` - Google's Maven repository +/// +/// Example: +/// ```json +/// { +/// "mavenRepositories": [ +/// { "type": "mavenCentral" }, +/// { "type": "maven", "url": "https://repo.example.com/maven2" }, +/// { "type": "mavenLocal", "includeGroups": ["com.example"] }, +/// { "type": "google" } +/// ] +/// } +/// ``` public enum MavenRepositoryDescriptor: Hashable, Codable { case maven(url: String, artifactUrls: [String]? = nil) case mavenCentral diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift index f7c37e669..eeb29dd48 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift @@ -22,6 +22,7 @@ public enum JavaSourceLevel: Int, Comparable, Sendable { case jdk21 = 21 case jdk22 = 22 case jdk24 = 24 + case jdk25 = 25 public static var `default`: Self { .jdk22 } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md index 38b740175..f0f080cb1 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md @@ -1,6 +1,10 @@ # Android -## R8/Proguard Rules +Hints and patterns for using swift-java on Android. + +## Overview + +### R8/Proguard Rules Since swift-java uses JNI and reflection APIs, we must tell the R8 optimizer to ignore our wrapped types, otherwise we will run into runtime crashes, because types are stripped from the APK/AAR. @@ -16,7 +20,7 @@ For example, if your library is named is `org.swift.exampleapp`, then add the fo -keep interface org.swift.exampleapp.** { *; } ``` -## Android Core Library Desugaring +### Android Core Library Desugaring If you are using [Core Library Desugaring](https://developer.android.com/studio/write/java8-support) in your Android project, you must enable the `AndroidCoreLibraryDesugaring` trait to ensure that the SwiftJava wrappers @@ -58,79 +62,3 @@ open class OldVersionedClass: JavaObject { Annotations are generated both for "since", "deprecated" and "removed" attributes. > Note: To use Android platform availability you must use at least Swift 6.3, which introduced the `Android` platform. - -## Reducing Binary Size - -When using the `jextract` tool to wrap your Swift APIs as a Java library targeting Android, several compiler and linker options can substantially reduce the final binary size by stripping dead code that would otherwise be retained. - -### Requirements - -Full binary-size optimization requires **Swift 6.3 or later**. Swift 6.3 introduced the `@used` attribute, which `JExtractSwiftPlugin` attaches to every generated JNI entry point so the compiler cannot eliminate them before the linker has a chance to see them. - -### Generated Version Script - -When using the `jextract` tool in JNI mode, `JExtractSwiftPlugin` automatically generates a linker version script alongside the Swift thunks. The version script lists every JNI entry point as a `global:` export and hides everything else with `local: *`, giving the linker precise control over which symbols must be kept and allowing it to discard all internal Swift symbols. - -The file is written to the plugin's work directory: - -``` -.build/plugins/outputs///JExtractSwiftPlugin/swift-java-jni-exports.map -``` - -### Optimization Flags - -The following flags, used together, produce the smallest possible binary: - -| Flag | Effect | -|---|---| -| `-Xswiftc -Osize` | Optimize for binary size rather than speed | -| `-Xlinker --version-script=` | Restrict exported symbols to JNI entry points; hides internal Swift symbols from the dynamic symbol table | -| `--experimental-lto-mode=full` | Full link-time optimization across all modules | -| `-Xfrontend -internalize-at-link` | Internalize Swift symbols at link time, enabling the linker to eliminate more dead code | - -### Package.swift - -Add the flags that don't depend on a dynamic path directly to your `Package.swift`, conditioned on release builds for Android: - -```swift -import PackageDescription - -let package = Package( - name: "MyLibrary", - products: [ - .library(name: "MySwiftLibrary", type: .dynamic, targets: ["MySwiftLibrary"]) - ], - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-java", from: "0.1.0"), - ], - targets: [ - .target( - name: "MySwiftLibrary", - dependencies: [ - .product(name: "SwiftJava", package: "swift-java") - ], - swiftSettings: [ - .unsafeFlags( - ["-Osize", "-Xfrontend", "-internalize-at-link"], - .when(platforms: [.android], configuration: .release) - ), - ], - plugins: [ - .plugin(name: "JExtractSwiftPlugin", package: "swift-java") - ] - ) - ] -) -``` - -Then pass the remaining flags on the command line when invoking the build: - -```bash -swift build \ - --swift-sdk aarch64-unknown-linux-android28 \ - -c release \ - --experimental-lto-mode=full \ - -Xlinker --version-script=.build/plugins/outputs/MyLibrary/MySwiftLibrary/JExtractSwiftPlugin/swift-java-jni-exports.map -``` - -> Tip: Adjust the `--version-script` path to match your package name and target name. Run `find .build/plugins/outputs -name swift-java-jni-exports.map` after the first build if you are unsure of the exact path. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md b/Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md new file mode 100644 index 000000000..163919e70 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md @@ -0,0 +1,132 @@ +# Command-line program with its main in Swift + +Build a Java command-line program whose entry point is implemented in Swift. + +## Overview + +> Note: The instructions here work, but we are still smoothing out the +> interoperability story. + +All JavaKit-based applications start execution within the Java Virtual Machine. +This example walks through the four pieces you need: a Java class that loads a +native Swift library and declares a `native` entry point, a SwiftPM dynamic +library product, the Swift implementation of that entry point, and the command +line invocation that ties them together. + +### 1. Create a Java class to wrap the Swift library + +First, define a Java class that loads your native Swift library and provides a +`native` entry point to get into the Swift code. Here is a minimal Java class +that has all of the program's logic written in Swift, including `main`: + +```java +package org.swift.javakit; + +public class HelloSwiftMain { + static { + System.loadLibrary("HelloSwift"); + } + + public native static void main(String[] args); +} +``` + +Compile this into a `.class` file with `javac` before you build the Swift half: + +```bash +javac Java/src/org/swift/javakit/HelloSwiftMain.java +``` + +### 2. Create a Swift library + +The Java class created above loads a native library `HelloSwift` that needs to +contain a definition of the `main` method in the class +`org.swift.javakit.HelloSwiftMain`. `HelloSwift` should be defined as a SwiftPM +dynamic library product: + +```swift + products: [ + .library( + name: "HelloSwift", + type: .dynamic, + targets: ["HelloSwift"] + ), + ] +``` + +with an associated target that depends on `SwiftJava`: + +```swift + .target( + name: "HelloSwift", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "SwiftJava", package: "swift-java") + ]) +``` + +### 3. Implement the native Java method in Swift + +Now, in the `HelloSwift` Swift library, define a `struct` that provides the +`main` method for the Java class you already defined: + +```swift +import SwiftJava + +@JavaImplementation("org.swift.javakit.HelloSwiftMain") +struct HelloSwiftMain { + @JavaStaticMethod + static func main(arguments: [String], environment: JNIEnvironment? = nil) { + print("Command line arguments are: \(arguments)") + } +} +``` + +Build this library with `swift build`, then find the directory containing the +resulting shared library (`HelloSwift.dylib`, `HelloSwift.so`, or +`HelloSwift.dll`, depending on platform). It is usually in `.build/debug/`. + +### 4. Putting it all together + +Finally, run the program on the command line: + +```bash +java -cp Java/src \ + -Djava.library.path=$(PATH_CONTAINING_HELLO_SWIFT)/ \ + org.swift.javakit.HelloSwiftMain -v argument +``` + +This prints the command-line arguments `-v` and `argument` as seen by Swift. + +### Bonus: Swift argument parser + +The easiest way to build a command-line program in Swift is with the +[Swift argument parser library](https://github.com/apple/swift-argument-parser). +You can extend the `HelloSwiftMain` type to conform to `ParsableCommand` and use +the Swift argument parser to process the arguments provided by Java: + +```swift +import ArgumentParser +import SwiftJava + +@JavaClass("org.swift.javakit.HelloSwiftMain") +struct HelloSwiftMain: ParsableCommand { + @Option(name: .shortAndLong, help: "Enable verbose output") + var verbose: Bool = false + + @JavaImplementation + static func main(arguments: [String], environment: JNIEnvironment? = nil) { + let command = Self.parseOrExit(arguments) + command.run(environment: environment) + } + + func run(environment: JNIEnvironment? = nil) { + print("Verbose = \(verbose)") + } +} +``` + +## See Also + +- +- diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md new file mode 100644 index 000000000..e008bce43 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md @@ -0,0 +1,336 @@ +# Features: JavaKit macros + +Detailed feature documentation for calling Java from Swift using JavaKit macros +(`@JavaClass`, `@JavaMethod`, `@JavaField`, `@JavaImplementation`, ...). + +## Overview + +JavaKit macros let you *hand-write* Swift declarations that mirror Java classes, +methods, and fields. This is the direct, one-off approach: pick a Java API, +declare a matching Swift shape, and the macros produce all the JNI plumbing. + +Use this approach when you want fine control over what surfaces on the Swift +side, or when you're implementing Java `native` methods in Swift. If you +instead want *automatic* bulk wrapping of an entire classpath or JAR, reach +for the source generator described in . + +For an orientation on which interop tool fits your task, see . + +> tip: The Java -> Swift direction is covered in the WWDC2025 session +> 'Explore Swift and Java interoperability' around the +> [7-minute mark](https://youtu.be/QSHO-GUGidA?si=vUXxphTeO-CHVZ3L&t=448), +> and the Swift -> Java direction around the +> [10-minute mark](https://youtu.be/QSHO-GUGidA?si=QyYP5-p2FL_BH7aD&t=616). + +### Wrapping Java classes: @JavaClass + +Declare a Swift class annotated with `@JavaClass("fully.qualified.JavaName")` +that inherits from `JavaObject`. Fields become `@JavaField` properties, +methods become `@JavaMethod` declarations. The macro writes the JNI binding +code so the Swift declaration acts as a first-class Swift type backed by +the underlying Java instance. + +```swift +@JavaClass("com.example.swift.HelloSwift") +open class HelloSwift: JavaObject { + @JavaField public var value: Double + @JavaField public var name: String + + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) + + @JavaMethod public func greet(_ name: String) + @JavaMethod public func sayHelloBack(_ i: Int32) -> Double +} +``` + +Once declared, the wrapper is used like any other Swift class. The tabs +below show usage from the sample and the underlying Java class it wraps. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitClassSwift.swift", slice: "classDefinition") + } + @Tab("Java") { + @Snippet(path: "Snippets/JavaKitClassJava", slice: "helloClass") + } +} + +### Java instance methods: @JavaMethod + +Declare a Swift method with `@JavaMethod` on a `@JavaClass`-annotated type. +The Swift signature drives the JNI dispatch: parameters and return type are +mapped between Swift and Java, throwing methods are surfaced with `throws`, +and calling the Swift method invokes the underlying Java method. + +Method names in Swift match the Java method verbatim by default. Use +`@JavaMethod("javaName")` to bind to a differently-named Java method. + +### Java static methods: @JavaStaticMethod + +Static methods live in an `extension` on the class's `JavaClass` metatype. +This keeps instance dispatch and static dispatch cleanly separated. + +```swift +extension JavaClass { + @JavaStaticMethod + public func valueOf(_ i: Int32) -> MyClass? +} +``` + +Static calls then go through the metatype: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitReflectionSwift.swift", slice: "probablyPrime") + } +} + +### Java fields: @JavaField and @JavaStaticField + +Java fields become Swift `var` properties on the class (instance) or on the +`JavaClass` metatype (static). Reads and writes are dispatched through +JNI just like methods. + +```swift +extension JavaClass { + @JavaStaticField public var initialValue: Double +} +``` + +Reading a static field then looks like this: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitClassSwift.swift", slice: "staticFieldAccess") + } + @Tab("Java") { + @Snippet(path: "Snippets/JavaKitClassJava", slice: "helloClass") + } +} + +### @JavaImplementation: implementing a Java native method in Swift + +Java classes can declare `native` methods whose implementation is provided by +another language. `@JavaImplementation("fully.qualified.JavaName")` on a Swift +extension provides those implementations. The macro generates the JNI export +symbols the JVM expects. + +The example below is the Swift-side implementation of the +`native String throwMessageFromSwift(String) throws` method declared in +`HelloSwift.java`. Throwing a Swift error from the implementation surfaces as a +Java exception to the caller: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitImplementationSwift.swift", slice: "implementation") + } + @Tab("Java") { + @Snippet(path: "Snippets/JavaKitImplementationJava", slice: "helloClass") + } +} + +The same extension also implements `native int sayHello(int, int)`; every +`native` method declared on the Java class needs a matching Swift method here. + +### Java constructors + +Java constructors are exposed as Swift initializers taking an +`environment: JNIEnvironment? = nil` parameter. When omitted, the current +thread's JNI environment is used. The macro handles the JNI class lookup +and `NewObject` call. + +### Throwing methods + +A Swift method declared `@JavaMethod ... throws` corresponds to a Java method +whose signature includes `throws Exception`. + +When the Java side throws, the exception is caught by the generated bridge and re-thrown as a Swift error. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitThrowsSwift.swift", slice: "throwingMethods") + } +} + +TODO: way more docs about how we map errors + +### Type casting: .as(T.self) + +`JavaObject` provides `as(_:)` for a runtime-checked downcast and `is(_:)` +for a runtime type check. Both consult the underlying Java class hierarchy, +so they work correctly across the `@JavaClass(..., extends:)` chain. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitCastSwift.swift", slice: "castPattern") + } +} + +### Type checking: .is(T.self) + +`is(_:)` performs a runtime type check without producing a new reference. It +consults the Java class hierarchy, so a base-class instance does not report +itself as an instance of a subclass: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitCastSwift.swift", slice: "isCheck") + } +} + +### Arrays + +Swift `[T]` maps to Java `T[]` for both parameters and return values. This +works out of the box for the primitive types (`Int8`/`byte`, `Int32`/`int`, +`Int64`/`long`, `Double`/`double`) and for object types like `String`. + +Once the array method is declared on the Swift wrapper, calling it looks +exactly like calling any Swift function that takes/returns `[T]`: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitArraysSwift.swift", slice: "arraysUsage") + } + @Tab("Java") { + @Snippet(path: "Snippets/JavaKitArraysJava", slice: "arrays") + } +} + +### Optionals and nullability + +Swift `Optional` maps to Java `Optional` when the Swift type is +`JavaString?` (a nullable JavaKit-wrapped object). Nullable primitives use +`OptionalLong` / `OptionalInt` / `OptionalDouble`. Passing `nil` on the Swift +side surfaces as `Optional.empty()` on the Java side. + +The wrapper's optional-typed methods and fields are used like any other +Swift optional: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitOptionalsSwift.swift", slice: "optionalsWrapper") + } + @Tab("Java") { + @Snippet(path: "Snippets/JavaKitOptionalsJava", slice: "threadSafeHelper") + } +} + +### Primitive type mapping + +| Swift | Java | +|----------|-----------| +| `Bool` | `boolean` | +| `Int8` | `byte` | +| `Int16` | `short` | +| `Int32` | `int` | +| `Int64` | `long` | +| `Float` | `float` | +| `Double` | `double` | + +Swift `String` bridges to `java.lang.String`. Where the underlying JNI type +matters (for example, storing a nullable string field), use the wrapper +`JavaString` explicitly. + +### JavaClass metatype and reflection + +`JavaClass` is the Swift representation of a Java `Class` object. Constructing +one performs the JNI class lookup. Static members are reached through it, and +custom extensions on `JavaClass` are the place to add `@JavaStaticMethod` +and `@JavaStaticField` declarations. + +### Java enum constants + +JavaKit does not yet import Java enums as Swift enums. In the meantime, +constants can be reached via `JavaClass` and `@JavaStaticField` - the same +pattern used for any static field. + +```swift +extension JavaClass { + @JavaStaticField public var HALF_UP: RoundingMode! +} +``` + +Reaching a constant from Swift then looks like this: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitEnumSwift.swift", slice: "sieveUsage") + } +} + +### Java type inheritance + +`@JavaClass("java.name", extends: Parent.self)` records the Java parent class. +The Swift type is expected to also inherit from that parent, so the Swift +class hierarchy mirrors the Java one. Downcasts made with `.as(T.self)` +respect this hierarchy. + +```swift +@JavaClass("com.example.swift.HelloSubclass", extends: HelloSwift.self) +open class HelloSubclass: HelloSwift { + @JavaMethod + @_nonoverride public convenience init(_ greeting: String, environment: JNIEnvironment? = nil) + + @JavaMethod public func greetMe() +} +``` + +Constructing a subclass from Swift mirrors the Java constructor call: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitInheritanceSwift.swift", slice: "inheritance") + } + @Tab("Java") { + @Snippet(path: "Snippets/JavaKitInheritanceJava", slice: "helloSubclass") + } +} + +### Generic type parameters + +Generic Java types like `java.util.ArrayList` are wrapped as generic Swift classes. + +Because of Java's type erasure, generic parameters used in method +signatures need a `typeErasedResult:` hint on `@JavaMethod` so the macro can +generate the right JNI signature. + +TODO: shoe example here + +### Method overloading + +Swift methods with different parameter types can bind to different Java +overloads with the same name - the macro-generated JNI signature disambiguates +which overload to invoke. See `Sources/JavaStdlib/JavaUtil/generated/ArrayList.swift` +for realistic examples (multiple `add(...)` overloads). + +### Annotating thread-safety with Swift's Sendable + +If you know a Java class is thread-safe (typically because it's annotated with +your project's own `@ThreadSafe` marker, or because its API is stateless), you +can declare its Swift wrapper `@unchecked Sendable` so it can be shared across +Swift concurrency isolation boundaries. + +```swift +@JavaClass("com.example.swift.ThreadSafeHelperClass") +open class ThreadSafeHelperClass: JavaObject, @unchecked Sendable { + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) +} +``` + +Once declared, the wrapped instance flows freely across isolation boundaries: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitSendableSwift.swift", slice: "sendableConformance") + } + @Tab("Java (class)") { + @Snippet(path: "Snippets/JavaKitSendableHelperJava", slice: "threadSafeHelper") + } + @Tab("Java (annotation)") { + @Snippet(path: "Snippets/JavaKitSendableAnnotationJava", slice: "threadSafe") + } +} + +TODO: note what annotations we automatically handle \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md new file mode 100644 index 000000000..a96112bb9 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -0,0 +1,830 @@ +# Features: jextract + +Detailed feature documentation for calling Swift from Java using jextract. + +## Overview + +The following sections describe each feature supported by jextract, +with Swift definitions alongside the generated Java API for both JNI and FFM modes. + +For guidance on choosing between JNI and FFM mode, see . + +### Initializers + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ClassesSwift.swift", slice: "initializers") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ClassesJavaJNI", slice: "classUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/ClassesJavaFFM", slice: "classUsageJava") + } +} + +Classes and structs can both have initializers imported. + +### Optional initializers / Throwing initializers + +Optional and throwing initializers are supported in JNI mode. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ThrowingInitSwift.swift", slice: "throwingInitDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ThrowingInitJavaJNI", slice: "throwingInitUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Deinitializers + +Classes and structs are automatically cleaned up when the enclosing arena +is closed (`SwiftArena` in JNI mode, `AllocatingSwiftArena` in FFM mode). +No explicit deinitialization calls are needed on the Java side. + +### Enums + +Swift enums with associated values are extracted into a corresponding Java `class`. +Each case becomes a static factory method, and associated values are accessed via +`getAsX` methods that return `Optional` records. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/EnumsSwift.swift", slice: "enumDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/EnumsJavaJNI", slice: "enumUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +#### Switching and pattern matching + +Use `getDiscriminator()` for simple switching without accessing associated values: + +@Snippet(path: "Snippets/EnumsJavaJNI", slice: "enumDiscriminatorUsageJava") + +For Java 21+, `getCase(arena)` returns a `Vehicle.Case` value that works with +[pattern matching for switch](https://openjdk.org/jeps/441), which lets you +bind the associated values of each case directly: + +@Snippet(path: "Snippets/EnumsJavaJNI", slice: "enumSwitchUsageJava") + +Each nested `Vehicle.Case.*` type is a `record`, so on Java 21+ you can also +destructure it positionally (`case Vehicle.Case.Car(var name, var trailer)`), and +on Java 16+ reach the same values with +[pattern matching for instanceof](https://openjdk.org/jeps/394). + +### RawRepresentable enums + +JExtract supports extracting enums that conform to `RawRepresentable`, +giving access to an optional initializer and the `rawValue` property. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/RawRepresentableEnumsSwift.swift", slice: "rawRepresentableEnum") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/RawRepresentableEnumsJavaJNI", slice: "rawRepresentableEnumUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Global functions + +Global Swift functions are imported as static methods on the generated library class. + +### Member functions + +Class and struct member functions are imported as instance methods on the +generated Java wrapper type. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/StructsSwift.swift", slice: "memberFunctions") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/StructsJavaJNI", slice: "structUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/StructsJavaFFM", slice: "structUsageJava") + } +} + +### Throwing functions + +Throwing Swift functions are imported as Java methods that throw exceptions. +In JNI mode the exception type is `Exception`; in FFM mode it is `SwiftJavaErrorException`. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ThrowingSwift.swift", slice: "throwingFunction") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ThrowingJavaJNI", slice: "throwUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/ThrowingJavaFFM", slice: "throwUsageJava") + } +} + +### Stored properties + +Stored `var` and `let` properties are imported as getter/setter methods. +Properties with `willSet` and `didSet` observers work transparently. + +### private(set) properties + +In JNI mode, properties declared `public private(set) var` are imported with +only a getter on the Java side; the setter is omitted so the property's +write-access restriction is preserved across the language boundary. FFM mode +does not yet apply this restriction. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ClassesSwift.swift", slice: "privateSetProperty") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ClassesJavaJNI", slice: "privateSetUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Computed properties + +Computed properties are imported the same way as stored properties: +as getter (and optionally setter) methods. Throwing computed properties are +supported in JNI mode but not yet in FFM mode. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ClassesSwift.swift", slice: "computedProperties") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ClassesJavaJNI", slice: "computedPropertiesUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Async functions + +Asynchronous functions in Swift are extracted using different modes: + +- **completable-future (default)**: `async` functions return `java.util.concurrent.CompletableFuture` +- **future**: For legacy platforms (e.g. Android 23 and below) where `CompletableFuture` is not available, `async` functions return `java.util.concurrent.Future`. Enable with `--async-func-mode future` or the `asyncFuncMode` config value. + +The Java snippet below holds the result in a `Future`, which the default +`CompletableFuture` return type satisfies. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/AsyncSwift.swift", slice: "asyncDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/AsyncJavaJNI", slice: "asyncUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Collections + +Swift's collection types cross the boundary either by copying (arrays) or by +handing Java a wrapper that points at the live Swift value (dictionaries, sets). + +#### Arrays + +Arrays of primitives (`[UInt8]`, `[Int32]`, `[Double]`, `[String]`) are +supported in both JNI and FFM modes and map to the corresponding Java array +types (`byte[]`, `int[]`, `double[]`, `String[]`). + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ArraysSwift.swift", slice: "primitiveArrays") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ArraysJavaJNI", slice: "primitiveArraysUsage") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/ArraysJavaFFM", slice: "primitiveArraysUsage") + } +} + +Arrays of user-defined jextract-imported types (`[MySwiftClass]`) and nested +arrays (`[[UInt8]]`, `[[String]]`) are supported in JNI mode. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ArraysSwift.swift", slice: "customTypeArrays") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ArraysJavaJNI", slice: "customTypeArraysUsage") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +#### InlineArray + +Fixed-size inline arrays (Swift's `InlineArray`, sugar `[N of T]`) are +recognized by jextract in JNI mode and imported with an equivalent Java surface. +Not yet supported in FFM mode. + +#### Dictionaries + +Swift dictionaries (`[Key: Value]`) are imported using the `SwiftDictionaryMap` +Java wrapper type. This wrapper refers to the actual Swift dictionary on the Swift heap +and does not copy it. Use `SwiftDictionaryMap::toJava` to explicitly copy into a Java `Map`. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/DictionariesSwift.swift", slice: "dictionaryDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/DictionariesJavaJNI", slice: "dictionaryUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +#### Sets + +Swift sets (`Set`) are imported using the `SwiftSet` Java wrapper. +Like dictionaries, the wrapper points at the Swift value on the Swift heap and does not +copy elements until explicitly requested. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/SetsSwift.swift", slice: "setDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/SetsJavaJNI", slice: "setUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Bytes and buffers + +Raw bytes are the most common thing to hand across the language boundary, and +jextract offers several shapes for it depending on whether you want a copy or a +view of the memory. + +#### Byte arrays + +`[UInt8]` maps to Java's `byte[]` in both modes, by copying. Note that Java's +`byte` is signed, so a Swift `UInt8` of `200` reads as `-56` on the Java side; +see . + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ArraysSwift.swift", slice: "primitiveArrays") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ArraysJavaJNI", slice: "primitiveArraysUsage") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/ArraysJavaFFM", slice: "primitiveArraysUsage") + } +} + +Nested byte arrays (`[[UInt8]]`) are supported in JNI mode; see +. + +#### Raw byte buffers + +A Swift function taking `UnsafeRawBufferPointer` is callable from both modes, +with the buffer surfacing differently on the Java side. + +In JNI mode it is a plain `byte[]`. jextract obtains the array's elements for the +duration of the call (via JNI's `GetByteArrayElements`) and hands Swift a buffer +over them; whether that memory is the array itself or a copy is up to the JVM. +In FFM mode it is a `MemorySegment`, which Swift reads in place without any copy. +In both modes the buffer is only valid for the duration of the call, so the Swift +side must not store it. + +`UnsafeMutableRawBufferPointer` is supported the same way, and in JNI mode writes +made by Swift are committed back to the Java array when the call returns. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/RawBufferSwift.swift", slice: "rawBufferDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/RawBufferJavaJNI", slice: "rawBufferUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/RawBufferJavaFFM", slice: "rawBufferUsageJava") + } +} + +In FFM mode a Swift closure parameter taking a buffer also hands the segment +straight to the Java lambda: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/RawBufferSwiftFFM.swift", slice: "rawBufferDefinition") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/RawBufferJavaFFM", slice: "withBufferUsageJava") + } +} + +#### Java ByteBuffer + +In FFM mode the generated `Data` wrapper converts to and from +`java.nio.ByteBuffer`. Bringing bytes in: + +@Snippet(path: "Snippets/DataJavaFFM", slice: "byteBufferUsageJava") + +and getting them back out: + +@Snippet(path: "Snippets/DataJavaFFM", slice: "byteBufferToUsageJava") + +In JNI mode, `Data.toByteArray()` is the way to copy bytes out. For the `Data` +type itself see . + +#### MemorySegment + +`toMemorySegment(arena)` hands back the bytes as a `java.lang.foreign.MemorySegment`, +which the JVM reads directly without copying. Use `withUnsafeBytes` when you only +need to read the bytes and do not want to materialize anything at all. + +> Important: `MemorySegment` is part of the Foreign Function & Memory API +> ([JEP 454](https://openjdk.org/jeps/454)) and requires JDK 25+. It is available +> in FFM mode only; there is no `MemorySegment` equivalent in JNI mode. + +@Snippet(path: "Snippets/DataJavaFFM", slice: "memorySegmentUsageJava") + +### Generic types + +Support for generic types is work-in-progress and limited. +Members containing type parameters (such as `T`) are not exported. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/GenericsSwift.swift", slice: "genericTypeDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/GenericsJavaJNI", slice: "genericTypeUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Generic type specialization + +Conditional/constrained extensions on types (e.g. `extension Box where Element == Fish`) +cannot be safely exposed on the generic Java wrapper. Instead, jextract detects typealiases +like `typealias FishBox = Box` and performs _specialization_ - exposing a dedicated +`FishBox` Java class with all matching extensions applied. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/SpecializationSwift.swift", slice: "boxSpecialization") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/SpecializationJavaJNI", slice: "specializationUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +`FishBox` carries the constrained extension's `describeFish()` in addition to +`Box`'s own members, and unlike the generic `Box` it has no Java type parameter. + +### Tuples + +Tuples are imported as `Tuple2`, `Tuple3`, etc. types with positional `$0`, `$1` accessors. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/TuplesSwift.swift", slice: "tupleDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/TuplesJavaJNI", slice: "tupleUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/TuplesJavaFFM", slice: "tupleUsageJava") + } +} + +In JNI mode tuple elements may also be `String` or wrapper types, and labeled +tuples get named accessors in addition to the positional ones. FFM mode +currently supports tuples of primitive elements only. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/TuplesSwift.swift", slice: "stringTupleDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/TuplesJavaJNI", slice: "stringTupleUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Protocols + +Swift `protocol` types are imported as Java `interface`s. Concrete types wrapping +a Swift instance can be passed to protocol-typed parameters. + +> Note: `any DataProtocol` is handled as `Foundation.Data` in FFM mode; see below. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ProtocolsSwift.swift", slice: "protocolDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ProtocolsJavaJNI", slice: "protocolUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +Protocol parameters using `any`, `some`, or generics are all imported as Java generics: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ProtocolsSwift.swift", slice: "protocolUsage") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ProtocolsJavaJNI", slice: "takeProtocolUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Existential and opaque parameters + +In JNI mode, existential (`any SomeProtocol`, `any (A & B)`) and opaque +(`some Builder`) parameters are all imported as Java generics with appropriate +bounds. Not yet supported in FFM mode. + +For example: +```swift +func f(x: S, y: any C, z: some D) +``` +becomes: +```java + void f(S x, T1 y, T2 z) +``` +Only Swift-backed instances may be passed; this enables passing concrete jextract-generated +types that conform to a given Swift protocol. + +### Returning protocol types + +Functions that return an existential (`any SomeProtocol`) or opaque (`some SomeProtocol`) +value of a single protocol are supported. The returned value is wrapped in a generated +*existential box*: a Java class named `Box` that implements the protocol's +Java `interface`. The box carries the concrete value together with its type metadata, +and dispatches each protocol requirement through a dedicated native thunk that reconstructs +the existential from that value. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ReturnProtocolSwift.swift", slice: "returnProtocolFunctions") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ReturnProtocolJavaJNI", slice: "returnProtocolUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +Using the returned value works just like using any other imported interface: its +requirements are callable through the box, it can be passed back into functions that +accept the protocol (including generic and opaque parameters), and refined protocols +expose both their own and their inherited requirements. + +> Note: Static requirements (`static func`, `init`) and returning a *composite* existential +> (`any (A & B)`) are not currently supported. + +### Foundation types + +A handful of Foundation value types are recognized by name and bridged to a +matching Java representation rather than being treated as opaque Swift values. + +#### Data + +Swift methods accepting or returning `Foundation.Data` are extracted using the +Java `Data` wrapper type. + +In **FFM mode**, the generated wrapper offers zero-copy access via `withUnsafeBytes`, +as well as `toByteArray`, `toByteBuffer`, and `toMemorySegment(arena)` for copying +to JVM-managed memory. + +In **JNI mode**, use `Data.toByteArray()` to copy the underlying native data into +a Java byte array. A true zero-copy `withUnsafeBytes` is not available in JNI mode. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/DataSwift.swift", slice: "dataDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/DataJavaJNI", slice: "dataUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/DataJavaFFM", slice: "dataUsageJava") + } +} + +In FFM mode the returned bytes can also be read without copying them into the +JVM at all: + +@Snippet(path: "Snippets/DataJavaFFM", slice: "withUnsafeBytesUsageJava") + +#### Date + +In JNI mode, `Foundation.Date` is imported as a generated `Date` wrapper class. +It converts to and from `java.time.Instant` via `toInstant()` and +`Date.fromInstant(instant, arena)`, and exposes `getTimeIntervalSince1970()`. +Not yet supported in FFM mode. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/FoundationTypesSwift.swift", slice: "dateDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/FoundationTypesJavaJNI", slice: "dateUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +#### UUID + +In JNI mode, `Foundation.UUID` maps directly to `java.util.UUID`, so UUIDs can be +passed in either direction without a wrapper type. Not yet supported in FFM mode. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/FoundationTypesSwift.swift", slice: "uuidDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/FoundationTypesJavaJNI", slice: "uuidUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +#### URL + +Both `Foundation.URL` and `FoundationEssentials.URL` are recognized and imported. +On the Java side they surface as a generated `URL` wrapper class backed by the +Swift value, so URLs can flow across the boundary without manual string +conversion. Because Swift's `URL(string:)` is failable, constructing one from +Java returns an `Optional`. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/FoundationTypesSwift.swift", slice: "foundationURLDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/FoundationTypesJavaJNI", slice: "foundationURLUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Optional parameters and return types + +Optional primitives use Java's `OptionalLong`, `OptionalInt`, etc. +Optional objects use `java.util.Optional`. Optional parameters work in both modes: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/OptionalsSwift.swift", slice: "optionalParameterDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/OptionalsJavaJNI", slice: "optionalParameterUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/OptionalsJavaFFM", slice: "optionalParameterUsageJava") + } +} + +Optional *return* types are supported in JNI mode only: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/OptionalsSwift.swift", slice: "optionalDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/OptionalsJavaJNI", slice: "optionalUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Primitive and unsigned types + +Java does not support unsigned numbers (other than the 16-bit wide `char`), so +Swift's unsigned integer types are mapped as their bit-width equivalents. This is +potentially dangerous - for example `200` stored in a `UInt8` would be interpreted +as a `byte` of value `-56` in Java. + +| Swift type | Java type | +|------------|----------------| +| `Int8` | `byte` | +| `UInt8` | `byte` (lossy) | +| `Int16` | `short` | +| `UInt16` | `char` | +| `Int32` | `int` | +| `UInt32` | `int` (lossy) | +| `Int64` | `long` | +| `UInt64` | `long` (lossy) | +| `Float` | `float` | +| `Double` | `double` | + +#### The @Unsigned annotation + +Because the Java type alone cannot express that a value was unsigned in Swift, +jextract marks every such value with the `@Unsigned` annotation from +`org.swift.swiftkit.core.annotations`. The annotation does not enforce anything +at runtime; it records that a negative Java value should be read as a large +positive one, and gives static analysis tools something to check against. It is +retained at runtime, so it is also visible through reflection. + +The annotation is placed on parameters directly, and on the method itself when +it is the *return* type that is unsigned: + +```swift +// Swift +public func takeUnsignedLong(arg: UInt64) +public func returnUnsignedLong() -> UInt64 +public func unsignedLong(first: UInt64, second: UInt32) -> UInt32 +``` + +generates: + +```java +// Java (generated) +public static void takeUnsignedLong(@Unsigned long arg) { ... } + +@Unsigned +public static long returnUnsignedLong() { ... } + +@Unsigned +public static int unsignedLong(@Unsigned long first, @Unsigned int second) { ... } +``` + +Collections of unsigned elements are annotated as a whole, rather than per +element: + +```swift +// Swift +public func acceptArray(array: [UInt8]) +public func returnArray() -> [UInt8] +``` + +```java +// Java (generated) +public static void acceptArray(@Unsigned byte[] array) { ... } + +@Unsigned +public static byte[] returnArray() { ... } +``` + +The same applies to `Set` and `Dictionary`, where an unsigned key *or* value is +enough for the annotation to appear, and it is applied recursively so that +`[[UInt8]]` is annotated too. + +> Note: `UInt16` is annotated as well, even though `char` is itself an unsigned +> 16-bit type and the conversion is lossless. The annotation records the Swift +> type faithfully in every case. + +Swift's word-sized `Int` and `UInt` are imported as `long`, which is wide enough +for a 64-bit Swift `Int`. Where the Swift side may be 32-bit the generated Java +range-checks arguments and return values, throwing +`SwiftIntegerOverflowException` for values that would not fit. + +### Strings + +Strings are passed by copying data across the language boundary. A Swift `String` +becomes a Java `String`, both as a parameter and as a return type, in both modes. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/StringsSwift.swift", slice: "stringFunction") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/StringsJavaJNI", slice: "stringUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/StringsJavaFFM", slice: "stringUsageJava") + } +} + +### Subscripts + +Swift subscripts are imported as `getSubscript`/`setSubscript` methods. +Subscripts taking parameters keep them on the generated Java methods. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/StructsSwift.swift", slice: "subscriptDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/SubscriptsJavaJNI", slice: "subscriptUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/StructsJavaFFM", slice: "subscriptUsageJava") + } +} + +### Closures + +Swift closure parameters become Java functional interfaces that can be +implemented with a lambda expression. + +#### Non-escaping closures + +Non-escaping closures with `Void` return or primitive arguments/results are supported +in both modes. + +> Note: In FFM mode, closures that return a value (such as `() -> Bool`) work, but +> `Void`-returning upcalls are not yet implemented. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ClosuresSwift.swift", slice: "closureDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ClosuresJavaJNI", slice: "closureUsageJava") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/ClosuresJavaFFM", slice: "closureUsageJava") + } +} + +#### Escaping closures + +`@escaping` closures with `Void` return or primitive arguments/results are supported. +The closure is stored on the Swift side and can be triggered multiple times. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/EscapingClosuresSwift.swift", slice: "escapingClosureDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/EscapingClosuresJavaJNI", slice: "escapingClosureUsageJava") + } + @Tab("Java (FFM): not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Type extensions + +Swift type extensions (e.g. `extension String { ... }`) are supported in both modes. +Extended methods appear on the generated Java wrapper type. + +### Nested types + +Nested types (e.g. `struct Hello { struct World {} }`) are supported in JNI mode. +Not yet supported in FFM mode. + +### ARC and lifetime safety + +Class instances are reference-counted using Swift's Automatic Reference Counting +(ARC). The Java arena +(`SwiftArena` in JNI, `AllocatingSwiftArena` in FFM) manages lifetimes - when +the arena is closed, all instances allocated within it are released. + +### Sendable and thread safety + +Swift types conforming to `Sendable` are surfaced on the Java side with the +`@ThreadSafe` annotation on the generated wrapper class, communicating to Java +callers that the wrapped Swift value is safe to share across threads. This +translation is applied by both JNI and FFM modes. + +> Note: `@Sendable` as a closure-parameter attribute is not yet supported; the +> environment captured inside the closure would need special handling. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md new file mode 100644 index 000000000..451c525e8 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md @@ -0,0 +1,45 @@ +# Features Overview + +Orientation guide for choosing the right swift-java tool for your interop task. + +## Overview + +swift-java has two directions of interop, each with a preferred entry point. +Start here to figure out which tool matches your task, then follow the links +to the per-tool documentation. + +### Which mode should I use? + +#### Calling Swift from Java + +When you need to call Swift code from Java, you will be using **jextract** and have a choice between its `jni` or `ffm` modes: + +- JNI mode is the broader-compatibility choice: it runs on any JDK, works on + Android, and supports a wider set of language features. +- FFM mode is the high-performance choice: it has a limited set of features, + and requires JDK 25+ because it relies on [JEP 454: Foreign Function & Memory](https://openjdk.org/jeps/454) APIs. + In some situations it is able to achieve less data copying between the language barriers, so consider it when + shipping large amounts of data between runtimes. + +Generally, it is fine to start with the JNI mode, unless you have specific needs which can only be met by the FFM mode. +Switching modes is simple, and you can do it by passing `--ffm/jni` options to the command line tool, or configuring the `"mode": "jni"|"ffm"` in `swift-java.config` +when using the . + +#### Calling Java from Swift + +It is possible to directly call into Java types as long as you create (or obtain) an in-process reference to a JVM in your Swift program. + +> Tip: This also works on Android. TODO: EXAMPLE + +**JavaKit macros vs wrap-java.** + +SwiftJava offers a collection of **Swift macros** that allow calling Java types from Swift directly. You can learn about the full set of macros and their capabilities in: + +- Manually writing `@JavaClass` and similar types: if you only need to access one or two entry points in a Java library, you may write those manually (see ). +- Use `swift-java wrap-java` source generation: to automatically generate Swift wrapper types for a whole Java API surface. Refer to to learn more about this. + +### Talks and videos + +If you'd like to watch some talks or introduction videos about this project, you can refer to the following materials: + +- [Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA) session from WWDC25. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md b/Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md new file mode 100644 index 000000000..7dda7eb76 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md @@ -0,0 +1,82 @@ +# Reducing Binary Size + +When using the `jextract` tool to wrap your Swift APIs as a Java library, several compiler and linker options can substantially reduce the final binary size by stripping dead code that would otherwise be retained. + +## Overview + +The compiler and linker options below trim the shipped library by stripping +Swift symbols that JNI never calls, then dead-stripping the resulting +unreachable code. Applied together, they can significantly reduce the final +`.so`/`.dylib` size compared to a default release build. + +### Requirements + +Full binary-size optimization requires **Swift 6.3 or later**. Swift 6.3 introduced the `@used` attribute, which `JExtractSwiftPlugin` attaches to every generated JNI entry point so the compiler cannot eliminate them before the linker has a chance to see them. + +### Generated Version Script + +When using the `jextract` tool in JNI mode, `JExtractSwiftPlugin` automatically generates a linker version script alongside the Swift thunks. The version script lists every JNI entry point as a `global:` export and hides everything else with `local: *`, giving the linker precise control over which symbols must be kept and allowing it to discard all internal Swift symbols. + +The file is written to the plugin's work directory: + +``` +.build/plugins/outputs///JExtractSwiftPlugin/swift-java-jni-exports.map +``` + +### Optimization Flags + +The following flags, used together, produce the smallest possible binary: + +| Flag | Effect | +|---|---| +| `-Xswiftc -Osize` | Optimize for binary size rather than speed | +| `-Xlinker --version-script=` | Restrict exported symbols to JNI entry points; hides internal Swift symbols from the dynamic symbol table | +| `--experimental-lto-mode=full` | Full link-time optimization across all modules | +| `-Xfrontend -internalize-at-link` | Internalize Swift symbols at link time, enabling the linker to eliminate more dead code | + +### Package.swift + +Add the flags that don't depend on a dynamic path directly to your `Package.swift`, conditioned on release builds for Android: + +```swift +import PackageDescription + +let package = Package( + name: "MyLibrary", + products: [ + .library(name: "MySwiftLibrary", type: .dynamic, targets: ["MySwiftLibrary"]) + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-java", from: "0.1.0"), + ], + targets: [ + .target( + name: "MySwiftLibrary", + dependencies: [ + .product(name: "SwiftJava", package: "swift-java") + ], + swiftSettings: [ + .unsafeFlags( + ["-Osize", "-Xfrontend", "-internalize-at-link"], + .when(platforms: [.android], configuration: .release) + ), + ], + plugins: [ + .plugin(name: "JExtractSwiftPlugin", package: "swift-java") + ] + ) + ] +) +``` + +Then pass the remaining flags on the command line when invoking the build: + +```bash +swift build \ + --swift-sdk aarch64-unknown-linux-android28 \ + -c release \ + --experimental-lto-mode=full \ + -Xlinker --version-script=.build/plugins/outputs/MyLibrary/MySwiftLibrary/JExtractSwiftPlugin/swift-java-jni-exports.map +``` + +> Tip: Adjust the `--version-script` path to match your package name and target name. Run `find .build/plugins/outputs -name swift-java-jni-exports.map` after the first build if you are unsure of the exact path. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md deleted file mode 100644 index f30149069..000000000 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ /dev/null @@ -1,632 +0,0 @@ -# Supported Features - -Summary of features supported by the swift-java interoperability libraries and tools. - -## Overview - -SwiftJava supports both directions of interoperability, using Swift macros and source generation -(via the `swift-java wrap-java` command). - -### Java -> Swift - -It is possible to use SwiftJava macros and the `wrap-java` command to simplify implementing -Java `native` functions. SwiftJava simplifies the type conversions - -> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' -> around the [7-minute mark](https://youtu.be/QSHO-GUGidA?si=vUXxphTeO-CHVZ3L&t=448). - -| Feature | Macro support | -|--------------------------------------------------|-------------------------| -| Java `static native` method implemented by Swift | ✅ `@JavaImplementation` | -| **This list is very work in progress** | | - -### Swift -> Java - - -> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' -> around the [10-minute mark](https://youtu.be/QSHO-GUGidA?si=QyYP5-p2FL_BH7aD&t=616). - -| Java Feature | Macro support | -|----------------------------------------|---------------| -| Java `class` | ✅ | -| Java class inheritance | ✅ | -| Java methods: `static`, member | ✅ `@JavaMethod` | -| Java `abstract class` | TODO | -| Java `enum` | ❌ | -| Java `record` (Java 16+) | ✅ `@JavaRecord` | -| Java `sealed class` / `sealed interface` (Java 17+) | 🟡 recognized, but missing special handling of `permits` list | -| **This list is very work in progress** | | - -### JExtract – calling Swift from Java - -SwiftJava's `swift-java jextract` tool automates generating Java bindings from Swift sources. - -> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' -> around the [14-minute mark](https://youtu.be/QSHO-GUGidA?si=b9YUwAWDWFGzhRXN&t=842). - - -| Swift Feature | FFM | JNI | -|--------------------------------------------------------------------------------------|----------|-----| -| Initializers: `class`, `struct` | ✅ | ✅ | -| Optional Initializers / Throwing Initializers | ❌ | ✅ | -| Deinitializers: `class`, `struct` | ✅ | ✅ | -| `enum` | ❌ | ✅ | -| `actor` | ❌ | ❌ | -| Global Swift `func` | ✅ | ✅ | -| Class/struct member `func` | ✅ | ✅ | -| Throwing functions: `func x() throws` | ❌ | ✅ | -| Typed throws: `func x() throws(E)` | ❌ | ❌ | -| Stored properties: `var`, `let` (with `willSet`, `didSet`) | ✅ | ✅ | -| Computed properties: `var` (incl. `throws`) | ✅ / TODO | ✅ | -| Async functions `func async` and properties: `var { get async {} }` | ❌ | ✅ | -| Arrays: `[UInt8]` | ✅ | ✅ | -| Arrays: `[MyType]`, `Array` etc | ❌ | ✅ | -| Dictionaries: `[String: Int]`, `[K:V]` | ❌ | ✅ | -| Generic type: `struct S` | ❌ | ✅ | -| Functions or properties using generic type param: `struct S { func f(_: T) {} }` | ❌ | ❌ | -| Generic parameters over `some DataProtocol` handled with efficient Java type | ✅ | ✅ | -| Generic type specialization and conditional extensions: `struct S{} extension S where T == Value {}` | ❌ | ✅ | -| Static functions or properties in generic type | ❌ | ❌ | -| Generic parameters in functions: `func f(x: T)` | ❌ | ✅ | -| Generic return values in functions: `func f() -> T` | ❌ | ❌ | -| Tuples: `(Int, String)`, `(A, B, C)` | ✅ | ✅ | -| Protocols: `protocol` | ❌ | ✅ | -| Protocols: `protocol` with associated types | ❌ | ❌ | -| Protocols static requirements: `static func`, `init(rawValue:)` | ❌ | ❌ | -| Existential parameters `f(x: any SomeProtocol)` (excepts `Any`) | ❌ | ✅ | -| Existential parameters `f(x: any (A & B)) ` | ❌ | ✅ | -| Existential return types of a single protocol: `f() -> any SomeProtocol` | ❌ | ✅ | -| Existential return types of a composite: `f() -> any (A & B)` | ❌ | ❌ | -| Downcasting a returned protocol value to its concrete type: `greeter.as(EnglishGreeter.class, arena)` | ❌ | ✅ | -| Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ✅ | -| Foundation Date: `f(date: Date) -> Date` | ❌ | ✅ | -| Foundation UUID: `f(uuid: UUID) -> UUID` | ❌ | ✅ | -| Opaque parameters: `func take(worker: some Builder) -> some Builder` | ❌ | ✅ | -| Opaque return types: `func get() -> some Builder` | ❌ | ✅ | -| Optional parameters: `func f(i: Int?, class: MyClass?)` | ✅ | ✅ | -| Optional return types: `func f() -> Int?`, `func g() -> MyClass?` | ❌ | ✅ | -| Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | -| Parameters: SwiftJava wrapped types `JavaLong`, `JavaInteger` | ❌ | ✅ | -| Return values: SwiftJava wrapped types `JavaLong`, `JavaInteger` | ❌ | ❌ | -| Unsigned primitive types: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` | ✅ * | ✅ * | -| String (with copying data) | ✅ | ✅ | -| Variadic parameters: `T...` | ❌ | ❌ | -| Parametrer packs / Variadic generics | ❌ | 🟡 | -| Ownership modifiers: `inout`, `borrowing`, `consuming` | ❌ | ❌ | -| Default parameter values: `func p(name: String = "")` | ❌ | ❌ | -| Operators: `+`, `-`, user defined | ❌ | ❌ | -| Subscripts: `subscript()` | ✅ | ✅ | -| Equatable | ❌ | ❌ | -| Pointers: `UnsafeRawPointer` | 🟡 | ❌ | -| Pointers as parameters: `UnsafeRawBufferPointer` (as `byte[]`) | ❌ | ✅ | -| Nested types: `struct Hello { struct World {} }` | ❌ | ✅ | -| Inheritance: `class Caplin: Capybara` | ❌ | ❌ | -| Non-escaping `Void` closures: `func callMe(maybe: () -> ())` | ✅ | ✅ | -| Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ | -| Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ | -| `@escaping` `Void` closures: `func callMe(_: @escaping () -> ())` | ❌ | ✅ | -| `@escaping` closures with primitive arguments/results: `func callMe(_: @escaping (Int64) -> (Double))` | ❌ | ✅ | -| `@escaping` closures with `String` arguments/results: `func callMe(_: @escaping (String) -> (String))` | ❌ | ✅ | -| `@escaping` closures with user defined types: `func callMe(_: @escaping (Obj) -> (Obj))` | ❌ | ❌ | -| `@escaping` closures returning other closures: `func callMe(_: (Obj) -> (() -> ()))` | ❌ | ❌ | -| Swift type extensions: `extension String { func uppercased() }` | ✅ | ✅ | -| Swift macros (maybe) | ❌ | ❌ | -| Result builders | ❌ | ❌ | -| Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ | -| Value semantic types (e.g. struct copying) | ❌ | ❌ | -| | | | -| | | | - -> tip: The list of features may be incomplete, please file an issue if something is unclear or should be clarified in this table. - -## Detailed jextract feature support discussion - -### Unsigned integers - -### Java <-> Swift Type mapping - -Java does not support unsigned numbers (other than the 16-bit wide `char`), and therefore mapping Swift's (and C) -unsigned integer types is somewhat problematic. - -SwiftJava's jextract mode, similar to OpenJDK jextract, does extract unsigned types from native code to Java -as their bit-width equivalents. This is potentially dangerous because values larger than the `MAX_VALUE` of a given -*signed* type in Java, e.g. `200` stored in an `UInt8` in Swift, would be interpreted as a `byte` of value `-56`, -because Java's `byte` type is _signed_. - -Because in many situations the data represented by such numbers is merely passed along, and not interpreted by Java, -this may be safe to pass along. However, interpreting unsigned values incorrectly like this can lead to subtle mistakes -on the Java side. - -| Swift type | Java type | -|------------|-----------| -| `Int8` | `byte` | -| `UInt8` | `byte` ⚠️ | -| `Int16` | `short` | -| `UInt16` | `char` | -| `Int32` | `int` | -| `UInt32` | `int` ⚠️ | -| `Int64` | `long` | -| `UInt64` | `long` ⚠️ | -| `Float` | `float` | -| `Double` | `double` | - -### Passing Foundation.Data - -`Data` is a common currency type in Swift for passing a bag of bytes. Some APIs use Data instead of `[UInt8]` or other types -like Swift-NIO's `ByteBuffer`, because it is so commonly used swift-java offers specialized support for it in order to avoid copying bytes unless necessary. - -### Data in jextract FFM mode - -When using jextract in FFM mode, the generated `Data` wrapper offers an efficient way to initialize the Swift `Data` type -from a `MemorySegment` as well as the `withUnsafeBytes` function which offers direct access to Data's underlying bytes -by exposing the unsafe base pointer as a `MemorySegment`: - -```swift -Data data = MySwiftLibrary.getSomeData(arena); -data.withUnsafeBytes((bytes) -> { - var str = bytes.getString(0); - System.out.println("string = " + str); -}); -``` - -This API avoids copying the data into the Java heap in order to perform operations on it, as we are able to manipulate -it directly thanks to the exposed `MemorySegment`. - -It is also possible to use the convenience functions `toByteBuffer` and `toByteArray` to obtain a `java.nio.ByteBuffer` or `[byte]` array respectively. Thos operations incurr a copy by moving the data to the JVM's heap. - -It is also possible to get the underlying memory copied into a new `MemorySegment` by using `toMemorySegment(arena)` which performs a copy from native memory to memory managed by the passed arena. The lifetime of that memory is managed by the arena and may outlive the original `Data`. - -It is preferable to use the `withUnsafeBytes` pattern if using the bytes only during a fixed scope, because it alows us to avoid copies into the JVM heap entirely. However when a JVM byte array is necessary, the price of copying will have to be paid anyway. Consider these various options when optimizing your FFI calls and patterns for performance. - -### Data in jextract JNI mode - -Swift methods which pass or accept the Foundation `Data` type are extracted using the wrapper Java `Data` type, -which offers utility methods to efficiently copy the underlying native data into a java byte array (`[byte]`). - -Unlike the FFM mode, a true zero-copy `withUnsafeBytes` is not available. - -### Collections - -SwiftJava automatically handles collections crossing the language boundary in the most efficient way possible. - -### Swift Dictionary as `java.util.Map` - -When extracting Swift methods which accept or return a Swift dictionary (often spelled as `[Key: Value]`), jextract (in JNI mode) will convert the return type to a `SwiftDictionaryMap` Java type. - -The `SwiftDictionaryMap` wrapper type refers to the actual Swift dictionary value on the Swift heap and does not copy it out into the Java heap, unless explicitly copied using the `SwiftDictionaryMap::toJava` method. - -### Enums - -> Note: Enums are currently only supported in JNI mode. - -Swift enums are extracted into a corresponding Java `class`. To support associated values -all cases are also extracted as Java `record`s. - -Consider the following Swift enum: -```swift -public enum Vehicle { - case car(String) - case bicycle(maker: String) -} -``` -You can then instantiate a case of `Vehicle` by using one of the static methods: -```java -try (var arena = SwiftArena.ofConfined()) { - Vehicle vehicle = Vehicle.car("BMW", arena); - Optional car = vehicle.getAsCar(); - assertEquals("BMW", car.orElseThrow().arg0()); -} -``` -As you can see above, to access the associated values of a case you can call one of the -`getAsX` methods that will return an Optional record with the associated values. -```java -try (var arena = SwiftArena.ofConfined()) { - Vehicle vehicle = Vehicle.bycicle("My Brand", arena); - Optional car = vehicle.getAsCar(); - assertFalse(car.isPresent()); - - Optional bicycle = vehicle.getAsBicycle(); - assertEquals("My Brand", bicycle.orElseThrow().maker()); -} -``` - -#### Switching and pattern matching - -If you only need to switch on the case and not access any associated values, -you can use the `getDiscriminator()` method: -```java -Vehicle vehicle = ...; -switch (vehicle.getDiscriminator()) { - case BICYCLE: - System.out.println("I am a bicycle!"); - break; - case CAR: - System.out.println("I am a car!"); - break; -} -``` -If you also want access to the associated values, you have various options -depending on the Java version you are using. -If you are running Java 21+ you can use [pattern matching for switch](https://openjdk.org/jeps/441): -```java -Vehicle vehicle = ...; -switch (vehicle.getCase()) { - case Vehicle.Case.Bicycle b: - System.out.println("Bicycle maker: " + b.maker()); - break; - case Vehicle.Case.Car c: - System.out.println("Car: " + c.arg0()); - break; -} -``` -or even, destructuring the records in the switch statement's pattern match directly: -```java -Vehicle vehicle = ...; -switch (vehicle.getCase()) { - case Vehicle.Case.Car(var name, var unused): - System.out.println("Car: " + name); - break; - default: - break; -} -``` - -For Java 16+ you can use [pattern matching for instanceof](https://openjdk.org/jeps/394) -```java -Vehicle vehicle = ...; -Vehicle.Case case = vehicle.getCase(); -if (case instanceof Vehicle.Case.Bicycle b) { - System.out.println("Bicycle maker: " + b.maker()); -} else if(case instanceof Vehicle.Case.Car c) { - System.out.println("Car: " + c.arg0()); -} -``` -For any previous Java versions you can resort to casting the `Case` to the expected type: -```java -Vehicle vehicle = ...; -Vehicle.Case case = vehicle.getCase(); -if (case instanceof Vehicle.Case.Bicycle) { - Vehicle.Bicycle b = (Vehicle.Case.Bicycle) case; - System.out.println("Bicycle maker: " + b.maker()); -} else if(case instanceof Vehicle.Case.Car) { - Vehicle.Car c = (Vehicle.Case.Car) case; - System.out.println("Car: " + c.arg0()); -} -``` - -#### RawRepresentable enums - -JExtract also supports extracting enums that conform to `RawRepresentable` -by giving access to an optional initializer and the `rawValue` variable. -Consider the following example: -```swift -public enum Alignment: String { - case horizontal - case vertical -} -``` -you can then initialize `Alignment` from a `String` and also retrieve back its `rawValue`: -```java -try (var arena = SwiftArena.ofConfined()) { - Optional alignment = Alignment.init("horizontal", arena); - assertEqual(HORIZONTAL, alignment.orElseThrow().getDiscriminator()); - assertEqual("horizontal", alignment.orElseThrow().getRawValue()); -} -``` - -### Protocols - -> Note: Protocols are currently only supported in JNI mode. -> -> With the exception of `any DataProtocol` which is handled as `Foundation.Data` in the FFM mode. - -Swift `protocol` types are imported as Java `interface`s. For now, we require that all -concrete types of an interface wrap a Swift instance. In the future, we will add support -for providing Java-based implementations of interfaces, that you can pass to Java functions. - -Consider the following Swift protocol: -```swift -protocol Named { - var name: String { get } - - func describe() -> String -} -``` -will be exported as -```java -interface Named extends JNISwiftInstance { - public String getName(); - - public String describe(); -} -``` - -#### Protocol types in parameters -Any opaque, existential or generic parameters are imported as Java generics. -This means that the following function: -```swift -func f(x: S, y: any C, z: some D) -``` -will be exported as -```java - void f(S x, T1 y, T2 z) -``` -On the Java side, only SwiftInstance implementing types may be passed; -so this isn't a way for compatibility with just any arbitrary Java interfaces, -but specifically, for allowing passing concrete binding types generated by jextract from Swift types -which conform a to a given Swift protocol. - -#### Returning protocol types - -> Note: Returning protocol types is currently only supported in JNI mode. - -Functions that return an existential (`any SomeProtocol`) or opaque (`some SomeProtocol`) -value of a single protocol are supported. For example: - -```swift -protocol Greeter { - func greeting() -> String -} - -func makeEnglishGreeter(name: String) -> any Greeter { ... } -func makeOpaqueGreeter(name: String) -> some Greeter { ... } -``` - -The returned value is wrapped in a generated *existential box* — a Java class named -`Box` that implements the protocol's Java `interface`. The box carries the -concrete (dynamic) value together with its type metadata, and dispatches each protocol -requirement through a dedicated native thunk that reconstructs the existential from that -value. This means the dynamic type of the returned value is preserved, and each -requirement is called on the underlying concrete conformer: - -```java -try (var arena = SwiftArena.ofConfined()) { - Greeter english = MySwiftLibrary.makeEnglishGreeter("World", arena); - Greeter danish = MySwiftLibrary.makeDanishGreeter("Verden", arena); - assertEquals("Hello, World!", english.greeting()); - assertEquals("Hej, Verden!", danish.greeting()); -} -``` - -Using the returned value works just like using any other imported interface: its -requirements are callable through the box, it can be passed back into functions that -accept the protocol (including generic and opaque parameters), and refined protocols -expose both their own and their inherited requirements. - -Static requirements (`static func`, `init`) and returning a *composite* existential -(`any (A & B)`) are not currently supported; such requirements are simply omitted from -the generated box, and composite returns are not extracted. - -#### Downcasting to the concrete type (`as`) - -> Note: Downcasting returned protocol values is currently only supported in JNI mode. - -A value returned as `any P` / `some P` preserves its concrete dynamic type, so it can be -recovered; This is equivalent to Swift's `as?`, of a checked `instanceof` cast in Java. Every imported protocol `interface` therefore -exposes an `as` method: - -```java - Optional as(Class type, SwiftArena arena); - Optional as(Class type); // uses the default arena -``` - -`type` must be a concrete jextracted type. -The cast succeeds only when the -value's dynamic Swift type is exactly that type, in which case you receive a fresh binding -registered in the given arena; otherwise the result is `Optional.empty()`. - -Continuing the `Greeter` example, where `makeEnglishGreeter` returns `any Greeter` backed -by a concrete `EnglishGreeter`: - -```java -try (var arena = SwiftArena.ofConfined()) { - Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); - - Optional english = greeter.as(EnglishGreeter.class, arena); - assertEquals("World", english.orElseThrow().getName()); - - // A cast to the wrong dynamic type yields an empty Optional: - assertTrue(greeter.as(DanishGreeter.class, arena).isEmpty()); -} -``` - -The cast returns an empty optional if the cast fails, which might happen when -the dynamic type differs, when `type` is not a concrete jextracted type (e.g. a generic type -or another protocol). - -### Swift closures - -Non-escaping closures are called synchronously by the Swift function they are passed to, -so their Java-side lifetime is trivially bounded by the enclosing native call. - -In Java, Swift closure parameters are represented as functional interfaces. - -SwiftJava generates ad-hoc functional interfaces per parameter, and the parameters -given and returned by the closure are also mapped between the languages just as a normal -top-level function's would be. - -From a Java developers perspective, calling Swift functions accepting callbacks is seamless: - -```java -myModule.setCallback(() -> System.out.println("called from Swift")); // functional interface is used -``` - - -> Note: Closures whose parameters or result are jextract-imported user types -(`@escaping (MyClass) -> MyClass`) are not supported yet. - - -#### Escaping closures (`@escaping`) - -> Note: `@escaping` closure parameters are currently only supported in JNI mode. - -SwiftJava also supports `@escaping` closures, however the runtime in support of them is a bit more involved, -because Swift function may store the closure and invoke it _after_ the -native call has already returned. The Java callback must therefore stay reachable -for as long as Swift retains a reference to the escaping closure. - -Lifetime of the Java object is ensured by SwiftJava automatically, which creates global references, -and removes them when the Swift-side closure gets destroyed. -This means that passing escaping closures to Swift functions does increase the global reference count, -something you may need to be cautious of when working on e.g. Android which limits the total numbers of global references. - -The Java side user experience is unchanged from the non-escaping use-case: - -```java -myModule.setCallback(() -> System.out.println("called from Swift")); // functional interface is used -``` - -> Note: Closures whose parameters or result are jextract-imported user types -(`@escaping (MyClass) -> MyClass`) are not supported yet. - -### `async` functions - -> Note: Importing `async` functions is currently only available in the JNI mode of jextract. - -Asynchronous functions in Swift can be extraced using different modes, which are explained below. - -#### Async function mode: completable-future (default) - -In this mode `async` functions in Swift are extracted as Java methods returning a `java.util.concurrent.CompletableFuture`. -This mode gives the most flexibility and should be prefered if your platform supports `CompletableFuture`. - -#### Async mode: future - -This is a mode for legacy platforms, where `CompletableFuture` is not available, such as Android 23 and below. -In this mode `async` functions in Swift are extracted as Java methods returning a `java.util.concurrent.Future`. -To enable this mode pass the `--async-func-mode future` command line option, -or set the `asyncFuncMode` configuration value in `swift-java.config` - -### Generic types - -> Note: Generic types are currently only supported in JNI mode. - -Support for generic types is still work-in-progress and limited. -Any members containing type parameters (such as T) are not exported. - -```swift -public struct MyID { - // Not exported: Contains the type parameter 'T' - public var rawValue: T - - // Not exported: The initializer depends on 'T' - public init(rawValue: T) { - self.rawValue = rawValue - } - - // Exported: Does not depend on 'T' - public var description: String { "\(rawValue)" } - - // Not exported: Although it doesn't use 'T' directly, - // it is a member of a generic context (MyID.foo). - public static func foo() -> String { "" } -} - -// Exported: A specialized function with a concrete type (MyID) -public func makeIntID() -> MyID { - ... -} -``` - -will be exported as - -```java -public final class MyID implements JNISwiftInstance { - public String getDescription(); -} - -public final class MySwiftLibrary { - public static MyID makeIntID(); -} -``` - -### Specializing generic types - -> Note: Generic specialization is currently only supported in JNI mode. - -Because Swift's rich generics and extensions system, it is possible to encounter APIs which are not safely expressible in Java, -such as conditional/constrained extensions on types when an element is of specific type. - -A common example of this is e.g. a container type which gains additional methods when the element is of some type, like this: - -```swift -struct Box { - var name: String -} -``` - -which is extended with a conditional `where` clause: - -```swift -extension Box where Element == Fish { - func watchTheFish() { } -} -``` - -This method is not available on any `Box` and therefore we cannot safely expose it on the Java `Box` wrapper type. - -It would be possible to expose it and check at runtime if the `Box.Element` is of the expected type, this however -would result in runtime throws and is not an ideal experience when developers primarily use some specific _specialize_ -types like the `FishBox`: - -```swift -typealias FishBox = Box -``` - -The jextract tool will automatically detect typealiases like this and perform _specialization_ on them, i.e. a new -`FishBox` type will be exposed on the Java side, and it will have all matching extensions applied to it, i.e. it -will have the `watchTheFish()` method available in a type-safe and always known to work correctly way. - -In other words, this results in a Java class like this: - -```java -/// Specialization of `Fish`. -public final class FishBox ... { - - public void watchTheFish() { ... } -} -``` - -> NOTE: Currently no helpers are available to convert between unspecialized types to specialized ones, but this can be offered -> as additional `box.as(FishBox.class)` conversion methods in the future. - - -> NOTE: Currently specialization for generic enums are not yet supported. - - -### Evaluating `#if` - -In jextract, `#if` branches are evaluated using [SwiftIfConfig](https://github.com/swiftlang/swift-syntax/blob/main/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md). -The evaluation parameters are fixed; for example, the `os` expression always evaluates to true, so in the following case the value of the variable will be `Linux`. - -```swift -#if os(Linux) -let os = "Linux" -#elseif os(Android) -let os = "Android" -#else -let os = "Other" -#endif -``` - -If you want the above situation to be evaluated as `Android`, you can override the evaluation parameters. -First, obtain a [StaticBuildConfiguration](https://github.com/swiftlang/swift-syntax/blob/main/Sources/SwiftIfConfig/StaticBuildConfiguration.swift) with the following command and save it to a file. -(Adjust `-target` to match the environment you want to build for. This command is available from Swift 6.3.) - -```sh -swift frontend -print-static-build-config -target aarch64-unknown-linux-android28 > static-build-config.json -``` - -Then pass the path to that file when running jextract. - -- When using the jextract command: `--static-build-config ` -- When configuring via `swift-java.config`: - ```json - { - ... - "staticBuildConfigurationFile": "" // Relative path from `swift-java.config` - } - ``` - -As a result, jextract will evaluate `os` as `Android`. - diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md index 823713836..13740d6b7 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md @@ -1,4 +1,4 @@ -# swift-java command line tool +# swift-java The `swift-java` command line tool offers multiple ways to interact your Java interoperability enabled projects. @@ -94,174 +94,3 @@ warning: Unable to translate 'java.util.jar.JarInputStream' method 'transferTo': The result of such warnings is that certain information won't be statically available in Swift, e.g., the superclass won't be known (so we will assume it is `JavaObject`), or the specified constructors or methods won't be translated. If you don't need these APIs, the warnings can be safely ignored. The APIs can still be called dynamically via JNI. The `--jar` option changes the operation of `swift-java`. Instead of wrapping Java classes in Swift, it scans the given input Jar file to find all public classes and outputs a configuration file `swift-java.config` mapping all of the Java classes in the Jar file to Swift types. The `--jar` mode is expected to be used to help import a Java library into Swift wholesale, after which swift-java should invoked again given the generated configuration file. - -### Under construction: Create a Java class to wrap the Swift library - -**NOTE**: the instructions here work, but we are still smoothing out the interoperability story. - -All JavaKit-based applications start execution within the Java Virtual Machine. First, define your own Java class that loads your native Swift library and provides a `native` entry point to get into the Swift code. Here is a minimal Java class that has all of the program's logic written in Swift, including `main`: - - -```java -package org.swift.javakit; - -public class HelloSwiftMain { - static { - System.loadLibrary("HelloSwift"); - } - - public native static void main(String[] args); -} -``` - -Compile this into a `.class` file with `javac` before we build the Swift half, e.g.,: - -``` -javac Java/src/org/swift/javakit/JavaClassTranslator.java -``` - -### Create a Swift library - -The Java class created above loads a native library `HelloSwift` that needs to contain a definition of the `main` method in the class `org.swift.javakit.HelloSwiftMain`. `HelloSwift` should be defined as a SwiftPM dynamic library product, e.g., - -```swift - products: [ - .library( - name: "HelloSwift", - type: .dynamic, - targets: ["HelloSwift"] - ), - ] -``` - -with an associated target that depends on `JavaKit`: - -```swift - .target( - name: "HelloSwift", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "JavaKit", package: "JavaKit") - ]) -``` - -### Implement the `native` Java method in Swift -Now, in the `HelloSwift` Swift library, define a `struct` that provides the `main` method for the Java class we already defined: - -```swift -import SwiftJNI - -@JavaImplementation("org.swift.javakit.HelloSwiftMain") -struct HelloSwiftMain { - @JavaStaticMethod - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - print("Command line arguments are: \(arguments)") - } -} -``` - -Go ahead and build this library with `swift build`, and find the path to the directory containing the resulting shared library (e.g., `HelloSwift.dylib`, `HelloSwift.so`, or `HelloSwift.dll`, depending on platform). It is often in `.build/debug/` if you ran `swift build` on the command line. - -### Putting it all together! - -Finally, run this program on the command line like this: - -``` -java -cp Java/src -Djava.library.path=$(PATH_CONTAINING_HELLO_SWIFT)/ org.swift.javakit.HelloSwiftMain -v argument -``` - -This will prints the command-line arguments `-v` and `argument` as seen by Swift. - -### Bonus: Swift argument parser - -The easiest way to build a command-line program in Swift is with the [Swift argument parser library](https://github.com/apple/swift-argument-parser). We can extend our `HelloSwiftMain` type to conform to `ParsableCommand` and using the Swift argument parser to process the arguments provided by Java: - -```swift -import ArgumentParser -import SwiftJNI - -@JavaClass("org.swift.jni.HelloSwiftMain") -struct HelloSwiftMain: ParsableCommand { - @Option(name: .shortAndLong, help: "Enable verbose output") - var verbose: Bool = false - - @JavaImplementation - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - let command = Self.parseOrExit(arguments) - command.run(environment: environment) - } - - func run(environment: JNIEnvironment? = nil) { - print("Verbose = \(verbose)") - } -} -``` - -### Download Java dependencies in Swift builds: swift-java resolve - -> TIP: See the `Samples/DependencySampleApp` for a fully functional showcase of this mode. - -TODO: documentation on this feature - -### Expose Swift code to Java: swift-java jextract - -The project is still very early days, however the general outline of using this approach is as follows: - -- **No code changes** need to be made to Swift libraries that are to be exposed to Java using jextract-swift. -- Swift sources are compiled to `.swiftinterface` files -- These `.swiftinterface` files are imported by jextract-swift which generates `*.java` files -- The generated Java files contain generated code for efficient native invocations. - -You can then use Swift libraries in Java just by calling the appropriate methods and initializers. - -### Generating Java bindings for Swift libraries - -This repository also includes the `jextract-swift` tool which is similar to the JDK's [`jextract`](https://github.com/openjdk/jextract/). - -This approach offers two modes of operation: - -- the default `--mode ffm` which uses the [JEP-424 Foreign function and Memory APIs](https://openjdk.org/jeps/424) which are available since JDK **22**. It promises much higher performance than traditional approaches using JNI, and is primarily aimed for calling native code from a Java application. - -> Tip: In order to use the ffm mode, you need to install a recent enough JDK (at least JDK 22). The recommended, and simplest way, to install the a JDK distribution of your choice is [sdkman](https://sdkman.io): -> -> ``` -> curl -s "https://get.sdkman.io" | bash -> sdk install java 22-open -> -> export JAVA_HOME=$(sdk home java 22-open) -> ``` - -`jextract-swift` can be pointed at `*.swiftinterface` files and will generate corresponding Java files that use the (new in Java 22) Foreign Function & Memory APIs to expose efficient ways to call "down" into Swift from Java. - -### Default jextract behaviors - -Only `public` functions, properties and types are imported. - -Global Swift functions become static functions on on a class with the same name as the Swift module in Java, - -```swift -// Swift (Sources/SomeModule/Example.swift) - -public func globalFunction() -``` - -becomes: - -```java -// Java (SomeModule.java) - -public final class SomeModule ... { - public static void globalFunction() { ... } -} -``` - -### The swift-java.config file - -Many of the tools–as well as SwiftPM plugin's–behaviors can be configured using the `swift-java.config` file. - -You can refer to the `SwiftJavaConfigurationShared/Configuration` struct to learn about the supported options. - -Configuration from the config files may be overriden or augmented by explicit command line parameters, -please refer to the options documentation for details on their behavior. - -> Note: **Comments in configuration**: The configuration is a JSON 5 file, which among other things allows `//` and `/* */` comments, so feel free to add line comments explaining rationale for some of the settings in youf configuration. \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md new file mode 100644 index 000000000..b9ad1e5b3 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -0,0 +1,560 @@ +# swift-java.config + +SwiftJava tools can be configured using the `swift-java.config` file. + +## Overview + +The `swift-java.config` file lives alongside each target that needs +swift-java code generation. It selects tool modes, sets output paths, +and controls per-mode filters. Below: the file layout, then the full +list of supported keys. + +### The swift-java.config file + +You can refer to the `SwiftJavaConfigurationShared/Configuration` struct to learn about the supported options. + +Configuration from the config files may be overriden or augmented by explicit command line parameters, +please refer to the options documentation for details on their behavior. + +### Comments + +The configuration is a JSON 5 file, which among other things allows `//` and `/* */` comments, so feel free to add line comments explaining rationale for some of the settings in your configuration. + +### Supported configuration options + + + + + +### General + +#### logLevel + +- **Type:** `LogLevel?` + +The minimum log level at which log messages will be printed at by swift-java. + +**Values:** + +- `trace` +- `debug` +- `info` +- `notice` +- `warning` +- `error` +- `critical` + +--- + +### jextract + +#### javaPackage + +- **Type:** `String?` + +The Java package the generated Java code should be emitted into. + +Example: +```swift +"com.example.mypackage" +``` + +--- + +#### swiftModule + +- **Type:** `String?` + +The name of the Swift module into which the resulting Swift types will be generated. + +--- + +#### nativeLibraryName + +- **Type:** `String?` + +The name of the native library to load at runtime via `System.loadLibrary()`. +Defaults to the Swift module name when not set. Use this when the dynamic +library product has a different name than the module being exported +(e.g. the module is `MyLibrary` but the dylib is `MyLibrarySwiftJava` or something else). + +--- + +#### overrideStaticBlockLibraryLoading + +- **Type:** `[String]?` + +When non-nil, overrides the library loading statements emitted in the +`static {}` / `initializeLibs()` block of generated Java classes. +Each string is emitted as a verbatim Java statement. + +When `nil` (the default), the standard loading calls are emitted. +When set to an empty array `[]`, no library loading code is emitted at all. + +--- + +#### inputSwiftDirectory + +- **Type:** `String?` + +Directory containing Swift files which should be extracted into Java bindings (jextract mode). +Must be paired with `outputSwiftDirectory` and `outputJavaDirectory`. + +--- + +#### outputSwiftDirectory + +- **Type:** `String?` + +The directory where generated Swift files should be written. Generally used with jextract mode. + +--- + +#### outputJavaDirectory + +- **Type:** `String?` + +The directory where generated Java files should be written. Generally used with jextract mode. + +--- + +#### mode + +- **Type:** `JExtractGenerationMode?` +- **Default:** `ffm` + +Determine `jextract` source generation mode, using JNI or FFM. + +**Values:** + +- `ffm` - Foreign Value and Memory API +- `jni` - Java Native Interface + +--- + +#### writeEmptyFiles + +- **Type:** `Bool?` +- **Default:** `false` + +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. + +--- + +#### minimumInputAccessLevelMode + +- **Type:** `AccessLevelMode?` +- **Default:** `public` + +The lowest access level of Swift declarations that should be extracted, defaults to `public`. + +**Values:** + +- `public` +- `package` +- `internal` + +--- + +#### memoryManagementMode + +- **Type:** `JExtractMemoryManagementMode?` +- **Default:** `explicit` + +The memory management mode to use for the generated code. By default, the user must explicitly +provide a `SwiftArena` to all calls that require it. By choosing `allowGlobalAutomatic`, the user +can omit this parameter and a global GC-based arena will be used. + +**Values:** + +- `explicit` - Force users to provide an explicit `SwiftArena` to all calls that require them. +- `allowGlobalAutomatic` - Provide both explicit `SwiftArena` support and a default global automatic `SwiftArena` that will deallocate memory when the GC decides to. + +--- + +#### asyncFuncMode + +- **Type:** `JExtractAsyncFuncMode?` +- **Default:** `completableFuture` + +The mode to use for extracting asynchronous Swift functions. By default async methods are +extracted as Java functions returning `CompletableFuture`. + +**Values:** + +- `completableFuture` - Extract Swift `async` APIs as Java functions that return `CompletableFuture`s. +- `legacyFuture` - Extract Swift `async` APIs as Java functions that return `Future`s. + +--- + +#### javaSourceLevel + +- **Type:** `JavaSourceLevel?` +- **Default:** `22` + +The Java source level to target when generating Java code. + +**Values:** + +- `17` +- `18` +- `21` +- `22` +- `24` +- `25` + +--- + +#### enableJavaCallbacks + +- **Type:** `Bool?` +- **Default:** `false` + +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, and is +only supported in `jni` mode. + +--- + +#### generatedJavaSourcesListFileOutput + +- **Type:** `String?` + +If specified, JExtract will output to this file a list of paths to all generated Java source files. + +--- + +#### singleType + +- **Type:** `String?` + +If set, only generate bindings for this single Swift type name + +--- + +#### linkerExportListOutput + +- **Type:** `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 +file to the linker via `-Xlinker --version-script=` to enable +precise dead-code elimination of unused Swift code in the final shared +library. + +--- + +#### swiftFilterInclude + +- **Type:** `[String]?` + +Include only Swift source files or types matching these patterns during jextract. + +File-path patterns (containing `/`, or ending in `.swift` / +`.swiftinterface`): matched against relative file paths. Supports `*` and +`**` wildcards. Example: `Models/**`, `**/User.swift`, `MyType.swift`. + +Type-name patterns (containing `.`): matched against the dotted nested +type path (e.g. `Outer.Inner`, `Outer.**`, `**.User`, `Logger.Internal*`). +The qualified name does NOT include the module prefix. + +`.` is the separator. `::` is reserved by Swift for module disambiguation +(SE-0491) and is NOT used by these filters. + +Plain names (no separator) match both: a filename without `.swift`, or the +top-level component of a type name + +--- + +#### swiftFilterExclude + +- **Type:** `[String]?` + +Exclude Swift source files or types matching these patterns during jextract. +Same pattern syntax as `swiftFilterInclude` + +--- + +#### importedModuleStubs + +- **Type:** `[String: [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 {}" + ] + } +} +``` + +--- + +#### specialize + +- **Type:** `[String: SpecializationConfigEntry]?` + +Force specialization of generic types, mapping them to a specific generated Java-facing name. +This allows generating generic specializations that can be used only with some specific bound generic argument, +rather than using the usual generic machinery. Sometimes useful if a generic type is only reasonably usable with some specific type. + +Generating specializations takes into account Swift extensions where the generic is bound to that type, for example, a `Box` +type, would automatically gain `T == Fish` specific methods in the generated Java sources if there is an `extension ... where T == Fish` declared in Swift: + +```swift +struct Box {} +extension Box where T == Fish { + func feedFish() +} +``` + +When configured as follows: + +```json +{ + "specialize": { + "FishBox": { + "base": "Box", + "typeArgs": {"Element": "Fish"} + }, + "ToolBox": { + "base": "Box", + "typeArgs": {"Element": "Tool"} + } + } +} +``` + +Would result in Java code with the generated `feedFish()` method on the `FishBox` Java type: + +```java +FishBox box = ...; +box.feedFish(); // type-safe generated specialized function +``` + +You can also possible to cause such specialization to occurr by declaring a typealias in Swift sources: + +```swift +typealias FishBox = Box +``` + +So this configuration option is geared towards times when you do not control the sources that wrappers are being generated for. + +**`SpecializationConfigEntry`:** + +Configuration entry for specializing a generic type into a concrete Java class. +The dictionary key is the Java-facing name; this entry provides the base type +and type argument mapping. + +- `base`: `String` - The base Swift type name (e.g. "Box") +- `typeArgs`: `[String: String]` - Mapping from generic parameter name to concrete type (e.g. {"Element": "Fish"}) + +--- + +#### staticBuildConfigurationFile + +- **Type:** `String?` + +If set, use this JSON file as the static build configuration for jextract. +This allows users to provide a custom StaticBuildConfiguration for #if resolution. + +You can generate one for a specific target triple using the Swift compiler itself: + +``` +swift frontend -print-static-build-config -target > static-build-config.json +``` + +Example: + +The configuration option is a path with a file generated like above, which will have a structure similar to this: + +```json +{ + "attributes": [], + "compilerVersion": { + "components": [6, 3] + }, + "customConditions": [ + "DEBUG" + ], + "endianness": "little", + "features": [], + "languageMode": { + "components": [5, 10] + }, + "targetArchitectures": [], + "targetAtomicBitWidths": [], + "targetEnvironments": [], + "targetOSs": [], + "targetObjectFileFormats": [], + "targetPointerAuthenticationSchemes": [], + "targetPointerBitWidth": 64, + "targetRuntimes": [] +} +``` + +--- + +### wrap-java + +#### classpath + +- **Type:** `String?` + +The Java class path that should be passed along to the swift-java tool. + +--- + +#### classes + +- **Type:** `[String: String]?` +- **Default:** empty dictionary (`[:]`) + +The Java classes that should be translated to Swift. The keys are +canonical Java class names (e.g., java.util.ArrayList) and the values are +the corresponding Swift names (e.g., JavaArrayList). + +Example: +```json +{ + "classes": { + "java.util.ArrayList": "JavaArrayList", + "java.util.HashMap": "JavaHashMap" + } +} +``` + +--- + +#### sourceCompatibility + +- **Type:** `JavaVersion?` + +Compile for the specified Java SE release. + +`JavaVersion` is an integer identifying a Java SE release, in the same +shape as `javaSourceLevel`. Supported values: + +- `17` +- `18` +- `21` +- `22` +- `24` +- `25` + +--- + +#### targetCompatibility + +- **Type:** `JavaVersion?` + +Generate class files suitable for the specified Java SE release. + +`JavaVersion` is an integer identifying a Java SE release, in the same +shape as `javaSourceLevel`. Supported values: + +- `17` +- `18` +- `21` +- `22` +- `24` +- `25` + +--- + +#### javaFilterInclude + +- **Type:** `[String]?` + +Filter input Java types by their package prefix if set + +--- + +#### javaFilterExclude + +- **Type:** `[String]?` + +Exclude input Java types by their package prefix or exact match + +--- + +#### singleSwiftFileOutput + +- **Type:** `String?` + +If set, place all generated code in this single Swift file instead of one file per class. + +--- + +### dependencies + +#### dependencies + +- **Type:** `[JavaDependencyDescriptor]?` + +Java dependencies we need to fetch for this target. + +**`JavaDependencyDescriptor`:** + +Represents a maven-style Java dependency. + +Encoded in JSON as a single `groupID:artifactID:version` coordinate string +(Gradle-style notation), not as a keyed object. + +Example: +```json +{ + "dependencies": [ + "com.google.code.gson:gson:2.10.1" + ] +} +``` + +- `groupID`: `String` +- `artifactID`: `String` +- `version`: `String` + +--- + +#### mavenRepositories + +- **Type:** `[MavenRepositoryDescriptor]?` + +Custom Maven repositories to use when resolving dependencies. +If not set, defaults to mavenCentral(). + +**`MavenRepositoryDescriptor`:** + +Describes a Maven-style repository for dependency resolution. + +Supported types based on https://docs.gradle.org/current/userguide/supported_repository_types.html: +- `maven(url:artifactUrls:)` - A custom Maven repository at the given URL +- `mavenCentral` - Maven Central repository +- `mavenLocal(includeGroups:)` - Local Maven cache (~/.m2/repository) +- `google` - Google's Maven repository + +Example: +```json +{ + "mavenRepositories": [ + { "type": "mavenCentral" }, + { "type": "maven", "url": "https://repo.example.com/maven2" }, + { "type": "mavenLocal", "includeGroups": ["com.example"] }, + { "type": "google" } + ] +} +``` + + +--- + + \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md new file mode 100644 index 000000000..09342b771 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md @@ -0,0 +1,70 @@ + +# swift-java jextract + +Automatic Java wrappers for existing Swift code, generated by `swift-java jextract`. + +## Overview + +`jextract` is the *Java source-generation* part of swift-java. It is similar in +spirit to the JDK's [`jextract`](https://github.com/openjdk/jextract/), but it +consumes Swift rather than C headers. + +**No code changes** need to be made to the Swift libraries you want to expose. +The outline of the approach is: + +- Swift sources are compiled to `.swiftinterface` files. +- Those `.swiftinterface` files are read by `swift-java jextract`, which + generates `*.java` files. +- The generated Java files contain the code for efficient native invocations. + +You can then use the Swift library from Java by calling the generated methods +and initializers. + +For the set of Swift language features that can be extracted, and what each one +looks like on the Java side, see . + +### Modes + +jextract can generate bindings in one of two modes, `jni` or `ffm`. See + for guidance on choosing between them. + +> Tip: To use the `ffm` mode you need JDK 25+, because it relies on +> [JEP 454: Foreign Function & Memory](https://openjdk.org/jeps/454) APIs. The +> simplest way to install a JDK distribution of your choice is +> [sdkman](https://sdkman.io): +> +> ```bash +> curl -s "https://get.sdkman.io" | bash +> sdk install java 25-open +> +> export JAVA_HOME=$(sdk home java 25-open) +> ``` + +### Default jextract behaviors + +Only `public` functions, properties and types are imported. + +Global Swift functions become static functions on a class with the same name as +the Swift module in Java: + +```swift +// Swift (Sources/SomeModule/Example.swift) + +public func globalFunction() +``` + +becomes: + +```java +// Java (SomeModule.java) + +public final class SomeModule ... { + public static void globalFunction() { ... } +} +``` + +## See Also + +- +- +- diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md new file mode 100644 index 000000000..cb6c092c3 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md @@ -0,0 +1,45 @@ +# swift-java resolve + +Download Java dependencies for use in Swift builds. + +## Overview + +`swift-java resolve` reads the Java dependencies declared in a + and fetches them, then writes a +`.swift-java.classpath` file into the `--output-directory`. + +Classpath files are picked up by `swift-java` commands which search for `*.swift-java.classpath` files +to create a classpath for Java operations, e.g. `wrap-java` uses such assebled classpath during wrapping Java classes for Swift. + +You can use `swift-java resolve` explicitly on the command line: + +```bash +swift-java resolve \ + --swift-module JavaCommonsCSV \ + -o .build/plugins/outputs/JavaCommonsCSV \ + "org.apache.commons:commons-csv:1.10.0" +``` + +Or, include dependency identifiers in gradle format: `::` +in `swift-java.config` configuration files, like so: + +```json +{ + "dependencies": [ + "org.apache.commons:commons-csv:1.12.0" + ] +} +``` + +This allows `swift-build` to automatically resolve and fetch such Java dependencies, +however it does mean that you will have to disable the SwiftPM sandbox (`--disable-sandbox`) +in order to build such package because this step requires network access which otherwise the build system will block using a security sandbox. +See for more details on caveats to using the build plugin in such scenarios. + +> Tip: See `Samples/JavaDependencySampleApp` for a fully functional showcase of this mode. + +## See Also + +- +- +- diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md new file mode 100644 index 000000000..bb2f06c2f --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md @@ -0,0 +1,194 @@ +# swift-java wrap-java + +Automatic Swift wrappers for existing Java code, generated by `swift-java wrap-java`. + +## Overview + +`wrap-java` is the *source-generation* half of Java-from-Swift interop. Point it +at a classpath - a JAR, a Maven artifact, or the JDK itself - and it emits Swift +files full of `@JavaClass`, `@JavaMethod`, `@JavaField`, and related JavaKit +macros for every public Java type you asked it to wrap. On the Swift side, the +result looks and feels like any other Swift API. + +Reach for `wrap-java` when you want to expose an entire Java library to Swift +without hand-writing every declaration. When you only need a handful of types, +or when you're implementing `native` methods in Swift, use the JavaKit macros +directly - see . + +For an orientation on which interop tool fits your task, see . + +### Workflow + +The typical pipeline has three steps, though the SwiftPM plugin can drive all +of them for you. + +### 1. Discover: swift-java configure + +Given a JAR, `swift-java configure --jar` walks its public classes and writes +a `swift-java.config` file listing each Java type together with a suggested +Swift name. Skip this step when you're hand-authoring the config. + +```bash +swift-java configure --jar \ + --swift-module MyLib \ + -o Sources/MyLib \ + path/to/library.jar +``` + +### 2. Resolve Maven dependencies: swift-java resolve + +For Maven artifacts, `swift-java resolve` downloads the dependency and produces +a `.swift-java.classpath` file that `wrap-java` and the runtime pick up +automatically. + +```bash +swift-java resolve \ + --swift-module JavaCommonsCSV \ + -o .build/plugins/outputs/JavaCommonsCSV \ + "org.apache.commons:commons-csv:1.10.0" +``` + +### 3. Generate wrappers: swift-java wrap-java + +`wrap-java` consumes the config plus classpath and writes one Swift file per +Java class (or a single combined file, see `singleSwiftFileOutput` below). + +```bash +swift-java wrap-java \ + --swift-module JavaSieve \ + -o Sources/JavaSieve/generated \ + --depends-on SwiftJava=Sources/SwiftJava/swift-java.config \ + --depends-on JavaUtil=Sources/JavaStdlib/JavaUtil/swift-java.config \ + Sources/JavaSieve/swift-java.config +``` + +### The SwiftPM plugin + +Most projects do **not** invoke the CLI directly. The `SwiftJavaPlugin` +build-tool plugin (see `Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift`) +detects `swift-java.config` in each target, resolves Maven dependencies, +and runs `wrap-java` at build time - all transparently. + +Wire it up in your `Package.swift`: + +```swift +.executableTarget( + name: "MyApp", + dependencies: ["JavaCommonsCSV"], + plugins: [ + .plugin(name: "SwiftJavaPlugin", package: "swift-java"), + ] +) +``` + +See for the plugin's full contract. + +### swift-java.config schema (wrap-java fields) + +The full schema lives in +`Sources/SwiftJavaConfigurationShared/Configuration.swift`. The fields +that matter for `wrap-java` are: + +| Field | Type | Purpose | +|---------------------------|-------------------|---------------------------------------------------------------------------------------------| +| `classes` | `[String: String]`| Map Java canonical class names to Swift type names, e.g. `"java.math.BigInteger": "BigInteger"`. | +| `classpath` | `String` | Colon-separated Java classpath entries. | +| `javaFilterInclude` | `[String]` | While scanning, wrap only types matching these package prefixes. | +| `javaFilterExclude` | `[String]` | Skip these packages or, using `Class#method`, skip individual methods. | +| `sourceCompatibility` | `Int` | Compile the wrapped API surface for this Java SE release (e.g. `11`, `17`, `21`). | +| `targetCompatibility` | `Int` | Emit class files for this Java SE release. | +| `singleSwiftFileOutput` | `String` | If set, place all generated code in this single Swift file instead of one file per class. | + +The config file is JSON5, so `//` and `/* */` comments are permitted. + +### CLI flags + +`swift-java wrap-java --help` (from +`Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift`) exposes: + +| Flag | Purpose | +|-------------------------------------------------|---------------------------------------------------------------------------------------------| +| `--swift-module ` | Required. Name of the Swift module the generated types will live in. | +| `--depends-on ` | Repeatable. Register a Swift module this one transitively depends on for cross-module types. | +| `--swift-native-implementation ` | Repeatable. Java classes whose `native` methods will be implemented in Swift. | +| `--cache-directory ` | Cache directory for intermediate results between runs. | +| `--swift-match-package-directory-structure` | Mirror Java package layout with directories under `-o`. | +| `--singleSwiftFileOutput ` | Emit everything into one Swift file at ``. | +| `--filter-include ` / `--filter-exclude ` | Same intent as the config-file filters, exposed on the CLI. | +| `--android-api-version-file ` | Consume Android's `api-versions.xml` to emit `@available` on wrapped decls. | +| `--cp ` / `--classpath ` | (Inherited) Extra classpath entries. | +| `-o ` / `--output-directory ` | (Inherited) Output directory. | +| `-l ` / `--log-level ` | (Inherited) `trace`, `debug`, `info`, `notice`, `warning`, `error`, `critical`. | + +### Walkthrough: wrapping a JDK class + +The simplest possible sample: wrap `java.math.BigInteger` and use it to test +primality. See `Samples/JavaProbablyPrime/`. + +@TabNavigator { + @Tab("swift-java.config") { + @Snippet(path: "Snippets/WrapJavaProbablyPrimeConfig") + } + @Tab("Swift") { + @Snippet(path: "Snippets/WrapJavaProbablyPrimeSwift.swift", slice: "probablyPrime") + } +} + +### Walkthrough: wrapping an external JAR + +`Samples/JavaSieve/` wraps a third-party Java library (a quadratic-sieve +prime finder) plus `java.math.RoundingMode` for enum-constant access. The +config lists both the classpath and the target classes. + +@TabNavigator { + @Tab("swift-java.config") { + @Snippet(path: "Snippets/WrapJavaSieveConfig") + } + @Tab("Swift") { + @Snippet(path: "Snippets/WrapJavaSieveSwift.swift", slice: "sieveUsage") + } +} + +### Walkthrough: Maven dependency + +`Samples/JavaDependencySampleApp/` wraps `org.apache.commons:commons-csv` +straight from Maven Central. The config here can be near-empty because +`swift-java resolve` and the plugin fill in the classpath. + +@TabNavigator { + @Tab("swift-java.config") { + @Snippet(path: "Snippets/WrapJavaDependencyConfig") + } + @Tab("Swift") { + @Snippet(path: "Snippets/WrapJavaDependencySwift.swift", slice: "dependencyUsage") + } +} + +### Feature matrix + +Legend: ✅ supported, ❌ not supported, 🟡 partial / work-in-progress. + +| Feature | Support | +|----------------------------------------------------------------------------------|---------| +| Auto-wrap every public class on the classpath | ✅ | +| Auto-wrap interfaces (as `@JavaInterface`) | ✅ | +| Auto-wrap Java enums as Swift enums | ❌ | +| Auto-wrap Java enums as classes with static-field constants | ✅ | +| Nested classes | 🟡 | +| Generic classes (with Java erasure) | 🟡 | +| Cross-module dependencies via `--depends-on Module=path/config` | ✅ | +| Package-scope include / exclude filtering | ✅ | +| Method-level exclusion (`javaFilterExclude: ["Class#method"]`) | ✅ | +| Maven dependency resolution via `swift-java resolve` | ✅ | +| Automatic classpath discovery from `.swift-java.classpath` files | ✅ | +| One Swift file per class, or a single combined file (`singleSwiftFileOutput`) | ✅ | +| Mirroring Java package directory layout | ✅ | +| `@available` annotations from Android's `api-versions.xml` | ✅ | +| JAR-only workflow (`swift-java configure --jar` -> `wrap-java`) | ✅ | +| SwiftPM plugin auto-invocation | ✅ | + +### See also + +- - the underlying macro surface that `wrap-java` generates. +- - full reference for the `swift-java` CLI (all subcommands). +- - how the plugin drives `wrap-java` from a normal `swift build`. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md index dd97224f6..f9c6d2691 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md @@ -1,4 +1,4 @@ -# SwiftJava SwiftPM Plugin +# SwiftPM Plugin The `SwiftJavaPlugin` automates `swift-java` command line tool invocations during the build process. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md index b5f370704..5929601d4 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -38,14 +38,28 @@ which is a quick overview of all the features and approaches offered by SwiftJav ### Supported Features -- +- +- +- ### Source Generation - +- +- +- - +- +### Examples + +- + +### Troubleshooting and optimization + +- ### Android Support - + diff --git a/scripts/generate-docs.sh b/scripts/generate-docs.sh new file mode 100755 index 000000000..861690a35 --- /dev/null +++ b/scripts/generate-docs.sh @@ -0,0 +1,58 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2026 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 +## +##===----------------------------------------------------------------------===## +## +## Builds the SwiftJavaDocumentation DocC catalog as a static HTML site. +## +## Usage: +## scripts/generate-docs.sh build static HTML into .build/documentation +## scripts/generate-docs.sh --preview rebuild the config docs and launch the live DocC preview server +## +set -eu + +cd "$(dirname "$0")/.." + +PREVIEW=0 +for arg in "$@"; do + case "$arg" in + --preview) + PREVIEW=1 + ;; + *) + echo "Unknown argument: $arg" >&2 + echo "Usage: $0 [--preview]" >&2 + exit 1 + ;; + esac +done + +echo "Regenerating swift-java.config option docs from Configuration.swift..." +swift run generate-config-docs + +# Opt in to the swift-docc-plugin dependency declared in Package.swift. +# Set DOCC_PLUGIN_PATH= instead to use a local swift-docc-plugin checkout. +export SWIFTJAVA_DOCC_PLUGIN_INSTALL=1 + +if [ "$PREVIEW" -eq 1 ]; then + echo "Starting DocC live preview for SwiftJavaDocumentation..." + swift package --disable-sandbox plugin preview-documentation --target SwiftJavaDocumentation +else + OUTPUT_PATH=".build/documentation" + echo "Generating static HTML documentation to $OUTPUT_PATH ..." + swift package --disable-sandbox plugin generate-documentation \ + --target SwiftJavaDocumentation \ + --output-path "$OUTPUT_PATH" \ + --transform-for-static-hosting + echo "Done. Open $OUTPUT_PATH/index.html in a browser." +fi diff --git a/scripts/release.sh b/scripts/release.sh index c89d2c597..9663f52ad 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -96,6 +96,28 @@ if [[ "$LOCAL_SHA" != "$REMOTE_SHA" ]]; then error "Local '$MAIN_BRANCH' is not up to date with origin. Please pull first." fi +# ==== ----------------------------------------------------------------------- +# MARK: Verify generated config docs are up to date + +# Regenerate the "Supported configuration options" section of +# SwiftJavaConfigFile.md from Configuration.swift. If this produces any working- +# tree changes, the checked-in docs are stale and the release must not proceed +# until the regenerated file is committed. +info "Regenerating swift-java.config option docs..." +if ! xcrun swift run --package-path "$REPO_ROOT" generate-config-docs; then + error "Failed to run generate-config-docs. Please investigate before releasing." +fi + +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then + echo "" + git -C "$REPO_ROOT" status --short + echo "" + error "Generated config docs are stale (see files listed above). + Please commit the regenerated docs and re-run the release script: + git add -A && git commit -m 'Regenerate config docs'" +fi +info "Generated config docs are up to date." + # ==== ----------------------------------------------------------------------- # MARK: Determine latest swift-java-jni-core release From 37b8b457f0188b2dc55f515af184b3f62668a403 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 5 Aug 2026 13:55:51 +0900 Subject: [PATCH 2/6] Automatically generate and show --help output in CLI pages --- Snippets/SwiftJavaCLIHelp.sh | 289 ++++++++++++++++++ .../Configuration.swift | 2 +- .../Documentation.docc/Android.md | 2 +- .../FeaturesJavaKitMacros.md | 2 +- .../SwiftJavaCommandLineTool.md | 56 +--- .../Documentation.docc/SwiftJavaConfigFile.md | 2 +- .../Documentation.docc/SwiftJavaJextract.md | 4 + .../Documentation.docc/SwiftJavaResolve.md | 6 +- .../Documentation.docc/SwiftJavaWrapJava.md | 4 + .../Commands/JExtractCommand.swift | 2 +- Sources/SwiftJavaTool/SwiftJava.swift | 4 +- scripts/generate-cli-help-snippets.sh | 85 ++++++ scripts/generate-docs.sh | 3 + 13 files changed, 403 insertions(+), 58 deletions(-) create mode 100644 Snippets/SwiftJavaCLIHelp.sh create mode 100755 scripts/generate-cli-help-snippets.sh diff --git a/Snippets/SwiftJavaCLIHelp.sh b/Snippets/SwiftJavaCLIHelp.sh new file mode 100644 index 000000000..d354d0a23 --- /dev/null +++ b/Snippets/SwiftJavaCLIHelp.sh @@ -0,0 +1,289 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2026 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 +## +##===----------------------------------------------------------------------===## + +# Auto-generated by scripts/generate-cli-help-snippets.sh - DO NOT EDIT. +# Run scripts/generate-docs.sh (or this script directly) to regenerate. + +# snippet.mainHelp +> swift-java --help + +OVERVIEW: Generate sources and configuration for Swift and Java +interoperability. + +USAGE: swift-java + +OPTIONS: + -h, --help Show help information. + +SUBCOMMANDS: + configure Configure and emit a swift-java.config file based on + an input dependency or jar file + resolve Resolve dependencies and write the resulting + swift-java.classpath file + wrap-java Wrap Java classes with corresponding Swift bindings. + jextract Wrap Swift functions and types with Java bindings, + making them available to be called from Java + + See 'swift-java help ' for detailed help. +# snippet.end + +# snippet.configureHelp +> swift-java configure --help + +OVERVIEW: Configure and emit a swift-java.config file based on an input +dependency or jar file + +USAGE: swift-java configure [--output-directory ] [--input-swift ] [--log-level ] [--config ] [--cp ...] [--jar] [--existing-config-file ] --swift-module [--swift-type-prefix ] [--filter-include ...] [--filter-exclude ...] + +OPTIONS: + -o, --output-directory + The directory in which to output generated SwiftJava + configuration files. + --input-swift + Directory containing Swift files which should be + extracted into Java bindings. Also known as + 'jextract' mode. Must be paired with --output-java + and --output-swift. + -l, --log-level + Configure the level of logs that should be printed + (values: trace, debug, info, notice, warning, error, + critical; default: log level) + --config A path to a custom swift-java.config to use + --cp, --classpath Class search path of directories and zip/jar files + from which Java classes can be loaded. + --jar Specifies that the input is a *.jar file whose public + classes will be loaded. The output of swift-java will + be a configuration file (swift-java.config) that can + be used as input to a subsequent swift-java + invocation to generate wrappers for those public + classes. + --existing-config-file + How to handle an existing swift-java.config; by + default 'overwrite' by can be changed to amending a + configuration (default: overwrite) + --swift-module + The name of the Swift module into which the resulting + Swift types will be generated. + --swift-type-prefix + A prefix that will be added to the names of the Swift + types + --filter-include + While scanning a classpath, inspect ONLY types + included in these packages + --filter-exclude + While scanning a classpath, skip types which match + the filter prefix. You can exclude specific methods + by using the `com.example.MyClass#method` format. + -h, --help Show help information. + +# snippet.end + +# snippet.resolveHelp +> swift-java resolve --help + +OVERVIEW: Resolve dependencies and write the resulting swift-java.classpath file + +USAGE: swift-java resolve [--output-directory ] [--input-swift ] [--log-level ] [--config ] [--cp ...] --swift-module [] + +ARGUMENTS: + Additional configuration paths (swift-java.config) + files, with defined 'dependencies', or dependency + descriptors formatted as 'groupID:artifactID:version' + separated by ','. May be empty, in which case the + target Swift module's configuration's 'dependencies' + will be used. + +OPTIONS: + -o, --output-directory + The directory in which to output generated SwiftJava + configuration files. + --input-swift + Directory containing Swift files which should be + extracted into Java bindings. Also known as + 'jextract' mode. Must be paired with --output-java + and --output-swift. + -l, --log-level + Configure the level of logs that should be printed + (values: trace, debug, info, notice, warning, error, + critical; default: log level) + --config A path to a custom swift-java.config to use + --cp, --classpath Class search path of directories and zip/jar files + from which Java classes can be loaded. + --swift-module + The name of the Swift module into which the resulting + Swift types will be generated. + -h, --help Show help information. + +# snippet.end + +# snippet.wrapJavaHelp +> swift-java wrap-java --help + +OVERVIEW: Wrap Java classes with corresponding Swift bindings. + +USAGE: swift-java wrap-java [] --swift-module + +OPTIONS: + -o, --output-directory + The directory in which to output generated SwiftJava + configuration files. + --input-swift + Directory containing Swift files which should be + extracted into Java bindings. Also known as + 'jextract' mode. Must be paired with --output-java + and --output-swift. + -l, --log-level + Configure the level of logs that should be printed + (values: trace, debug, info, notice, warning, error, + critical; default: log level) + --config A path to a custom swift-java.config to use + --cp, --classpath Class search path of directories and zip/jar files + from which Java classes can be loaded. + --swift-module + The name of the Swift module into which the resulting + Swift types will be generated. + --depends-on + A swift-java configuration file for a given Swift + module name on which this module depends, + e.g., + JavaKitJar=Sources/JavaKitJar/swift-java.config. + There should be one of these options + for each Swift module that this module depends on + (transitively) that contains wrapped Java sources. + --swift-native-implementation + The names of Java classes whose declared native + methods will be implemented in Swift. + --cache-directory + Cache directory for intermediate results and other + outputs between runs + --swift-match-package-directory-structure + Match java package directory structure with generated + Swift files (default: false) + --single-swift-file-output + If specified, a single Swift file will be generated + containing all the generated code + --filter-include + While scanning a classpath, inspect ONLY types + included in these packages + --filter-exclude + While scanning a classpath, skip types which match + the filter prefix. You can exclude specific methods + by using the `com.example.MyClass#method` format. + --android-api-version-file + Path to Android api-versions.xml for generating + @available attributes based on API level data + -h, --help Show help information. + +# snippet.end + +# snippet.jextractHelp +> swift-java jextract --help + +OVERVIEW: Wrap Swift functions and types with Java bindings, making them +available to be called from Java + +USAGE: swift-java jextract [] --swift-module --output-swift --output-java + +OPTIONS: + -o, --output-directory + The directory in which to output generated SwiftJava + configuration files. + --input-swift + Directory containing Swift files which should be + extracted into Java bindings. Also known as + 'jextract' mode. Must be paired with --output-java + and --output-swift. + -l, --log-level + Configure the level of logs that should be printed + (values: trace, debug, info, notice, warning, error, + critical; default: log level) + --config A path to a custom swift-java.config to use + --mode The mode of generation to use for the output files. + Used with jextract mode. + --swift-module + The name of the Swift module into which the resulting + Swift types will be generated. + --java-package + The Java package the generated Java code should be + emitted into. + --output-swift + The directory where generated Swift files should be + written. Generally used with jextract mode. + --output-java + The directory where generated Java files should be + written. Generally used with jextract mode. + --write-empty-files/--no-write-empty-files + 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. + --minimum-input-access-level-mode + The lowest access level of Swift declarations that + should be extracted, defaults to 'public'. + --memory-management-mode + The memory management mode to use for the generated + code. By default, the user must explicitly provide + `SwiftArena` to all calls that require it. By + choosing `allowGlobalAutomatic`, user can omit this + parameter and a global GC-based arena will be used. + --depends-on + A swift-java configuration file for a given Swift + module name on which this module depends, + e.g., Sources/JavaJar/swift-java.config. There should + be one of these options + for each Swift module that this module depends on + (transitively) that contains wrapped Java sources. + --async-func-mode + The mode to use for extracting asynchronous Swift + functions. By default async methods are extracted as + Java functions returning CompletableFuture. + --enable-java-callbacks/--no-enable-java-callbacks + 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 only + supported in 'jni' mode. + --generated-java-sources-list-file-output + If specified, JExtract will output to this file a + list of paths to all generated Java source files + --linker-export-list-output + If specified, JExtract (JNI mode) will write a linker + version script to this path. The file lists every + generated JNI @_cdecl entry-point symbol as a global + export and hides all other symbols with local: *, + enabling dead-code elimination of unreachable Swift + code: + -Xlinker --version-script= + --filter-include + Include only Swift source files matching these + patterns during jextract. Patterns are matched + against relative file paths (without .swift + extension). Supports * (single-segment wildcard) and + ** (recursive wildcard). Example: --filter-include + 'Models/**' + --filter-exclude + Exclude Swift source files matching these patterns + during jextract. Same pattern syntax as + --filter-include. Example: --filter-exclude + 'Internal/*' + --single-type + If specified, only generate bindings for this single + Swift type name + --static-build-config + Path to a JSON file containing a + StaticBuildConfiguration. Used to resolve #if + conditional compilation blocks. + -h, --help Show help information. + +# snippet.end diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 67d142420..9a98c0a3b 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -248,7 +248,7 @@ public struct Configuration: Codable { /// box.feedFish(); // type-safe generated specialized function /// ``` /// - /// You can also possible to cause such specialization to occurr by declaring a typealias in Swift sources: + /// It is also possible to cause such specialization to occur by declaring a typealias in Swift sources: /// /// ```swift /// typealias FishBox = Box diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md index f0f080cb1..7ec9d9e98 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md @@ -9,7 +9,7 @@ Hints and patterns for using swift-java on Android. Since swift-java uses JNI and reflection APIs, we must tell the R8 optimizer to ignore our wrapped types, otherwise we will run into runtime crashes, because types are stripped from the APK/AAR. -The `SwiftKit` Java library already contains a [Proguard consumer file](https://developer.anbdroid.com/topic/performance/app-optimization/library-optimization), +The `SwiftKit` Java library already contains a [Proguard consumer file](https://developer.android.com/topic/performance/app-optimization/library-optimization), which is automatically detected by R8, so any `org.swift.swiftkit` types are already ignored. However, you must still provide rules for your own types. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md index e008bce43..cfeeb21a8 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md @@ -295,7 +295,7 @@ Because of Java's type erasure, generic parameters used in method signatures need a `typeErasedResult:` hint on `@JavaMethod` so the macro can generate the right JNI signature. -TODO: shoe example here +TODO: show example here ### Method overloading diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md index 13740d6b7..70475efcb 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md @@ -1,30 +1,14 @@ # swift-java -The `swift-java` command line tool offers multiple ways to interact your Java interoperability enabled projects. +The `swift-java` command line tool offers multiple ways to interact with your Java interoperability enabled projects. ## Overview -The `swift-java` command line tool offers multiple modes which you can use to prepare your Swift and Java code to interact with eachother. +The `swift-java` command line tool offers multiple modes which you can use to prepare your Swift and Java code to interact with each other. -The following sections will explain the modes in depth. When in doubt, you can always use the command line `--help` to get additional -guidance about the tool and available options: +When in doubt, you can always use the command line `--help` to get additional guidance about the tool and available options: -```bash -> swift-java --help - -USAGE: swift-java - -OPTIONS: - -h, --help Show help information. - -SUBCOMMANDS: - configure Configure and emit a swift-java.config file based on an input dependency or jar file - resolve Resolve dependencies and write the resulting swift-java.classpath file - wrap-java Wrap Java classes with corresponding Swift bindings. - jextract Wrap Swift functions and types with Java bindings, making them available to be called from Java - - See 'swift-java help ' for detailed help. -``` +@Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "mainHelp") ### Expose Java classes to Swift: swift-java wrap-java @@ -37,37 +21,7 @@ swift-java help wrap-java to produce help output like the following: -``` -USAGE: swift-java wrap-java [--output-directory ] [--input-swift ] [--log-level ] [--cp ...] [--filter-java-package ] --swift-module [--depends-on ...] [--swift-native-implementation ...] [--cache-directory ] [--swift-match-package-directory-structure ] - -ARGUMENTS: - Path to .jar file whose Java classes should be wrapped using Swift bindings - -OPTIONS: - -o, --output-directory - The directory in which to output generated SwiftJava configuration files. - --input-swift - Directory containing Swift files which should be extracted into Java bindings. Also known as 'jextract' mode. Must be paired with --output-java and --output-swift. - -l, --log-level - Configure the level of logs that should be printed (values: trace, debug, info, notice, warning, error, critical; default: log level) - --cp, --classpath Class search path of directories and zip/jar files from which Java classes can be loaded. - -f, --filter-java-package - While scanning a classpath, inspect only types included in this package - --swift-module - The name of the Swift module into which the resulting Swift types will be generated. - --depends-on - A swift-java configuration file for a given Swift module name on which this module depends, - e.g., JavaKitJar=Sources/JavaKitJar/swift-java.config. There should be one of these options - for each Swift module that this module depends on (transitively) that contains wrapped Java sources. - --swift-native-implementation - The names of Java classes whose declared native methods will be implemented in Swift. - --cache-directory - Cache directory for intermediate results and other outputs between runs - --swift-match-package-directory-structure - Match java package directory structure with generated Swift files (default: false) - -h, --help Show help information. - -``` +@Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "wrapJavaHelp") For example, the `JavaKitJar` library is generated with this command line: diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md index b9ad1e5b3..fb166fff1 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -341,7 +341,7 @@ FishBox box = ...; box.feedFish(); // type-safe generated specialized function ``` -You can also possible to cause such specialization to occurr by declaring a typealias in Swift sources: +It is also possible to cause such specialization to occur by declaring a typealias in Swift sources: ```swift typealias FishBox = Box diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md index 09342b771..256028084 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md @@ -20,6 +20,10 @@ The outline of the approach is: You can then use the Swift library from Java by calling the generated methods and initializers. +You can always use the command line `--help` to get additional guidance about the tool and available options: + +@Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "jextractHelp") + For the set of Swift language features that can be extracted, and what each one looks like on the Java side, see . diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md index cb6c092c3..087ced0da 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md @@ -9,7 +9,7 @@ Download Java dependencies for use in Swift builds. `.swift-java.classpath` file into the `--output-directory`. Classpath files are picked up by `swift-java` commands which search for `*.swift-java.classpath` files -to create a classpath for Java operations, e.g. `wrap-java` uses such assebled classpath during wrapping Java classes for Swift. +to create a classpath for Java operations, e.g. `wrap-java` uses such assembled classpath during wrapping Java classes for Swift. You can use `swift-java resolve` explicitly on the command line: @@ -20,6 +20,10 @@ swift-java resolve \ "org.apache.commons:commons-csv:1.10.0" ``` +You can always use the command line `--help` to get additional guidance about the tool and available options: + +@Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "resolveHelp") + Or, include dependency identifiers in gradle format: `::` in `swift-java.config` configuration files, like so: diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md index bb2f06c2f..0be54f1f7 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md @@ -17,6 +17,10 @@ directly - see . For an orientation on which interop tool fits your task, see . +You can always use the command line `--help` to get additional guidance about the tool and available options: + +@Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "wrapJavaHelp") + ### Workflow The typical pipeline has three steps, though the SwiftPM plugin can drive all diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index 020ae8202..606eb72ea 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -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 only supported in 'jni' mode.", ) var enableJavaCallbacks: Bool? diff --git a/Sources/SwiftJavaTool/SwiftJava.swift b/Sources/SwiftJavaTool/SwiftJava.swift index 5a514e2aa..25afbfe12 100644 --- a/Sources/SwiftJavaTool/SwiftJava.swift +++ b/Sources/SwiftJavaTool/SwiftJava.swift @@ -50,7 +50,9 @@ struct SwiftJava: AsyncParsableCommand { try command.run() } } catch { - print("Invocation: \(CommandLine.arguments.joined(separator: " "))") + if Self.exitCode(for: error) != .success { + print("Invocation: \(CommandLine.arguments.joined(separator: " "))") + } exit(withError: error) } } diff --git a/scripts/generate-cli-help-snippets.sh b/scripts/generate-cli-help-snippets.sh new file mode 100755 index 000000000..9446687a8 --- /dev/null +++ b/scripts/generate-cli-help-snippets.sh @@ -0,0 +1,85 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2026 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 +## +##===----------------------------------------------------------------------===## +## +## Captures the live --help output of the swift-java CLI and writes it to +## Snippets/SwiftJavaCLIHelp.sh, sliced per subcommand so the DocC pages can +## embed the *actual* --help/USAGE text via @Snippet instead of a pasted copy +## that can drift out of sync with the tool. +## +## Usage: +## scripts/generate-cli-help-snippets.sh +## +set -eu + +cd "$(dirname "$0")/.." + +OUTPUT_FILE="Snippets/SwiftJavaCLIHelp.sh" + +echo "Building swift-java..." +swift build --product swift-java +BIN_PATH="$(swift build --product swift-java --show-bin-path)/swift-java" + +# Subcommand:slice pairs to capture, in the order they should appear if ever +# listed together. Keep in sync with the subcommands array in +# Sources/SwiftJavaTool/SwiftJava.swift (excluding commands with +# `shouldDisplay: false`, e.g. java-callbacks-build). +SUBCOMMAND_SLICES=( + "configure:configureHelp" + "resolve:resolveHelp" + "wrap-java:wrapJavaHelp" + "jextract:jextractHelp" +) + +echo "Capturing --help output into $OUTPUT_FILE..." +{ + cat <<'HEADER' +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2026 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 +## +##===----------------------------------------------------------------------===## + +# Auto-generated by scripts/generate-cli-help-snippets.sh - DO NOT EDIT. +# Run scripts/generate-docs.sh (or this script directly) to regenerate. +HEADER + + echo + echo "# snippet.mainHelp" + echo "> swift-java --help" + echo + "$BIN_PATH" --help + echo "# snippet.end" + + for entry in "${SUBCOMMAND_SLICES[@]}"; do + subcommand="${entry%%:*}" + slice="${entry##*:}" + echo + echo "# snippet.${slice}" + echo "> swift-java $subcommand --help" + echo + "$BIN_PATH" "$subcommand" --help + echo "# snippet.end" + done +} > "$OUTPUT_FILE" + +echo "Done." diff --git a/scripts/generate-docs.sh b/scripts/generate-docs.sh index 861690a35..f3c2bbf5f 100755 --- a/scripts/generate-docs.sh +++ b/scripts/generate-docs.sh @@ -40,6 +40,9 @@ done echo "Regenerating swift-java.config option docs from Configuration.swift..." swift run generate-config-docs +echo "Preparing help and usage output snippets..." +scripts/generate-cli-help-snippets.sh + # Opt in to the swift-docc-plugin dependency declared in Package.swift. # Set DOCC_PLUGIN_PATH= instead to use a local swift-docc-plugin checkout. export SWIFTJAVA_DOCC_PLUGIN_INSTALL=1 From b5a5c8c606d73bf44c3c101b47bd8aac5eed4c5a Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 5 Aug 2026 15:09:48 +0900 Subject: [PATCH 3/6] add some more examples --- .../Documentation.docc/FeaturesJextract.md | 28 +++++++++++++++---- .../Documentation.docc/SwiftJavaResolve.md | 10 ++++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md index a96112bb9..6a5a6facb 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -181,13 +181,12 @@ supported in JNI mode but not yet in FFM mode. ### Async functions -Asynchronous functions in Swift are extracted using different modes: +Asynchronous functions are imported as Future-type returning Java functions. -- **completable-future (default)**: `async` functions return `java.util.concurrent.CompletableFuture` -- **future**: For legacy platforms (e.g. Android 23 and below) where `CompletableFuture` is not available, `async` functions return `java.util.concurrent.Future`. Enable with `--async-func-mode future` or the `asyncFuncMode` config value. +There are two modes of extracting them, configurable using the `asyncFuncMode` setting in `swift-java.config`, or using the equivalent `--async-func-mode` command line option: -The Java snippet below holds the result in a `Future`, which the default -`CompletableFuture` return type satisfies. +- **completable-future (default)**: `async` functions return `java.util.concurrent.CompletableFuture` +- **future**: For legacy platforms (e.g. Android 23 and below) where `CompletableFuture` is not available, `async` functions return `java.util.concurrent.Future`. @TabNavigator { @Tab("Swift") { @@ -201,6 +200,24 @@ The Java snippet below holds the result in a `Future`, which the default } } +#### Asynchronous functions and Kotlin async/await + +Because Kotlin offers extension `suspending fun` methods on Future types, it is possible to `.await()` +on such Swift extracted `async` function in Kotlin, like so: + +```kotlin +import com.example.swift.MySwiftLibrary +import kotlinx.coroutines.future.await + +suspend fun useAsyncSum() { + // MySwiftLibrary.asyncSum returns a java.util.concurrent.CompletableFuture + val result: Long = MySwiftLibrary.asyncSum(10, 12).await() + println("result = $result") +} +``` + +This relies on the Kotlin `kotlinx-coroutines-jdk8` library which adds the `await()` extension function on `java.util.concurrent.CompletableFuture`. + ### Collections Swift's collection types cross the boundary either by copying (arrays) or by @@ -243,7 +260,6 @@ arrays (`[[UInt8]]`, `[[String]]`) are supported in JNI mode. Fixed-size inline arrays (Swift's `InlineArray`, sugar `[N of T]`) are recognized by jextract in JNI mode and imported with an equivalent Java surface. -Not yet supported in FFM mode. #### Dictionaries diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md index 087ced0da..63c59a278 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md @@ -11,6 +11,12 @@ Download Java dependencies for use in Swift builds. Classpath files are picked up by `swift-java` commands which search for `*.swift-java.classpath` files to create a classpath for Java operations, e.g. `wrap-java` uses such assembled classpath during wrapping Java classes for Swift. +You can always use the command line `--help` to get additional guidance about the tool and available options: + +@Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "resolveHelp") + +### Example + You can use `swift-java resolve` explicitly on the command line: ```bash @@ -20,10 +26,6 @@ swift-java resolve \ "org.apache.commons:commons-csv:1.10.0" ``` -You can always use the command line `--help` to get additional guidance about the tool and available options: - -@Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "resolveHelp") - Or, include dependency identifiers in gradle format: `::` in `swift-java.config` configuration files, like so: From 26a4ec72bd7470861659c412574efb521a2ea2fc Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 5 Aug 2026 16:05:41 +0900 Subject: [PATCH 4/6] update contributing instructions to mention the docs generation --- .github/scripts/validate_docs.sh | 6 ++ CONTRIBUTING.md | 4 ++ .../Documentation.docc/FeaturesJextract.md | 60 ++++++++++++++----- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/.github/scripts/validate_docs.sh b/.github/scripts/validate_docs.sh index 73e4edc6f..fed346489 100755 --- a/.github/scripts/validate_docs.sh +++ b/.github/scripts/validate_docs.sh @@ -5,6 +5,12 @@ set -x swift run generate-config-docs --check +./scripts/generate-cli-help-snippets.sh +if ! git diff --exit-code -- Snippets/SwiftJavaCLIHelp.sh; then + echo "::error::Snippets/SwiftJavaCLIHelp.sh is out of date with the swift-java CLI's --help output (likely a command's flags or abstract text changed). Run scripts/generate-cli-help-snippets.sh and commit the result." + exit 1 +fi + DEPENDENCY='.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0")' if grep -q "$DEPENDENCY" Package.swift; then diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05b02de7f..319daf6d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,6 +103,10 @@ Which makes the script execute, and only allow the `git push` to complete if the In the case of formatting issues, you can then `git add` the formatting changes, and attempt the push again. +### Regenerating generated docs + +Some documentation is generated from source and checked in, so CI can catch it drifting out of date. If you changed `Configuration.swift` or a CLI command's flags/help text, run `scripts/generate-docs.sh` and commit the generated changes. + ## How to contribute your work Please open a pull request at https://github.com/swiftlang/swift-java. Make sure the CI passes, and then wait for code review. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md index 6a5a6facb..d21e47f4f 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -283,7 +283,7 @@ and does not copy it. Use `SwiftDictionaryMap::toJava` to explicitly copy into a Swift sets (`Set`) are imported using the `SwiftSet` Java wrapper. Like dictionaries, the wrapper points at the Swift value on the Swift heap and does not -copy elements until explicitly requested. +copy it. Use `SwiftSet::toJava` to explicitly copy into a Java `Set`. @TabNavigator { @Tab("Swift") { @@ -300,13 +300,14 @@ copy elements until explicitly requested. ### Bytes and buffers Raw bytes are the most common thing to hand across the language boundary, and -jextract offers several shapes for it depending on whether you want a copy or a +SwiftJava offers several shapes for it depending on whether you want a copy or a view of the memory. #### Byte arrays -`[UInt8]` maps to Java's `byte[]` in both modes, by copying. Note that Java's -`byte` is signed, so a Swift `UInt8` of `200` reads as `-56` on the Java side; +`[UInt8]` maps to Java's `byte[]` in both modes by copying. + +Note that Java's `byte` is signed, so a Swift `UInt8` of `200` reads as `-56` on the Java side; see . @TabNavigator { @@ -321,8 +322,7 @@ see . } } -Nested byte arrays (`[[UInt8]]`) are supported in JNI mode; see -. +Nested byte arrays (`[[UInt8]]`) are also supported in JNI mode. #### Raw byte buffers @@ -332,12 +332,12 @@ with the buffer surfacing differently on the Java side. In JNI mode it is a plain `byte[]`. jextract obtains the array's elements for the duration of the call (via JNI's `GetByteArrayElements`) and hands Swift a buffer over them; whether that memory is the array itself or a copy is up to the JVM. + In FFM mode it is a `MemorySegment`, which Swift reads in place without any copy. In both modes the buffer is only valid for the duration of the call, so the Swift side must not store it. -`UnsafeMutableRawBufferPointer` is supported the same way, and in JNI mode writes -made by Swift are committed back to the Java array when the call returns. +`UnsafeMutableRawBufferPointer` is supported the same way: @TabNavigator { @Tab("Swift") { @@ -351,8 +351,7 @@ made by Swift are committed back to the Java array when the call returns. } } -In FFM mode a Swift closure parameter taking a buffer also hands the segment -straight to the Java lambda: +It is also possible to write closures which accept a buffer from the Swift side, like this: @TabNavigator { @Tab("Swift") { @@ -368,20 +367,27 @@ straight to the Java lambda: In FFM mode the generated `Data` wrapper converts to and from `java.nio.ByteBuffer`. Bringing bytes in: -@Snippet(path: "Snippets/DataJavaFFM", slice: "byteBufferUsageJava") +@TabNavigator { + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/DataJavaFFM", slice: "byteBufferUsageJava") + } +} and getting them back out: -@Snippet(path: "Snippets/DataJavaFFM", slice: "byteBufferToUsageJava") +@TabNavigator { + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/DataJavaFFM", slice: "byteBufferToUsageJava") + } +} In JNI mode, `Data.toByteArray()` is the way to copy bytes out. For the `Data` type itself see . -#### MemorySegment +#### Java MemorySegment (FFM only) -`toMemorySegment(arena)` hands back the bytes as a `java.lang.foreign.MemorySegment`, -which the JVM reads directly without copying. Use `withUnsafeBytes` when you only -need to read the bytes and do not want to materialize anything at all. +`Data.toMemorySegment(arena)` hands back the bytes as a `java.lang.foreign.MemorySegment`, +which the JVM reads directly without copying. > Important: `MemorySegment` is part of the Foreign Function & Memory API > ([JEP 454](https://openjdk.org/jeps/454)) and requires JDK 25+. It is available @@ -428,6 +434,28 @@ like `typealias FishBox = Box` and performs _specialization_ - exposing a `FishBox` carries the constrained extension's `describeFish()` in addition to `Box`'s own members, and unlike the generic `Box` it has no Java type parameter. +#### Specializing via swift-java.config + +The `typealias` above works when you control the Swift source being wrapped. If +you don't, for example you are running jextract against a dependency you can't +edit, the same specialization can be requested from `swift-java.config` using the +`specialize` key, without touching the Swift source at all: + +```json +{ + "specialize": { + "FishBox": { + "base": "Box", + "typeArgs": {"Element": "Fish"} + } + } +} +``` + +This produces the same `FishBox` Java class, with the same constrained +extension members applied, as the `typealias FishBox = Box` approach. +See for the full field reference. + ### Tuples Tuples are imported as `Tuple2`, `Tuple3`, etc. types with positional `$0`, `$1` accessors. From 758c0ca9bd30dabc1eaa5027a595ea611f7ab42c Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 5 Aug 2026 16:51:49 +0900 Subject: [PATCH 5/6] make docs only sample into a real one Samples/JavaKitSampleApp and lots of small fixups --- CONTRIBUTING.md | 21 ++- README.md | 101 ++++++++------- Samples/JavaKitSampleApp/Package.swift | 4 +- .../SwiftArgumentParserMain.swift | 60 +++++++++ .../swift/SwiftArgumentParserMain.java | 34 +++++ .../SwiftArgumentParserMainRuntimeTests.swift | 50 ++++++++ Samples/JavaKitSampleApp/ci-validate.sh | 22 +++- Samples/JavaProbablyPrime/README.md | 4 +- Samples/JavaSieve/README.md | 6 +- Snippets/ArgumentParserMainJava.java | 1 + .../ArgumentParserMainSwift.swift.symlink | 1 + Snippets/SwiftJavaCLIHelp.sh | 9 +- .../JavaStdlib/README_PACKAGE_CONVENTION.md | 8 +- .../SwiftJava/Documentation.docc/SwiftJava.md | 51 ++++---- .../Documentation.docc/Android.md | 8 +- .../ExampleCommandLineProgram.md | 93 ++++++++------ .../FeaturesJavaKitMacros.md | 120 ++++++++++++------ .../Documentation.docc/FeaturesJextract.md | 41 +++--- .../Documentation.docc/FeaturesOverview.md | 37 +++--- .../SwiftJavaCommandLineTool.md | 33 +++-- .../Documentation.docc/SwiftJavaResolve.md | 13 +- .../Documentation.docc/SwiftJavaWrapJava.md | 48 ++----- .../Documentation.docc/SwiftPMPlugin.md | 48 ++++--- .../Documentation.docc/index.md | 38 +++--- .../Commands/WrapJavaCommand.swift | 5 +- 25 files changed, 540 insertions(+), 316 deletions(-) create mode 100644 Samples/JavaKitSampleApp/Sources/JavaKitExample/SwiftArgumentParserMain.swift create mode 100644 Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/SwiftArgumentParserMain.java create mode 100644 Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/SwiftArgumentParserMainRuntimeTests.swift create mode 120000 Snippets/ArgumentParserMainJava.java create mode 120000 Snippets/ArgumentParserMainSwift.swift.symlink diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 319daf6d5..910f4de5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Please ensure to specify the following: * Commit hash * Contextual information (e.g. what you were trying to achieve with swift-java) * Simplest possible steps to reproduce - * More complex the steps are, lower the priority will be. + * The more complex the steps, the lower the priority. * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. * Anything that might be relevant in your opinion, such as: * Swift version or the output of `swift --version` @@ -24,7 +24,7 @@ Please ensure to specify the following: Commit hash: b17a8a9f0f814c01a56977680cb68d8a779c951f Context: -While testing my application that uses with swift-openapi-generator, I noticed that ... +While testing my application that uses swift-java, I noticed that ... Steps to reproduce: 1. ... @@ -78,7 +78,7 @@ reflected in your working directory: % act --bind workflow_call -j soundness --input format_check_enabled=true ``` -If you'd like `act` to always run with certain flags, these can be be placed in +If you'd like `act` to always run with certain flags, these can be placed in an `.actrc` file either in the current working directory or your home directory, for example: @@ -88,20 +88,19 @@ directory, for example: --action-offline-mode ``` -For frequent contributors, we recommend adding the script as a [git pre-push hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), which you can do via executing the following command in the project root directory: +For frequent contributors, we recommend running the soundness checks before pushing. You can wire that up as a [git pre-push hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks): ```bash -cat << EOF > .git/hooks/pre-push - -if [[ -f "scripts/soundness.sh" ]]; then - scripts/soundness.sh -fi +cat << 'EOF' > .git/hooks/pre-push +#!/bin/sh +exec act workflow_call -j soundness --input format_check_enabled=true --input shell_check_enabled=true EOF +chmod +x .git/hooks/pre-push ``` -Which makes the script execute, and only allow the `git push` to complete if the check has passed. +This only allows the `git push` to complete if the checks pass. -In the case of formatting issues, you can then `git add` the formatting changes, and attempt the push again. +In the case of formatting issues, `git add` the formatting changes and attempt the push again. ### Regenerating generated docs diff --git a/README.md b/README.md index 32a23b5e1..4eaaad461 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ This project contains tools and libraries that facilitate **Swift & Java Interop ## Introduction -If you'd like to check out a quick introduction to Swift & Java interoperability, you may be interested in this presentation from WWDC25: [WWDC25: Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA). +For a quick introduction to Swift & Java interoperability, see the WWDC25 session [Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA). -While we work on more quickstarts and documentation, please refer to the Sample projects located in [Samples/](Samples/) that showcase the various ways you can use swift-java in your Swift or Java projects. +While we work on more quickstarts and documentation, refer to the sample projects in [Samples/](Samples/), which showcase the various ways to use swift-java in Swift or Java projects. ## Dependencies @@ -42,18 +42,17 @@ sdk install java 25.0.1-amzn ## Self-publish supporting Java libraries -Swift-java relies on supporting libraries that are under active development and not yet published to Maven Central. To use the project, you'll need to self-publish these libraries locally so your Java project can depend on them. +swift-java relies on supporting libraries that are under active development and not yet published to Maven Central. To use the project, you'll need to self-publish these libraries locally so your Java project can depend on them. -To publish the libraries to your local maven repository (`$HOME/.m2`), you can run: +To publish the libraries to your local maven repository (`$HOME/.m2`), run this in `swift-java/`: -``` -// in swift-java/ +```bash ./gradlew publishToMavenLocal ``` -To consume these libraries in your Java project built using Gradle, you can then include the local repository in the repositories to resolve dependencies from: +To consume these libraries in your Java project built using Gradle, include the local repository in the repositories to resolve dependencies from: -``` +```kotlin repositories { mavenLocal() mavenCentral() @@ -62,44 +61,45 @@ repositories { We anticipate simplifying this in the future. -## SwiftJava macros +## SwiftJava macros: calling Java from Swift -SwiftJava is a Swift library offering macros which simplify writing JNI code "by hand" but also calling Java code from Swift. +SwiftJava is a Swift library offering macros which simplify writing JNI code "by hand", and calling Java code from Swift. -It is possible to generate Swift bindings to Java libraries using SwiftJava by using the `swift-java wrap-java` command. +You can also generate Swift bindings to Java libraries with the `swift-java wrap-java` command. Required language/runtime versions: - **JDK 17+**, any recent JDK installation should be sufficient, as only general reflection and JNI APIs are used by this integration - **Swift 6.2.x**, because the library uses modern Swift macros -**swift-java jextract** +## swift-java jextract: calling Swift from Java + +`jextract` is a source generator which **generates Java bindings to existing Swift libraries**. +Its inputs are Swift sources or packages, and its outputs are the generated Swift and Java code needed to call those functions efficiently from Java. -Is a source generator which will **generate Java bindings to existing Swift libraries**. -Its inputs are Swift sources or packages, and outputs are generated Swift and Java code necessary to call these functions efficiently from Java. +It has two modes, `ffm` and `jni`. -## swift-java jextract --mode=ffm (default) +### swift-java jextract --mode=ffm (default) -This mode provides the most flexibility and performance, and allows to decrease the amount of data being copied between Swift and Java. -This does require the use of the relatively recent [JEP-454: Foreign Function & Memory API](https://openjdk.org/jeps/454), which is only available since JDK22, and will become part of JDK LTS releases with JDK 25 (depending on your JDK vendor). +This mode offers the most flexibility and performance, and can reduce the amount of data copied between Swift and Java. +It requires [JEP-454: Foreign Function & Memory API](https://openjdk.org/jeps/454), which is final since JDK 22. This is the primary way we envision calling Swift code from server-side Java libraries and applications. Required language/runtime versions: - **Swift 6.2**, because of dependence on rich swift interface files - **JDK 25+** - - We are validating the implementation using the currently supported non-LTE release, which at present means JDK-25. + - We validate the implementation against the currently supported non-LTS release, which at present means JDK 25. -## swift-java jextract --mode=jni +### swift-java jextract --mode=jni -In this mode, the generated sources will use the legacy JNI approach to calling native code. +In this mode the generated sources use JNI to call native code. -This mode is more limited in some performance and flexibility that it can offer, however it is the most compatible, since even very old JVM's as well as even Android systems can be supported by this mode. -We recommend this mode when FFM is not available, or wide ranging deployment compatibility is your priority. When performance is paramaunt, we recommend the FFM mode instead. +This mode offers less performance and flexibility than FFM, but it is the most compatible: it works on older JVMs as well as on Android. +Use it when FFM is not available, or when wide deployment compatibility is your priority. When performance is paramount, prefer FFM. Required language/runtime versions: -- **Swift 6.2**, because of dependence on rich swift interface files -- **Java 7+**, including - +- **Swift 6.2**, because of dependence on rich swift interface files +- **JDK 17+**; the generated Java sources target the `javaSourceLevel` setting, which supports 17 through 25 (default 22) ## Development and Testing @@ -107,20 +107,20 @@ This project contains multiple builds, living side by side together. You will need to have: - Swift (6.2.x+) -- Java (25+ for FFM, even though we support lower JDK targets) +- Java (25+ to build this project, even though the published libraries target lower JDK versions) - Gradle (installed by "Gradle wrapper" automatically when you run gradle through `./gradlew`) ### Preparing your environment -Install **Swift**, the easiest way to do this is to use **Swiftly**: [swift.org/install/](https://www.swift.org/install/). -This should automatically install a recent Swift, but you can always make sure by running: +Install **Swift**; the easiest way is [Swiftly](https://www.swift.org/install/). +This installs a recent Swift, but you can pin the version explicitly: ```bash swiftly install 6.2 --use ``` -Install a recent enough Java distribution. We validate this project using Corretto so you can choose to use that as well, -however any recent enough Java distribution should work correctly. You can use sdkman to install Java: +Install a recent enough Java distribution. We validate this project using Corretto, so you may want to use that as well, +though any recent enough distribution should work. You can use sdkman to install Java: ```bash # Install sdkman from: https://sdkman.io @@ -142,8 +142,7 @@ export JAVA_HOME="$(sdk home java current)" ### Testing your changes -Many tests, including source generation tests, are written in Swift and you can execute them all by running the -swift package manager test command: +Many tests, including source generation tests, are written in Swift. Run them all with: ```bash > swift test @@ -151,43 +150,43 @@ swift package manager test command: When adding tests in `Tests/...` targets, you can run these tests (or filter a specific test using `swift test --filter type-or-method-name`). -Some tests are implemented in Java and therefore need to be executed using Gradle. -Please always use the gradle wrapper (`./gradlew`) to make sure to use the appropriate Gradle version +Some tests are implemented in Java and need to be executed using Gradle. +Always use the Gradle wrapper (`./gradlew`) so the correct Gradle version is used: ```bash > ./gradlew test ``` -> Tip: A lot of the **runtime tests** for code relying on `jextract` are **located in sample apps**, -> so if you need to runtime test any code relying on source generation steps of jextract, consider adding the tests -> to an appropriate Sample. These tests are also executed in CI (which you can check in the `ci-validate.sh` script -> contained in every sample repository). +> Tip: Many of the **runtime tests** for code relying on `jextract` are **located in sample apps**, +> so if you need to runtime test code that depends on jextract's source generation, consider adding the tests +> to an appropriate Sample. These tests also run in CI; see the `ci-validate.sh` script in each sample +> that has one. ### Sample apps & tests Sample apps are located in the `Samples/` directory, and they showcase full "roundtrip" usage of the library and/or tools. -Samples are build by default by Gradle. Building samples can be skipped by appending the flag `-PskipSamples=true` to a gradle command. +Samples are built by default by Gradle. Building samples can be skipped by appending the flag `-PskipSamples=true` to a gradle command. -#### SwiftJava (Swift -> Java) +#### SwiftJava (calling Java from Swift) -To run a simple app showcasing a Swift process calling into a Java library you can run: +To run a simple app showcasing a Swift process calling into a Java library: ```bash -cd Samples/SwiftJavaExtractFFMSampleApp -./ci-validate.sh # which is just `swift build` and a `java -cp ...` invocation of the compiled program +cd Samples/JavaProbablyPrime +./ci-validate.sh # which is just a `swift run JavaProbablyPrime 1337` ``` -#### jextract (Java -> Swift) +#### jextract (calling Swift from Java) -To run a simple example app showcasing the jextract (Java calling Swift) approach you can: +To run a simple example app showcasing a Java program calling into Swift: ```bash ./gradlew Samples:SwiftJavaExtractFFMSampleApp:run ``` -This will also generate the necessary sources (by invoking jextract, extracting the `Sources/ExampleSwiftLibrary`) -and generating Java sources in `src/generated/java`. +This also generates the necessary sources (by invoking jextract on `Sources/MySwiftLibrary`) +and the Java sources in `src/generated/java`. #### Other sample apps @@ -197,7 +196,7 @@ Please refer to the [Samples](Samples) directory for more sample apps which show You can run Swift [ordo-one/package-benchmark](https://github.com/ordo-one/package-benchmark) and OpenJDK [JMH](https://github.com/openjdk/jmh) benchmarks in this project. -Swift benchmarks are located under `Benchmarks/` and JMH benchmarks are currently part of the SwiftKit sample project: `Samples/SwiftJavaExtractFFMSampleApp/src/jmh` because they depend on generated sources from the sample. +Swift benchmarks are located under `Benchmarks/`. JMH benchmarks live in `Samples/SwiftJavaExtractFFMSampleApp/src/jmh`, because they depend on sources generated by that sample. ### Swift benchmarks @@ -217,7 +216,7 @@ cd Samples/SwiftJavaExtractFFMSampleApp ./gradlew jmh ``` -Please read documentation of both performance testing tools and understand that results must be interpreted and not just taken at face value. Benchmarking is tricky and environment sensitive task, so please be careful when constructing and reading benchmarks and their results. If in doubt, please reach out on the forums. +Please read the documentation of both performance testing tools, and note that results must be interpreted rather than taken at face value. Benchmarking is tricky and environment sensitive, so be careful when constructing and reading benchmarks and their results. If in doubt, reach out on the forums. ## User Guide @@ -241,6 +240,6 @@ xcrun docc preview Sources/SwiftJavaDocumentation/Documentation.docc ## Project Status -**This project is under active development. We welcome feedback about any issues you encounter.** +**This project is under active development. We welcome feedback about any issues you encounter.** -There is no guarantee about API stability until the project reaches a 1.0 release. +There is no guarantee of API stability until the project reaches a 1.0 release. diff --git a/Samples/JavaKitSampleApp/Package.swift b/Samples/JavaKitSampleApp/Package.swift index b01a77bb3..f9746f2f4 100644 --- a/Samples/JavaKitSampleApp/Package.swift +++ b/Samples/JavaKitSampleApp/Package.swift @@ -22,7 +22,8 @@ let package = Package( ], dependencies: [ - .package(name: "swift-java", path: "../../") + .package(name: "swift-java", path: "../../"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ @@ -32,6 +33,7 @@ let package = Package( .product(name: "SwiftJava", package: "swift-java"), .product(name: "JavaUtilFunction", package: "swift-java"), .product(name: "JavaUtilJar", package: "swift-java"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), ], swiftSettings: [ .swiftLanguageMode(.v5) diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/SwiftArgumentParserMain.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/SwiftArgumentParserMain.swift new file mode 100644 index 000000000..610fca571 --- /dev/null +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/SwiftArgumentParserMain.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 ArgumentParser +import SwiftJava +import SwiftJavaJNICore + +@JavaClass("com.example.swift.SwiftArgumentParserMain") +open class SwiftArgumentParserMain: JavaObject { +} + +/// Lets tests call the Java `static native` entry point, which dispatches back +/// into the Swift implementation below +extension JavaClass { + @JavaStaticMethod + public func runSwiftMain(_ args: [String]) -> String +} + +/// Describes the Java `native` methods of ``SwiftArgumentParserMain``. +protocol SwiftArgumentParserMainNativeMethods { + static func runSwiftMain(_ args: [String], environment: JNIEnvironment) -> String +} + +// snippet.argumentParserImplementation +/// The command that the Java entry point delegates its arguments to. +struct HelloCommand: ParsableCommand { + @Flag(name: .shortAndLong, help: "Enable verbose output") + var verbose: Bool = false + + @Argument(help: "Who to greet") + var name: String = "world" + + func greeting() -> String { + verbose ? "Hello, \(name)! (verbose)" : "Hello, \(name)!" + } +} + +@JavaImplementation("com.example.swift.SwiftArgumentParserMain") +extension SwiftArgumentParserMain: SwiftArgumentParserMainNativeMethods { + @JavaMethod + static func runSwiftMain(_ args: [String], environment: JNIEnvironment) -> String { + do { + return try HelloCommand.parse(args).greeting() + } catch { + return HelloCommand.fullMessage(for: error) + } + } +} +// snippet.end diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/SwiftArgumentParserMain.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/SwiftArgumentParserMain.java new file mode 100644 index 000000000..a810b77f4 --- /dev/null +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/SwiftArgumentParserMain.java @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +// snippet.argumentParserMain +/** + * A Java entry point whose implementation lives in Swift, where the arguments + * are parsed using swift-argument-parser. + */ +public class SwiftArgumentParserMain { + static { + System.loadLibrary("JavaKitExample"); + } + + /** Implemented in Swift, see SwiftArgumentParserMain.swift */ + public static native String runSwiftMain(String[] args); + + public static void main(String[] args) { + System.out.println(runSwiftMain(args)); + } +} +// snippet.end diff --git a/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/SwiftArgumentParserMainRuntimeTests.swift b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/SwiftArgumentParserMainRuntimeTests.swift new file mode 100644 index 000000000..9a2267b27 --- /dev/null +++ b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/SwiftArgumentParserMainRuntimeTests.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaKitExample +import SwiftJava +import Testing + +/// Exercises a Java `static native` entry point whose implementation is written +/// in Swift and parses its arguments with swift-argument-parser. +@Suite +struct SwiftArgumentParserMainRuntimeTests { + + let jvm = try JavaKitSampleJVM.shared + + @Test + func defaultArguments() throws { + let mainClass = try JavaClass(environment: jvm.environment()) + #expect(mainClass.runSwiftMain([]) == "Hello, world!") + } + + @Test + func positionalArgument() throws { + let mainClass = try JavaClass(environment: jvm.environment()) + #expect(mainClass.runSwiftMain(["Swift"]) == "Hello, Swift!") + } + + @Test + func verboseFlag() throws { + let mainClass = try JavaClass(environment: jvm.environment()) + #expect(mainClass.runSwiftMain(["--verbose", "Swift"]) == "Hello, Swift! (verbose)") + } + + @Test + func invalidArgumentsReportParserError() throws { + let mainClass = try JavaClass(environment: jvm.environment()) + let output = mainClass.runSwiftMain(["--nope"]) + #expect(output.contains("--nope")) + } +} diff --git a/Samples/JavaKitSampleApp/ci-validate.sh b/Samples/JavaKitSampleApp/ci-validate.sh index 8568e115f..9fc98bed3 100755 --- a/Samples/JavaKitSampleApp/ci-validate.sh +++ b/Samples/JavaKitSampleApp/ci-validate.sh @@ -12,12 +12,26 @@ fi swift build --build-tests $DISABLE_EXPERIMENTAL_PREBUILTS +run_java() { + "$JAVA_HOME/bin/java" \ + -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ + -Djava.library.path=.build/debug \ + "$@" +} + echo "java application run: ..." -"$JAVA_HOME/bin/java" \ - -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ - -Djava.library.path=.build/debug \ - "com.example.swift.JavaKitSampleMain" +run_java "com.example.swift.JavaKitSampleMain" echo "java application run: OK" +# A Java entry point whose implementation and argument parsing live in Swift +echo "java application run (swift-argument-parser main): ..." +ARG_PARSER_OUTPUT="$(run_java "com.example.swift.SwiftArgumentParserMain" --verbose Swift)" +echo "$ARG_PARSER_OUTPUT" +if [ "$ARG_PARSER_OUTPUT" != "Hello, Swift! (verbose)" ]; then + echo "error: expected 'Hello, Swift! (verbose)' but got '$ARG_PARSER_OUTPUT'" + exit 1 +fi +echo "java application run (swift-argument-parser main): OK" + swift test $DISABLE_EXPERIMENTAL_PREBUILTS \ No newline at end of file diff --git a/Samples/JavaProbablyPrime/README.md b/Samples/JavaProbablyPrime/README.md index 819ecf047..ca0015986 100644 --- a/Samples/JavaProbablyPrime/README.md +++ b/Samples/JavaProbablyPrime/README.md @@ -8,5 +8,5 @@ swift run JavaProbablyPrime The package itself demonstrates how to: -* Use the Java2Swift build tool plugin to wrap the `java.math.BigInteger` type in Swift. -* Create an instance of `BigInteger` in Swift and use its `isProbablyPrime`. +* Use the `SwiftJavaPlugin` build tool plugin to wrap the `java.math.BigInteger` type in Swift. +* Create an instance of `BigInteger` in Swift and call its `isProbablePrime` method. diff --git a/Samples/JavaSieve/README.md b/Samples/JavaSieve/README.md index 09fbde83f..93a5ae5ca 100644 --- a/Samples/JavaSieve/README.md +++ b/Samples/JavaSieve/README.md @@ -2,8 +2,8 @@ This package contains an example program that demonstrates importing a Java library distributed as a Jar file into Swift and using some APIs from that library. It demonstrates how to: -* Use the Java2Swift tool to discover the classes in a Jar file and make them available in Swift -* Layer Swift wrappers for Java classes as separate Swift modules using Java2Swift +* Use the `swift-java` tool to discover the classes in a Jar file and make them available in Swift +* Layer Swift wrappers for Java classes as separate Swift modules * Access static methods of Java classes from Swift This example wraps an [open-source Java library](https://github.com/gazman-sdk/quadratic-sieve-Java) implementing the [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes) algorithm for finding prime numbers, among other algorithms. To get started, clone that repository and build a Jar file containing the library: @@ -24,7 +24,7 @@ swift run JavaSieve The core of the example code is in `Sources/JavaSieve/main.swift`, using the static Java method `SieveOfEratosthenes.findPrimes`: ```swift -let sieveClass = try JavaClass(in: jvm.environment()) +let sieveClass = try JavaClass(environment: jvm.environment()) for prime in sieveClass.findPrimes(100)! { print("Found prime: \(prime.intValue())") } diff --git a/Snippets/ArgumentParserMainJava.java b/Snippets/ArgumentParserMainJava.java new file mode 120000 index 000000000..c8d5d932f --- /dev/null +++ b/Snippets/ArgumentParserMainJava.java @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/SwiftArgumentParserMain.java \ No newline at end of file diff --git a/Snippets/ArgumentParserMainSwift.swift.symlink b/Snippets/ArgumentParserMainSwift.swift.symlink new file mode 120000 index 000000000..6648960d2 --- /dev/null +++ b/Snippets/ArgumentParserMainSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/SwiftArgumentParserMain.swift \ No newline at end of file diff --git a/Snippets/SwiftJavaCLIHelp.sh b/Snippets/SwiftJavaCLIHelp.sh index d354d0a23..97494f340 100644 --- a/Snippets/SwiftJavaCLIHelp.sh +++ b/Snippets/SwiftJavaCLIHelp.sh @@ -157,10 +157,11 @@ OPTIONS: A swift-java configuration file for a given Swift module name on which this module depends, e.g., - JavaKitJar=Sources/JavaKitJar/swift-java.config. - There should be one of these options - for each Swift module that this module depends on - (transitively) that contains wrapped Java sources. + JavaUtil=Sources/JavaStdlib/JavaUtil/swift-java.config. + There should be one of these + options for each Swift module that this module + depends on (transitively) that contains + wrapped Java sources. --swift-native-implementation The names of Java classes whose declared native methods will be implemented in Swift. diff --git a/Sources/JavaStdlib/README_PACKAGE_CONVENTION.md b/Sources/JavaStdlib/README_PACKAGE_CONVENTION.md index ab65fa173..4ab5c80bd 100644 --- a/Sources/JavaStdlib/README_PACKAGE_CONVENTION.md +++ b/Sources/JavaStdlib/README_PACKAGE_CONVENTION.md @@ -1,13 +1,13 @@ # Extracted Java Modules This directory contains Swift bindings for common Java standard library packages. -These pre-built bindings to solve a circular dependency problem - SwiftJava tools need these types to process and generate other bindings. +These pre-built bindings solve a circular dependency problem: the SwiftJava tools need these types in order to generate other bindings. You can also use these bindings directly in your SwiftJava programs to call Java classes without having to generate wrappers each time. -The naming follows this pattern: Java package names become Swift target names. Example: `java.lang.util` becomes `JavaLangUtil`. +The naming follows this pattern: Java package names become Swift target names. Example: `java.util` becomes `JavaUtil`, and `java.lang.reflect` becomes `JavaLangReflect`. -Since Swift doesn't have namespaces like Java, all types appear at the top level in Swift. To avoid naming conflicts, -some types may be prefixed with 'J' (e.g. `JList` to avoid confusion with Swift native types). +Since Swift doesn't have namespaces like Java, all types appear at the top level in Swift. To avoid naming conflicts, +some types are prefixed with `Java` (e.g. `java.lang.String` becomes `JavaString`, to avoid clashing with Swift's `String`). To see which Java types are included and any naming changes, check the `swift-java.config` file in each module. \ No newline at end of file diff --git a/Sources/SwiftJava/Documentation.docc/SwiftJava.md b/Sources/SwiftJava/Documentation.docc/SwiftJava.md index d4b5dacd7..48431a489 100644 --- a/Sources/SwiftJava/Documentation.docc/SwiftJava.md +++ b/Sources/SwiftJava/Documentation.docc/SwiftJava.md @@ -73,7 +73,7 @@ if bigInt.isProbablePrime(10) { } ``` -Swift ensures that the Java garbage collector will keep the object alive until `bigInt` (and any copies of it) are been destroyed. +Swift keeps a reference that stops the Java garbage collector from reclaiming the object until `bigInt` (and any copies of it) are destroyed. ### Creating a Java Virtual Machine instance from Swift @@ -99,8 +99,8 @@ let bigInt = BigInteger(veryBigNumber, environment: jniEnvironment) Java libraries are often distributed as Jar files. The `swift-java` tool can inspect a Jar file to create a `swift-java.config` file that will wrap all of the public classes for use in Swift. Following the example in `swift-java/Samples/JavaSieve`, we will wrap a small [Java library for computing prime numbers](https://github.com/gazman-sdk/quadratic-sieve-Java) for use in Swift. Assuming we have a Jar file `QuadraticSieve-1.0.jar` in the package directory, run the following command: -```swift -swift-java configure --swift-module JavaSieve --jar QuadraticSieve-1.0.jar +```bash +swift-java configure --jar --swift-module JavaSieve --cp QuadraticSieve-1.0.jar ``` The resulting configuration file will look something like this: @@ -142,7 +142,7 @@ As with the previous `JavaProbablyPrime` sample, the `JavaSieve` target in `Pack If you inspect the build output, there are a number of warnings that look like this: -```swift +``` warning: Unable to translate 'com.gazman.quadratic_sieve.QuadraticSieve' method 'generateN': Java class 'java.math.BigInteger' has not been translated into Swift ``` @@ -160,7 +160,7 @@ These warnings mean that some of the APIs in the Java library aren't available i ), ``` -Then define a a swift-java configuration file in `Sources/JavaMath/swift-java.config` to bring in the types we need: +Then define a swift-java configuration file in `Sources/JavaMath/swift-java.config` to bring in the types we need: ```json { @@ -200,7 +200,8 @@ let primes = sieveClass.findPrimes(100) // returns a List? Putting it all together, we can define a main program in `Sources/JavaSieve/main.swift` that looks like this: ```swift -import SwiftJNI +import JavaMath +import SwiftJava let jvm = try JavaVirtualMachine.shared(classpath: ["QuadraticSieve-1.0.jar"]) do { @@ -217,19 +218,19 @@ Note that we are passing the Jar file in the `classpath` argument when initializ ### Downcasting -All Java classes available in Swift provide `is` and `as` methods to check whether an object dynamically matches another type. The `is` operation is the equivalent of Java's `instanceof` and Swift's `is` operator, and will checking whether a given object is of the specified type, e.g., +All Java classes available in Swift provide `is` and `as` methods to check whether an object dynamically matches another type. The `is` operation is the equivalent of Java's `instanceof` and Swift's `is` operator, and checks whether a given object is of the specified type, e.g., ```swift -if myObject.is(URL.self) { - // myObject is a Java URL. +if myObject.is(JavaURL.self) { + // myObject is a java.net.URL } ``` Often, one also wants to cast to that type. The `as` method returns an optional of the specified type, so it works well with `if let`: ```swift -if let url = myObject.as(URL.self) { - // okay, url is a Java URL +if let url = myObject.as(JavaURL.self) { + // okay, url is a java.net.URL } ``` @@ -253,10 +254,10 @@ public class HelloSwift { On the Swift side, the Java class needs to be exposed to Swift through `swift-java.config`, e.g.,: -```swift +```json { "classes" : { - "org.swift.swiftjava.example.HelloSwift" : "Hello", + "org.swift.swiftjava.example.HelloSwift" : "Hello" } } ``` @@ -278,9 +279,9 @@ Java native methods that throw any checked exception should be marked as `throws The Swift implementations of Java `native` constructors and static methods require an additional Swift parameter `environment: JNIEnvironment? = nil`, which will receive the JNI environment in which the function is being executed. In case of nil, the `JavaVirtualMachine.shared().environment()` value will be used. -## SwiftJava: Using Java libraries from Swift +## Reference: how Java maps into Swift -This section describes how Java libraries and mapped into Swift and their use from Swift. +This section describes how Java libraries are mapped into Swift, and how to use them from Swift. ### Translation from Java classes into Swift @@ -312,12 +313,12 @@ open class JarFile: ZipFile { } ``` -The `JavaClass` macro provides information about the Java class itself: it's canonical name (here, `java.util.jar.Jarfile`), the type it extends as a metatype of a Java class projected into Swift (here `ZipFile`, for `java.util.zip.ZipFile`) which will be `JavaObject` if omitted, and an optional list of interfaces it implements (as metatypes for Java interfaces projected into Swift). This is the equivalent to the Java class declaration: +The `JavaClass` macro provides information about the Java class itself: its canonical name (here, `java.util.jar.JarFile`), the type it extends as a metatype of a Java class projected into Swift (here `ZipFile`, for `java.util.zip.ZipFile`) which will be `JavaObject` if omitted, and an optional list of interfaces it implements (as metatypes for Java interfaces projected into Swift). This is the equivalent to the Java class declaration: ```java package java.util.jar -public class JarFile extends java.util.zip.ZipFile implements java.lang.AutoClosable { ... } +public class JarFile extends java.util.zip.ZipFile implements java.lang.AutoCloseable { ... } ``` Each of the public Java constructors, methods, and fields in the Java class will have a corresponding Swift declaration. Java constructors are written as Swift initializers, e.g., @@ -330,7 +331,7 @@ Each of the public Java constructors, methods, and fields in the Java class will corresponds to the Java constructor: ```java -public JarFile(String arg0, bool arg1) +public JarFile(String arg0, boolean arg1) ``` The `environment` parameter is the pointer to the JNI environment (`JNIEnv*` in C) in which the underlying Java object lives. It is available to all methods that are written in or exposed to Java, @@ -375,21 +376,21 @@ between Java types and their Swift counterparts that conform to `JavaValue`: | `T[]` | `[T]` | | `String` | `String` | -For Swift projections of Java classes, the Swift type itself conforms to the `AnyJavaObject` protocol. This conformance is added automatically by the `JavaClass` macro. Swift projects of Java classes can be generic. In such cases, each generic parameter should itself conform to the `AnyJavaObject` protocol. +For Swift projections of Java classes, the Swift type itself conforms to the `AnyJavaObject` protocol. This conformance is added automatically by the `JavaClass` macro. Swift projections of Java classes can be generic. In such cases, each generic parameter should itself conform to the `AnyJavaObject` protocol. -Because Java has implicitly nullability of references, `AnyJavaObject` types do not directly conform to `JavaValue`: rather, optionals of `AnyJavaObject`-conforming type conform to `JavaValue`. This requires Swift code to deal with the optionality +Because Java references are implicitly nullable, `AnyJavaObject` types do not directly conform to `JavaValue`: rather, optionals of `AnyJavaObject`-conforming type conform to `JavaValue`. This requires Swift code to deal with the optionality at interface boundaries rather than invite implicit NULL pointer dereferences. A number of SwiftJava modules provide Swift projections of Java classes and interfaces. Here are a few: | Java class | Swift class | Swift module | | --------------------- | -------------- | ---------------- | -| `java.lang.Object` | `JavaObject` | `SwiftJava` | -| `java.lang.Class` | `JavaClass` | `SwiftJava` | -| `java.lang.Throwable` | `Throwable` | `SwiftJava` | -| `java.net.URL` | `URL` | `JavaNet` | +| `java.lang.Object` | `JavaObject` | `SwiftJava` | +| `java.lang.Class` | `JavaClass` | `SwiftJava` | +| `java.lang.Throwable` | `Throwable` | `SwiftJava` | +| `java.net.URL` | `JavaURL` | `SwiftJava` | -The `swift-java` tool can translate any other Java classes into Swift projections. The easiest way to use `swift-java` is with the SwiftPM plugin described above. More information about using this tool directly are provided later in this document +The `swift-java` tool can translate any other Java classes into Swift projections. The easiest way to use it is with the SwiftPM plugin described above; see for invoking the tool directly. #### Improve parameter names of imported Java methods When building Java libraries you can pass the `-parameters` option to javac diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md index 7ec9d9e98..417ede71f 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md @@ -6,14 +6,14 @@ Hints and patterns for using swift-java on Android. ### R8/Proguard Rules -Since swift-java uses JNI and reflection APIs, we must tell the R8 optimizer to ignore our wrapped types, -otherwise we will run into runtime crashes, because types are stripped from the APK/AAR. +Since swift-java uses JNI and reflection APIs, the R8 optimizer must be told to keep the wrapped types. +Otherwise they are stripped from the APK/AAR and calls into them crash at runtime. The `SwiftKit` Java library already contains a [Proguard consumer file](https://developer.android.com/topic/performance/app-optimization/library-optimization), which is automatically detected by R8, so any `org.swift.swiftkit` types are already ignored. However, you must still provide rules for your own types. -For example, if your library is named is `org.swift.exampleapp`, then add the following rules to your proguard file: +For example, if your library's package is `org.swift.exampleapp`, add the following rules to your proguard file: ``` -keep class org.swift.exampleapp.** { *; } @@ -59,6 +59,6 @@ open class OldVersionedClass: JavaObject { } ``` -Annotations are generated both for "since", "deprecated" and "removed" attributes. +Annotations are generated for the "since", "deprecated" and "removed" attributes. > Note: To use Android platform availability you must use at least Swift 6.3, which introduced the `Android` platform. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md b/Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md index 163919e70..fe47a4e40 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/ExampleCommandLineProgram.md @@ -7,17 +7,19 @@ Build a Java command-line program whose entry point is implemented in Swift. > Note: The instructions here work, but we are still smoothing out the > interoperability story. -All JavaKit-based applications start execution within the Java Virtual Machine. -This example walks through the four pieces you need: a Java class that loads a -native Swift library and declares a `native` entry point, a SwiftPM dynamic -library product, the Swift implementation of that entry point, and the command -line invocation that ties them together. +In this example the program starts in the JVM: a Java class declares a `native` +method, and Swift provides its implementation. This walks through the four pieces +you need: the Java class that loads a native Swift library and declares the `native` +entry point, a SwiftPM dynamic library product, the Swift implementation of that +entry point, and the command line invocation that ties them together. + +(If instead you want a Swift executable that starts a JVM to call Java APIs, see +`Samples/JavaProbablyPrime` and .) ### 1. Create a Java class to wrap the Swift library -First, define a Java class that loads your native Swift library and provides a -`native` entry point to get into the Swift code. Here is a minimal Java class -that has all of the program's logic written in Swift, including `main`: +First, define a Java class that loads your native Swift library and declares a +`native` entry point into the Swift code: ```java package org.swift.javakit; @@ -27,7 +29,11 @@ public class HelloSwiftMain { System.loadLibrary("HelloSwift"); } - public native static void main(String[] args); + public static native String runSwiftMain(String[] args); + + public static void main(String[] args) { + System.out.println(runSwiftMain(args)); + } } ``` @@ -67,23 +73,39 @@ with an associated target that depends on `SwiftJava`: ### 3. Implement the native Java method in Swift -Now, in the `HelloSwift` Swift library, define a `struct` that provides the -`main` method for the Java class you already defined: +Implementations of `native` methods live in an `@JavaImplementation` extension of the +Swift wrapper for the Java class. Declare the wrapper with `@JavaClass`, list the +`native` methods in a `NativeMethods` protocol, and mark each implementation with +`@JavaMethod`. A `static native` method is implemented by a `static func` that takes +an `environment: JNIEnvironment` parameter: ```swift import SwiftJava +@JavaClass("org.swift.javakit.HelloSwiftMain") +open class HelloSwiftMain: JavaObject { +} + +protocol HelloSwiftMainNativeMethods { + static func runSwiftMain(_ args: [String], environment: JNIEnvironment) -> String +} + @JavaImplementation("org.swift.javakit.HelloSwiftMain") -struct HelloSwiftMain { - @JavaStaticMethod - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - print("Command line arguments are: \(arguments)") +extension HelloSwiftMain: HelloSwiftMainNativeMethods { + @JavaMethod + static func runSwiftMain(_ args: [String], environment: JNIEnvironment) -> String { + "Command line arguments are: \(args)" } } ``` +> Important: use `@JavaMethod static func` here, not `@JavaStaticMethod`. +> `@JavaStaticMethod` declares a Java static method that Swift *calls*; +> `@JavaImplementation` + `@JavaMethod` *provides* the implementation of a Java +> `native` method. + Build this library with `swift build`, then find the directory containing the -resulting shared library (`HelloSwift.dylib`, `HelloSwift.so`, or +resulting shared library (`libHelloSwift.dylib`, `libHelloSwift.so`, or `HelloSwift.dll`, depending on platform). It is usually in `.build/debug/`. ### 4. Putting it all together @@ -100,31 +122,26 @@ This prints the command-line arguments `-v` and `argument` as seen by Swift. ### Bonus: Swift argument parser -The easiest way to build a command-line program in Swift is with the +The easiest way to process the arguments Java hands you is the [Swift argument parser library](https://github.com/apple/swift-argument-parser). -You can extend the `HelloSwiftMain` type to conform to `ParsableCommand` and use -the Swift argument parser to process the arguments provided by Java: +Declare a `ParsableCommand` and parse into it from the native method's +implementation: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ArgumentParserMainSwift.swift", slice: "argumentParserImplementation") + } + @Tab("Java") { + @Snippet(path: "Snippets/ArgumentParserMainJava", slice: "argumentParserMain") + } +} -```swift -import ArgumentParser -import SwiftJava +Note that the command is a separate `ParsableCommand` type rather than the +`@JavaClass` wrapper itself: the wrapper is a class backed by a Java instance, +so it cannot also satisfy `ParsableCommand`'s value semantics. -@JavaClass("org.swift.javakit.HelloSwiftMain") -struct HelloSwiftMain: ParsableCommand { - @Option(name: .shortAndLong, help: "Enable verbose output") - var verbose: Bool = false - - @JavaImplementation - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - let command = Self.parseOrExit(arguments) - command.run(environment: environment) - } - - func run(environment: JNIEnvironment? = nil) { - print("Verbose = \(verbose)") - } -} -``` +`Samples/JavaKitSampleApp` contains this example, along with runtime tests that +call the Java entry point and check the parsed result. ## See Also diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md index cfeeb21a8..ccc411a79 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md @@ -5,14 +5,12 @@ Detailed feature documentation for calling Java from Swift using JavaKit macros ## Overview -JavaKit macros let you *hand-write* Swift declarations that mirror Java classes, -methods, and fields. This is the direct, one-off approach: pick a Java API, -declare a matching Swift shape, and the macros produce all the JNI plumbing. +JavaKit macros let you hand-write Swift declarations that mirror Java classes, +methods, and fields; the macros generate the JNI calls. -Use this approach when you want fine control over what surfaces on the Swift -side, or when you're implementing Java `native` methods in Swift. If you -instead want *automatic* bulk wrapping of an entire classpath or JAR, reach -for the source generator described in . +Use them when you want control over exactly what surfaces on the Swift side, or when +you're implementing Java `native` methods in Swift. To wrap an entire classpath or +JAR instead, use the source generator described in . For an orientation on which interop tool fits your task, see . @@ -68,8 +66,8 @@ Method names in Swift match the Java method verbatim by default. Use ### Java static methods: @JavaStaticMethod -Static methods live in an `extension` on the class's `JavaClass` metatype. -This keeps instance dispatch and static dispatch cleanly separated. +Static methods live in an `extension` on the class's `JavaClass` metatype, +keeping instance and static dispatch separate. ```swift extension JavaClass { @@ -142,10 +140,14 @@ and `NewObject` call. ### Throwing methods -A Swift method declared `@JavaMethod ... throws` corresponds to a Java method -whose signature includes `throws Exception`. +A Swift method declared `@JavaMethod ... throws` corresponds to a Java method whose +signature includes `throws`. -When the Java side throws, the exception is caught by the generated bridge and re-thrown as a Swift error. +When the Java side throws, the generated bridge clears the pending JNI exception and +rethrows it as a Swift `Throwable`, which conforms to `Error`. Catch it as +`Throwable` to reach the underlying Java object (`getMessage()`, +`printStackTrace(_:)`, `.as(IOException.self)`, ...), or as a plain `error` if you +only need the description. @TabNavigator { @Tab("Swift") { @@ -153,7 +155,15 @@ When the Java side throws, the exception is caught by the generated bridge and r } } -TODO: way more docs about how we map errors +> Important: if you omit `throws` on a Swift declaration whose Java method can throw, +> a Java exception becomes a `fatalError` with the Java stack trace attached, because +> there is nowhere to propagate it to. Declare `throws` whenever the Java signature +> can throw. + +In the other direction, a Swift error thrown out of a `@JavaImplementation` method +is converted to a Java exception: if the error is itself a wrapped Java `Throwable` +it is rethrown as-is, otherwise a `java.lang.Exception` carrying the error's +description is thrown. ### Type casting: .as(T.self) @@ -181,9 +191,9 @@ itself as an instance of a subclass: ### Arrays -Swift `[T]` maps to Java `T[]` for both parameters and return values. This -works out of the box for the primitive types (`Int8`/`byte`, `Int32`/`int`, -`Int64`/`long`, `Double`/`double`) and for object types like `String`. +Swift `[T]` maps to Java `T[]` for both parameters and return values, for the +primitive types (`Int8`/`byte`, `Int32`/`int`, `Int64`/`long`, `Double`/`double`, +...) as well as object types like `String`. Once the array method is declared on the Swift wrapper, calling it looks exactly like calling any Swift function that takes/returns `[T]`: @@ -199,13 +209,15 @@ exactly like calling any Swift function that takes/returns `[T]`: ### Optionals and nullability -Swift `Optional` maps to Java `Optional` when the Swift type is -`JavaString?` (a nullable JavaKit-wrapped object). Nullable primitives use -`OptionalLong` / `OptionalInt` / `OptionalDouble`. Passing `nil` on the Swift -side surfaces as `Optional.empty()` on the Java side. +Java's `Optional`, `OptionalInt`, `OptionalLong` and `OptionalDouble` are wrapped +as JavaKit object types (`JavaOptional`, `JavaOptionalInt`, ...) like any other +Java class. On top of that, `wrap-java` emits a second accessor with an `Optional` +suffix that uses a native Swift optional instead, so a Java +`Optional getText()` yields both `getText() -> JavaOptional!` +and `getTextOptional() -> JavaString?`. The same applies to fields: an +`Optional text` field yields `text` and `textOptional`. -The wrapper's optional-typed methods and fields are used like any other -Swift optional: +Prefer the `Optional`-suffixed accessors; they are what the sample below uses: @TabNavigator { @Tab("Swift") { @@ -216,6 +228,10 @@ Swift optional: } } +Note that this is separate from Java's implicit reference nullability, which is +covered under Primitive type mapping below: every wrapped Java object type is +projected into Swift as an optional. + ### Primitive type mapping | Swift | Java | @@ -289,13 +305,28 @@ Constructing a subclass from Swift mirrors the Java constructor call: ### Generic type parameters -Generic Java types like `java.util.ArrayList` are wrapped as generic Swift classes. +Generic Java types like `java.util.ArrayList` are wrapped as generic Swift classes. + +Because of Java's type erasure, a generic parameter used in a method signature needs +a `typeErasedResult:` hint on `@JavaMethod` so the macro can generate the right JNI +signature. The hint spells the Swift return type as written, and the macro uses the +erased type (`java.lang.Object`) for the actual JNI call: -Because of Java's type erasure, generic parameters used in method -signatures need a `typeErasedResult:` hint on `@JavaMethod` so the macro can -generate the right JNI signature. +```swift +@JavaClass("java.util.Stack") +open class Stack: JavaObject { + public typealias E = Stack_E -TODO: show example here + // public synchronized E java.util.Stack.pop() + @JavaMethod(typeErasedResult: "E!") + open func pop() -> E! +} +``` + +Note the `Stack_E` parameter name plus an `E` typealias: `wrap-java` prefixes type +parameters with the class name to avoid collisions, then restores the Java spelling +via the typealias. See `Sources/JavaStdlib/JavaUtil/generated/Stack.swift` for the +full generated type. ### Method overloading @@ -304,22 +335,20 @@ overloads with the same name - the macro-generated JNI signature disambiguates which overload to invoke. See `Sources/JavaStdlib/JavaUtil/generated/ArrayList.swift` for realistic examples (multiple `add(...)` overloads). -### Annotating thread-safety with Swift's Sendable +### Thread-safety and Sendable -If you know a Java class is thread-safe (typically because it's annotated with -your project's own `@ThreadSafe` marker, or because its API is stateless), you -can declare its Swift wrapper `@unchecked Sendable` so it can be shared across -Swift concurrency isolation boundaries. +`wrap-java` recognizes thread-safety annotations on the Java class and emits the +matching Swift conformance. Matching is by simple name, so the annotation may come +from `javax.annotation.concurrent`, `net.jcip.annotations`, or your own project: -```swift -@JavaClass("com.example.swift.ThreadSafeHelperClass") -open class ThreadSafeHelperClass: JavaObject, @unchecked Sendable { - @JavaMethod - @_nonoverride public convenience init(environment: JNIEnvironment? = nil) -} -``` +| Java annotation | Generated Swift | +|------------------|--------------------------------------------------------| +| `@ThreadSafe` | `extension X: @unchecked Swift.Sendable { }` | +| `@Immutable` | `extension X: @unchecked Swift.Sendable { }` | +| `@NotThreadSafe` | `@available(unavailable, *) extension X: Swift.Sendable { }` | -Once declared, the wrapped instance flows freely across isolation boundaries: +So an annotated Java class flows across Swift concurrency isolation boundaries +without any extra work on your side: @TabNavigator { @Tab("Swift") { @@ -333,4 +362,13 @@ Once declared, the wrapped instance flows freely across isolation boundaries: } } -TODO: note what annotations we automatically handle \ No newline at end of file +When you hand-write a wrapper, or the Java class carries no such annotation but you +know it is thread-safe, declare the conformance yourself: + +```swift +@JavaClass("com.example.swift.ThreadSafeHelperClass") +open class ThreadSafeHelperClass: JavaObject, @unchecked Sendable { + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) +} +``` \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md index d21e47f4f..5b5802b75 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -84,7 +84,7 @@ on Java 16+ reach the same values with ### RawRepresentable enums -JExtract supports extracting enums that conform to `RawRepresentable`, +jextract supports extracting enums that conform to `RawRepresentable`, giving access to an optional initializer and the `rawValue` property. @TabNavigator { @@ -185,8 +185,8 @@ Asynchronous functions are imported as Future-type returning Java functions. There are two modes of extracting them, configurable using the `asyncFuncMode` setting in `swift-java.config`, or using the equivalent `--async-func-mode` command line option: -- **completable-future (default)**: `async` functions return `java.util.concurrent.CompletableFuture` -- **future**: For legacy platforms (e.g. Android 23 and below) where `CompletableFuture` is not available, `async` functions return `java.util.concurrent.Future`. +- **`completableFuture` (default)**: `async` functions return `java.util.concurrent.CompletableFuture` +- **`legacyFuture`**: for platforms (e.g. Android 23 and below) where `CompletableFuture` is not available, `async` functions return `java.util.concurrent.Future` @TabNavigator { @Tab("Swift") { @@ -220,8 +220,8 @@ This relies on the Kotlin `kotlinx-coroutines-jdk8` library which adds the `awai ### Collections -Swift's collection types cross the boundary either by copying (arrays) or by -handing Java a wrapper that points at the live Swift value (dictionaries, sets). +Arrays are copied across the boundary. Dictionaries and sets are handed to Java as +a wrapper over the live Swift value. #### Arrays @@ -259,13 +259,15 @@ arrays (`[[UInt8]]`, `[[String]]`) are supported in JNI mode. #### InlineArray Fixed-size inline arrays (Swift's `InlineArray`, sugar `[N of T]`) are -recognized by jextract in JNI mode and imported with an equivalent Java surface. +recognized when parsing Swift sources, but are not yet extracted in either mode. +Declarations using them are skipped. #### Dictionaries Swift dictionaries (`[Key: Value]`) are imported using the `SwiftDictionaryMap` -Java wrapper type. This wrapper refers to the actual Swift dictionary on the Swift heap -and does not copy it. Use `SwiftDictionaryMap::toJava` to explicitly copy into a Java `Map`. +Java wrapper type. Creating the wrapper does not copy the dictionary; it implements +`java.util.Map` by making a JNI downcall into the Swift value for each operation. +Use `SwiftDictionaryMap::toJavaMap` to copy into a `java.util.Map` on the Java heap. @TabNavigator { @Tab("Swift") { @@ -282,8 +284,8 @@ and does not copy it. Use `SwiftDictionaryMap::toJava` to explicitly copy into a #### Sets Swift sets (`Set`) are imported using the `SwiftSet` Java wrapper. -Like dictionaries, the wrapper points at the Swift value on the Swift heap and does not -copy it. Use `SwiftSet::toJava` to explicitly copy into a Java `Set`. +Like dictionaries, creating the wrapper does not copy, and each operation is a JNI +downcall. Use `SwiftSet::toJavaSet` to copy into a `java.util.Set` on the Java heap. @TabNavigator { @Tab("Swift") { @@ -299,13 +301,12 @@ copy it. Use `SwiftSet::toJava` to explicitly copy into a Java `Set`. ### Bytes and buffers -Raw bytes are the most common thing to hand across the language boundary, and -SwiftJava offers several shapes for it depending on whether you want a copy or a -view of the memory. +There are several ways to pass raw bytes, differing in whether the bytes are +copied or viewed in place. #### Byte arrays -`[UInt8]` maps to Java's `byte[]` in both modes by copying. +`[UInt8]` maps to Java's `byte[]` in both modes by copying. Note that Java's `byte` is signed, so a Swift `UInt8` of `200` reads as `-56` on the Java side; see . @@ -386,11 +387,13 @@ type itself see . #### Java MemorySegment (FFM only) -`Data.toMemorySegment(arena)` hands back the bytes as a `java.lang.foreign.MemorySegment`, -which the JVM reads directly without copying. +`Data.toMemorySegment(arena)` copies the bytes into memory allocated in the given +arena and returns it as a `java.lang.foreign.MemorySegment`. This is one copy, and +the JVM then reads that native memory without a second copy onto the Java heap. +To read the bytes without copying at all, use `withUnsafeBytes` (see below). > Important: `MemorySegment` is part of the Foreign Function & Memory API -> ([JEP 454](https://openjdk.org/jeps/454)) and requires JDK 25+. It is available +> ([JEP 454](https://openjdk.org/jeps/454)), final since JDK 22. It is available > in FFM mode only; there is no `MemorySegment` equivalent in JNI mode. @Snippet(path: "Snippets/DataJavaFFM", slice: "memorySegmentUsageJava") @@ -559,7 +562,7 @@ the existential from that value. } } -Using the returned value works just like using any other imported interface: its +Using the returned value works like using any other imported interface: its requirements are callable through the box, it can be passed back into functions that accept the protocol (including generic and opaque parameters), and refined protocols expose both their own and their inherited requirements. @@ -570,7 +573,7 @@ expose both their own and their inherited requirements. ### Foundation types A handful of Foundation value types are recognized by name and bridged to a -matching Java representation rather than being treated as opaque Swift values. +matching Java representation, rather than being treated as opaque Swift values. #### Data diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md index 451c525e8..9bfd73abc 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md @@ -1,6 +1,6 @@ # Features Overview -Orientation guide for choosing the right swift-java tool for your interop task. +Choosing the right swift-java tool for your interop task. ## Overview @@ -12,34 +12,33 @@ to the per-tool documentation. #### Calling Swift from Java -When you need to call Swift code from Java, you will be using **jextract** and have a choice between its `jni` or `ffm` modes: +To call Swift code from Java you use **jextract**, in either its `jni` or `ffm` mode: -- JNI mode is the broader-compatibility choice: it runs on any JDK, works on - Android, and supports a wider set of language features. -- FFM mode is the high-performance choice: it has a limited set of features, +- JNI mode is the broader-compatibility choice: it works on Android, and supports a + wider set of language features. The generated Java sources target the + `javaSourceLevel` setting, which supports JDK 17 through 25. +- FFM mode is the high-performance choice: it supports a smaller set of features, and requires JDK 25+ because it relies on [JEP 454: Foreign Function & Memory](https://openjdk.org/jeps/454) APIs. - In some situations it is able to achieve less data copying between the language barriers, so consider it when - shipping large amounts of data between runtimes. - -Generally, it is fine to start with the JNI mode, unless you have specific needs which can only be met by the FFM mode. -Switching modes is simple, and you can do it by passing `--ffm/jni` options to the command line tool, or configuring the `"mode": "jni"|"ffm"` in `swift-java.config` -when using the . + It can avoid some copying between the two runtimes, so consider it when + shipping large amounts of data across the boundary. -#### Calling Java from Swift +Start with JNI mode unless you have a specific need only FFM mode can meet. +Switching modes means passing `--mode jni` or `--mode ffm` to the command line tool, +or setting `"mode": "jni"` / `"mode": "ffm"` in `swift-java.config` when using the +. -It is possible to directly call into Java types as long as you create (or obtain) an in-process reference to a JVM in your Swift program. +#### Calling Java from Swift -> Tip: This also works on Android. TODO: EXAMPLE +Swift can call Java types directly as long as your Swift program creates or obtains +an in-process reference to a JVM. This also works on Android. **JavaKit macros vs wrap-java.** -SwiftJava offers a collection of **Swift macros** that allow calling Java types from Swift directly. You can learn about the full set of macros and their capabilities in: +SwiftJava offers a collection of **Swift macros** for calling Java types from Swift directly. The full set of macros and their capabilities is covered in . -- Manually writing `@JavaClass` and similar types: if you only need to access one or two entry points in a Java library, you may write those manually (see ). -- Use `swift-java wrap-java` source generation: to automatically generate Swift wrapper types for a whole Java API surface. Refer to to learn more about this. +- Hand-write `@JavaClass` and friends when you only need one or two entry points in a Java library (see ). +- Use `swift-java wrap-java` source generation to generate Swift wrapper types for a whole Java API surface. See . ### Talks and videos -If you'd like to watch some talks or introduction videos about this project, you can refer to the following materials: - - [Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA) session from WWDC25. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md index 70475efcb..d010909a6 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md @@ -12,10 +12,9 @@ When in doubt, you can always use the command line `--help` to get additional gu ### Expose Java classes to Swift: swift-java wrap-java -The `swift-java` is a Swift program that uses Java's runtime reflection facilities to translate the requested Java classes into their Swift projections. The output is a number of Swift source files, each of which corresponds to a -single Java class. The `swift-java` can be executed like this: +`swift-java` uses Java's runtime reflection facilities to translate the requested Java classes into their Swift projections. The output is a number of Swift source files, one per Java class. You can see its help with: -``` +```bash swift-java help wrap-java ``` @@ -23,20 +22,26 @@ to produce help output like the following: @Snippet(path: "Snippets/SwiftJavaCLIHelp", slice: "wrapJavaHelp") -For example, the `JavaKitJar` library is generated with this command line: +For example, the `JavaUtilJar` module in this repository is generated with this command line: -```swift -swift-java wrap-java --swift-module JavaKitJar --depends-on SwiftJNI=Sources/SwiftJNI/swift-java.config -o Sources/JavaKitJar/generated Sources/JavaKitJar/swift-java.config +```bash +swift-java wrap-java \ + --swift-module JavaStdlib/JavaUtilJar \ + -o Sources/JavaStdlib/JavaUtilJar/generated \ + --config Sources/JavaStdlib/JavaUtilJar/swift-java.config \ + --depends-on SwiftJava=Sources/SwiftJava/swift-java.config \ + --depends-on JavaUtil=Sources/JavaStdlib/JavaUtil/swift-java.config ``` -The `--swift-module JavaKitJar` parameter describes the name of the Swift module in which the code will be generated. +See `scripts/wrap-java-generate.sh` for the full set of invocations used to regenerate the bundled Java standard library bindings. + +The `--swift-module` parameter is the name of the Swift module the code is generated into. -The `--depends-on` option is followed by the swift-java configuration files for any library on which this Swift library depends. Each `--depends-on` option is of the form `=`, and tells swift-java which other Java classes have already been translated to Swift. For example, if your Java class uses `java.net.URL`, then you should include -`JavaKitNetwork`'s configuration file as a dependency here. +Each `--depends-on` option takes `=` and tells swift-java which other Java classes have already been translated to Swift. For example, if your Java class uses `java.net.URL`, include `JavaNet`'s configuration file as a dependency here. -The `-o` option specifies the output directory. Typically, this will be `Sources//generated` or similar to keep the generated Swift files separate from any hand-written ones. To see the output on the terminal rather than writing files to disk, pass `-` for this option. +The `-o` option specifies the output directory. Typically this is `Sources//generated` or similar, to keep the generated Swift files separate from hand-written ones. To see the output on the terminal rather than writing files to disk, pass `-` for this option. -Finally, the command line should contain the `swift-java.config` file containing the list of classes that should be translated into Swift and their corresponding Swift type names. The tool will output a single `.swift` file for each class, along with warnings for any public API that cannot be translated into Swift. The most common warnings are due to missing Swift projections for Java classes. For example, here we have not translated (or provided the translation manifests for) the Java classes +The `--config` option points at the `swift-java.config` file listing the classes to translate and their corresponding Swift type names. The tool outputs a single `.swift` file for each class, along with warnings for any public API that cannot be translated into Swift. The most common warnings are due to missing Swift projections for Java classes. For example, here we have not translated (or provided the translation manifests for) the Java classes `java.util.zip.ZipOutputStream` and `java.io.OutputStream`: ``` @@ -45,6 +50,8 @@ warning: Unable to translate 'java.util.jar.JarOutputStream' constructor: Java c warning: Unable to translate 'java.util.jar.JarInputStream' method 'transferTo': Java class 'java.io.OutputStream' has not been translated into Swift ``` -The result of such warnings is that certain information won't be statically available in Swift, e.g., the superclass won't be known (so we will assume it is `JavaObject`), or the specified constructors or methods won't be translated. If you don't need these APIs, the warnings can be safely ignored. The APIs can still be called dynamically via JNI. +Such warnings mean that some information won't be statically available in Swift: the superclass won't be known (so it is assumed to be `JavaObject`), or the affected constructors and methods won't be translated. If you don't need those APIs, the warnings can be safely ignored. The APIs can still be called dynamically via JNI. + +### Scan a Jar file: swift-java configure --jar -The `--jar` option changes the operation of `swift-java`. Instead of wrapping Java classes in Swift, it scans the given input Jar file to find all public classes and outputs a configuration file `swift-java.config` mapping all of the Java classes in the Jar file to Swift types. The `--jar` mode is expected to be used to help import a Java library into Swift wholesale, after which swift-java should invoked again given the generated configuration file. +`swift-java configure --jar` scans the given input Jar file for all public classes and writes a `swift-java.config` file mapping every Java class in the Jar to a Swift type. Use it to import an entire Java jar's API surface, and then invoke `swift-java wrap-java` with the generated configuration file to produce the Swift wrappers. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md index 63c59a278..42f94d2c1 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaResolve.md @@ -8,8 +8,8 @@ Download Java dependencies for use in Swift builds. and fetches them, then writes a `.swift-java.classpath` file into the `--output-directory`. -Classpath files are picked up by `swift-java` commands which search for `*.swift-java.classpath` files -to create a classpath for Java operations, e.g. `wrap-java` uses such assembled classpath during wrapping Java classes for Swift. +`swift-java` commands search for `*.swift-java.classpath` files and assemble them into a classpath +for Java operations; `wrap-java` uses that classpath when wrapping Java classes for Swift. You can always use the command line `--help` to get additional guidance about the tool and available options: @@ -23,7 +23,7 @@ You can use `swift-java resolve` explicitly on the command line: swift-java resolve \ --swift-module JavaCommonsCSV \ -o .build/plugins/outputs/JavaCommonsCSV \ - "org.apache.commons:commons-csv:1.10.0" + "org.apache.commons:commons-csv:1.12.0" ``` Or, include dependency identifiers in gradle format: `::` @@ -37,10 +37,9 @@ in `swift-java.config` configuration files, like so: } ``` -This allows `swift-build` to automatically resolve and fetch such Java dependencies, -however it does mean that you will have to disable the SwiftPM sandbox (`--disable-sandbox`) -in order to build such package because this step requires network access which otherwise the build system will block using a security sandbox. -See for more details on caveats to using the build plugin in such scenarios. +This lets `swift build` resolve and fetch the Java dependencies automatically. Because fetching +needs network access, which the SwiftPM sandbox blocks, you must pass `--disable-sandbox` when +building such a package. See for more on the build plugin's caveats here. > Tip: See `Samples/JavaDependencySampleApp` for a fully functional showcase of this mode. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md index 0be54f1f7..418d669d1 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md @@ -4,16 +4,14 @@ Automatic Swift wrappers for existing Java code, generated by `swift-java wrap-j ## Overview -`wrap-java` is the *source-generation* half of Java-from-Swift interop. Point it -at a classpath - a JAR, a Maven artifact, or the JDK itself - and it emits Swift -files full of `@JavaClass`, `@JavaMethod`, `@JavaField`, and related JavaKit -macros for every public Java type you asked it to wrap. On the Swift side, the -result looks and feels like any other Swift API. +`wrap-java` is the source-generation half of Java-from-Swift interop. Point it at a +classpath - a JAR, a Maven artifact, or the JDK itself - and it emits Swift files +using `@JavaClass`, `@JavaMethod`, `@JavaField` and the related JavaKit macros for +every public Java type you asked it to wrap. -Reach for `wrap-java` when you want to expose an entire Java library to Swift -without hand-writing every declaration. When you only need a handful of types, -or when you're implementing `native` methods in Swift, use the JavaKit macros -directly - see . +Use `wrap-java` to expose an entire Java library to Swift without hand-writing every +declaration. When you only need a handful of types, or you're implementing `native` +methods in Swift, use the JavaKit macros directly - see . For an orientation on which interop tool fits your task, see . @@ -36,7 +34,7 @@ Swift name. Skip this step when you're hand-authoring the config. swift-java configure --jar \ --swift-module MyLib \ -o Sources/MyLib \ - path/to/library.jar + --cp path/to/library.jar ``` ### 2. Resolve Maven dependencies: swift-java resolve @@ -61,9 +59,9 @@ Java class (or a single combined file, see `singleSwiftFileOutput` below). swift-java wrap-java \ --swift-module JavaSieve \ -o Sources/JavaSieve/generated \ + --config Sources/JavaSieve/swift-java.config \ --depends-on SwiftJava=Sources/SwiftJava/swift-java.config \ - --depends-on JavaUtil=Sources/JavaStdlib/JavaUtil/swift-java.config \ - Sources/JavaSieve/swift-java.config + --depends-on JavaUtil=Sources/JavaStdlib/JavaUtil/swift-java.config ``` ### The SwiftPM plugin @@ -89,8 +87,7 @@ See for the plugin's full contract. ### swift-java.config schema (wrap-java fields) -The full schema lives in -`Sources/SwiftJavaConfigurationShared/Configuration.swift`. The fields +The full schema is documented in . The fields that matter for `wrap-java` are: | Field | Type | Purpose | @@ -99,31 +96,12 @@ that matter for `wrap-java` are: | `classpath` | `String` | Colon-separated Java classpath entries. | | `javaFilterInclude` | `[String]` | While scanning, wrap only types matching these package prefixes. | | `javaFilterExclude` | `[String]` | Skip these packages or, using `Class#method`, skip individual methods. | -| `sourceCompatibility` | `Int` | Compile the wrapped API surface for this Java SE release (e.g. `11`, `17`, `21`). | -| `targetCompatibility` | `Int` | Emit class files for this Java SE release. | +| `sourceCompatibility` | `JavaVersion` | Compile the wrapped API surface for this Java SE release (`17`, `18`, `21`, `22`, `24`, `25`). | +| `targetCompatibility` | `JavaVersion` | Emit class files for this Java SE release, same supported values. | | `singleSwiftFileOutput` | `String` | If set, place all generated code in this single Swift file instead of one file per class. | The config file is JSON5, so `//` and `/* */` comments are permitted. -### CLI flags - -`swift-java wrap-java --help` (from -`Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift`) exposes: - -| Flag | Purpose | -|-------------------------------------------------|---------------------------------------------------------------------------------------------| -| `--swift-module ` | Required. Name of the Swift module the generated types will live in. | -| `--depends-on ` | Repeatable. Register a Swift module this one transitively depends on for cross-module types. | -| `--swift-native-implementation ` | Repeatable. Java classes whose `native` methods will be implemented in Swift. | -| `--cache-directory ` | Cache directory for intermediate results between runs. | -| `--swift-match-package-directory-structure` | Mirror Java package layout with directories under `-o`. | -| `--singleSwiftFileOutput ` | Emit everything into one Swift file at ``. | -| `--filter-include ` / `--filter-exclude ` | Same intent as the config-file filters, exposed on the CLI. | -| `--android-api-version-file ` | Consume Android's `api-versions.xml` to emit `@available` on wrapped decls. | -| `--cp ` / `--classpath ` | (Inherited) Extra classpath entries. | -| `-o ` / `--output-directory ` | (Inherited) Output directory. | -| `-l ` / `--log-level ` | (Inherited) `trace`, `debug`, `info`, `notice`, `warning`, `error`, `critical`. | - ### Walkthrough: wrapping a JDK class The simplest possible sample: wrap `java.math.BigInteger` and use it to test diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md index f9c6d2691..ce84de5fa 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md @@ -1,15 +1,23 @@ # SwiftPM Plugin -The `SwiftJavaPlugin` automates `swift-java` command line tool invocations during the build process. +The swift-java SwiftPM plugins automate `swift-java` command line tool invocations during the build. ## Overview +There are three plugins, and you only include the ones your target needs: + +| Plugin | What it does | +|----------------------|-------------------------------------------------------------------------------------------------| +| `SwiftJavaPlugin` | Runs `wrap-java` to generate Swift wrappers for the Java classes listed in `swift-java.config`, resolving any declared Maven `dependencies` first. | +| `JExtractSwiftPlugin`| Runs `jextract` to generate Java bindings (plus Swift thunks) for the target's Swift sources. | +| `JavaCompilerPlugin` | Compiles `.java` sources kept alongside the Swift target with `javac`. | + ### Installing the plugin -To install the SwiftPM plugin in your target of choice include the `swift-java` package dependency: +Add the `swift-java` package dependency and list the plugins on the target: ```swift -// swift-tools-version: 6.3 +// swift-tools-version: 6.1 import PackageDescription @@ -18,28 +26,28 @@ let package = Package( products: [ .library( - name: "JavaKitExample", + name: "MyProject", type: .dynamic, - targets: ["JavaKitExample"] + targets: ["MyProject"] ), ], dependencies: [ - .package(url: "https://github.com/swiftlang/swift-java", from: "..."), + .package(url: "https://github.com/swiftlang/swift-java", branch: "main"), ], targets: [ .target( name: "MyProject", dependencies: [ - // ... + .product(name: "SwiftJava", package: "swift-java"), ], swiftSettings: [ - // Some swift-java generated code is not yet compatible with swift 6 + // Some swift-java generated code is not yet compatible with Swift 6 language mode .swiftLanguageMode(.v5) ], plugins: [ - // Include here the plugins you need + // Include only the plugins you need .plugin(name: "JavaCompilerPlugin", package: "swift-java"), .plugin(name: "JExtractSwiftPlugin", package: "swift-java"), .plugin(name: "SwiftJavaPlugin", package: "swift-java"), @@ -49,15 +57,23 @@ let package = Package( ) ``` -> Note: Depending on the use case, swift-java may require running Gradle or accessing files outside the Swift package. Ensure that your environment allows Gradle to run, and add the `--disable-sandbox` parameter when invoking the `swift build` command to build the package. +> Note: Depending on the use case, swift-java may need to run Gradle or access files outside the Swift package. Resolving Maven `dependencies` in particular requires network access. Pass `--disable-sandbox` to `swift build` in those cases, since the SwiftPM sandbox blocks them. See . ### Handling cross module Swift type dependencies -Sometimes you may be wanting to treat a specific module with swift-java jextract and expose it to Java, only to find -that it is also exposing types from other modules. +A module you run jextract over may expose types that come from other modules. + +In that case, also add a `swift-java.config` to the other module and configure it +appropriately. When the plugin then runs in the main module, it picks up the +dependency (because your Swift module depends on the other one) and detects the +swift-java configuration there. + +This tells the source generator the location and Java package of the other module's +generated sources, and lets it compile the generated sources in your main module. -In this situation it is best to also add a `swift-java.config` configuration into the other module, -and configure it appropriately. Next, when you run the plugin in the main module, it will automatically -pick up the dependency (since your Swift module depends on the other one) and detect there is swift-java configuration there. +## See Also -This informs the source generator about the location and package of the generated sources and allows it to compile the generated sources in your main module. \ No newline at end of file +- +- +- +- diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md index 5929601d4..91317eb58 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -8,31 +8,35 @@ The Swift-Java project enables interoperability between Swift and Java. ## Overview -This project contains a number of support packages, java libraries, tools and plugins that provide a complete -Swift and Java interoperability story. +This project contains support packages, Java libraries, tools and plugins for calling +Java from Swift and Swift from Java. Please refer to articles about the specific direction of interoperability you are interested in. ### Getting started -**SwiftJava** provides a set of tools and libraries to enable Java and Swift interoperability. It allows developers to generate bindings to either language from the other, by using either source generation (for Java consuming Swift code) or a combination of Swift macros and source generation (for Swift consuming Java libraries). +**SwiftJava** generates bindings to either language from the other: source generation +for Java consuming Swift code, and a combination of Swift macros and source generation +for Swift consuming Java libraries. -The generated code is highly efficient and less error-prone than manually mapping, and also guarantees memory safety across the boundaries between the languages. +The generated bindings replace hand-written JNI or FFM glue code, which is repetitive +and easy to get wrong. Object lifetimes are managed for you: Swift instances handed to +Java are tied to an arena, and Java objects held from Swift keep a reference that stops +the collector from reclaiming them. -Reasons why you might want to reach for Swift and Java interoperability include, but are not limited to, the following scenarios: +Reasons to reach for Swift and Java interoperability include: - Incremental adoption of Swift in an existing Java codebase -- Reuse existing libraries which exist in one ecosystem, but don't have a direct equivalent in the other - -SwiftJava is offering several core libraries which support language interoperability: -- `SwiftJava` (Swift -> Java) - JNI-based support library and Swift macros -- `SwiftKit` (Java -> Swift) - Support library for Java calling Swift code (either using JNI or FFM) -- `swift-java` - command line tool; Supports source generation and also dependency management operations -- Build tool integration - SwiftPM Plugin - -If you prefer a video introduction, you may want to watch this -[Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA) -WWDC 2025 session, -which is a quick overview of all the features and approaches offered by SwiftJava. +- Reusing a library that exists in one ecosystem but has no direct equivalent in the other + +SwiftJava offers several core libraries: +- `SwiftJava` (calling Java from Swift) - JNI-based support library and Swift macros +- `SwiftKit` (calling Swift from Java) - support library for the generated Java code, in either JNI or FFM mode +- `swift-java` - command line tool for source generation and dependency management +- Build tool integration - SwiftPM plugins + +If you prefer a video introduction, the +[Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA) +WWDC 2025 session is a quick overview of the features and approaches offered by SwiftJava. ## Topics diff --git a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift index cda9a445d..ea8f9b40e 100644 --- a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift +++ b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift @@ -44,8 +44,9 @@ extension SwiftJava { @Option( help: """ A swift-java configuration file for a given Swift module name on which this module depends, - e.g., JavaKitJar=Sources/JavaKitJar/swift-java.config. There should be one of these options - for each Swift module that this module depends on (transitively) that contains wrapped Java sources. + e.g., JavaUtil=Sources/JavaStdlib/JavaUtil/swift-java.config. There should be one of these + options for each Swift module that this module depends on (transitively) that contains + wrapped Java sources. """ ) var dependsOn: [String] = [] From 81dcc7751373ebe733d295833b130f86a111a658 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 5 Aug 2026 17:44:27 +0900 Subject: [PATCH 6/6] CI: fix docs/soundness checks for the CLI help snippet --- .github/scripts/validate_docs.sh | 4 ++-- .github/workflows/pull_request.yml | 2 ++ Snippets/{SwiftJavaCLIHelp.sh => SwiftJavaCLIHelp.txt} | 0 scripts/generate-cli-help-snippets.sh | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) rename Snippets/{SwiftJavaCLIHelp.sh => SwiftJavaCLIHelp.txt} (100%) diff --git a/.github/scripts/validate_docs.sh b/.github/scripts/validate_docs.sh index fed346489..481be7e5a 100755 --- a/.github/scripts/validate_docs.sh +++ b/.github/scripts/validate_docs.sh @@ -6,8 +6,8 @@ set -x swift run generate-config-docs --check ./scripts/generate-cli-help-snippets.sh -if ! git diff --exit-code -- Snippets/SwiftJavaCLIHelp.sh; then - echo "::error::Snippets/SwiftJavaCLIHelp.sh is out of date with the swift-java CLI's --help output (likely a command's flags or abstract text changed). Run scripts/generate-cli-help-snippets.sh and commit the result." +if ! git diff --exit-code -- Snippets/SwiftJavaCLIHelp.txt; then + echo "::error::Snippets/SwiftJavaCLIHelp.txt is out of date with the swift-java CLI's --help output (likely a command's flags or abstract text changed). Run scripts/generate-cli-help-snippets.sh and commit the result." exit 1 fi diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ff4f6aae8..872ac7fc7 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -38,6 +38,8 @@ jobs: jdk_vendor: ['corretto'] steps: - uses: actions/checkout@v7 + - name: Mark workspace as safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Prepare CI Environment uses: ./.github/actions/prepare_env - name: Cache .build diff --git a/Snippets/SwiftJavaCLIHelp.sh b/Snippets/SwiftJavaCLIHelp.txt similarity index 100% rename from Snippets/SwiftJavaCLIHelp.sh rename to Snippets/SwiftJavaCLIHelp.txt diff --git a/scripts/generate-cli-help-snippets.sh b/scripts/generate-cli-help-snippets.sh index 9446687a8..368914a20 100755 --- a/scripts/generate-cli-help-snippets.sh +++ b/scripts/generate-cli-help-snippets.sh @@ -14,7 +14,7 @@ ##===----------------------------------------------------------------------===## ## ## Captures the live --help output of the swift-java CLI and writes it to -## Snippets/SwiftJavaCLIHelp.sh, sliced per subcommand so the DocC pages can +## Snippets/SwiftJavaCLIHelp.txt, sliced per subcommand so the DocC pages can ## embed the *actual* --help/USAGE text via @Snippet instead of a pasted copy ## that can drift out of sync with the tool. ## @@ -25,7 +25,7 @@ set -eu cd "$(dirname "$0")/.." -OUTPUT_FILE="Snippets/SwiftJavaCLIHelp.sh" +OUTPUT_FILE="Snippets/SwiftJavaCLIHelp.txt" echo "Building swift-java..." swift build --product swift-java