From 3403620c82678d006066b9ccd47ba090d104d36c Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 8 Jul 2026 13:19:10 +0900 Subject: [PATCH 1/3] Update structure of docs to use snippets --- Package.swift | 13 +- .../Sources/JavaDependencySample/main.swift | 2 + .../JavaKitExample/JavaKitExample.swift | 23 +- .../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 + .../com/example/swift/DataImportTest.java | 2 + .../java/com/example/swift/FFMArraysTest.java | 38 +- .../java/com/example/swift/FFMTupleTest.java | 2 + .../com/example/swift/MySwiftClassTest.java | 2 + .../com/example/swift/OptionalImportTest.java | 2 + .../swift/swiftkitffm/MySwiftStructTest.java | 2 + .../Sources/MySwiftLibrary/Alignment.swift | 2 + .../Sources/MySwiftLibrary/Arrays.swift | 28 +- .../Sources/MySwiftLibrary/Async.swift | 2 + .../MySwiftLibrary/BoxSpecialization.swift | 2 + .../Sources/MySwiftLibrary/Closures.swift | 2 + .../MySwiftLibrary/EscapingClosures.swift | 2 + .../MySwiftLibrary/FoundationTypes.swift | 2 + .../Sources/MySwiftLibrary/GenericType.swift | 2 + .../Sources/MySwiftLibrary/MySwiftClass.swift | 4 + .../MySwiftLibrary/MySwiftStruct.swift | 2 + .../Sources/MySwiftLibrary/Optionals.swift | 2 + .../Sources/MySwiftLibrary/ProtocolA.swift | 4 + .../MySwiftLibrary/ReturnProtocol.swift | 4 + .../Sources/MySwiftLibrary/Set.swift | 2 + .../Sources/MySwiftLibrary/Throw.swift | 2 + .../Sources/MySwiftLibrary/Tuples.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 + .../java/com/example/swift/ClosuresTest.java | 6 + .../example/swift/EscapingClosuresTest.java | 14 +- .../com/example/swift/FoundationTypeTest.java | 2 + .../com/example/swift/GenericTypeTest.java | 2 + .../com/example/swift/MySwiftClassTest.java | 4 + .../com/example/swift/MySwiftStructTest.java | 2 + .../java/com/example/swift/OptionalsTest.java | 2 + .../java/com/example/swift/ProtocolTest.java | 2 + .../com/example/swift/ReturnProtocolTest.java | 2 + .../java/com/example/swift/SwiftSetTest.java | 2 + .../java/com/example/swift/ThrowTest.java | 2 + .../java/com/example/swift/TupleTest.java | 2 + .../com/example/swift/VehicleEnumTest.java | 2 + 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/ClosuresJavaJNI.java | 1 + Snippets/ClosuresSwift.swift.symlink | 1 + Snippets/DataJavaFFM.java | 1 + Snippets/EnumsJavaJNI.java | 1 + Snippets/EnumsSwift.swift.symlink | 1 + Snippets/EscapingClosuresJavaJNI.java | 1 + Snippets/EscapingClosuresSwift.swift.symlink | 1 + Snippets/FoundationURLJavaJNI.java | 1 + Snippets/FoundationURLSwift.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 | 8 + Snippets/NotSupportedYetJavaJNI.java | 8 + Snippets/OptionalsJavaFFM.java | 1 + Snippets/OptionalsJavaJNI.java | 1 + Snippets/OptionalsSwift.swift.symlink | 1 + Snippets/ProtocolsJavaJNI.java | 1 + Snippets/ProtocolsSwift.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/SpecializationSwift.swift.symlink | 1 + Snippets/StructsJavaFFM.java | 1 + Snippets/StructsJavaJNI.java | 1 + Snippets/StructsSwift.swift.symlink | 1 + Snippets/ThrowingInitJavaJNI.java | 1 + Snippets/ThrowingInitSwift.swift.symlink | 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 + .../Documentation.docc/Android.md | 76 --- .../FeaturesJavaKitMacros.md | 320 +++++++++ .../Documentation.docc/FeaturesJextract.md | 593 ++++++++++++++++ .../Documentation.docc/FeaturesOverview.md | 39 ++ .../Documentation.docc/ReducingBinarySize.md | 75 +++ .../Documentation.docc/SupportedFeatures.md | 631 ------------------ .../SwiftJavaCommandLineTool.md | 12 +- .../Documentation.docc/SwiftJavaConfigFile.md | 20 + .../Documentation.docc/SwiftJavaJextract.md | 12 + .../Documentation.docc/SwiftJavaWrapJava.md | 194 ++++++ .../Documentation.docc/SwiftPMPlugin.md | 2 +- .../Documentation.docc/index.md | 13 +- .../AnalysisResultTests.swift | 25 + 130 files changed, 1609 insertions(+), 806 deletions(-) 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/ClosuresJavaJNI.java create mode 120000 Snippets/ClosuresSwift.swift.symlink create mode 120000 Snippets/DataJavaFFM.java 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/FoundationURLJavaJNI.java create mode 120000 Snippets/FoundationURLSwift.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/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/SpecializationSwift.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/ThrowingInitJavaJNI.java create mode 120000 Snippets/ThrowingInitSwift.swift.symlink 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/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/SwiftJavaWrapJava.md diff --git a/Package.swift b/Package.swift index 4c28d1d68..dabc73fc7 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,17 @@ if let localPath = Context.environment["SWIFT_JAVA_JNI_CORE_PATH"] { swiftJavaJNICoreDep = .package(url: "https://github.com/swiftlang/swift-java-jni-core", branch: "main") } +// Opt-in swift-docc-plugin dependency, only pulled in when generating documentation +// Set DOCC_PLUGIN=1 to enable, or DOCC_PLUGIN_PATH= to point at a local checkout +let doccPluginDep: [Package.Dependency] +if let localPath = Context.environment["DOCC_PLUGIN_PATH"] { + doccPluginDep = [.package(path: localPath)] +} else if Context.environment["DOCC_PLUGIN"] != nil { + doccPluginDep = [.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0")] +} else { + doccPluginDep = [] +} + let package = Package( name: "swift-java", platforms: [ @@ -159,7 +170,7 @@ let package = Package( // Benchmarking .package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")), - ], + ] + doccPluginDep, targets: [ .target( name: "SwiftJavaDocumentation", 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..e280d8cba 100644 --- a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -19,6 +19,7 @@ enum SwiftWrappedError: Error { case message(String) } +// snippet.implementation @JavaImplementation("com.example.swift.HelloSwift") extension HelloSwift: HelloSwiftNativeMethods { @JavaMethod @@ -27,7 +28,9 @@ extension HelloSwift: HelloSwiftNativeMethods { 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 +38,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 +54,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,6 +69,7 @@ extension HelloSwift: HelloSwiftNativeMethods { } else { fatalError("Expected subclass here") } + // snippet.end // Check escaped name assert(self.`init`(42) == 42) @@ -70,25 +79,34 @@ extension HelloSwift: HelloSwiftNativeMethods { assert(newHello.is(HelloSwift.self)) assert(!newHello.is(HelloSubclass.self)) - // 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.end + // snippet.optionalsWrapper func checkOptionals(helper: ThreadSafeHelperClass) { let text: JavaString? = helper.textOptional let value: String? = helper.getValueOptional(Optional.none) @@ -101,6 +119,7 @@ extension HelloSwift: HelloSwiftNativeMethods { print("Optional double function returned \(doubleOpt)") print("Optional long function returned \(longOpt)") } + // snippet.end @JavaMethod func throwMessageFromSwift(_ message: String) throws -> String { 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..dee677514 100644 --- a/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitArrayRuntimeTests.swift +++ b/Samples/JavaKitSampleApp/Tests/JavaKitExampleTests/JavaKitArrayRuntimeTests.swift @@ -21,6 +21,7 @@ struct JavaKitArrayRuntimeTests { let jvm = try JavaKitSampleJVM.shared + // snippet.arraysUsage @Test func getFixedBytes() throws { let env = try jvm.environment() @@ -31,30 +32,40 @@ 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) + let reversed: [Int8] = arrays.reverseBytes([10, 20, 30]) + #expect(reversed == [30, 20, 10]) } @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"]) } + // snippet.end @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/src/test/java/com/example/swift/DataImportTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java index 82fb09464..923b75499 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java @@ -24,6 +24,7 @@ public class DataImportTest { @Test void test_Data_receiveAndReturn() { + // snippet.dataUsageJava try (var arena = AllocatingSwiftArena.ofConfined()) { var origBytes = arena.allocateFrom("foobar"); var origDat = Data.init(origBytes, origBytes.byteSize(), arena); @@ -37,6 +38,7 @@ void test_Data_receiveAndReturn() { assertEquals("foobar", str); }); } + // 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..853460827 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,25 @@ public class FFMArraysTest { + @Test + void test_getArray() { + // snippet.primitiveArraysUsage + byte[] javaBytes = MySwiftLibrary.getArray(); // automatically converted [UInt8] to byte[] + assertArrayEquals(new byte[]{1, 2, 3}, javaBytes); + // snippet.end + } + + @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 +64,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..5ee32626e 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java @@ -30,9 +30,11 @@ public class FFMTupleTest { @Test void ffmTupleReturnPair_roundTrip() { + // snippet.tupleUsageJava Tuple2 result = MySwiftLibrary.ffmTupleReturnPair(); assertEquals(42, result.$0); assertEquals(43L, result.$1); + // snippet.end } @Test 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..77d968758 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -51,11 +51,13 @@ void test_MySwiftClass_voidMethod() { @Test void test_MySwiftClass_makeIntMethod() { + // snippet.classUsageJava try(var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass o = MySwiftClass.init(12, 42, arena); var got = o.makeIntMethod(); assertEquals(12, got); } + // snippet.end } @Test 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..68b48f591 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java @@ -25,11 +25,13 @@ public class OptionalImportTest { @Test void test_Optional_receive() { + // snippet.optionalUsageJava try (var arena = AllocatingSwiftArena.ofConfined()) { var origBytes = arena.allocateFrom("foobar"); var data = Data.init(origBytes, origBytes.byteSize(), arena); assertEquals(0, MySwiftLibrary.globalReceiveOptional(OptionalLong.empty(), Optional.empty())); assertEquals(3, MySwiftLibrary.globalReceiveOptional(OptionalLong.of(12), Optional.of(data))); } + // 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..b5598066c 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java @@ -24,6 +24,7 @@ public class MySwiftStructTest { @Test void create_struct() { + // snippet.structUsageJava try (var arena = AllocatingSwiftArena.ofConfined()) { long cap = 12; long len = 34; @@ -32,6 +33,7 @@ void create_struct() { assertEquals(cap, struct.getCapacity()); assertEquals(len, struct.getLength()); } + // snippet.end } @Test 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 4ee87109f..d2fbdd8d1 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() } @@ -27,3 +28,4 @@ public func closureMultipleArguments( ) -> Int64 { closure(input1, input2) } +// snippet.end 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..b674663d8 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationTypes.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationTypes.swift @@ -36,6 +36,8 @@ public func makeUUID() -> UUID { UUID() } +// 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 9a2c3368b..18fd37e53 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -14,6 +14,7 @@ import SwiftJava +// snippet.classDefinition public class MySwiftClass { public let x: Int64 public let y: Int64 @@ -61,6 +62,7 @@ public class MySwiftClass { self.y = 5 } + // snippet.throwingInitDefinition convenience public init(throwing: Bool) throws { if throwing { throw MySwiftError.swiftError @@ -68,6 +70,7 @@ public class MySwiftClass { self.init() } } + // snippet.end deinit { } @@ -100,6 +103,7 @@ public class MySwiftClass { self.x + other.longValue() } } +// snippet.end extension MySwiftClass: CustomStringConvertible { public var description: String { diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 4c7ce1bc3..676be02b2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.structDefinition public struct MySwiftStruct { private var cap: Int64 public var len: Int64 @@ -66,3 +67,4 @@ public struct MySwiftStruct { false } } +// snippet.end diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift index b58423013..8c3029423 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -40,6 +40,7 @@ public func optionalInt(input: Int32?) -> Int32? { input } +// snippet.optionalDefinition public func optionalLong(input: Int64?) -> Int64? { input } @@ -67,6 +68,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..0f470bece 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +// snippet.tupleDefinition public func returnPair() -> (Int64, String) { (42, "hello") } @@ -23,6 +24,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/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..1f074bc9e 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.asyncUsageJava Future future = MySwiftLibrary.asyncSleep(); future.get(); + // snippet.end } @Test void asyncCopy() throws Exception { + // snippet.asyncUsageJava 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/ClosuresTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java index b8389d419..c65b35768 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ClosuresTest.java @@ -23,22 +23,28 @@ public class ClosuresTest { @Test void emptyClosure() { + // snippet.closureUsageJava AtomicBoolean closureCalled = new AtomicBoolean(false); MySwiftLibrary.emptyClosure(() -> { closureCalled.set(true); }); assertTrue(closureCalled.get()); + // snippet.end } @Test void closureWithInt() { + // snippet.closureUsageJava long result = MySwiftLibrary.closureWithInt(10, (value) -> value * 2); assertEquals(20, result); + // snippet.end } @Test void closureMultipleArguments() { + // snippet.closureUsageJava long result = MySwiftLibrary.closureMultipleArguments(5, 10, (a, b) -> a + b); assertEquals(15, result); + // snippet.end } } \ No newline at end of file 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..47a49eb4d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java @@ -75,6 +75,7 @@ void makeUUID() { @Test void echoURL() { + // snippet.foundationURLUsageJava try (var arena = SwiftArena.ofConfined()) { var url = URL.init("http://example.com", arena); assertDoesNotThrow(() -> { @@ -82,5 +83,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 e69ac9a14..dd835b1bf 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 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..fb7cae531 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java @@ -25,11 +25,13 @@ public class MySwiftStructTest { @Test void init() { + // snippet.structUsageJava try (var arena = SwiftArena.ofConfined()) { MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); assertEquals(1337, s.getCapacity()); assertEquals(42, s.getLen()); } + // snippet.end } @Test 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..a83a70b2d 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,10 @@ 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 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..bf9336806 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -109,6 +109,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 +118,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/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..68c2e8188 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java @@ -28,9 +28,11 @@ public class TupleTest { @Test void returnPair() { + // snippet.tupleUsageJava 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/VehicleEnumTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index 8fbe14c76..32ab42491 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 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/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/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/FoundationURLJavaJNI.java b/Snippets/FoundationURLJavaJNI.java new file mode 120000 index 000000000..61ca76218 --- /dev/null +++ b/Snippets/FoundationURLJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationTypeTest.java \ No newline at end of file diff --git a/Snippets/FoundationURLSwift.swift.symlink b/Snippets/FoundationURLSwift.swift.symlink new file mode 120000 index 000000000..8018d72f7 --- /dev/null +++ b/Snippets/FoundationURLSwift.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..79ed2061c --- /dev/null +++ b/Snippets/JavaKitArraysSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.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..4115271e5 --- /dev/null +++ b/Snippets/NotSupportedYetJavaFFM.java @@ -0,0 +1,8 @@ +// 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..d6aa0a033 --- /dev/null +++ b/Snippets/NotSupportedYetJavaJNI.java @@ -0,0 +1,8 @@ +// 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/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/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/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/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/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/SwiftJavaDocumentation/Documentation.docc/Android.md b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md index 38b740175..8822cda6a 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md @@ -58,79 +58,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/FeaturesJavaKitMacros.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md new file mode 100644 index 000000000..c9b3bcf9b --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJavaKitMacros.md @@ -0,0 +1,320 @@ +# 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 the full feature matrix, 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 `native int sayHello(int, int)` +declared in `HelloSwift.java`: + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/JavaKitImplementationSwift.swift", slice: "implementation") + } + @Tab("Java") { + @Snippet(path: "Snippets/JavaKitImplementationJava", slice: "helloClass") + } +} + +### 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) + +TODO: give an example + +### 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: "arraysWrapper") + } + @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") { + @Snippet(path: "Snippets/JavaKitSendableHelperJava", slice: "threadSafeHelper") + } +} + +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..4df478a3c --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -0,0 +1,593 @@ +# 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 the full feature matrix, see . + +### Initializers + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ClassesSwift.swift", slice: "classDefinition") + } + @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: +```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; +} +``` +For Java 21+, use [pattern matching for switch](https://openjdk.org/jeps/441): +```java +Vehicle vehicle = ...; +switch (vehicle.getCase()) { + case Vehicle.Bicycle b: + System.out.println("Bicycle maker: " + b.maker()); + break; + case Vehicle.Car c: + System.out.println("Car: " + c.arg0()); + break; +} +``` +or destructure the records directly: +```java +Vehicle vehicle = ...; +switch (vehicle.getCase()) { + case Vehicle.Car(var name, var unused): + System.out.println("Car: " + name); + break; + default: + break; +} +``` + +For Java 16+, use [pattern matching for instanceof](https://openjdk.org/jeps/394): +```java +Vehicle vehicle = ...; +Vehicle.Case case = vehicle.getCase(); +if (case instanceof Vehicle.Bicycle b) { + System.out.println("Bicycle maker: " + b.maker()); +} else if(case instanceof Vehicle.Car c) { + System.out.println("Car: " + c.arg0()); +} +``` + +### 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: "structDefinition") + } + @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) — not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### 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. + +### 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. + +### 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. + +@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") + } +} + +### 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") + } +} + +### 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`. + +### 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") + } +} + +### 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. + +### 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) — not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaJNI", slice: "notSupportedYet") + } + @Tab("Java (FFM) — not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### Tuples + +Tuples are imported as `Tuple2`, `Tuple3`, etc. types with positional `$0`, `$1` accessors. +Labeled tuples also get named accessors in JNI mode. + +@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") + } +} + +### 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: "protocolUsageJava") + } + @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. + +### Downcasting a returned protocol value to its 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). + +### Foundation 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/OptionalsSwift.swift", slice: "optionalDefinition") + } + @Tab("Java (JNI) — not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaJNI", slice: "notSupportedYet") + } + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/DataJavaFFM", slice: "dataUsageJava") + } +} + +### Foundation Date and UUID + +In JNI mode, `Foundation.Date` and `Foundation.UUID` parameters and return +types are mapped to appropriate Java wrapper types. Not yet supported in FFM mode. + +### Foundation URL + +Both `Foundation.URL` and `FoundationEssentials.URL` are recognized and imported. +On the Java side they surface as `java.net.URL`, so URLs can flow across the boundary +without manual string conversion. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/FoundationURLSwift.swift", slice: "foundationURLDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/FoundationURLJavaJNI", 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 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)") { + @Snippet(path: "Snippets/OptionalsJavaFFM", slice: "optionalUsageJava") + } +} + +### 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` | + +### Strings + +Strings are passed by copying data across the language boundary. + +### Subscripts + +Swift subscripts are imported as `getSubscript`/`setSubscript` methods. + +### Non-escaping closures + +Non-escaping closures with `Void` return or primitive arguments/results are supported +in both modes. The Swift closure parameter becomes a Java functional interface that +can be implemented with a lambda. + +@TabNavigator { + @Tab("Swift") { + @Snippet(path: "Snippets/ClosuresSwift.swift", slice: "closureDefinition") + } + @Tab("Java (JNI)") { + @Snippet(path: "Snippets/ClosuresJavaJNI", slice: "closureUsageJava") + } + @Tab("Java (FFM) — not supported") { + @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + } +} + +### 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 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..eee0b37b6 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md @@ -0,0 +1,39 @@ +# Features Overview + +Orientation guide for choosing the right swift-java tool for your interop task. + +### 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..ba254f07a --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md @@ -0,0 +1,75 @@ +# 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. + +### 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 a76787756..000000000 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ /dev/null @@ -1,631 +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 `abstract class` | TODO | -| Java `enum` | ❌ | -| Java methods: `static`, member | ✅ `@JavaMethod` | -| **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..3b9b60038 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. @@ -255,13 +255,3 @@ public final class SomeModule ... { } ``` -### 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..df3671504 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -0,0 +1,20 @@ +# swift-java.config + +Many of the tools–as well as SwiftPM plugin's–behaviors can be configured using the `swift-java.config` file. + +### 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 youf configuration. + +## Supported configuration options + + + + \ 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..1a1803bf9 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md @@ -0,0 +1,12 @@ + +# 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. + +## Workflow + +TODO: document all this \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaWrapJava.md new file mode 100644 index 000000000..799d0549d --- /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 the full feature matrix, 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 3d7e892fd..a37c559d0 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -34,18 +34,23 @@ which is a quick overview of all the features and approaches offered by SwiftJav ### Supported Features -- - +- +- +- ### Source Generation - +- +- - +- -### Distribution +### Troubleshooting and optimization -- +- ### Android Support - + diff --git a/Tests/SwiftExtractTests/AnalysisResultTests.swift b/Tests/SwiftExtractTests/AnalysisResultTests.swift index 703e27ef6..3d3c5659f 100644 --- a/Tests/SwiftExtractTests/AnalysisResultTests.swift +++ b/Tests/SwiftExtractTests/AnalysisResultTests.swift @@ -372,6 +372,31 @@ struct AnalysisResultSuite { #expect(result.extractedTypes["OnlyWhenImportable"] == nil) } + @Test func customOperator() throws { + var config = DefaultSwiftExtractConfiguration() + config.availableImportModules = ["MadeUpModule"] + + let result = try analyze( + sources: [ + ( + "/fake/Source.swift", + """ + struct Box { + static prefix func +++ (other: inout Box) -> Box { + return other + } + } + """ + ) + ], + moduleName: "Aquarium", + config: config + ) + + #expect(result.extractedTypes["AlwaysHere"] != nil) + #expect(result.extractedTypes["OnlyWhenImportable"] != nil) + } + /// Adding the module to `availableImportModules` activates the /// `#if canImport()` clause so its declarations are extracted. @Test func availableImportModulesActivatesCanImportClause() throws { From 45bced411ffc2a88e2532883b1bad6f5d318c7b2 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 9 Jul 2026 08:58:11 +0900 Subject: [PATCH 2/3] Update swift-java.config docs generator and docs --- .github/scripts/validate_docs.sh | 2 + Package.swift | 17 + Sources/GenerateConfigDocs/ConfigParser.swift | 595 ++++++++++++++++++ .../GenerateConfigDocs.swift | 124 ++++ .../GenerateConfigDocs/MarkdownRenderer.swift | 198 ++++++ .../GenerateConfigDocs/MarkerSplicer.swift | 35 ++ .../Configuration.swift | 136 +++- .../JExtract/JavaSourceLevel.swift | 1 + .../Documentation.docc/SwiftJavaConfigFile.md | 507 ++++++++++++++- scripts/generated-docs.sh | 57 ++ scripts/release.sh | 22 + 11 files changed, 1682 insertions(+), 12 deletions(-) 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 100755 scripts/generated-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/Package.swift b/Package.swift index dabc73fc7..54528462c 100644 --- a/Package.swift +++ b/Package.swift @@ -427,6 +427,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/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[..` + /// 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 --------------------------------------------------------- @@ -231,11 +322,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 +340,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 +406,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..6112fc55f 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 jdk24 = 25 public static var `default`: Self { .jdk22 } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md index df3671504..64c36b61a 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -1,6 +1,6 @@ # swift-java.config -Many of the tools–as well as SwiftPM plugin's–behaviors can be configured using the `swift-java.config` file. +SwiftJava tools can be configured using the `swift-java.config` file. ### The swift-java.config file @@ -11,10 +11,513 @@ 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 youf 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 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.Vector) and the values are +the corresponding Swift names (e.g., JavaVector). + +--- + +#### sourceCompatibility + +- **Type:** `JavaVersion?` + +Compile for the specified Java SE release. + +--- + +#### targetCompatibility + +- **Type:** `JavaVersion?` + +Generate class files suitable for the specified Java SE release. + +--- + +#### 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/scripts/generated-docs.sh b/scripts/generated-docs.sh new file mode 100755 index 000000000..60fddb7e4 --- /dev/null +++ b/scripts/generated-docs.sh @@ -0,0 +1,57 @@ +#!/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/generated-docs.sh build static HTML into .build/documentation +## scripts/generated-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. +export DOCC_PLUGIN=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 89dfa97ca121fb530a6611317403e511f69e1b11 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 9 Jul 2026 10:57:55 +0900 Subject: [PATCH 3/3] Some more docs --- .licenseignore | 1 + Snippets/NotSupportedYetJavaFFM.java | 14 +++++++ Snippets/NotSupportedYetJavaJNI.java | 14 +++++++ .../Configuration.swift | 38 +++++++++++++++++-- .../JExtract/JavaSourceLevel.swift | 2 +- .../Documentation.docc/SwiftJavaConfigFile.md | 14 ++++++- 6 files changed, 76 insertions(+), 7 deletions(-) diff --git a/.licenseignore b/.licenseignore index 40673100a..f38d3e0b3 100644 --- a/.licenseignore +++ b/.licenseignore @@ -53,3 +53,4 @@ Sources/_Subprocess/** Sources/_SubprocessCShims/** Samples/gradle SwiftKitCore/src/main/resources/META-INF/proguard/consumer-proguard-rules.pro +Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ diff --git a/Snippets/NotSupportedYetJavaFFM.java b/Snippets/NotSupportedYetJavaFFM.java index 4115271e5..b0da78bfd 100644 --- a/Snippets/NotSupportedYetJavaFFM.java +++ b/Snippets/NotSupportedYetJavaFFM.java @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// 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. diff --git a/Snippets/NotSupportedYetJavaJNI.java b/Snippets/NotSupportedYetJavaJNI.java index d6aa0a033..7a5600880 100644 --- a/Snippets/NotSupportedYetJavaJNI.java +++ b/Snippets/NotSupportedYetJavaJNI.java @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// 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. diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index e39a400ba..ed56f7b45 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -306,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 diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift index 6112fc55f..eeb29dd48 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift @@ -22,7 +22,7 @@ public enum JavaSourceLevel: Int, Comparable, Sendable { case jdk21 = 21 case jdk22 = 22 case jdk24 = 24 - case jdk24 = 25 + case jdk25 = 25 public static var `default`: Self { .jdk22 } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md index 64c36b61a..094efa3f3 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -413,8 +413,18 @@ The Java class path that should be passed along to the swift-java tool. - **Default:** empty dictionary (`[:]`) 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" + } +} +``` ---