Skip to content

Commit 7013b01

Browse files
committed
jextract/ffm: Support returning Strings, we copy and have to free it on
java side Add runtime tests in ffm sample jextract/ffm: Support returning Strings, we copy and have to free it on java side
1 parent 6c9340b commit 7013b01

11 files changed

Lines changed: 163 additions & 3 deletions

File tree

Package.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,5 +396,15 @@ let package = Package(
396396
.swiftLanguageMode(.v5)
397397
]
398398
),
399+
400+
.testTarget(
401+
name: "SwiftRuntimeFunctionsTests",
402+
dependencies: [
403+
"SwiftRuntimeFunctions",
404+
],
405+
swiftSettings: [
406+
.swiftLanguageMode(.v5)
407+
]
408+
),
399409
]
400410
)

Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public class MySwiftClass {
4646
12
4747
}
4848

49+
public func describe() -> String {
50+
"MySwiftClass(len: \(len), cap: \(cap))"
51+
}
52+
4953
public func makeRandomIntMethod() -> Int {
5054
Int.random(in: 1..<256)
5155
}

Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int {
114114
}
115115
}
116116

117+
// ==== -----------------------------------------------------------------------
118+
// MARK: String returns
119+
120+
public func globalMakeString() -> String {
121+
"Hello from Swift!"
122+
}
123+
124+
public func globalStringIdentity(string: String) -> String {
125+
string
126+
}
127+
117128
// ==== -----------------------------------------------------------------------
118129
// MARK: Overloaded functions
119130

Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ void test_MySwiftClass_makeIntMethod() {
5858
}
5959
}
6060

61+
@Test
62+
void test_MySwiftClass_describe() {
63+
try(var arena = AllocatingSwiftArena.ofConfined()) {
64+
MySwiftClass o = MySwiftClass.init(12, 42, arena);
65+
var got = o.describe();
66+
assertEquals("MySwiftClass(len: 12, cap: 42)", got);
67+
}
68+
}
69+
6170
@Test
6271
@Disabled // TODO: Need var mangled names in interfaces
6372
void test_MySwiftClass_property_len() {

Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ void call_writeString_jni() {
5353
assertEquals(string.length(), reply);
5454
}
5555

56+
@Test
57+
void call_globalMakeString() {
58+
String result = MySwiftLibrary.globalMakeString();
59+
assertEquals("Hello from Swift!", result);
60+
}
61+
62+
@Test
63+
void call_globalStringIdentity() {
64+
String input = "round-trip test!";
65+
String result = MySwiftLibrary.globalStringIdentity(input);
66+
assertEquals(input, result);
67+
}
68+
69+
@Test
70+
void call_globalStringIdentity_empty() {
71+
String result = MySwiftLibrary.globalStringIdentity("");
72+
assertEquals("", result);
73+
}
74+
75+
76+
5677
@Test
5778
@Disabled("Upcalls not yet implemented in new scheme")
5879
@SuppressWarnings({"Convert2Lambda", "Convert2MethodRef"})

Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,19 @@ struct CdeclLowering {
736736
case .foundationData, .essentialsData:
737737
break
738738

739-
case .string, .optional:
739+
case .string:
740+
// String returned as heap-allocated C string (caller frees).
741+
return LoweredResult(
742+
cdeclResultType: knownTypes.unsafeMutablePointer(knownTypes.int8),
743+
cdeclOutParameters: [],
744+
conversion: .method(
745+
base: "_swiftjava_stringToCString",
746+
methodName: nil,
747+
arguments: [.init(label: nil, argument: .placeholder)]
748+
)
749+
)
750+
751+
case .optional:
740752
// Not supported at this point.
741753
throw LoweringError.unhandledType(type)
742754

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,8 +768,12 @@ extension FFMSwift2JavaGenerator {
768768
// FIXME: Implement
769769
throw JavaTranslationError.unhandledType(swiftType)
770770
case .string:
771-
// FIXME: Implement
772-
throw JavaTranslationError.unhandledType(swiftType)
771+
return TranslatedResult(
772+
javaResultType: .javaLangString,
773+
annotations: resultAnnotations,
774+
outParameters: [],
775+
conversion: .call(.placeholder, function: "SwiftRuntime.fromCString", withArena: false)
776+
)
773777
default:
774778
throw JavaTranslationError.unhandledType(swiftType)
775779
}

Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ public func _swiftjava_swift_retainCount(object: UnsafeMutableRawPointer) -> Int
3232
@_silgen_name("swift_isUniquelyReferenced")
3333
public func _swiftjava_swift_isUniquelyReferenced(object: UnsafeMutableRawPointer) -> Bool
3434

35+
/// Copies a Swift String to a heap-allocated NULL-terminated C string.
36+
/// The caller (Java FFM) must call free() on the returned pointer.
37+
public func _swiftjava_stringToCString(_ string: String) -> UnsafeMutablePointer<CChar> {
38+
var string = string
39+
return string.withUTF8 { utf8 in
40+
let buffer = UnsafeMutablePointer<CChar>.allocate(capacity: utf8.count + 1)
41+
UnsafeMutableRawPointer(buffer).copyMemory(from: utf8.baseAddress!, byteCount: utf8.count)
42+
buffer[utf8.count] = 0
43+
return buffer
44+
}
45+
}
46+
3547
@_alwaysEmitIntoClient @_transparent
3648
func _swiftjava_withHeapObject<R>(
3749
of object: AnyObject,

SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,16 @@ public static MemorySegment toCString(String str, Arena arena) {
405405
return arena.allocateFrom(str);
406406
}
407407

408+
/**
409+
* Read a heap-allocated C string into a Java String, then free the native memory.
410+
*/
411+
public static String fromCString(MemorySegment cStr) {
412+
if (cStr.equals(MemorySegment.NULL)) return null;
413+
String result = cStr.reinterpret(Long.MAX_VALUE).getString(0);
414+
cFree(cStr);
415+
return result;
416+
}
417+
408418
public static MemorySegment toOptionalSegmentInt(OptionalInt opt, Arena arena) {
409419
return opt.isPresent() ? arena.allocateFrom(ValueLayout.JAVA_INT, opt.getAsInt()) : MemorySegment.NULL;
410420
}

Tests/JExtractSwiftTests/FunctionLoweringTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,4 +535,20 @@ final class FunctionLoweringTests {
535535
expectedCFunction: "void c_value(const void *newValue, const void *self)"
536536
)
537537
}
538+
539+
@Test("Lowering String return")
540+
func lowerStringReturn() throws {
541+
try assertLoweredFunction(
542+
"""
543+
func bar() -> String { }
544+
""",
545+
expectedCDecl: """
546+
@_cdecl("c_bar")
547+
public func c_bar() -> UnsafeMutablePointer<Int8> {
548+
return _swiftjava_stringToCString(bar())
549+
}
550+
""",
551+
expectedCFunction: "int8_t *c_bar(void)"
552+
)
553+
}
538554
}

0 commit comments

Comments
 (0)