diff --git a/.github/scripts/validate_docs.sh b/.github/scripts/validate_docs.sh index 5e5539430..73e4edc6f 100755 --- a/.github/scripts/validate_docs.sh +++ b/.github/scripts/validate_docs.sh @@ -3,6 +3,8 @@ set -e set -x +swift run generate-config-docs --check + DEPENDENCY='.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0")' if grep -q "$DEPENDENCY" Package.swift; then diff --git a/.licenseignore b/.licenseignore index 40673100a..ca8600a84 100644 --- a/.licenseignore +++ b/.licenseignore @@ -45,6 +45,7 @@ gradlew.bat **/gradlew.bat **/ci-validate.sh **/DO_NOT_EDIT.txt +Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ Plugins/**/_PluginsShared Plugins/**/0_PLEASE_SYMLINK* Plugins/PluginsShared/JavaKitConfigurationShared diff --git a/Package.swift b/Package.swift index 14cc44f25..79e9de87c 100644 --- a/Package.swift +++ b/Package.swift @@ -11,12 +11,15 @@ if let localPath = Context.environment["SWIFT_JAVA_JNI_CORE_PATH"] { swiftJavaJNICoreDep = .package(url: "https://github.com/swiftlang/swift-java-jni-core", branch: "main") } -// Set SWIFTJAVA_DOCC_PLUGIN_INSTALL=1 to install the docc-plugin automatically. +// Set SWIFTJAVA_DOCC_PLUGIN_INSTALL=1 to install the docc-plugin automatically, +// or DOCC_PLUGIN_PATH= to point at a local checkout. // This is a workaround because swift-subprocess includes the plugin explicitly, // which breaks tools trying to add `swift package add-dependency` the plugin // to swift-java because it thinks the plugin was already added, but it is not. let extraDependencies: [Package.Dependency] -if Context.environment["SWIFTJAVA_DOCC_PLUGIN_INSTALL"] == "1" { +if let localPath = Context.environment["DOCC_PLUGIN_PATH"] { + extraDependencies = [.package(path: localPath)] +} else if Context.environment["SWIFTJAVA_DOCC_PLUGIN_INSTALL"] == "1" { extraDependencies = [ .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.5.0") ] @@ -429,6 +432,23 @@ let package = Package( ] ), + // Dev-time tool: regenerates the "Supported configuration options" table in + // `Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md` + // straight from the doc comments on `Configuration.swift` and the enums it + // references, so the two never drift apart. + // + // Run manually via `swift run generate-config-docs`; CI validates freshness + // with `swift run generate-config-docs --check`. + .executableTarget( + name: "generate-config-docs", + dependencies: [ + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Sources/GenerateConfigDocs" + ), + .plugin( name: "_StaticBuildConfigPlugin", capability: .buildTool(), diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift index 64088d31a..01ac7ce6d 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift @@ -28,6 +28,7 @@ print("Start Sample app...") // TODO: locating the classpath is more complex, need to account for dependencies of our module let swiftJavaClasspath = findSwiftJavaClasspaths() // scans for .classpath files +// snippet.dependencyUsage // 1) Start a JVM with appropriate classpath let jvm = try JavaVirtualMachine.shared(classpath: swiftJavaClasspath) @@ -50,5 +51,6 @@ for record in try CSVFormatClass.RFC4180.parse(reader)!.getRecords()! { print("Field: \(field)") } } +// snippet.end print("Done.") diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift index 2c7ee67aa..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..b0da78bfd --- /dev/null +++ b/Snippets/NotSupportedYetJavaFFM.java @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// This file is an intentional exception to the "Snippets/ is symlink-only" rule +// documented in Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ . It is a pedagogical +// placeholder for @Tab("Java (FFM) - not supported") blocks in the DocC pages. +// If FFM ever supports the feature in question, replace usages of this file +// with a symlink to a real sample. +// snippet.notSupportedYet +// not supported yet +// snippet.end diff --git a/Snippets/NotSupportedYetJavaJNI.java b/Snippets/NotSupportedYetJavaJNI.java new file mode 100644 index 000000000..7a5600880 --- /dev/null +++ b/Snippets/NotSupportedYetJavaJNI.java @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// This file is an intentional exception to the "Snippets/ is symlink-only" rule +// documented in Snippets/__DO_NOT_EDIT_SYMLINK_ONLY__ . It is a pedagogical +// placeholder for @Tab("Java (JNI) - not supported") blocks in the DocC pages. +// If JNI ever supports the feature in question, replace usages of this file +// with a symlink to a real sample. +// snippet.notSupportedYet +// not supported yet +// snippet.end diff --git a/Snippets/OptionalsJavaFFM.java b/Snippets/OptionalsJavaFFM.java new file mode 120000 index 000000000..4a99f7b85 --- /dev/null +++ b/Snippets/OptionalsJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OptionalImportTest.java \ No newline at end of file diff --git a/Snippets/OptionalsJavaJNI.java b/Snippets/OptionalsJavaJNI.java new file mode 120000 index 000000000..6364fb8fb --- /dev/null +++ b/Snippets/OptionalsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java \ No newline at end of file diff --git a/Snippets/OptionalsSwift.swift.symlink b/Snippets/OptionalsSwift.swift.symlink new file mode 120000 index 000000000..727e40234 --- /dev/null +++ b/Snippets/OptionalsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift \ No newline at end of file diff --git a/Snippets/ProtocolsJavaJNI.java b/Snippets/ProtocolsJavaJNI.java new file mode 120000 index 000000000..6d38e6be5 --- /dev/null +++ b/Snippets/ProtocolsJavaJNI.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java \ No newline at end of file diff --git a/Snippets/ProtocolsSwift.swift.symlink b/Snippets/ProtocolsSwift.swift.symlink new file mode 120000 index 000000000..80b9ef763 --- /dev/null +++ b/Snippets/ProtocolsSwift.swift.symlink @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift \ No newline at end of file diff --git a/Snippets/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/GenerateConfigDocs/ConfigParser.swift b/Sources/GenerateConfigDocs/ConfigParser.swift new file mode 100644 index 000000000..fa3d72b8b --- /dev/null +++ b/Sources/GenerateConfigDocs/ConfigParser.swift @@ -0,0 +1,595 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftParser +import SwiftSyntax + +// ==== ----------------------------------------------------------------------- +// MARK: Parsed model + +/// A single case on an enum we care about. +struct EnumCaseInfo { + var name: String + /// For raw-value / bare cases: the display string (raw value if present, else the case name). + var display: String? + /// For associated-value cases: the parameter clause verbatim (without parens). + var associatedSignature: String? + /// Doc-comment paragraph lines (already stripped of `///` prefix). + var docLines: [String] +} + +struct EnumInfo { + var name: String + var docLines: [String] + var cases: [EnumCaseInfo] + /// The name of the `.` value returned by `public static var \`default\``, if any. + var defaultCase: String? + + var hasAssociatedValues: Bool { + cases.contains { $0.associatedSignature != nil } + } + + /// First paragraph of doc-lines, joined with spaces (blank line ends the paragraph). + var firstParagraph: String { + var out: [String] = [] + for l in docLines { + if l.trimmingCharacters(in: .whitespaces).isEmpty { break } + out.append(l) + } + return out.joined(separator: " ").trimmingCharacters(in: .whitespaces) + } + + /// Plain / raw-value cases with their first-paragraph docs (associated-value cases skipped). + var caseDocs: [(display: String, doc: String)] { + cases.compactMap { c in + guard c.associatedSignature == nil, let display = c.display else { return nil } + var first: [String] = [] + for l in c.docLines { + if l.trimmingCharacters(in: .whitespaces).isEmpty { break } + first.append(l) + } + return (display, first.joined(separator: " ").trimmingCharacters(in: .whitespaces)) + } + } +} + +struct StructPropertyInfo { + var name: String + var type: String + var docLines: [String] +} + +struct StructInfo { + var name: String + var docLines: [String] + var properties: [StructPropertyInfo] +} + +/// One field on the `Configuration` struct. +struct ConfigField { + var name: String + var type: String + /// The literal RHS of `= ` on the stored-property line, if any (e.g. `nil`, `"[:]"`, `false`). + var defaultLiteral: String? + var docLines: [String] + var section: String +} + +// ==== ----------------------------------------------------------------------- +// MARK: Trivia helpers + +/// Doc-comment paragraph lines pulled off of leading trivia. +/// +/// Ignores section markers which start with `// ====`. +func docLines(from trivia: Trivia) -> [String] { + var out: [String] = [] + for piece in trivia.pieces { + switch piece { + case .docLineComment(let raw): + var text = raw + if text.hasPrefix("///") { text.removeFirst(3) } + if text.hasPrefix(" ") { text.removeFirst() } + out.append(text) + + case .lineComment(let raw): + // Skip section-divider comments; they're structural, not documentation. + if sectionName(fromLineComment: raw) != nil { continue } + var text = raw + if text.hasPrefix("//") { text.removeFirst(2) } + if text.hasPrefix(" ") { text.removeFirst() } + out.append(text) + + case .docBlockComment(let raw): + // `/** ... */`. Strip the delimiters and split into lines. + var body = raw + if body.hasPrefix("/**") { body.removeFirst(3) } + if body.hasSuffix("*/") { body.removeLast(2) } + for line in body.split(separator: "\n", omittingEmptySubsequences: false) { + var t = String(line).trimmingCharacters(in: .whitespaces) + if t.hasPrefix("*") { t.removeFirst() } + if t.hasPrefix(" ") { t.removeFirst() } + out.append(t) + } + + case .newlines(let n): + // A blank line (2+ newlines) between comment runs resets the buffer, + // matching the Python parser's `pending_doc = []` on empty lines. + if n >= 2 { out.removeAll() } + + case .blockComment: + // Non-doc block comments (e.g. copyright headers) reset the buffer. + out.removeAll() + + case _: + continue + + @unknown default: + continue + } + } + return out +} + +/// The `// ==== ---` divider convention used inside `Configuration`. +/// Returns the section name if the trivia piece is a divider line comment. +func sectionName(fromLineComment raw: String) -> String? { + // Line-comment form: "// ==== Foo Bar -----" + var body = raw + if body.hasPrefix("//") { body.removeFirst(2) } + body = body.trimmingCharacters(in: .whitespaces) + guard body.hasPrefix("====") else { return nil } + body.removeFirst(4) + // Trim trailing `---...`. + while body.hasSuffix("-") { body.removeLast() } + return body.trimmingCharacters(in: .whitespaces).isEmpty + ? nil + : body.trimmingCharacters(in: .whitespaces) +} + +// ==== ----------------------------------------------------------------------- +// MARK: Type shape helpers + +/// The inner named type for `T?`, `[T]`, `[K: T]`, or bare `T`; else nil. +func resolveContainerElementType(_ typeRaw: String) -> String? { + var core = typeRaw.trimmingCharacters(in: .whitespaces) + while core.hasSuffix("?") { core.removeLast() } + core = core.trimmingCharacters(in: .whitespaces) + + // [T] + if core.hasPrefix("["), core.hasSuffix("]"), !core.contains(":") { + let inner = String(core.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces) + return isBareIdentifier(inner) ? inner : nil + } + + // [K: V] + if core.hasPrefix("["), core.hasSuffix("]"), let colon = core.firstIndex(of: ":") { + var value = String(core[core.index(after: colon).. Bool { + guard !s.isEmpty else { return false } + return s.allSatisfy { $0.isLetter || $0.isNumber || $0 == "_" } +} + +// ==== ----------------------------------------------------------------------- +// MARK: The parser + +struct ConfigParser { + var enums: [String: EnumInfo] = [:] + var structs: [String: StructInfo] = [:] + + static func parse(rootDirs: [URL]) throws -> ConfigParser { + var parser = ConfigParser() + let fm = FileManager.default + var files: [URL] = [] + for dir in rootDirs { + guard let enumerator = fm.enumerator(at: dir, includingPropertiesForKeys: nil) else { continue } + for case let url as URL in enumerator { + if url.pathExtension == "swift" { + files.append(url) + } + } + } + files.sort { $0.path < $1.path } + for file in files { + let src = try String(contentsOf: file, encoding: .utf8) + let tree = Parser.parse(source: src) + parser.ingest(tree) + } + return parser + } + + mutating func ingest(_ tree: SourceFileSyntax) { + let visitor = TopLevelVisitor(viewMode: .sourceAccurate) + visitor.walk(tree) + for (name, info) in visitor.enums { + enums[name] = info + } + for (name, info) in visitor.structs { + structs[name] = info + } + // Apply any `public static var \`default\`: T { .caseName }` assignments now that + // enums/extensions from this file have been recorded (they may target enums + // declared in a different file we've already ingested, or in this one). + for (owner, caseName) in visitor.defaults { + if var info = enums[owner] { + info.defaultCase = caseName + enums[owner] = info + } + } + } +} + +// ==== ----------------------------------------------------------------------- +// MARK: SwiftSyntax visitor + +/// Walks a `SourceFileSyntax` collecting public enum / struct declarations and +/// any `public static var \`default\`` bindings on those types (whether declared +/// inline or in an `extension EnumName { ... }`). +private final class TopLevelVisitor: SyntaxVisitor { + var enums: [String: EnumInfo] = [:] + var structs: [String: StructInfo] = [:] + /// (owner type name, case name) pairs from `public static var \`default\`: T { .case }`. + var defaults: [(owner: String, caseName: String)] = [] + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + guard node.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + return .skipChildren + } + let name = node.name.text + var info = EnumInfo( + name: name, + docLines: docLines(from: node.leadingTrivia), + cases: [], + defaultCase: nil + ) + collectEnumMembers(node.memberBlock, into: &info, owner: name) + enums[name] = info + return .skipChildren + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + guard node.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + return .skipChildren + } + let name = node.name.text + var info = StructInfo( + name: name, + docLines: docLines(from: node.leadingTrivia), + properties: [] + ) + collectStructMembers(node.memberBlock, into: &info) + structs[name] = info + return .skipChildren + } + + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + // Only interested in extensions that add `public static var \`default\``. + let owner = node.extendedType.trimmedDescription + for member in node.memberBlock.members { + if let vd = member.decl.as(VariableDeclSyntax.self), + let caseName = defaultCaseName(from: vd) + { + defaults.append((owner: owner, caseName: caseName)) + } + } + return .skipChildren + } + + private func collectEnumMembers( + _ block: MemberBlockSyntax, + into info: inout EnumInfo, + owner: String + ) { + for member in block.members { + if let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) { + let docs = docLines(from: caseDecl.leadingTrivia) + // One `case` decl can list multiple elements (`case a, b`); doc lines + // apply to all of them. + for element in caseDecl.elements { + let caseName = element.name.text.trimmingBackticks() + if let params = element.parameterClause { + // Associated-value case. + let sig = params.parameters.map { $0.trimmedDescription }.joined(separator: ", ") + info.cases.append( + EnumCaseInfo( + name: caseName, + display: nil, + associatedSignature: sig, + docLines: docs + ) + ) + } else { + var display = caseName + if let raw = element.rawValue { + display = raw.value.trimmedDescription + } + info.cases.append( + EnumCaseInfo( + name: caseName, + display: display, + associatedSignature: nil, + docLines: docs + ) + ) + } + } + } else if let vd = member.decl.as(VariableDeclSyntax.self), + let caseName = defaultCaseName(from: vd) + { + info.defaultCase = caseName + } + } + _ = owner // silence unused-owner lint in some SwiftSyntax builds + } + + private func collectStructMembers( + _ block: MemberBlockSyntax, + into info: inout StructInfo + ) { + for member in block.members { + guard let vd = member.decl.as(VariableDeclSyntax.self) else { continue } + // Only `public var name: Type` stored properties (no accessor block). + guard vd.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + continue + } + guard vd.bindingSpecifier.tokenKind == .keyword(.var) else { continue } + guard let binding = vd.bindings.first, vd.bindings.count == 1 else { continue } + guard binding.accessorBlock == nil else { continue } + guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else { continue } + guard let typeAnn = binding.typeAnnotation else { continue } + let name = ident.identifier.text.trimmingBackticks() + let type = typeAnn.type.trimmedDescription + info.properties.append( + StructPropertyInfo( + name: name, + type: type, + docLines: docLines(from: vd.leadingTrivia) + ) + ) + } + } + + /// If `vd` is `public static var \`default\`: T { .caseName }` (or `{ return .caseName }`, + /// or `{ .caseName }` with the trailing whitespace), return `caseName`; else nil. + private func defaultCaseName(from vd: VariableDeclSyntax) -> String? { + guard vd.modifiers.contains(where: { $0.name.tokenKind == .keyword(.static) }) else { + return nil + } + guard vd.bindingSpecifier.tokenKind == .keyword(.var) else { return nil } + guard let binding = vd.bindings.first else { return nil } + guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else { return nil } + guard ident.identifier.text.trimmingBackticks() == "default" else { return nil } + guard let accessor = binding.accessorBlock else { return nil } + + // Body forms: + // `{ .caseName }` -> AccessorBlockSyntax.accessors == .getter(CodeBlockItemListSyntax) + // `{ get { .caseName } }` -> .accessors(AccessorDeclListSyntax) + var body: CodeBlockItemListSyntax? + switch accessor.accessors { + case .getter(let items): + body = items + case .accessors(let list): + for acc in list where acc.accessorSpecifier.tokenKind == .keyword(.get) { + body = acc.body?.statements + } + } + guard let statements = body, let first = statements.first else { return nil } + // Strip an optional leading `return`. + let expr: ExprSyntax + if let ret = first.item.as(ReturnStmtSyntax.self), let e = ret.expression { + expr = e + } else if let e = first.item.as(ExprSyntax.self) { + expr = e + } else { + return nil + } + // Expect `.caseName` (a MemberAccessExprSyntax with no base). + guard let member = expr.as(MemberAccessExprSyntax.self), member.base == nil else { + return nil + } + return member.declName.baseName.text.trimmingBackticks() + } +} + +/// Walks the `Configuration` struct's members in source order to produce the +/// ordered list of fields (with section headings from `// ==== Name -----` +/// dividers) and the `effective` -> fallback map. +struct ConfigurationBody { + var fields: [ConfigField] = [] + var effectiveFallbacks: [String: String] = [:] + + static func parse(source: String) throws -> ConfigurationBody { + let tree = Parser.parse(source: source) + let finder = ConfigStructFinder(viewMode: .sourceAccurate) + finder.walk(tree) + guard let decl = finder.decl else { + throw ConfigDocsError( + "Could not find 'public struct Configuration' in the config source" + ) + } + var body = ConfigurationBody() + var currentSection = "General" + var pendingDoc: [String] = [] + + for member in decl.memberBlock.members { + // Any `// ==== Name ----` line comment in the leading trivia of a member + // resets the current section. + for piece in member.leadingTrivia.pieces { + if case .lineComment(let raw) = piece, let name = sectionName(fromLineComment: raw) { + currentSection = name + pendingDoc = [] + } + } + + // Collect doc-lines from the member's leading trivia. + let docs = docLines(from: member.leadingTrivia) + if !docs.isEmpty { + pendingDoc = docs + } + + guard let vd = member.decl.as(VariableDeclSyntax.self) else { + // Functions and other members reset the pending doc buffer to mirror + // the Python parser (which drops docs on non-property members). + pendingDoc = [] + continue + } + guard vd.modifiers.contains(where: { $0.name.tokenKind == .keyword(.public) }) else { + pendingDoc = [] + continue + } + guard vd.bindingSpecifier.tokenKind == .keyword(.var) else { + pendingDoc = [] + continue + } + guard let binding = vd.bindings.first, vd.bindings.count == 1 else { + pendingDoc = [] + continue + } + guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else { + pendingDoc = [] + continue + } + guard let typeAnn = binding.typeAnnotation else { + pendingDoc = [] + continue + } + let name = ident.identifier.text.trimmingBackticks() + let type = typeAnn.type.trimmedDescription + + if let accessor = binding.accessorBlock { + // Computed property. Detect `effective` and extract the `?? ` RHS. + if name.hasPrefix("effective"), + let fallback = nilCoalescingFallback(in: accessor) + { + let suffix = String(name.dropFirst("effective".count)) + guard let firstChar = suffix.first else { + pendingDoc = [] + continue + } + let underlying = String(firstChar).lowercased() + suffix.dropFirst() + body.effectiveFallbacks[underlying] = fallback + } + pendingDoc = [] + continue + } + + // Stored property. Record the field. + let defaultLit = binding.initializer.map { init_ in + init_.value.trimmedDescription.trimmingCharacters(in: .whitespaces) + } + body.fields.append( + ConfigField( + name: name, + type: type, + defaultLiteral: defaultLit, + docLines: pendingDoc, + section: currentSection + ) + ) + pendingDoc = [] + } + return body + } +} + +private final class ConfigStructFinder: SyntaxVisitor { + var decl: StructDeclSyntax? + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + if node.name.text == "Configuration" { + decl = node + return .skipChildren + } + return .visitChildren + } +} + +/// Given the accessor block of `effective: T { }`, if the body is +/// a single `?? ` expression (optionally with a `return`), return the +/// textual RHS (e.g. `.default`, `false`, `.public`). Otherwise nil. +private func nilCoalescingFallback(in accessor: AccessorBlockSyntax) -> String? { + let stmts: CodeBlockItemListSyntax? + switch accessor.accessors { + case .getter(let items): + stmts = items + case .accessors(let list): + stmts = + list.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) })? + .body?.statements + } + guard let statements = stmts, let first = statements.first else { return nil } + + var expr: ExprSyntax? + if let ret = first.item.as(ReturnStmtSyntax.self) { + expr = ret.expression + } else if let e = first.item.as(ExprSyntax.self) { + expr = e + } + guard let expression = expr else { return nil } + + // `a ?? b` parses as SequenceExpr(a, ??, b) *before* folding, or as + // InfixOperatorExpr(a, ??, b) after folding. + if let seq = expression.as(SequenceExprSyntax.self) { + let elements = Array(seq.elements) + for i in stride(from: 0, to: elements.count - 2, by: 2) { + if let op = elements[i + 1].as(BinaryOperatorExprSyntax.self), + op.operator.text == "??" + { + return elements[i + 2].trimmedDescription + } + } + } + if let infix = expression.as(InfixOperatorExprSyntax.self), + let op = infix.operator.as(BinaryOperatorExprSyntax.self), + op.operator.text == "??" + { + return infix.rightOperand.trimmedDescription + } + return nil +} + +// ==== ----------------------------------------------------------------------- +// MARK: Errors + +struct ConfigDocsError: Error, CustomStringConvertible { + let message: String + init(_ message: String) { self.message = message } + var description: String { message } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Small string helpers + +extension String { + fileprivate func trimmingBackticks() -> String { + var s = self + if s.hasPrefix("`") { s.removeFirst() } + if s.hasSuffix("`") { s.removeLast() } + return s + } +} + +extension Substring { + fileprivate func trimmingBackticks() -> String { + String(self).trimmingBackticks() + } +} diff --git a/Sources/GenerateConfigDocs/GenerateConfigDocs.swift b/Sources/GenerateConfigDocs/GenerateConfigDocs.swift new file mode 100644 index 000000000..b5c5b2bf9 --- /dev/null +++ b/Sources/GenerateConfigDocs/GenerateConfigDocs.swift @@ -0,0 +1,124 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + +import ArgumentParser +import Foundation + +/// Regenerates swift-java.config documentation in SwiftJavaConfigFile.md +/// straight from the doc comments on `SwiftJavaConfigurationShared.Configuration`. +/// +/// Usage: +/// swift run generate-config-docs regenerate the doc section in place +/// swift run generate-config-docs --check exit 1 if the doc section is stale, without writing +@main +struct GenerateConfigDocs: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "generate-config-docs", + abstract: + "Regenerate the Supported Configuration Options section of SwiftJavaConfigFile.md from Configuration.swift." + ) + + @Flag(name: .long, help: "Exit non-zero if the doc section is stale, without writing.") + var check: Bool = false + + func run() throws { + let repoRoot = try Self.detectRepoRoot() + + let configFile = + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftJavaConfigurationShared", isDirectory: true) + .appendingPathComponent("Configuration.swift", isDirectory: false) + + let docFile = + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftJavaDocumentation", isDirectory: true) + .appendingPathComponent("Documentation.docc", isDirectory: true) + .appendingPathComponent("SwiftJavaConfigFile.md", isDirectory: false) + + let scanDirs: [URL] = [ + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftJavaConfigurationShared", isDirectory: true), + repoRoot + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent("SwiftExtractConfigurationShared", isDirectory: true), + ] + + // Parse enums and structs referenced by Configuration. + let parsed = try ConfigParser.parse(rootDirs: scanDirs) + + // Parse the Configuration struct itself for its fields, sections, and effective fallbacks. + let configSource = try String(contentsOf: configFile, encoding: .utf8) + let body = try ConfigurationBody.parse(source: configSource) + + // Render Markdown and splice into SwiftJavaConfigFile.md. + let renderer = MarkdownRenderer( + enums: parsed.enums, + structs: parsed.structs, + fields: body.fields, + effectiveFallbacks: body.effectiveFallbacks + ) + let generated = renderer.render() + + let docText = try String(contentsOf: docFile, encoding: .utf8) + let newText = try MarkerSplicer.splice(into: docText, generated: generated) + + let relDoc = docFile.path.replacingOccurrences(of: repoRoot.path + "/", with: "") + + if newText == docText { + print("\(relDoc) already up to date (\(body.fields.count) options documented)") + return + } + + if check { + FileHandle.standardError.write( + Data( + """ + error: \(relDoc) is stale. + Run 'swift run generate-config-docs' and commit the result. + + """.utf8 + ) + ) + throw ExitCode(1) + } + + try newText.write(to: docFile, atomically: true, encoding: .utf8) + print("Updated \(relDoc) (\(body.fields.count) options documented)") + } + + /// Walk up from the executable's file location (this source file, at build + /// time, and the invoking working directory otherwise) to find the repo root + /// (identified by `Package.swift`). + private static func detectRepoRoot() throws -> URL { + // Start from the current working directory - `swift run` always chdir's to + // the package root, which is exactly what we want. + let fm = FileManager.default + var dir = URL(fileURLWithPath: fm.currentDirectoryPath, isDirectory: true) + for _ in 0..<20 { + if fm.fileExists(atPath: dir.appendingPathComponent("Package.swift").path) { + return dir + } + let parent = dir.deletingLastPathComponent() + if parent.path == dir.path { break } + dir = parent + } + throw ConfigDocsError( + "Could not locate Package.swift from current directory \(fm.currentDirectoryPath)" + ) + } +} diff --git a/Sources/GenerateConfigDocs/MarkdownRenderer.swift b/Sources/GenerateConfigDocs/MarkdownRenderer.swift new file mode 100644 index 000000000..8aa93d35c --- /dev/null +++ b/Sources/GenerateConfigDocs/MarkdownRenderer.swift @@ -0,0 +1,198 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Renders the parsed configuration model to the same Markdown shape the +/// previous Python generator produced, so the two can be diffed byte-for-byte +/// during the port and the DocC output stays unchanged. +struct MarkdownRenderer { + var enums: [String: EnumInfo] + var structs: [String: StructInfo] + var fields: [ConfigField] + var effectiveFallbacks: [String: String] + + func render() -> String { + var seen: [String: [ConfigField]] = [:] + var sectionOrder: [String] = [] + for field in fields { + if seen[field.section] == nil { sectionOrder.append(field.section) } + seen[field.section, default: []].append(field) + } + + var out: [String] = [] + for section in sectionOrder { + out.append("### \(section)") + out.append("") + for field in seen[section] ?? [] { + out.append(renderField(field)) + out.append("") + } + } + // Trim trailing whitespace-only lines, keep a single trailing newline. + var joined = out.joined(separator: "\n") + while joined.hasSuffix("\n") || joined.hasSuffix(" ") { + joined.removeLast() + } + return joined + "\n" + } + + private func renderField(_ field: ConfigField) -> String { + let typeClean = field.type.trimmingCharacters(in: .whitespaces).trimmingSuffix("?") + let isBareEnum = isBareIdentifier(typeClean) && enums[typeClean] != nil + let enumInfo = isBareEnum ? enums[typeClean] : nil + + var lines: [String] = [] + lines.append("#### \(field.name)") + lines.append("") + + lines.append("- **Type:** `\(field.type)`") + + var defaultDisplay = literalDisplay(field.defaultLiteral) + if defaultDisplay == nil, let fallback = effectiveFallbacks[field.name] { + if fallback == ".default", let ei = enumInfo, let defaultCase = ei.defaultCase { + defaultDisplay = "`\(resolveDefaultDisplay(ei, defaultCase))`" + } else if fallback.hasPrefix(".") { + defaultDisplay = "`\(fallback.dropFirst())`" + } else { + defaultDisplay = "`\(fallback)`" + } + } + if let display = defaultDisplay { + lines.append("- **Default:** \(display)") + } + + lines.append("") + + if !field.docLines.isEmpty { + lines.append(contentsOf: field.docLines) + } else if let ei = enumInfo, !ei.firstParagraph.isEmpty { + lines.append(ei.firstParagraph) + } else { + lines.append( + "_Undocumented - please add a doc comment on this property in `Configuration.swift`._" + ) + } + + if let ei = enumInfo, !ei.hasAssociatedValues { + let docs = ei.caseDocs + if !docs.isEmpty { + lines.append("") + lines.append("**Values:**") + lines.append("") + for (display, doc) in docs { + if doc.isEmpty { + lines.append("- `\(display)`") + } else { + lines.append("- `\(display)` - \(doc)") + } + } + } + } + + if let elementType = resolveContainerElementType(field.type), elementType != typeClean { + if structs[elementType] != nil + || (enums[elementType]?.hasAssociatedValues == true) + { + lines.append("") + lines.append(contentsOf: renderTypeSchemaLines(elementType)) + } + } + + lines.append("") + lines.append("---") + return lines.joined(separator: "\n") + } + + private func renderTypeSchemaLines(_ name: String) -> [String] { + var lines: [String] = ["**`\(name)`:**", ""] + if let s = structs[name] { + if !s.docLines.isEmpty { + lines.append(contentsOf: s.docLines) + lines.append("") + } + for prop in s.properties { + let doc = prop.docLines.joined(separator: " ").trimmingCharacters(in: .whitespaces) + if doc.isEmpty { + lines.append("- `\(prop.name)`: `\(prop.type)`") + } else { + lines.append("- `\(prop.name)`: `\(prop.type)` - \(doc)") + } + } + } else if let e = enums[name] { + if !e.docLines.isEmpty { + lines.append(contentsOf: e.docLines) + lines.append("") + } + if e.hasAssociatedValues { + var caseLines: [String] = [] + for c in e.cases { + let doc = c.docLines.joined(separator: " ").trimmingCharacters(in: .whitespaces) + if doc.isEmpty { continue } + let label: String + if let sig = c.associatedSignature { + label = "\(c.name)(\(sig))" + } else { + label = c.name + } + caseLines.append("- `\(label)` - \(doc)") + } + if caseLines.isEmpty && e.docLines.isEmpty { + for c in e.cases { + let label: String + if let sig = c.associatedSignature { + label = "\(c.name)(\(sig))" + } else { + label = c.name + } + caseLines.append("- `\(label)`") + } + } + lines.append(contentsOf: caseLines) + } else { + for (display, doc) in e.caseDocs { + if doc.isEmpty { + lines.append("- `\(display)`") + } else { + lines.append("- `\(display)` - \(doc)") + } + } + } + } + return lines + } + + private func resolveDefaultDisplay(_ enumInfo: EnumInfo, _ caseName: String) -> String { + for c in enumInfo.cases where c.name == caseName { + return c.display ?? caseName + } + return caseName + } + + private func literalDisplay(_ literal: String?) -> String? { + guard let raw = literal else { return nil } + if raw == "nil" { return nil } + if raw == "[:]" { return "empty dictionary (`[:]`)" } + if raw == "[]" { return "empty array (`[]`)" } + return "`\(raw)`" + } +} + +extension String { + fileprivate func trimmingSuffix(_ s: Character) -> String { + var r = self + while r.hasSuffix(String(s)) { r.removeLast() } + return r.trimmingCharacters(in: .whitespaces) + } +} diff --git a/Sources/GenerateConfigDocs/MarkerSplicer.swift b/Sources/GenerateConfigDocs/MarkerSplicer.swift new file mode 100644 index 000000000..cac01768a --- /dev/null +++ b/Sources/GenerateConfigDocs/MarkerSplicer.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Splices generated content into a Markdown file between START/END markers. +struct MarkerSplicer { + static let startMarker = "" + static let endMarker = "" + + static func splice(into doc: String, generated: String) throws -> String { + guard let startRange = doc.range(of: startMarker) else { + throw ConfigDocsError("Could not find '\(startMarker)' marker in doc file") + } + guard let endRange = doc.range(of: endMarker), + endRange.lowerBound > startRange.upperBound + else { + throw ConfigDocsError("Could not find '\(endMarker)' marker (after start) in doc file") + } + let before = doc[..`` to enable + /// file to the linker via `-Xlinker --version-script=` to enable /// precise dead-code elimination of unused Swift code in the final shared /// library. public var linkerExportListOutput: String? @@ -181,10 +210,22 @@ public struct Configuration: Codable { importedModuleStubs?.keys.contains(moduleName) ?? false } - /// Specialization entries for generic types, mapping a Java-facing name - /// to its base Swift type and concrete type arguments. + /// Force specialization of generic types, mapping them to a specific generated Java-facing name. + /// This allows generating generic specializations that can be used only with some specific bound generic argument, + /// rather than using the usual generic machinery. Sometimes useful if a generic type is only reasonably usable with some specific type. + /// + /// Generating specializations takes into account Swift extensions where the generic is bound to that type, for example, a `Box` + /// type, would automatically gain `T == Fish` specific methods in the generated Java sources if there is an `extension ... where T == Fish` declared in Swift: + /// + /// ```swift + /// struct Box {} + /// extension Box where T == Fish { + /// func feedFish() + /// } + /// ``` + /// + /// When configured as follows: /// - /// Example: /// ```json /// { /// "specialize": { @@ -199,10 +240,60 @@ public struct Configuration: Codable { /// } /// } /// ``` + /// + /// Would result in Java code with the generated `feedFish()` method on the `FishBox` Java type: + /// + /// ```java + /// FishBox box = ...; + /// box.feedFish(); // type-safe generated specialized function + /// ``` + /// + /// You can also possible to cause such specialization to occurr by declaring a typealias in Swift sources: + /// + /// ```swift + /// typealias FishBox = Box + /// ``` + /// + /// So this configuration option is geared towards times when you do not control the sources that wrappers are being generated for. public var specialize: [String: SpecializationConfigEntry]? /// If set, use this JSON file as the static build configuration for jextract. /// This allows users to provide a custom StaticBuildConfiguration for #if resolution. + /// + /// You can generate one for a specific target triple using the Swift compiler itself: + /// + /// ``` + /// swift frontend -print-static-build-config -target > static-build-config.json + /// ``` + /// + /// Example: + /// + /// The configuration option is a path with a file generated like above, which will have a structure similar to this: + /// + /// ```json + /// { + /// "attributes": [], + /// "compilerVersion": { + /// "components": [6, 3] + /// }, + /// "customConditions": [ + /// "DEBUG" + /// ], + /// "endianness": "little", + /// "features": [], + /// "languageMode": { + /// "components": [5, 10] + /// }, + /// "targetArchitectures": [], + /// "targetAtomicBitWidths": [], + /// "targetEnvironments": [], + /// "targetOSs": [], + /// "targetObjectFileFormats": [], + /// "targetPointerAuthenticationSchemes": [], + /// "targetPointerBitWidth": 64, + /// "targetRuntimes": [] + /// } + /// ``` public var staticBuildConfigurationFile: String? // ==== wrap-java --------------------------------------------------------- @@ -215,14 +306,44 @@ public struct Configuration: Codable { } /// The Java classes that should be translated to Swift. The keys are - /// canonical Java class names (e.g., java.util.Vector) and the values are - /// the corresponding Swift names (e.g., JavaVector). + /// canonical Java class names (e.g., java.util.ArrayList) and the values are + /// the corresponding Swift names (e.g., JavaArrayList). + /// + /// Example: + /// ```json + /// { + /// "classes": { + /// "java.util.ArrayList": "JavaArrayList", + /// "java.util.HashMap": "JavaHashMap" + /// } + /// } + /// ``` public var classes: [String: String]? = [:] - // Compile for the specified Java SE release. + /// Compile for the specified Java SE release. + /// + /// `JavaVersion` is an integer identifying a Java SE release, in the same + /// shape as `javaSourceLevel`. Supported values: + /// + /// - `17` + /// - `18` + /// - `21` + /// - `22` + /// - `24` + /// - `25` public var sourceCompatibility: JavaVersion? - // Generate class files suitable for the specified Java SE release. + /// Generate class files suitable for the specified Java SE release. + /// + /// `JavaVersion` is an integer identifying a Java SE release, in the same + /// shape as `javaSourceLevel`. Supported values: + /// + /// - `17` + /// - `18` + /// - `21` + /// - `22` + /// - `24` + /// - `25` public var targetCompatibility: JavaVersion? /// Filter input Java types by their package prefix if set @@ -231,11 +352,12 @@ public struct Configuration: Codable { /// Exclude input Java types by their package prefix or exact match public var javaFilterExclude: [String]? + /// If set, place all generated code in this single Swift file instead of one file per class. public var singleSwiftFileOutput: String? // ==== dependencies --------------------------------------------------------- - // Java dependencies we need to fetch for this target. + /// Java dependencies we need to fetch for this target. public var dependencies: [JavaDependencyDescriptor]? /// Custom Maven repositories to use when resolving dependencies. @@ -248,6 +370,18 @@ public struct Configuration: Codable { } /// Represents a maven-style Java dependency. +/// +/// Encoded in JSON as a single `groupID:artifactID:version` coordinate string +/// (Gradle-style notation), not as a keyed object. +/// +/// Example: +/// ```json +/// { +/// "dependencies": [ +/// "com.google.code.gson:gson:2.10.1" +/// ] +/// } +/// ``` public struct JavaDependencyDescriptor: Hashable, Codable { public var groupID: String public var artifactID: String @@ -302,10 +436,22 @@ public struct JavaDependencyDescriptor: Hashable, Codable { /// Describes a Maven-style repository for dependency resolution. /// /// Supported types based on https://docs.gradle.org/current/userguide/supported_repository_types.html: -/// - `maven(url:artifactUrls:)` β€” A custom Maven repository at the given URL -/// - `mavenCentral` β€” Maven Central repository -/// - `mavenLocal(includeGroups:)` β€” Local Maven cache (~/.m2/repository) -/// - `google` β€” Google's Maven repository +/// - `maven(url:artifactUrls:)` - A custom Maven repository at the given URL +/// - `mavenCentral` - Maven Central repository +/// - `mavenLocal(includeGroups:)` - Local Maven cache (~/.m2/repository) +/// - `google` - Google's Maven repository +/// +/// Example: +/// ```json +/// { +/// "mavenRepositories": [ +/// { "type": "mavenCentral" }, +/// { "type": "maven", "url": "https://repo.example.com/maven2" }, +/// { "type": "mavenLocal", "includeGroups": ["com.example"] }, +/// { "type": "google" } +/// ] +/// } +/// ``` public enum MavenRepositoryDescriptor: Hashable, Codable { case maven(url: String, artifactUrls: [String]? = nil) case mavenCentral diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift index f7c37e669..eeb29dd48 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JavaSourceLevel.swift @@ -22,6 +22,7 @@ public enum JavaSourceLevel: Int, Comparable, Sendable { case jdk21 = 21 case jdk22 = 22 case jdk24 = 24 + case jdk25 = 25 public static var `default`: Self { .jdk22 } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md index 38b740175..f0f080cb1 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/Android.md @@ -1,6 +1,10 @@ # Android -## R8/Proguard Rules +Hints and patterns for using swift-java on Android. + +## Overview + +### R8/Proguard Rules Since swift-java uses JNI and reflection APIs, we must tell the R8 optimizer to ignore our wrapped types, otherwise we will run into runtime crashes, because types are stripped from the APK/AAR. @@ -16,7 +20,7 @@ For example, if your library is named is `org.swift.exampleapp`, then add the fo -keep interface org.swift.exampleapp.** { *; } ``` -## Android Core Library Desugaring +### Android Core Library Desugaring If you are using [Core Library Desugaring](https://developer.android.com/studio/write/java8-support) in your Android project, you must enable the `AndroidCoreLibraryDesugaring` trait to ensure that the SwiftJava wrappers @@ -58,79 +62,3 @@ open class OldVersionedClass: JavaObject { Annotations are generated both for "since", "deprecated" and "removed" attributes. > Note: To use Android platform availability you must use at least Swift 6.3, which introduced the `Android` platform. - -## Reducing Binary Size - -When using the `jextract` tool to wrap your Swift APIs as a Java library targeting Android, several compiler and linker options can substantially reduce the final binary size by stripping dead code that would otherwise be retained. - -### Requirements - -Full binary-size optimization requires **Swift 6.3 or later**. Swift 6.3 introduced the `@used` attribute, which `JExtractSwiftPlugin` attaches to every generated JNI entry point so the compiler cannot eliminate them before the linker has a chance to see them. - -### Generated Version Script - -When using the `jextract` tool in JNI mode, `JExtractSwiftPlugin` automatically generates a linker version script alongside the Swift thunks. The version script lists every JNI entry point as a `global:` export and hides everything else with `local: *`, giving the linker precise control over which symbols must be kept and allowing it to discard all internal Swift symbols. - -The file is written to the plugin's work directory: - -``` -.build/plugins/outputs///JExtractSwiftPlugin/swift-java-jni-exports.map -``` - -### Optimization Flags - -The following flags, used together, produce the smallest possible binary: - -| Flag | Effect | -|---|---| -| `-Xswiftc -Osize` | Optimize for binary size rather than speed | -| `-Xlinker --version-script=` | Restrict exported symbols to JNI entry points; hides internal Swift symbols from the dynamic symbol table | -| `--experimental-lto-mode=full` | Full link-time optimization across all modules | -| `-Xfrontend -internalize-at-link` | Internalize Swift symbols at link time, enabling the linker to eliminate more dead code | - -### Package.swift - -Add the flags that don't depend on a dynamic path directly to your `Package.swift`, conditioned on release builds for Android: - -```swift -import PackageDescription - -let package = Package( - name: "MyLibrary", - products: [ - .library(name: "MySwiftLibrary", type: .dynamic, targets: ["MySwiftLibrary"]) - ], - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-java", from: "0.1.0"), - ], - targets: [ - .target( - name: "MySwiftLibrary", - dependencies: [ - .product(name: "SwiftJava", package: "swift-java") - ], - swiftSettings: [ - .unsafeFlags( - ["-Osize", "-Xfrontend", "-internalize-at-link"], - .when(platforms: [.android], configuration: .release) - ), - ], - plugins: [ - .plugin(name: "JExtractSwiftPlugin", package: "swift-java") - ] - ) - ] -) -``` - -Then pass the remaining flags on the command line when invoking the build: - -```bash -swift build \ - --swift-sdk aarch64-unknown-linux-android28 \ - -c release \ - --experimental-lto-mode=full \ - -Xlinker --version-script=.build/plugins/outputs/MyLibrary/MySwiftLibrary/JExtractSwiftPlugin/swift-java-jni-exports.map -``` - -> Tip: Adjust the `--version-script` path to match your package name and target name. Run `find .build/plugins/outputs -name swift-java-jni-exports.map` after the first build if you are unsure of the exact path. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/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..77a940715 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -0,0 +1,556 @@ +# 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. + +### 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..451c525e8 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesOverview.md @@ -0,0 +1,45 @@ +# Features Overview + +Orientation guide for choosing the right swift-java tool for your interop task. + +## Overview + +swift-java has two directions of interop, each with a preferred entry point. +Start here to figure out which tool matches your task, then follow the links +to the per-tool documentation. + +### Which mode should I use? + +#### Calling Swift from Java + +When you need to call Swift code from Java, you will be using **jextract** and have a choice between its `jni` or `ffm` modes: + +- JNI mode is the broader-compatibility choice: it runs on any JDK, works on + Android, and supports a wider set of language features. +- FFM mode is the high-performance choice: it has a limited set of features, + and requires JDK 25+ because it relies on [JEP 454: Foreign Function & Memory](https://openjdk.org/jeps/454) APIs. + In some situations it is able to achieve less data copying between the language barriers, so consider it when + shipping large amounts of data between runtimes. + +Generally, it is fine to start with the JNI mode, unless you have specific needs which can only be met by the FFM mode. +Switching modes is simple, and you can do it by passing `--ffm/jni` options to the command line tool, or configuring the `"mode": "jni"|"ffm"` in `swift-java.config` +when using the . + +#### Calling Java from Swift + +It is possible to directly call into Java types as long as you create (or obtain) an in-process reference to a JVM in your Swift program. + +> Tip: This also works on Android. TODO: EXAMPLE + +**JavaKit macros vs wrap-java.** + +SwiftJava offers a collection of **Swift macros** that allow calling Java types from Swift directly. You can learn about the full set of macros and their capabilities in: + +- Manually writing `@JavaClass` and similar types: if you only need to access one or two entry points in a Java library, you may write those manually (see ). +- Use `swift-java wrap-java` source generation: to automatically generate Swift wrapper types for a whole Java API surface. Refer to to learn more about this. + +### Talks and videos + +If you'd like to watch some talks or introduction videos about this project, you can refer to the following materials: + +- [Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA) session from WWDC25. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md b/Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md new file mode 100644 index 000000000..7dda7eb76 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/ReducingBinarySize.md @@ -0,0 +1,82 @@ +# Reducing Binary Size + +When using the `jextract` tool to wrap your Swift APIs as a Java library, several compiler and linker options can substantially reduce the final binary size by stripping dead code that would otherwise be retained. + +## Overview + +The compiler and linker options below trim the shipped library by stripping +Swift symbols that JNI never calls, then dead-stripping the resulting +unreachable code. Applied together, they can significantly reduce the final +`.so`/`.dylib` size compared to a default release build. + +### Requirements + +Full binary-size optimization requires **Swift 6.3 or later**. Swift 6.3 introduced the `@used` attribute, which `JExtractSwiftPlugin` attaches to every generated JNI entry point so the compiler cannot eliminate them before the linker has a chance to see them. + +### Generated Version Script + +When using the `jextract` tool in JNI mode, `JExtractSwiftPlugin` automatically generates a linker version script alongside the Swift thunks. The version script lists every JNI entry point as a `global:` export and hides everything else with `local: *`, giving the linker precise control over which symbols must be kept and allowing it to discard all internal Swift symbols. + +The file is written to the plugin's work directory: + +``` +.build/plugins/outputs///JExtractSwiftPlugin/swift-java-jni-exports.map +``` + +### Optimization Flags + +The following flags, used together, produce the smallest possible binary: + +| Flag | Effect | +|---|---| +| `-Xswiftc -Osize` | Optimize for binary size rather than speed | +| `-Xlinker --version-script=` | Restrict exported symbols to JNI entry points; hides internal Swift symbols from the dynamic symbol table | +| `--experimental-lto-mode=full` | Full link-time optimization across all modules | +| `-Xfrontend -internalize-at-link` | Internalize Swift symbols at link time, enabling the linker to eliminate more dead code | + +### Package.swift + +Add the flags that don't depend on a dynamic path directly to your `Package.swift`, conditioned on release builds for Android: + +```swift +import PackageDescription + +let package = Package( + name: "MyLibrary", + products: [ + .library(name: "MySwiftLibrary", type: .dynamic, targets: ["MySwiftLibrary"]) + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-java", from: "0.1.0"), + ], + targets: [ + .target( + name: "MySwiftLibrary", + dependencies: [ + .product(name: "SwiftJava", package: "swift-java") + ], + swiftSettings: [ + .unsafeFlags( + ["-Osize", "-Xfrontend", "-internalize-at-link"], + .when(platforms: [.android], configuration: .release) + ), + ], + plugins: [ + .plugin(name: "JExtractSwiftPlugin", package: "swift-java") + ] + ) + ] +) +``` + +Then pass the remaining flags on the command line when invoking the build: + +```bash +swift build \ + --swift-sdk aarch64-unknown-linux-android28 \ + -c release \ + --experimental-lto-mode=full \ + -Xlinker --version-script=.build/plugins/outputs/MyLibrary/MySwiftLibrary/JExtractSwiftPlugin/swift-java-jni-exports.map +``` + +> Tip: Adjust the `--version-script` path to match your package name and target name. Run `find .build/plugins/outputs -name swift-java-jni-exports.map` after the first build if you are unsure of the exact path. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md deleted file mode 100644 index f30149069..000000000 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ /dev/null @@ -1,632 +0,0 @@ -# Supported Features - -Summary of features supported by the swift-java interoperability libraries and tools. - -## Overview - -SwiftJava supports both directions of interoperability, using Swift macros and source generation -(via the `swift-java wrap-java` command). - -### Java -> Swift - -It is possible to use SwiftJava macros and the `wrap-java` command to simplify implementing -Java `native` functions. SwiftJava simplifies the type conversions - -> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' -> around the [7-minute mark](https://youtu.be/QSHO-GUGidA?si=vUXxphTeO-CHVZ3L&t=448). - -| Feature | Macro support | -|--------------------------------------------------|-------------------------| -| Java `static native` method implemented by Swift | βœ… `@JavaImplementation` | -| **This list is very work in progress** | | - -### Swift -> Java - - -> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' -> around the [10-minute mark](https://youtu.be/QSHO-GUGidA?si=QyYP5-p2FL_BH7aD&t=616). - -| Java Feature | Macro support | -|----------------------------------------|---------------| -| Java `class` | βœ… | -| Java class inheritance | βœ… | -| Java methods: `static`, member | βœ… `@JavaMethod` | -| Java `abstract class` | TODO | -| Java `enum` | ❌ | -| Java `record` (Java 16+) | βœ… `@JavaRecord` | -| Java `sealed class` / `sealed interface` (Java 17+) | 🟑 recognized, but missing special handling of `permits` list | -| **This list is very work in progress** | | - -### JExtract – calling Swift from Java - -SwiftJava's `swift-java jextract` tool automates generating Java bindings from Swift sources. - -> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' -> around the [14-minute mark](https://youtu.be/QSHO-GUGidA?si=b9YUwAWDWFGzhRXN&t=842). - - -| Swift Feature | FFM | JNI | -|--------------------------------------------------------------------------------------|----------|-----| -| Initializers: `class`, `struct` | βœ… | βœ… | -| Optional Initializers / Throwing Initializers | ❌ | βœ… | -| Deinitializers: `class`, `struct` | βœ… | βœ… | -| `enum` | ❌ | βœ… | -| `actor` | ❌ | ❌ | -| Global Swift `func` | βœ… | βœ… | -| Class/struct member `func` | βœ… | βœ… | -| Throwing functions: `func x() throws` | ❌ | βœ… | -| Typed throws: `func x() throws(E)` | ❌ | ❌ | -| Stored properties: `var`, `let` (with `willSet`, `didSet`) | βœ… | βœ… | -| Computed properties: `var` (incl. `throws`) | βœ… / TODO | βœ… | -| Async functions `func async` and properties: `var { get async {} }` | ❌ | βœ… | -| Arrays: `[UInt8]` | βœ… | βœ… | -| Arrays: `[MyType]`, `Array` etc | ❌ | βœ… | -| Dictionaries: `[String: Int]`, `[K:V]` | ❌ | βœ… | -| Generic type: `struct S` | ❌ | βœ… | -| Functions or properties using generic type param: `struct S { func f(_: T) {} }` | ❌ | ❌ | -| Generic parameters over `some DataProtocol` handled with efficient Java type | βœ… | βœ… | -| Generic type specialization and conditional extensions: `struct S{} extension S where T == Value {}` | ❌ | βœ… | -| Static functions or properties in generic type | ❌ | ❌ | -| Generic parameters in functions: `func f(x: T)` | ❌ | βœ… | -| Generic return values in functions: `func f() -> T` | ❌ | ❌ | -| Tuples: `(Int, String)`, `(A, B, C)` | βœ… | βœ… | -| Protocols: `protocol` | ❌ | βœ… | -| Protocols: `protocol` with associated types | ❌ | ❌ | -| Protocols static requirements: `static func`, `init(rawValue:)` | ❌ | ❌ | -| Existential parameters `f(x: any SomeProtocol)` (excepts `Any`) | ❌ | βœ… | -| Existential parameters `f(x: any (A & B)) ` | ❌ | βœ… | -| Existential return types of a single protocol: `f() -> any SomeProtocol` | ❌ | βœ… | -| Existential return types of a composite: `f() -> any (A & B)` | ❌ | ❌ | -| Downcasting a returned protocol value to its concrete type: `greeter.as(EnglishGreeter.class, arena)` | ❌ | βœ… | -| Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | βœ… | βœ… | -| Foundation Date: `f(date: Date) -> Date` | ❌ | βœ… | -| Foundation UUID: `f(uuid: UUID) -> UUID` | ❌ | βœ… | -| Opaque parameters: `func take(worker: some Builder) -> some Builder` | ❌ | βœ… | -| Opaque return types: `func get() -> some Builder` | ❌ | βœ… | -| Optional parameters: `func f(i: Int?, class: MyClass?)` | βœ… | βœ… | -| Optional return types: `func f() -> Int?`, `func g() -> MyClass?` | ❌ | βœ… | -| Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | βœ… | βœ… | -| Parameters: SwiftJava wrapped types `JavaLong`, `JavaInteger` | ❌ | βœ… | -| Return values: SwiftJava wrapped types `JavaLong`, `JavaInteger` | ❌ | ❌ | -| Unsigned primitive types: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` | βœ… * | βœ… * | -| String (with copying data) | βœ… | βœ… | -| Variadic parameters: `T...` | ❌ | ❌ | -| Parametrer packs / Variadic generics | ❌ | 🟑 | -| Ownership modifiers: `inout`, `borrowing`, `consuming` | ❌ | ❌ | -| Default parameter values: `func p(name: String = "")` | ❌ | ❌ | -| Operators: `+`, `-`, user defined | ❌ | ❌ | -| Subscripts: `subscript()` | βœ… | βœ… | -| Equatable | ❌ | ❌ | -| Pointers: `UnsafeRawPointer` | 🟑 | ❌ | -| Pointers as parameters: `UnsafeRawBufferPointer` (as `byte[]`) | ❌ | βœ… | -| Nested types: `struct Hello { struct World {} }` | ❌ | βœ… | -| Inheritance: `class Caplin: Capybara` | ❌ | ❌ | -| Non-escaping `Void` closures: `func callMe(maybe: () -> ())` | βœ… | βœ… | -| Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | βœ… | βœ… | -| Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ | -| `@escaping` `Void` closures: `func callMe(_: @escaping () -> ())` | ❌ | βœ… | -| `@escaping` closures with primitive arguments/results: `func callMe(_: @escaping (Int64) -> (Double))` | ❌ | βœ… | -| `@escaping` closures with `String` arguments/results: `func callMe(_: @escaping (String) -> (String))` | ❌ | βœ… | -| `@escaping` closures with user defined types: `func callMe(_: @escaping (Obj) -> (Obj))` | ❌ | ❌ | -| `@escaping` closures returning other closures: `func callMe(_: (Obj) -> (() -> ()))` | ❌ | ❌ | -| Swift type extensions: `extension String { func uppercased() }` | βœ… | βœ… | -| Swift macros (maybe) | ❌ | ❌ | -| Result builders | ❌ | ❌ | -| Automatic Reference Counting of class types / lifetime safety | βœ… | βœ… | -| Value semantic types (e.g. struct copying) | ❌ | ❌ | -| | | | -| | | | - -> tip: The list of features may be incomplete, please file an issue if something is unclear or should be clarified in this table. - -## Detailed jextract feature support discussion - -### Unsigned integers - -### Java <-> Swift Type mapping - -Java does not support unsigned numbers (other than the 16-bit wide `char`), and therefore mapping Swift's (and C) -unsigned integer types is somewhat problematic. - -SwiftJava's jextract mode, similar to OpenJDK jextract, does extract unsigned types from native code to Java -as their bit-width equivalents. This is potentially dangerous because values larger than the `MAX_VALUE` of a given -*signed* type in Java, e.g. `200` stored in an `UInt8` in Swift, would be interpreted as a `byte` of value `-56`, -because Java's `byte` type is _signed_. - -Because in many situations the data represented by such numbers is merely passed along, and not interpreted by Java, -this may be safe to pass along. However, interpreting unsigned values incorrectly like this can lead to subtle mistakes -on the Java side. - -| Swift type | Java type | -|------------|-----------| -| `Int8` | `byte` | -| `UInt8` | `byte` ⚠️ | -| `Int16` | `short` | -| `UInt16` | `char` | -| `Int32` | `int` | -| `UInt32` | `int` ⚠️ | -| `Int64` | `long` | -| `UInt64` | `long` ⚠️ | -| `Float` | `float` | -| `Double` | `double` | - -### Passing Foundation.Data - -`Data` is a common currency type in Swift for passing a bag of bytes. Some APIs use Data instead of `[UInt8]` or other types -like Swift-NIO's `ByteBuffer`, because it is so commonly used swift-java offers specialized support for it in order to avoid copying bytes unless necessary. - -### Data in jextract FFM mode - -When using jextract in FFM mode, the generated `Data` wrapper offers an efficient way to initialize the Swift `Data` type -from a `MemorySegment` as well as the `withUnsafeBytes` function which offers direct access to Data's underlying bytes -by exposing the unsafe base pointer as a `MemorySegment`: - -```swift -Data data = MySwiftLibrary.getSomeData(arena); -data.withUnsafeBytes((bytes) -> { - var str = bytes.getString(0); - System.out.println("string = " + str); -}); -``` - -This API avoids copying the data into the Java heap in order to perform operations on it, as we are able to manipulate -it directly thanks to the exposed `MemorySegment`. - -It is also possible to use the convenience functions `toByteBuffer` and `toByteArray` to obtain a `java.nio.ByteBuffer` or `[byte]` array respectively. Thos operations incurr a copy by moving the data to the JVM's heap. - -It is also possible to get the underlying memory copied into a new `MemorySegment` by using `toMemorySegment(arena)` which performs a copy from native memory to memory managed by the passed arena. The lifetime of that memory is managed by the arena and may outlive the original `Data`. - -It is preferable to use the `withUnsafeBytes` pattern if using the bytes only during a fixed scope, because it alows us to avoid copies into the JVM heap entirely. However when a JVM byte array is necessary, the price of copying will have to be paid anyway. Consider these various options when optimizing your FFI calls and patterns for performance. - -### Data in jextract JNI mode - -Swift methods which pass or accept the Foundation `Data` type are extracted using the wrapper Java `Data` type, -which offers utility methods to efficiently copy the underlying native data into a java byte array (`[byte]`). - -Unlike the FFM mode, a true zero-copy `withUnsafeBytes` is not available. - -### Collections - -SwiftJava automatically handles collections crossing the language boundary in the most efficient way possible. - -### Swift Dictionary as `java.util.Map` - -When extracting Swift methods which accept or return a Swift dictionary (often spelled as `[Key: Value]`), jextract (in JNI mode) will convert the return type to a `SwiftDictionaryMap` Java type. - -The `SwiftDictionaryMap` wrapper type refers to the actual Swift dictionary value on the Swift heap and does not copy it out into the Java heap, unless explicitly copied using the `SwiftDictionaryMap::toJava` method. - -### Enums - -> Note: Enums are currently only supported in JNI mode. - -Swift enums are extracted into a corresponding Java `class`. To support associated values -all cases are also extracted as Java `record`s. - -Consider the following Swift enum: -```swift -public enum Vehicle { - case car(String) - case bicycle(maker: String) -} -``` -You can then instantiate a case of `Vehicle` by using one of the static methods: -```java -try (var arena = SwiftArena.ofConfined()) { - Vehicle vehicle = Vehicle.car("BMW", arena); - Optional car = vehicle.getAsCar(); - assertEquals("BMW", car.orElseThrow().arg0()); -} -``` -As you can see above, to access the associated values of a case you can call one of the -`getAsX` methods that will return an Optional record with the associated values. -```java -try (var arena = SwiftArena.ofConfined()) { - Vehicle vehicle = Vehicle.bycicle("My Brand", arena); - Optional car = vehicle.getAsCar(); - assertFalse(car.isPresent()); - - Optional bicycle = vehicle.getAsBicycle(); - assertEquals("My Brand", bicycle.orElseThrow().maker()); -} -``` - -#### Switching and pattern matching - -If you only need to switch on the case and not access any associated values, -you can use the `getDiscriminator()` method: -```java -Vehicle vehicle = ...; -switch (vehicle.getDiscriminator()) { - case BICYCLE: - System.out.println("I am a bicycle!"); - break; - case CAR: - System.out.println("I am a car!"); - break; -} -``` -If you also want access to the associated values, you have various options -depending on the Java version you are using. -If you are running Java 21+ you can use [pattern matching for switch](https://openjdk.org/jeps/441): -```java -Vehicle vehicle = ...; -switch (vehicle.getCase()) { - case Vehicle.Case.Bicycle b: - System.out.println("Bicycle maker: " + b.maker()); - break; - case Vehicle.Case.Car c: - System.out.println("Car: " + c.arg0()); - break; -} -``` -or even, destructuring the records in the switch statement's pattern match directly: -```java -Vehicle vehicle = ...; -switch (vehicle.getCase()) { - case Vehicle.Case.Car(var name, var unused): - System.out.println("Car: " + name); - break; - default: - break; -} -``` - -For Java 16+ you can use [pattern matching for instanceof](https://openjdk.org/jeps/394) -```java -Vehicle vehicle = ...; -Vehicle.Case case = vehicle.getCase(); -if (case instanceof Vehicle.Case.Bicycle b) { - System.out.println("Bicycle maker: " + b.maker()); -} else if(case instanceof Vehicle.Case.Car c) { - System.out.println("Car: " + c.arg0()); -} -``` -For any previous Java versions you can resort to casting the `Case` to the expected type: -```java -Vehicle vehicle = ...; -Vehicle.Case case = vehicle.getCase(); -if (case instanceof Vehicle.Case.Bicycle) { - Vehicle.Bicycle b = (Vehicle.Case.Bicycle) case; - System.out.println("Bicycle maker: " + b.maker()); -} else if(case instanceof Vehicle.Case.Car) { - Vehicle.Car c = (Vehicle.Case.Car) case; - System.out.println("Car: " + c.arg0()); -} -``` - -#### RawRepresentable enums - -JExtract also supports extracting enums that conform to `RawRepresentable` -by giving access to an optional initializer and the `rawValue` variable. -Consider the following example: -```swift -public enum Alignment: String { - case horizontal - case vertical -} -``` -you can then initialize `Alignment` from a `String` and also retrieve back its `rawValue`: -```java -try (var arena = SwiftArena.ofConfined()) { - Optional alignment = Alignment.init("horizontal", arena); - assertEqual(HORIZONTAL, alignment.orElseThrow().getDiscriminator()); - assertEqual("horizontal", alignment.orElseThrow().getRawValue()); -} -``` - -### Protocols - -> Note: Protocols are currently only supported in JNI mode. -> -> With the exception of `any DataProtocol` which is handled as `Foundation.Data` in the FFM mode. - -Swift `protocol` types are imported as Java `interface`s. For now, we require that all -concrete types of an interface wrap a Swift instance. In the future, we will add support -for providing Java-based implementations of interfaces, that you can pass to Java functions. - -Consider the following Swift protocol: -```swift -protocol Named { - var name: String { get } - - func describe() -> String -} -``` -will be exported as -```java -interface Named extends JNISwiftInstance { - public String getName(); - - public String describe(); -} -``` - -#### Protocol types in parameters -Any opaque, existential or generic parameters are imported as Java generics. -This means that the following function: -```swift -func f(x: S, y: any C, z: some D) -``` -will be exported as -```java - void f(S x, T1 y, T2 z) -``` -On the Java side, only SwiftInstance implementing types may be passed; -so this isn't a way for compatibility with just any arbitrary Java interfaces, -but specifically, for allowing passing concrete binding types generated by jextract from Swift types -which conform a to a given Swift protocol. - -#### Returning protocol types - -> Note: Returning protocol types is currently only supported in JNI mode. - -Functions that return an existential (`any SomeProtocol`) or opaque (`some SomeProtocol`) -value of a single protocol are supported. For example: - -```swift -protocol Greeter { - func greeting() -> String -} - -func makeEnglishGreeter(name: String) -> any Greeter { ... } -func makeOpaqueGreeter(name: String) -> some Greeter { ... } -``` - -The returned value is wrapped in a generated *existential box* β€” a Java class named -`Box` that implements the protocol's Java `interface`. The box carries the -concrete (dynamic) value together with its type metadata, and dispatches each protocol -requirement through a dedicated native thunk that reconstructs the existential from that -value. This means the dynamic type of the returned value is preserved, and each -requirement is called on the underlying concrete conformer: - -```java -try (var arena = SwiftArena.ofConfined()) { - Greeter english = MySwiftLibrary.makeEnglishGreeter("World", arena); - Greeter danish = MySwiftLibrary.makeDanishGreeter("Verden", arena); - assertEquals("Hello, World!", english.greeting()); - assertEquals("Hej, Verden!", danish.greeting()); -} -``` - -Using the returned value works just like using any other imported interface: its -requirements are callable through the box, it can be passed back into functions that -accept the protocol (including generic and opaque parameters), and refined protocols -expose both their own and their inherited requirements. - -Static requirements (`static func`, `init`) and returning a *composite* existential -(`any (A & B)`) are not currently supported; such requirements are simply omitted from -the generated box, and composite returns are not extracted. - -#### Downcasting to the concrete type (`as`) - -> Note: Downcasting returned protocol values is currently only supported in JNI mode. - -A value returned as `any P` / `some P` preserves its concrete dynamic type, so it can be -recovered; This is equivalent to Swift's `as?`, of a checked `instanceof` cast in Java. Every imported protocol `interface` therefore -exposes an `as` method: - -```java - Optional as(Class type, SwiftArena arena); - Optional as(Class type); // uses the default arena -``` - -`type` must be a concrete jextracted type. -The cast succeeds only when the -value's dynamic Swift type is exactly that type, in which case you receive a fresh binding -registered in the given arena; otherwise the result is `Optional.empty()`. - -Continuing the `Greeter` example, where `makeEnglishGreeter` returns `any Greeter` backed -by a concrete `EnglishGreeter`: - -```java -try (var arena = SwiftArena.ofConfined()) { - Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); - - Optional english = greeter.as(EnglishGreeter.class, arena); - assertEquals("World", english.orElseThrow().getName()); - - // A cast to the wrong dynamic type yields an empty Optional: - assertTrue(greeter.as(DanishGreeter.class, arena).isEmpty()); -} -``` - -The cast returns an empty optional if the cast fails, which might happen when -the dynamic type differs, when `type` is not a concrete jextracted type (e.g. a generic type -or another protocol). - -### Swift closures - -Non-escaping closures are called synchronously by the Swift function they are passed to, -so their Java-side lifetime is trivially bounded by the enclosing native call. - -In Java, Swift closure parameters are represented as functional interfaces. - -SwiftJava generates ad-hoc functional interfaces per parameter, and the parameters -given and returned by the closure are also mapped between the languages just as a normal -top-level function's would be. - -From a Java developers perspective, calling Swift functions accepting callbacks is seamless: - -```java -myModule.setCallback(() -> System.out.println("called from Swift")); // functional interface is used -``` - - -> Note: Closures whose parameters or result are jextract-imported user types -(`@escaping (MyClass) -> MyClass`) are not supported yet. - - -#### Escaping closures (`@escaping`) - -> Note: `@escaping` closure parameters are currently only supported in JNI mode. - -SwiftJava also supports `@escaping` closures, however the runtime in support of them is a bit more involved, -because Swift function may store the closure and invoke it _after_ the -native call has already returned. The Java callback must therefore stay reachable -for as long as Swift retains a reference to the escaping closure. - -Lifetime of the Java object is ensured by SwiftJava automatically, which creates global references, -and removes them when the Swift-side closure gets destroyed. -This means that passing escaping closures to Swift functions does increase the global reference count, -something you may need to be cautious of when working on e.g. Android which limits the total numbers of global references. - -The Java side user experience is unchanged from the non-escaping use-case: - -```java -myModule.setCallback(() -> System.out.println("called from Swift")); // functional interface is used -``` - -> Note: Closures whose parameters or result are jextract-imported user types -(`@escaping (MyClass) -> MyClass`) are not supported yet. - -### `async` functions - -> Note: Importing `async` functions is currently only available in the JNI mode of jextract. - -Asynchronous functions in Swift can be extraced using different modes, which are explained below. - -#### Async function mode: completable-future (default) - -In this mode `async` functions in Swift are extracted as Java methods returning a `java.util.concurrent.CompletableFuture`. -This mode gives the most flexibility and should be prefered if your platform supports `CompletableFuture`. - -#### Async mode: future - -This is a mode for legacy platforms, where `CompletableFuture` is not available, such as Android 23 and below. -In this mode `async` functions in Swift are extracted as Java methods returning a `java.util.concurrent.Future`. -To enable this mode pass the `--async-func-mode future` command line option, -or set the `asyncFuncMode` configuration value in `swift-java.config` - -### Generic types - -> Note: Generic types are currently only supported in JNI mode. - -Support for generic types is still work-in-progress and limited. -Any members containing type parameters (such as T) are not exported. - -```swift -public struct MyID { - // Not exported: Contains the type parameter 'T' - public var rawValue: T - - // Not exported: The initializer depends on 'T' - public init(rawValue: T) { - self.rawValue = rawValue - } - - // Exported: Does not depend on 'T' - public var description: String { "\(rawValue)" } - - // Not exported: Although it doesn't use 'T' directly, - // it is a member of a generic context (MyID.foo). - public static func foo() -> String { "" } -} - -// Exported: A specialized function with a concrete type (MyID) -public func makeIntID() -> MyID { - ... -} -``` - -will be exported as - -```java -public final class MyID implements JNISwiftInstance { - public String getDescription(); -} - -public final class MySwiftLibrary { - public static MyID makeIntID(); -} -``` - -### Specializing generic types - -> Note: Generic specialization is currently only supported in JNI mode. - -Because Swift's rich generics and extensions system, it is possible to encounter APIs which are not safely expressible in Java, -such as conditional/constrained extensions on types when an element is of specific type. - -A common example of this is e.g. a container type which gains additional methods when the element is of some type, like this: - -```swift -struct Box { - var name: String -} -``` - -which is extended with a conditional `where` clause: - -```swift -extension Box where Element == Fish { - func watchTheFish() { } -} -``` - -This method is not available on any `Box` and therefore we cannot safely expose it on the Java `Box` wrapper type. - -It would be possible to expose it and check at runtime if the `Box.Element` is of the expected type, this however -would result in runtime throws and is not an ideal experience when developers primarily use some specific _specialize_ -types like the `FishBox`: - -```swift -typealias FishBox = Box -``` - -The jextract tool will automatically detect typealiases like this and perform _specialization_ on them, i.e. a new -`FishBox` type will be exposed on the Java side, and it will have all matching extensions applied to it, i.e. it -will have the `watchTheFish()` method available in a type-safe and always known to work correctly way. - -In other words, this results in a Java class like this: - -```java -/// Specialization of `Fish`. -public final class FishBox ... { - - public void watchTheFish() { ... } -} -``` - -> NOTE: Currently no helpers are available to convert between unspecialized types to specialized ones, but this can be offered -> as additional `box.as(FishBox.class)` conversion methods in the future. - - -> NOTE: Currently specialization for generic enums are not yet supported. - - -### Evaluating `#if` - -In jextract, `#if` branches are evaluated using [SwiftIfConfig](https://github.com/swiftlang/swift-syntax/blob/main/Sources/SwiftIfConfig/SwiftIfConfig.docc/SwiftIfConfig.md). -The evaluation parameters are fixed; for example, the `os` expression always evaluates to true, so in the following case the value of the variable will be `Linux`. - -```swift -#if os(Linux) -let os = "Linux" -#elseif os(Android) -let os = "Android" -#else -let os = "Other" -#endif -``` - -If you want the above situation to be evaluated as `Android`, you can override the evaluation parameters. -First, obtain a [StaticBuildConfiguration](https://github.com/swiftlang/swift-syntax/blob/main/Sources/SwiftIfConfig/StaticBuildConfiguration.swift) with the following command and save it to a file. -(Adjust `-target` to match the environment you want to build for. This command is available from Swift 6.3.) - -```sh -swift frontend -print-static-build-config -target aarch64-unknown-linux-android28 > static-build-config.json -``` - -Then pass the path to that file when running jextract. - -- When using the jextract command: `--static-build-config ` -- When configuring via `swift-java.config`: - ```json - { - ... - "staticBuildConfigurationFile": "" // Relative path from `swift-java.config` - } - ``` - -As a result, jextract will evaluate `os` as `Android`. - diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md index 823713836..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..72e6c5dd9 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -0,0 +1,560 @@ +# swift-java.config + +SwiftJava tools can be configured using the `swift-java.config` file. + +## Overview + +The `swift-java.config` file lives alongside each target that needs +swift-java code generation. It selects tool modes, sets output paths, +and controls per-mode filters. Below: the file layout, then the full +list of supported keys. + +### The swift-java.config file + +You can refer to the `SwiftJavaConfigurationShared/Configuration` struct to learn about the supported options. + +Configuration from the config files may be overriden or augmented by explicit command line parameters, +please refer to the options documentation for details on their behavior. + +### Comments + +The configuration is a JSON 5 file, which among other things allows `//` and `/* */` comments, so feel free to add line comments explaining rationale for some of the settings in your configuration. + +### Supported configuration options + + + + + +### General + +#### logLevel + +- **Type:** `LogLevel?` + +The minimum log level at which log messages will be printed at by swift-java. + +**Values:** + +- `trace` +- `debug` +- `info` +- `notice` +- `warning` +- `error` +- `critical` + +--- + +### jextract + +#### javaPackage + +- **Type:** `String?` + +The Java package the generated Java code should be emitted into. + +Example: +```swift +"com.example.mypackage" +``` + +--- + +#### swiftModule + +- **Type:** `String?` + +The name of the Swift module into which the resulting Swift types will be generated. + +--- + +#### nativeLibraryName + +- **Type:** `String?` + +The name of the native library to load at runtime via `System.loadLibrary()`. +Defaults to the Swift module name when not set. Use this when the dynamic +library product has a different name than the module being exported +(e.g. the module is `MyLibrary` but the dylib is `MyLibrarySwiftJava` or something else). + +--- + +#### overrideStaticBlockLibraryLoading + +- **Type:** `[String]?` + +When non-nil, overrides the library loading statements emitted in the +`static {}` / `initializeLibs()` block of generated Java classes. +Each string is emitted as a verbatim Java statement. + +When `nil` (the default), the standard loading calls are emitted. +When set to an empty array `[]`, no library loading code is emitted at all. + +--- + +#### inputSwiftDirectory + +- **Type:** `String?` + +Directory containing Swift files which should be extracted into Java bindings (jextract mode). +Must be paired with `outputSwiftDirectory` and `outputJavaDirectory`. + +--- + +#### outputSwiftDirectory + +- **Type:** `String?` + +The directory where generated Swift files should be written. Generally used with jextract mode. + +--- + +#### outputJavaDirectory + +- **Type:** `String?` + +The directory where generated Java files should be written. Generally used with jextract mode. + +--- + +#### mode + +- **Type:** `JExtractGenerationMode?` +- **Default:** `ffm` + +Determine `jextract` source generation mode, using JNI or FFM. + +**Values:** + +- `ffm` - Foreign Value and Memory API +- `jni` - Java Native Interface + +--- + +#### writeEmptyFiles + +- **Type:** `Bool?` +- **Default:** `false` + +Some build systems require an output to be present when it was "expected", even if empty. +This is used by the JExtractSwiftPlugin build plugin, but otherwise should not be necessary. + +--- + +#### minimumInputAccessLevelMode + +- **Type:** `AccessLevelMode?` +- **Default:** `public` + +The lowest access level of Swift declarations that should be extracted, defaults to `public`. + +**Values:** + +- `public` +- `package` +- `internal` + +--- + +#### memoryManagementMode + +- **Type:** `JExtractMemoryManagementMode?` +- **Default:** `explicit` + +The memory management mode to use for the generated code. By default, the user must explicitly +provide a `SwiftArena` to all calls that require it. By choosing `allowGlobalAutomatic`, the user +can omit this parameter and a global GC-based arena will be used. + +**Values:** + +- `explicit` - Force users to provide an explicit `SwiftArena` to all calls that require them. +- `allowGlobalAutomatic` - Provide both explicit `SwiftArena` support and a default global automatic `SwiftArena` that will deallocate memory when the GC decides to. + +--- + +#### asyncFuncMode + +- **Type:** `JExtractAsyncFuncMode?` +- **Default:** `completableFuture` + +The mode to use for extracting asynchronous Swift functions. By default async methods are +extracted as Java functions returning `CompletableFuture`. + +**Values:** + +- `completableFuture` - Extract Swift `async` APIs as Java functions that return `CompletableFuture`s. +- `legacyFuture` - Extract Swift `async` APIs as Java functions that return `Future`s. + +--- + +#### javaSourceLevel + +- **Type:** `JavaSourceLevel?` +- **Default:** `22` + +The Java source level to target when generating Java code. + +**Values:** + +- `17` +- `18` +- `21` +- `22` +- `24` +- `25` + +--- + +#### enableJavaCallbacks + +- **Type:** `Bool?` +- **Default:** `false` + +By enabling this mode, JExtract will generate Java code that allows you to implement Swift +protocols using Java classes. This feature requires disabling the SwiftPM sandbox, and is +only supported in `jni` mode. + +--- + +#### generatedJavaSourcesListFileOutput + +- **Type:** `String?` + +If specified, JExtract will output to this file a list of paths to all generated Java source files. + +--- + +#### singleType + +- **Type:** `String?` + +If set, only generate bindings for this single Swift type name + +--- + +#### linkerExportListOutput + +- **Type:** `String?` + +If set, JExtract (JNI mode) will write a linker version script to this +path, listing all generated JNI `@_cdecl` entry-point symbols as +global exports and hiding everything else with `local: *`. Pass this +file to the linker via `-Xlinker --version-script=` to enable +precise dead-code elimination of unused Swift code in the final shared +library. + +--- + +#### swiftFilterInclude + +- **Type:** `[String]?` + +Include only Swift source files or types matching these patterns during jextract. + +File-path patterns (containing `/`, or ending in `.swift` / +`.swiftinterface`): matched against relative file paths. Supports `*` and +`**` wildcards. Example: `Models/**`, `**/User.swift`, `MyType.swift`. + +Type-name patterns (containing `.`): matched against the dotted nested +type path (e.g. `Outer.Inner`, `Outer.**`, `**.User`, `Logger.Internal*`). +The qualified name does NOT include the module prefix. + +`.` is the separator. `::` is reserved by Swift for module disambiguation +(SE-0491) and is NOT used by these filters. + +Plain names (no separator) match both: a filename without `.swift`, or the +top-level component of a type name + +--- + +#### swiftFilterExclude + +- **Type:** `[String]?` + +Exclude Swift source files or types matching these patterns during jextract. +Same pattern syntax as `swiftFilterInclude` + +--- + +#### importedModuleStubs + +- **Type:** `[String: [String]]?` + +Stub type declarations for imported modules whose source is not available +to the jextract tool. Keyed by module name, values are arrays of Swift +declaration strings that will be parsed as if they belonged to that module. + +Example: +```json +{ + "importedModuleStubs": { + "ExternalModule": [ + "public enum Outer {}", + "public struct Config {}" + ] + } +} +``` + +--- + +#### specialize + +- **Type:** `[String: SpecializationConfigEntry]?` + +Force specialization of generic types, mapping them to a specific generated Java-facing name. +This allows generating generic specializations that can be used only with some specific bound generic argument, +rather than using the usual generic machinery. Sometimes useful if a generic type is only reasonably usable with some specific type. + +Generating specializations takes into account Swift extensions where the generic is bound to that type, for example, a `Box` +type, would automatically gain `T == Fish` specific methods in the generated Java sources if there is an `extension ... where T == Fish` declared in Swift: + +```swift +struct Box {} +extension Box where T == Fish { + func feedFish() +} +``` + +When configured as follows: + +```json +{ + "specialize": { + "FishBox": { + "base": "Box", + "typeArgs": {"Element": "Fish"} + }, + "ToolBox": { + "base": "Box", + "typeArgs": {"Element": "Tool"} + } + } +} +``` + +Would result in Java code with the generated `feedFish()` method on the `FishBox` Java type: + +```java +FishBox box = ...; +box.feedFish(); // type-safe generated specialized function +``` + +You can also possible to cause such specialization to occurr by declaring a typealias in Swift sources: + +```swift +typealias FishBox = Box +``` + +So this configuration option is geared towards times when you do not control the sources that wrappers are being generated for. + +**`SpecializationConfigEntry`:** + +Configuration entry for specializing a generic type into a concrete Java class. +The dictionary key is the Java-facing name; this entry provides the base type +and type argument mapping. + +- `base`: `String` - The base Swift type name (e.g. "Box") +- `typeArgs`: `[String: String]` - Mapping from generic parameter name to concrete type (e.g. {"Element": "Fish"}) + +--- + +#### staticBuildConfigurationFile + +- **Type:** `String?` + +If set, use this JSON file as the static build configuration for jextract. +This allows users to provide a custom StaticBuildConfiguration for #if resolution. + +You can generate one for a specific target triple using the Swift compiler itself: + +``` +swift frontend -print-static-build-config -target > static-build-config.json +``` + +Example: + +The configuration option is a path with a file generated like above, which will have a structure similar to this: + +```json +{ + "attributes": [], + "compilerVersion": { + "components": [6, 3] + }, + "customConditions": [ + "DEBUG" + ], + "endianness": "little", + "features": [], + "languageMode": { + "components": [5, 10] + }, + "targetArchitectures": [], + "targetAtomicBitWidths": [], + "targetEnvironments": [], + "targetOSs": [], + "targetObjectFileFormats": [], + "targetPointerAuthenticationSchemes": [], + "targetPointerBitWidth": 64, + "targetRuntimes": [] +} +``` + +--- + +### wrap-java + +#### classpath + +- **Type:** `String?` + +The Java class path that should be passed along to the swift-java tool. + +--- + +#### classes + +- **Type:** `[String: String]?` +- **Default:** empty dictionary (`[:]`) + +The Java classes that should be translated to Swift. The keys are +canonical Java class names (e.g., java.util.ArrayList) and the values are +the corresponding Swift names (e.g., JavaArrayList). + +Example: +```json +{ + "classes": { + "java.util.ArrayList": "JavaArrayList", + "java.util.HashMap": "JavaHashMap" + } +} +``` + +--- + +#### sourceCompatibility + +- **Type:** `JavaVersion?` + +Compile for the specified Java SE release. + +`JavaVersion` is an integer identifying a Java SE release, in the same +shape as `javaSourceLevel`. Supported values: + +- `17` +- `18` +- `21` +- `22` +- `24` +- `25` + +--- + +#### targetCompatibility + +- **Type:** `JavaVersion?` + +Generate class files suitable for the specified Java SE release. + +`JavaVersion` is an integer identifying a Java SE release, in the same +shape as `javaSourceLevel`. Supported values: + +- `17` +- `18` +- `21` +- `22` +- `24` +- `25` + +--- + +#### javaFilterInclude + +- **Type:** `[String]?` + +Filter input Java types by their package prefix if set + +--- + +#### javaFilterExclude + +- **Type:** `[String]?` + +Exclude input Java types by their package prefix or exact match + +--- + +#### singleSwiftFileOutput + +- **Type:** `String?` + +If set, place all generated code in this single Swift file instead of one file per class. + +--- + +### dependencies + +#### dependencies + +- **Type:** `[JavaDependencyDescriptor]?` + +Java dependencies we need to fetch for this target. + +**`JavaDependencyDescriptor`:** + +Represents a maven-style Java dependency. + +Encoded in JSON as a single `groupID:artifactID:version` coordinate string +(Gradle-style notation), not as a keyed object. + +Example: +```json +{ + "dependencies": [ + "com.google.code.gson:gson:2.10.1" + ] +} +``` + +- `groupID`: `String` +- `artifactID`: `String` +- `version`: `String` + +--- + +#### mavenRepositories + +- **Type:** `[MavenRepositoryDescriptor]?` + +Custom Maven repositories to use when resolving dependencies. +If not set, defaults to mavenCentral(). + +**`MavenRepositoryDescriptor`:** + +Describes a Maven-style repository for dependency resolution. + +Supported types based on https://docs.gradle.org/current/userguide/supported_repository_types.html: +- `maven(url:artifactUrls:)` - A custom Maven repository at the given URL +- `mavenCentral` - Maven Central repository +- `mavenLocal(includeGroups:)` - Local Maven cache (~/.m2/repository) +- `google` - Google's Maven repository + +Example: +```json +{ + "mavenRepositories": [ + { "type": "mavenCentral" }, + { "type": "maven", "url": "https://repo.example.com/maven2" }, + { "type": "mavenLocal", "includeGroups": ["com.example"] }, + { "type": "google" } + ] +} +``` + + +--- + + \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaJextract.md new file mode 100644 index 000000000..4515f9a36 --- /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..13a99e9ed --- /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 { 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