From 860f2aa57a20d8e6132f1fe6dfc05ca97defa39a Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 16 Dec 2024 11:08:17 +0900 Subject: [PATCH 1/4] Fix JMH benchmark --- .../org/swift/swiftkit/JavaToSwiftBenchmark.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java index 614697a3..a31aaaa0 100644 --- a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java +++ b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java @@ -36,19 +36,15 @@ public static class BenchmarkState { MySwiftClass obj; @Setup(Level.Trial) - public void beforeALl() { - System.loadLibrary("swiftCore"); - System.loadLibrary("ExampleSwiftLibrary"); - - // Tune down debug statements so they don't fill up stdout - System.setProperty("jextract.trace.downcalls", "false"); - + public void beforeAll() { obj = new MySwiftClass(1, 2); } } - @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) - public void simpleSwiftApiCall(BenchmarkState state, Blackhole blackhole) { - blackhole.consume(state.obj.makeRandomIntMethod()); + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public long simpleSwiftApiCall(BenchmarkState state, Blackhole blackhole) { + return state.obj.makeRandomIntMethod(); } } From f61a5d2bf8e0aafa477009690d326ca9cc9fed36 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 16 Dec 2024 16:56:16 +0900 Subject: [PATCH 2/4] Implement passing Strings to Swift, with copying add benchmarks for String -> Swift and returning a count --- Samples/SwiftKitSampleApp/Package.swift | 2 + .../MySwiftLibrary/MySwiftLibrary.swift | 17 +++- .../jni/JNIImplementations.swift | 33 +++++++ Samples/SwiftKitSampleApp/build.gradle | 11 +++ .../swift/swiftkit/JavaToSwiftBenchmark.java | 48 +++++++---- .../swiftkit/StringPassingBenchmark.java | 86 +++++++++++++++++++ .../com/example/swift/HelloJava2Swift.java | 4 + .../com/example/swift/MySwiftLibraryTest.java | 21 +++++ Sources/JExtractSwift/ImportedDecls.swift | 12 +++ Sources/JExtractSwift/JavaTypes.swift | 5 ++ .../Swift2JavaTranslator+Printing.swift | 64 ++++++++++++-- Sources/JExtractSwift/Swift2JavaVisitor.swift | 2 +- .../JExtractSwift/SwiftThunkTranslator.swift | 61 ++++++++++--- Sources/JExtractSwift/TranslatedType.swift | 11 +++ .../BridgedValues/JavaValue+String.swift | 2 +- Sources/JavaTypes/JavaType+SwiftNames.swift | 12 +++ .../FuncCallbackImportTests.swift | 2 +- .../StringPassingTests.swift | 55 ++++++++++++ 18 files changed, 409 insertions(+), 39 deletions(-) create mode 100644 Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift create mode 100644 Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/StringPassingBenchmark.java create mode 100644 Tests/JExtractSwiftTests/StringPassingTests.swift diff --git a/Samples/SwiftKitSampleApp/Package.swift b/Samples/SwiftKitSampleApp/Package.swift index bfd3ecbe..36d05878 100644 --- a/Samples/SwiftKitSampleApp/Package.swift +++ b/Samples/SwiftKitSampleApp/Package.swift @@ -60,6 +60,8 @@ let package = Package( .target( name: "MySwiftLibrary", dependencies: [ + .product(name: "JavaKit", package: "swift-java"), + .product(name: "JavaRuntime", package: "swift-java"), .product(name: "SwiftKitSwift", package: "swift-java"), ], exclude: [ diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 84e4618f..ddb14569 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -31,6 +31,14 @@ public func globalTakeInt(i: Int) { p("i:\(i)") } +public func globalMakeInt() -> Int { + return 42 +} + +public func globalWriteString(string: String) -> Int { + return string.count +} + public func globalTakeIntInt(i: Int, j: Int) { p("i:\(i), j:\(j)") } @@ -79,6 +87,11 @@ public class MySwiftClass { return 12 } + public func writeString(string: String) -> Int { + p("echo -> \(string)") + return string.count + } + public func makeRandomIntMethod() -> Int { return Int.random(in: 1..<256) } @@ -87,8 +100,8 @@ public class MySwiftClass { // ==== Internal helpers private func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { - print("[swift][\(file):\(line)](\(function)) \(msg)") - fflush(stdout) +// print("[swift][\(file):\(line)](\(function)) \(msg)") +// fflush(stdout) } #if os(Linux) diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift new file mode 100644 index 00000000..90df0344 --- /dev/null +++ b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift @@ -0,0 +1,33 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import JavaKit +import JavaRuntime + +@JavaClass("com.example.swift.HelloJava2Swift") +open class HelloJava2Swift: JavaObject { +} + +extension JavaClass { +} + +/// Describes the Java `native` methods for ``HelloJava2Swift``. +/// +/// To implement all of the `native` methods for HelloSwift in Swift, +/// extend HelloSwift to conform to this protocol and mark each +/// implementation of the protocol requirement with `@JavaMethod`. +protocol HelloJava2SwiftNativeMethods { + func jniWriteString(_ message: String) -> Int32 + func jniGetInt() -> Int32 +} + +@JavaImplementation("com.example.swift.HelloJava2Swift") +extension HelloJava2Swift: HelloJava2SwiftNativeMethods { + @JavaMethod + func jniWriteString(_ message: String) -> Int32 { + return Int32(message.count) + } + + @JavaMethod + func jniGetInt() -> Int32 { + return 12 + } +} diff --git a/Samples/SwiftKitSampleApp/build.gradle b/Samples/SwiftKitSampleApp/build.gradle index ff9e1837..fdd38a11 100644 --- a/Samples/SwiftKitSampleApp/build.gradle +++ b/Samples/SwiftKitSampleApp/build.gradle @@ -145,10 +145,21 @@ application { ] } +String jmhIncludes = findProperty("jmhIncludes") + jmh { + if (jmhIncludes != null) { + includes = [jmhIncludes] + } + jvmArgsAppend = [ + "--enable-native-access=ALL-UNNAMED", + "-Djava.library.path=" + (BuildUtils.javaLibraryPaths(rootDir) + BuildUtils.javaLibraryPaths(project.projectDir)).join(":"), + + // Enable tracing downcalls (to Swift) + "-Djextract.trace.downcalls=false" ] } diff --git a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java index a31aaaa0..18ced030 100644 --- a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java +++ b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java @@ -14,37 +14,51 @@ package org.swift.swiftkit; -import java.util.concurrent.TimeUnit; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Level; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.infra.Blackhole; +import com.example.swift.HelloJava2Swift; +import com.example.swift.MySwiftLibrary; +import org.openjdk.jmh.annotations.*; import com.example.swift.MySwiftClass; -@SuppressWarnings("unused") +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 3, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) public class JavaToSwiftBenchmark { @State(Scope.Benchmark) public static class BenchmarkState { + ClosableSwiftArena arena; MySwiftClass obj; @Setup(Level.Trial) public void beforeAll() { - obj = new MySwiftClass(1, 2); + arena = SwiftArena.ofConfined(); + obj = new MySwiftClass(arena, 1, 2); + } + + @TearDown(Level.Trial) + public void afterAll() { + arena.close(); } } @Benchmark - @BenchmarkMode(Mode.AverageTime) - @OutputTimeUnit(TimeUnit.NANOSECONDS) - public long simpleSwiftApiCall(BenchmarkState state, Blackhole blackhole) { - return state.obj.makeRandomIntMethod(); + public long jextract_getInt_ffm(BenchmarkState state) { + return MySwiftLibrary.globalMakeInt(); } + + @Benchmark + public long getInt_global_jni(BenchmarkState state) { + return HelloJava2Swift.jniGetInt(); + } + + @Benchmark + public long getInt_member_ffi(BenchmarkState state) { + return state.obj.makeIntMethod(); + } + } diff --git a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/StringPassingBenchmark.java b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/StringPassingBenchmark.java new file mode 100644 index 00000000..1cc55e69 --- /dev/null +++ b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/StringPassingBenchmark.java @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit; + +import com.example.swift.HelloJava2Swift; +import com.example.swift.MySwiftClass; +import com.example.swift.MySwiftLibrary; +import org.openjdk.jmh.annotations.*; + +import java.lang.foreign.Arena; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Fork(value = 2, jvmArgsAppend = {"--enable-native-access=ALL-UNNAMED"}) +public class StringPassingBenchmark { + + @Param({ + "5", + "10", + "100", + "200" + }) + public int stringLen; + public String string; + + ClosableSwiftArena arena; + MySwiftClass obj; + + @Setup(Level.Trial) + public void beforeAll() { + arena = SwiftArena.ofConfined(); + obj = new MySwiftClass(arena, 1, 2); + string = makeString(stringLen); + } + + @TearDown(Level.Trial) + public void afterAll() { + arena.close(); + } + + @Benchmark + public long writeString_global_fmm() { + return MySwiftLibrary.globalWriteString(string); + } + + @Benchmark + public long writeString_global_jni() { + return HelloJava2Swift.jniWriteString(string); + } + + @Benchmark + public long writeString_baseline() { + return string.length(); + } + + static String makeString(int size) { + var text = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut in augue ullamcorper, mattis lacus tincidunt, " + + "accumsan massa. Morbi gravida purus ut porttitor iaculis. Vestibulum lacinia, mi in tincidunt hendrerit," + + "lectus est placerat magna, vitae vestibulum nulla ligula at massa. Pellentesque nibh quam, pulvinar eu " + + "nunc congue, molestie molestie augue. Nam convallis consectetur velit, at dictum risus ullamcorper iaculis. " + + "Vestibulum lacinia nisi in elit consectetur vulputate. Praesent id odio tristique, tincidunt arcu et, convallis velit. " + + "Sed vitae pulvinar arcu. Curabitur euismod mattis dui in suscipit. Morbi aliquet facilisis vulputate. Phasellus " + + "non lectus dapibus, semper magna eu, aliquet magna. Suspendisse vel enim at augue luctus gravida. Suspendisse " + + "venenatis justo non accumsan sollicitudin. Suspendisse vitae ornare odio, id blandit nibh. Nulla facilisi. " + + "Nulla nulla orci, finibus nec luctus et, faucibus et ligula."; + return text.substring(0, size); + } +} diff --git a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java index 2a86e403..1fafaf0c 100644 --- a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -56,4 +56,8 @@ static void examples() { System.out.println("DONE."); } + + public static native long jniWriteString(String str); + public static native long jniGetInt(); + } diff --git a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index ffa90359..007b06bd 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -14,6 +14,7 @@ package com.example.swift; +import com.example.swift.MySwiftLibrary; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -27,6 +28,10 @@ public class MySwiftLibraryTest { + static { + System.loadLibrary(MySwiftLibrary.LIB_NAME); + } + @Test void call_helloWorld() { MySwiftLibrary.helloWorld(); @@ -41,6 +46,22 @@ void call_globalTakeInt() { assertNotNull(MySwiftLibrary.globalTakeInt$address()); } + @Test + void call_writeString_jextract() { + var string = "Hello Swift!"; + long reply = MySwiftLibrary.globalWriteString(string); + + assertEquals(string.length(), reply); + } + + @Test + void call_writeString_jni() { + var string = "Hello Swift!"; + long reply = HelloJava2Swift.jniWriteString(string); + + assertEquals(string.length(), reply); + } + @Test @Disabled("Upcalls not yet implemented in new scheme") @SuppressWarnings({"Convert2Lambda", "Convert2MethodRef"}) diff --git a/Sources/JExtractSwift/ImportedDecls.swift b/Sources/JExtractSwift/ImportedDecls.swift index cbff9969..794753aa 100644 --- a/Sources/JExtractSwift/ImportedDecls.swift +++ b/Sources/JExtractSwift/ImportedDecls.swift @@ -90,6 +90,10 @@ public struct ImportedParam { var effectiveName: String? { firstName ?? secondName } + + var effectiveValueName: String { + secondName ?? firstName ?? "_" + } // The Swift type as-is from the swift interface var swiftType: String { @@ -113,6 +117,14 @@ extension ImportedParam { } } +public enum ParameterVariant { + /// Used when declaring the "Swift thunks" we call through into Swift. + /// + /// Some types need to be represented as raw pointers and recovered into + /// Swift types inside the thunks when we do this. + case cDeclThunk +} + // TODO: this is used in different contexts and needs a cleanup // Perhaps this is "which parameter passing style"? public enum SelfParameterVariant { diff --git a/Sources/JExtractSwift/JavaTypes.swift b/Sources/JExtractSwift/JavaTypes.swift index 01a69aaf..633943ef 100644 --- a/Sources/JExtractSwift/JavaTypes.swift +++ b/Sources/JExtractSwift/JavaTypes.swift @@ -20,6 +20,11 @@ extension JavaType { .class(package: "java.lang.foreign", name: "MemorySegment") } + /// The description of the type java.lang.String. + static var javaLangString: JavaType { + .class(package: "java.lang", name: "String") + } + /// The description of the type java.lang.Runnable. static var javaLangRunnable: JavaType { .class(package: "java.lang", name: "Runnable") diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index 76721e46..d292467f 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -44,6 +44,19 @@ extension Swift2JavaTranslator { print("[swift-java] Generated: \(ty.javaClassName.bold).java (at \(outputFile))") } } + + do { + let filename = "\(self.swiftModuleName).java" + log.info("Printing contents: \(filename)") + printModule(&printer) + + if let outputFile = try printer.writeContents( + outputDirectory: outputDirectory, + javaPackagePath: javaPackagePath, + filename: filename) { + print("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile)") + } + } } public func writeSwiftThunkSources(outputDirectory: String) throws { @@ -94,6 +107,13 @@ extension Swift2JavaTranslator { public func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { let stt = SwiftThunkTranslator(self) + printer.print( + """ + // Generated by swift-java + + import SwiftKitSwift + """) + for thunk in stt.renderGlobalThunks() { printer.print(thunk) printer.println() @@ -163,6 +183,7 @@ extension Swift2JavaTranslator { printModuleClass(&printer) { printer in // TODO: print all "static" methods for decl in importedGlobalFuncs { + self.log.trace("Print imported decl: \(decl)") printFunctionDowncallMethods(&printer, decl) } } @@ -711,9 +732,8 @@ extension Swift2JavaTranslator { var mh$ = \(decl.baseIdentifier).\(handleName); \(renderTry(withArena: needsArena)) """, - """ - \(renderUpcallHandles(decl)) - """, + renderUpcallHandles(decl), + renderParameterDowncallConversions(decl), """ if (SwiftKit.TRACE_DOWNCALLS) { SwiftKit.traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: .memorySegment))); @@ -807,6 +827,10 @@ extension Swift2JavaTranslator { if p.type.javaType.isSwiftClosure { return true } + + if p.type.javaType.isString { + return true + } } return false @@ -814,7 +838,7 @@ extension Swift2JavaTranslator { public func renderTry(withArena: Bool) -> String { if withArena { - "try (Arena arena = Arena.ofConfined()) {" + "try (var arena = Arena.ofConfined()) {" } else { "try {" } @@ -839,7 +863,10 @@ extension Swift2JavaTranslator { } // TODO: these are stateless, find new place for them? - public func renderSwiftParamDecls(_ decl: ImportedFunc, paramPassingStyle: SelfParameterVariant?) -> String { + public func renderSwiftParamDecls( + _ decl: ImportedFunc, + paramPassingStyle: SelfParameterVariant?, + style: ParameterVariant? = nil) -> String { var ps: [String] = [] var pCounter = 0 @@ -853,7 +880,9 @@ extension Swift2JavaTranslator { let secondName = p.secondName ?? p.firstName ?? nextUniqueParamName() let paramTy: String = - if paramPassingStyle == .swiftThunkSelf { + if style == .cDeclThunk, p.type.javaType.isString { + "UnsafeMutablePointer" // TODO: is this ok? + } else if paramPassingStyle == .swiftThunkSelf { "\(p.type.cCompatibleSwiftType)" } else { p.type.swiftTypeName.description @@ -892,6 +921,21 @@ extension Swift2JavaTranslator { return printer.contents } + public func renderParameterDowncallConversions(_ decl: ImportedFunc) -> String { + var printer = CodePrinter() + for p in decl.parameters { + if p.type.javaType.isString { + printer.print( + """ + var \(p.effectiveValueName)$ = arena.allocateFrom(\(p.effectiveValueName)); + """ + ) + } + } + + return printer.contents + } + public func renderForwardJavaParams(_ decl: ImportedFunc, paramPassingStyle: SelfParameterVariant?) -> String { var ps: [String] = [] var pCounter = 0 @@ -907,6 +951,14 @@ extension Swift2JavaTranslator { if p.effectiveName == "self$" { precondition(paramPassingStyle == .memorySegment) param = "self$" + } else if p.type.javaType.isString { + // TODO: make this less one-off and maybe call it "was adapted"? + if paramPassingStyle == .wrapper { + // pass it raw, we're not performing adaptation here it seems as we're passing wrappers around + param = "\(p.effectiveValueName)" + } else { + param = "\(p.effectiveValueName)$" + } } else { param = "\(p.renderParameterForwarding() ?? nextUniqueParamName())" } diff --git a/Sources/JExtractSwift/Swift2JavaVisitor.swift b/Sources/JExtractSwift/Swift2JavaVisitor.swift index 3feceec3..44138018 100644 --- a/Sources/JExtractSwift/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwift/Swift2JavaVisitor.swift @@ -94,7 +94,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { params = try node.signature.parameterClause.parameters.map { param in // TODO: more robust parameter handling // TODO: More robust type handling - return ImportedParam( + ImportedParam( syntax: param, type: try cCompatibleType(for: param.type) ) diff --git a/Sources/JExtractSwift/SwiftThunkTranslator.swift b/Sources/JExtractSwift/SwiftThunkTranslator.swift index 5fc01279..c530be3b 100644 --- a/Sources/JExtractSwift/SwiftThunkTranslator.swift +++ b/Sources/JExtractSwift/SwiftThunkTranslator.swift @@ -50,6 +50,7 @@ struct SwiftThunkTranslator { decls.append(contentsOf: render(forFunc: decl)) } +// TODO: handle variables // for v in nominal.variables { // if let acc = v.accessorFunc(kind: .get) { // decls.append(contentsOf: render(forFunc: acc)) @@ -102,35 +103,73 @@ struct SwiftThunkTranslator { st.log.trace("Rendering thunks for: \(decl.baseIdentifier)") let thunkName = st.thunkNameRegistry.functionThunkName(module: st.swiftModuleName, decl: decl) + let returnArrowTy = + if decl.returnType.cCompatibleJavaMemoryLayout == .primitive(.void) { + "/* \(decl.returnType.swiftTypeName) */" + } else { + "-> \(decl.returnType.cCompatibleSwiftType) /* \(decl.returnType.swiftTypeName) */" + } + // Do we need to pass a self parameter? let paramPassingStyle: SelfParameterVariant? + let callBase: String let callBaseDot: String if let parent = decl.parent { paramPassingStyle = .swiftThunkSelf - callBaseDot = "unsafeBitCast(_self, to: \(parent.originalSwiftType).self)." - // callBaseDot = "(_self as! \(parent.originalSwiftType))." + callBase = "let self$ = unsafeBitCast(_self, to: \(parent.originalSwiftType).self)" + callBaseDot = "self$." } else { paramPassingStyle = nil + callBase = "" callBaseDot = "" } - let returnArrowTy = - if decl.returnType.cCompatibleJavaMemoryLayout == .primitive(.void) { - "/* \(decl.returnType.swiftTypeName) */" - } else { - "-> \(decl.returnType.cCompatibleSwiftType) /* \(decl.returnType.swiftTypeName) */" - } - // FIXME: handle in thunk: errors + let returnStatement: String + if decl.returnType.javaType.isString { + returnStatement = + """ + let adaptedReturnValue = fatalError("Not implemented: adapting return types in Swift thunks") + return adaptedReturnValue + """ + } else { + returnStatement = "return returnValue" + } + + let declParams = st.renderSwiftParamDecls( + decl, + paramPassingStyle: paramPassingStyle, + style: .cDeclThunk + ) return [ """ @_cdecl("\(raw: thunkName)") - public func \(raw: thunkName)(\(raw: st.renderSwiftParamDecls(decl, paramPassingStyle: paramPassingStyle))) \(raw: returnArrowTy) { - return \(raw: callBaseDot)\(raw: decl.baseIdentifier)(\(raw: st.renderForwardSwiftParams(decl, paramPassingStyle: paramPassingStyle))) + public func \(raw: thunkName)(\(raw: declParams)) \(raw: returnArrowTy) { + \(raw: adaptArgumentsInThunk(decl)) + \(raw: callBase) + let returnValue = \(raw: callBaseDot)\(raw: decl.baseIdentifier)(\(raw: st.renderForwardSwiftParams(decl, paramPassingStyle: paramPassingStyle))) + \(raw: returnStatement) } """ ] } + + func adaptArgumentsInThunk(_ decl: ImportedFunc) -> String { + var lines: [String] = [] + for p in decl.parameters { + if p.type.javaType.isString { + // FIXME: is there a way we can avoid the copying here? + let adaptedType = + """ + let \(p.effectiveValueName) = String(cString: \(p.effectiveValueName)) + """ + + lines += [adaptedType] + } + } + + return lines.joined(separator: "\n") + } } diff --git a/Sources/JExtractSwift/TranslatedType.swift b/Sources/JExtractSwift/TranslatedType.swift index 9dc12e07..eef4140d 100644 --- a/Sources/JExtractSwift/TranslatedType.swift +++ b/Sources/JExtractSwift/TranslatedType.swift @@ -116,6 +116,17 @@ extension Swift2JavaVisitor { ) } + // We special handle String types + if parent == nil, name == "String" { + return TranslatedType( + cCompatibleConvention: .direct, + originalSwiftType: "\(raw: name)", + cCompatibleSwiftType: "Swift.\(raw: name)", + cCompatibleJavaMemoryLayout: .heapObject, // FIXME: or specialize string? + javaType: .javaLangString + ) + } + // Identify the various pointer types from the standard library. if let (requiresArgument, _, hasCount) = name.isNameOfSwiftPointerType, !hasCount { // Dig out the pointee type if needed. diff --git a/Sources/JavaKit/BridgedValues/JavaValue+String.swift b/Sources/JavaKit/BridgedValues/JavaValue+String.swift index 5e98634d..213842cb 100644 --- a/Sources/JavaKit/BridgedValues/JavaValue+String.swift +++ b/Sources/JavaKit/BridgedValues/JavaValue+String.swift @@ -30,7 +30,7 @@ extension String: JavaValue { } let cString = environment.interface.GetStringUTFChars(environment, value, nil)! defer { environment.interface.ReleaseStringUTFChars(environment, value, cString) } - self = String(cString: cString) + self = String(cString: cString) // copy } public func getJNIValue(in environment: JNIEnvironment) -> JNIType { diff --git a/Sources/JavaTypes/JavaType+SwiftNames.swift b/Sources/JavaTypes/JavaType+SwiftNames.swift index e1604eaa..d01398b8 100644 --- a/Sources/JavaTypes/JavaType+SwiftNames.swift +++ b/Sources/JavaTypes/JavaType+SwiftNames.swift @@ -45,6 +45,18 @@ extension JavaType { } } + public var isString: Bool { + switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, + .array: + return false + case .class(package: "java.lang", name: "String"): + return true + case .class: + return false + } + } + /// Produce the Swift type name for this Java type. public func swiftTypeName(resolver: JavaToSwiftClassNameResolver) rethrows -> String { switch self { diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 2f2c8c8c..696c4b93 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -61,7 +61,7 @@ final class FuncCallbackImportTests { */ public static void callMe(java.lang.Runnable callback) { var mh$ = callMe.HANDLE; - try (Arena arena = Arena.ofConfined()) { + try (var arena = Arena.ofConfined()) { FunctionDescriptor callMe_callback_desc$ = FunctionDescriptor.ofVoid(); MethodHandle callMe_callback_handle$ = MethodHandles.lookup() .findVirtual(Runnable.class, "run", diff --git a/Tests/JExtractSwiftTests/StringPassingTests.swift b/Tests/JExtractSwiftTests/StringPassingTests.swift new file mode 100644 index 00000000..d377f166 --- /dev/null +++ b/Tests/JExtractSwiftTests/StringPassingTests.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwift +import Testing + +final class StringPassingTests { + let class_interfaceFile = + """ + public func writeString(string: String) -> Int { + return string.count + } + """ + + @Test("Import: public func writeString(string: String) -> Int") + func method_helloWorld() throws { + let st = Swift2JavaTranslator( + javaPackage: "com.example.swift", + swiftModuleName: "__FakeModule" + ) + st.log.logLevel = .trace + + try assertOutput( + st, input: class_interfaceFile, .java, + detectChunkByInitialLines: 1, + expectedChunks: + [ + """ + public static long writeString(java.lang.String string) { + var mh$ = writeString.HANDLE; + try (var arena = Arena.ofConfined()) { + var string$ = arena.allocateFrom(string); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(); + } + return (long) mh$.invokeExact(string$); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """ + ]) + } +} From b3039e522b55396976c0e1959aedce270e5fd6da Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Tue, 17 Dec 2024 14:48:26 +0900 Subject: [PATCH 3/4] fix license header --- .../MySwiftLibrary/jni/JNIImplementations.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift index 90df0344..ef0e2e58 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift +++ b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/jni/JNIImplementations.swift @@ -1,4 +1,17 @@ -// Auto-generated by Java-to-Swift wrapper generator. +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import JavaKit import JavaRuntime From c57b70a3c50ecac73246ae5a86f139ff28852ab5 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Tue, 17 Dec 2024 17:39:40 +0900 Subject: [PATCH 4/4] adjust Swift tests to new source generation --- .../Swift2JavaTranslator+Printing.swift | 1 - Tests/JExtractSwiftTests/MethodImportTests.swift | 16 +++++----------- .../JExtractSwiftTests/StringPassingTests.swift | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index d292467f..61d7e981 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -252,7 +252,6 @@ extension Swift2JavaTranslator { } public func printHeader(_ printer: inout CodePrinter) { - assert(printer.isEmpty) printer.print( """ // Generated by jextract-swift diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index c866cb60..67c7c210 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -38,7 +38,6 @@ final class MethodImportTests { public func globalTakeIntLongString(i32: Int32, l: Int64, s: String) extension MySwiftClass { - // MANGLED NAME: $s14MySwiftLibrary0aB5ClassC22helloMemberFunctionInExtension public func helloMemberInExtension() } @@ -54,12 +53,6 @@ final class MethodImportTests { @objc deinit } - - // FIXME: Hack to allow us to translate "String", even though it's not - // actually available - // MANGLED NAME: $ss - public class String { - } """ @Test("Import: public func helloWorld()") @@ -174,13 +167,14 @@ final class MethodImportTests { * public func globalTakeIntLongString(i32: Int32, l: Int64, s: String) * } */ - public static void globalTakeIntLongString(int i32, long l, com.example.swift.String s) { + public static void globalTakeIntLongString(int i32, long l, java.lang.String s) { var mh$ = globalTakeIntLongString.HANDLE; - try { + try (var arena = Arena.ofConfined()) { + var s$ = arena.allocateFrom(s); if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(i32, l, s.$memorySegment()); + SwiftKit.traceDowncall(i32, l, s$); } - mh$.invokeExact(i32, l, s.$memorySegment()); + mh$.invokeExact(i32, l, s$); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } diff --git a/Tests/JExtractSwiftTests/StringPassingTests.swift b/Tests/JExtractSwiftTests/StringPassingTests.swift index d377f166..c51d3549 100644 --- a/Tests/JExtractSwiftTests/StringPassingTests.swift +++ b/Tests/JExtractSwiftTests/StringPassingTests.swift @@ -42,7 +42,7 @@ final class StringPassingTests { try (var arena = Arena.ofConfined()) { var string$ = arena.allocateFrom(string); if (SwiftKit.TRACE_DOWNCALLS) { - SwiftKit.traceDowncall(); + SwiftKit.traceDowncall(string$); } return (long) mh$.invokeExact(string$); } catch (Throwable ex$) {