Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ let package = Package(
dependencies: [
"SwiftJava",
"SwiftJavaRuntimeSupport",
"SwiftRuntimeFunctions",
"SwiftRuntimeFunctions"
]
),

Expand Down Expand Up @@ -396,5 +396,15 @@ let package = Package(
.swiftLanguageMode(.v5)
]
),

.testTarget(
name: "SwiftRuntimeFunctionsTests",
dependencies: [
"SwiftRuntimeFunctions",
],
swiftSettings: [
.swiftLanguageMode(.v5)
]
),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<CChar> {
var string = string
return string.withUTF8 { utf8 in
let buffer = UnsafeMutablePointer<CChar>.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<R>(
of object: AnyObject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
16 changes: 16 additions & 0 deletions Tests/JExtractSwiftTests/FunctionLoweringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int8> {
return _swiftjava_stringToCString(bar())
}
""",
expectedCFunction: "int8_t *c_bar(void)"
)
}
}
51 changes: 51 additions & 0 deletions Tests/SwiftRuntimeFunctionsTests/StringToCStringTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading