diff --git a/Package.swift b/Package.swift index 8ee61c25..8f149129 100644 --- a/Package.swift +++ b/Package.swift @@ -134,7 +134,7 @@ let package = Package( dependencies: [ "SwiftJava", "SwiftJavaRuntimeSupport", - "SwiftRuntimeFunctions", + "SwiftRuntimeFunctions" ] ), @@ -396,5 +396,15 @@ let package = Package( .swiftLanguageMode(.v5) ] ), + + .testTarget( + name: "SwiftRuntimeFunctionsTests", + dependencies: [ + "SwiftRuntimeFunctions", + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] + ), ] ) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index b1a4e489..a9bd34d2 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -46,6 +46,10 @@ public class MySwiftClass { 12 } + public func describe() -> String { + "MySwiftClass(len: \(len), cap: \(cap))" + } + public func makeRandomIntMethod() -> Int { Int.random(in: 1..<256) } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index f67e0bfc..e3fcea92 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -114,6 +114,17 @@ public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int { } } +// ==== ----------------------------------------------------------------------- +// MARK: String returns + +public func globalMakeString() -> String { + "Hello from Swift!" +} + +public func globalStringIdentity(string: String) -> String { + string +} + // ==== ----------------------------------------------------------------------- // MARK: Overloaded functions 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 1f0464eb..e6ac42b6 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -58,6 +58,15 @@ void test_MySwiftClass_makeIntMethod() { } } + @Test + void test_MySwiftClass_describe() { + try(var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftClass o = MySwiftClass.init(12, 42, arena); + var got = o.describe(); + assertEquals("MySwiftClass(len: 12, cap: 42)", got); + } + } + @Test @Disabled // TODO: Need var mangled names in interfaces void test_MySwiftClass_property_len() { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index a6c34b57..35576ed2 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -53,6 +53,27 @@ void call_writeString_jni() { assertEquals(string.length(), reply); } + @Test + void call_globalMakeString() { + String result = MySwiftLibrary.globalMakeString(); + assertEquals("Hello from Swift!", result); + } + + @Test + void call_globalStringIdentity() { + String input = "round-trip test!"; + String result = MySwiftLibrary.globalStringIdentity(input); + assertEquals(input, result); + } + + @Test + void call_globalStringIdentity_empty() { + String result = MySwiftLibrary.globalStringIdentity(""); + assertEquals("", result); + } + + + @Test @Disabled("Upcalls not yet implemented in new scheme") @SuppressWarnings({"Convert2Lambda", "Convert2MethodRef"}) diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index d8406016..9e3c4228 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -736,7 +736,19 @@ struct CdeclLowering { case .foundationData, .essentialsData: break - case .string, .optional: + case .string: + // String returned as heap-allocated C string (caller frees). + return LoweredResult( + cdeclResultType: knownTypes.unsafeMutablePointer(knownTypes.int8), + cdeclOutParameters: [], + conversion: .method( + base: "_swiftjava_stringToCString", + methodName: nil, + arguments: [.init(label: nil, argument: .placeholder)] + ) + ) + + case .optional: // Not supported at this point. throw LoweringError.unhandledType(type) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 221ef889..d9fd611f 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -768,8 +768,12 @@ extension FFMSwift2JavaGenerator { // FIXME: Implement throw JavaTranslationError.unhandledType(swiftType) case .string: - // FIXME: Implement - throw JavaTranslationError.unhandledType(swiftType) + return TranslatedResult( + javaResultType: .javaLangString, + annotations: resultAnnotations, + outParameters: [], + conversion: .call(.placeholder, function: "SwiftRuntime.fromCString", withArena: false) + ) default: throw JavaTranslationError.unhandledType(swiftType) } diff --git a/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift b/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift index f083232b..4f362bd3 100644 --- a/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift +++ b/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift @@ -32,6 +32,18 @@ public func _swiftjava_swift_retainCount(object: UnsafeMutableRawPointer) -> Int @_silgen_name("swift_isUniquelyReferenced") public func _swiftjava_swift_isUniquelyReferenced(object: UnsafeMutableRawPointer) -> Bool +/// Copies a Swift String to a heap-allocated NULL-terminated C string. +/// The caller (Java FFM) must call free() on the returned pointer. +public func _swiftjava_stringToCString(_ string: String) -> UnsafeMutablePointer { + var string = string + return string.withUTF8 { utf8 in + let buffer = UnsafeMutablePointer.allocate(capacity: utf8.count + 1) + UnsafeMutableRawPointer(buffer).copyMemory(from: utf8.baseAddress!, byteCount: utf8.count) + buffer[utf8.count] = 0 + return buffer + } +} + @_alwaysEmitIntoClient @_transparent func _swiftjava_withHeapObject( of object: AnyObject, diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java index 961f2324..889356f4 100644 --- a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java @@ -405,6 +405,16 @@ public static MemorySegment toCString(String str, Arena arena) { return arena.allocateFrom(str); } + /** + * Read a heap-allocated C string into a Java String, then free the native memory. + */ + public static String fromCString(MemorySegment cStr) { + if (cStr.equals(MemorySegment.NULL)) return null; + String result = cStr.reinterpret(Long.MAX_VALUE).getString(0); + cFree(cStr); + return result; + } + public static MemorySegment toOptionalSegmentInt(OptionalInt opt, Arena arena) { return opt.isPresent() ? arena.allocateFrom(ValueLayout.JAVA_INT, opt.getAsInt()) : MemorySegment.NULL; } diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index c26d68ec..f56e3040 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -535,4 +535,20 @@ final class FunctionLoweringTests { expectedCFunction: "void c_value(const void *newValue, const void *self)" ) } + + @Test("Lowering String return") + func lowerStringReturn() throws { + try assertLoweredFunction( + """ + func bar() -> String { } + """, + expectedCDecl: """ + @_cdecl("c_bar") + public func c_bar() -> UnsafeMutablePointer { + return _swiftjava_stringToCString(bar()) + } + """, + expectedCFunction: "int8_t *c_bar(void)" + ) + } } diff --git a/Tests/SwiftRuntimeFunctionsTests/StringToCStringTests.swift b/Tests/SwiftRuntimeFunctionsTests/StringToCStringTests.swift new file mode 100644 index 00000000..4ff88a91 --- /dev/null +++ b/Tests/SwiftRuntimeFunctionsTests/StringToCStringTests.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftRuntimeFunctions +import Testing + +@Suite("_swiftjava_stringToCString tests") +struct StringToCStringTests { + + @Test func ascii() { + let cStr = _swiftjava_stringToCString("Hello") + defer { cStr.deallocate() } + + #expect(String(cString: cStr) == "Hello") + } + + @Test func empty() { + let cStr = _swiftjava_stringToCString("") + defer { cStr.deallocate() } + + #expect(cStr[0] == 0) + #expect(String(cString: cStr) == "") + } + + @Test func emoji() { + let input = "hello 🦫 beaver!" + let cStr = _swiftjava_stringToCString(input) + defer { cStr.deallocate() } + + #expect(String(cString: cStr) == input) + } + + @Test func roundTrip() { + let input = "café ☕ naïve 日本語" + let cStr = _swiftjava_stringToCString(input) + defer { cStr.deallocate() } + + #expect(String(cString: cStr) == input) + } +}