From 45dc84e5cf416c30207a5e12f2d0c68ac9978426 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sat, 12 Jul 2025 11:13:41 +0200 Subject: [PATCH 1/2] generate code for member methods --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 16 +++ .../com/example/swift/HelloJava2SwiftJNI.java | 6 + .../com/example/swift/MySwiftClassTest.java | 26 ++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 37 ++++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 124 +++++++++++------- .../JNI/JNIClassTests.swift | 47 +++++++ 6 files changed, 209 insertions(+), 47 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index fca6236f..82958464 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -34,4 +34,20 @@ public class MySwiftClass { deinit { p("deinit called!") } + + public func sum() -> Int64 { + return x + y + } + + public func xMultiplied(by z: Int64) -> Int64 { + return x * z; + } + + enum MySwiftClassError: Error { + case swiftError + } + + public func throwingFunction() throws { + throw MySwiftClassError.swiftError + } } diff --git a/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java b/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java index 38860c06..575859ec 100644 --- a/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java +++ b/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java @@ -44,6 +44,12 @@ static void examples() { try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) { MySwiftClass myClass = MySwiftClass.init(10, 5, arena); MySwiftClass myClass2 = MySwiftClass.init(arena); + + try { + myClass.throwingFunction(); + } catch (Exception e) { + System.out.println("Caught exception: " + e.getMessage()); + } } System.out.println("DONE."); diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index ec0e31dd..3982b480 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -35,4 +35,30 @@ void init_withParameters() { assertNotNull(c); } } + + @Test + void sum() { + try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(30, c.sum()); + } + } + + @Test + void xMultiplied() { + try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + assertEquals(200, c.xMultiplied(10)); + } + } + + @Test + void throwingFunction() { + try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) { + MySwiftClass c = MySwiftClass.init(20, 10, arena); + Exception exception = assertThrows(Exception.class, () -> c.throwingFunction()); + + assertEquals("swiftError", exception.getMessage()); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 6a1c549a..dce399a9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -164,12 +164,49 @@ extension JNISwift2JavaGenerator { } private func printFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + if decl.isStatic || decl.isInitializer || !decl.hasParent { + printStaticFunctionBinding(&printer, decl) + } else { + printMemberMethodBindings(&printer, decl) + } + } + + private func printStaticFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) { printDeclDocumentation(&printer, decl) printer.print( "public static native \(renderFunctionSignature(decl));" ) } + /// Renders Java bindings for member methods + /// + /// Member methods are generated as a function that extracts the `selfPointer` + /// and passes it down to another native function along with the arguments + /// to call the Swift implementation. + private func printMemberMethodBindings(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + let translatedDecl = translatedDecl(for: decl) + + printDeclDocumentation(&printer, decl) + printer.printBraceBlock("public \(renderFunctionSignature(decl))") { printer in + var arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.name) + arguments.append("selfPointer") + + let returnKeyword = translatedDecl.translatedFunctionSignature.resultType.isVoid ? "" : "return " + + printer.print( + """ + long selfPointer = this.pointer(); + \(returnKeyword)\(translatedDecl.parentName).$\(translatedDecl.name)(\(arguments.joined(separator: ", "))); + """ + ) + } + + let returnType = translatedDecl.translatedFunctionSignature.resultType + var parameters = translatedDecl.translatedFunctionSignature.parameters.map(\.asParameter) + parameters.append("long selfPointer") + printer.print("private static native \(returnType) $\(translatedDecl.name)(\(parameters.joined(separator: ", ")));") + } + private func printInitializerBindings(_ printer: inout CodePrinter, _ decl: ImportedFunc, type: ImportedNominalType) { let translatedDecl = translatedDecl(for: decl) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index a14c2078..24532ca1 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -136,67 +136,97 @@ extension JNISwift2JavaGenerator { _ printer: inout CodePrinter, _ decl: ImportedFunc ) { + // Free functions does not have a parent + if decl.isStatic || !decl.hasParent { + self.printSwiftStaticFunctionThunk(&printer, decl) + } else { + self.printSwiftMemberFunctionThunk(&printer, decl) + } + } + + private func printSwiftStaticFunctionThunk(_ printer: inout CodePrinter, _ decl: ImportedFunc) { let translatedDecl = self.translatedDecl(for: decl) - let parentName = translatedDecl.parentName - let swiftReturnType = decl.functionSignature.result.type - printCDecl(&printer, decl) { printer in - let downcallParameters = renderDowncallArguments( - swiftFunctionSignature: decl.functionSignature, - translatedFunctionSignature: translatedDecl.translatedFunctionSignature + printCDecl( + &printer, + javaMethodName: translatedDecl.name, + parentName: translatedDecl.parentName, + parameters: translatedDecl.translatedFunctionSignature.parameters, + isStatic: true, + resultType: translatedDecl.translatedFunctionSignature.resultType + ) { printer in + // For free functions the parent is the Swift module + let parentName = decl.parentType?.asNominalTypeDeclaration?.qualifiedName ?? swiftModuleName + self.printFunctionDowncall(&printer, decl, calleeName: parentName) + } + } + + private func printSwiftMemberFunctionThunk(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + let translatedDecl = self.translatedDecl(for: decl) + let swiftParentName = decl.parentType!.asNominalTypeDeclaration!.qualifiedName + + printCDecl( + &printer, + javaMethodName: "$\(translatedDecl.name)", + parentName: translatedDecl.parentName, + parameters: translatedDecl.translatedFunctionSignature.parameters + [ + JavaParameter(name: "selfPointer", type: .long) + ], + isStatic: true, + resultType: translatedDecl.translatedFunctionSignature.resultType + ) { printer in + printer.print( + """ + let self$ = UnsafeMutablePointer<\(swiftParentName)>(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment!)))! + """ ) - let tryClause: String = decl.isThrowing ? "try " : "" - let functionDowncall = - "\(tryClause)\(parentName).\(decl.name)(\(downcallParameters))" - - let innerBody = - if swiftReturnType.isVoid { - functionDowncall - } else { - """ - let result = \(functionDowncall) - return result.getJNIValue(in: environment) - """ - } + self.printFunctionDowncall(&printer, decl, calleeName: "self$.pointee") + } + } + + private func printFunctionDowncall( + _ printer: inout CodePrinter, + _ decl: ImportedFunc, + calleeName: String + ) { + let translatedDecl = self.translatedDecl(for: decl) + let swiftReturnType = decl.functionSignature.result.type + + let downcallParameters = renderDowncallArguments( + swiftFunctionSignature: decl.functionSignature, + translatedFunctionSignature: translatedDecl.translatedFunctionSignature + ) + let tryClause: String = decl.isThrowing ? "try " : "" + let functionDowncall = "\(tryClause)\(calleeName).\(decl.name)(\(downcallParameters))" + + let returnStatement = + if swiftReturnType.isVoid { + functionDowncall + } else { + """ + let result = \(functionDowncall) + return result.getJNIValue(in: environment) + """ + } - if decl.isThrowing { - let dummyReturn = - !swiftReturnType.isVoid ? "return \(swiftReturnType).jniPlaceholderValue" : "" - printer.print( + if decl.isThrowing { + let dummyReturn = + !swiftReturnType.isVoid ? "return \(swiftReturnType).jniPlaceholderValue" : "" + printer.print( """ do { - \(innerBody) + \(returnStatement) } catch { environment.throwAsException(error) \(dummyReturn) } """ - ) - } else { - printer.print(innerBody) - } + ) + } else { + printer.print(returnStatement) } } - private func printCDecl( - _ printer: inout CodePrinter, - _ decl: ImportedFunc, - _ body: (inout CodePrinter) -> Void - ) { - let translatedDecl = translatedDecl(for: decl) - let parentName = translatedDecl.parentName - - printCDecl( - &printer, - javaMethodName: translatedDecl.name, - parentName: parentName, - parameters: translatedDecl.translatedFunctionSignature.parameters, - isStatic: decl.isStatic || decl.isInitializer || !decl.hasParent, - resultType: translatedDecl.translatedFunctionSignature.resultType, - body - ) - } - private func printCDecl( _ printer: inout CodePrinter, javaMethodName: String, diff --git a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift index 0c0f4f02..41802e55 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift @@ -33,6 +33,8 @@ struct JNIClassTests { self.x = 0 self.y = 0 } + + public func doSomething(x: Int64) {} } """ @@ -206,4 +208,49 @@ struct JNIClassTests { ] ) } + + @Test + func memberMethod_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func doSomething(x: Int64) + * } + */ + public void doSomething(long x) { + long selfPointer = this.pointer(); + MyClass.$doSomething(x, selfPointer); + } + """, + """ + private static native void $doSomething(long x, long selfPointer); + """ + ] + ) + } + + @Test + func memberMethod_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyClass__00024doSomething__JJ") + func Java_com_example_swift_MyClass__00024doSomething__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, selfPointer: jlong) { + let self$ = UnsafeMutablePointer(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment!)))! + self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment!)) + } + """, + ] + ) + } } From 3d70cfecc6d03d940b818bcf33f80a791b13f8e8 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sat, 12 Jul 2025 11:13:49 +0200 Subject: [PATCH 2/2] fix JNI throwing exceptions --- Sources/JavaKit/Exceptions/ExceptionHandling.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaKit/Exceptions/ExceptionHandling.swift b/Sources/JavaKit/Exceptions/ExceptionHandling.swift index 02537281..d5ca1107 100644 --- a/Sources/JavaKit/Exceptions/ExceptionHandling.swift +++ b/Sources/JavaKit/Exceptions/ExceptionHandling.swift @@ -39,7 +39,7 @@ extension JNIEnvironment { } // Otherwise, create a exception with a message. - _ = try! JavaClass.withJNIClass(in: self) { exceptionClass in + _ = try! Exception.withJNIClass(in: self) { exceptionClass in interface.ThrowNew(self, exceptionClass, String(describing: error)) } }