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
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,15 @@ public class MySwiftClass {
return self.x + other.longValue()
}
}

extension MySwiftClass: CustomStringConvertible {
public var description: String {
"MySwiftClass(x: \(x), y: \(y))"
}
}

extension MySwiftClass: CustomDebugStringConvertible {
public var debugDescription: String {
"debug: MySwiftClass(x: \(x), y: \(y))"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,20 @@ void getAsyncVariable() throws Exception {
assertEquals(42, c1.getGetAsync().get());
}
}

@Test
void toStringTest() {
try (var arena = SwiftArena.ofConfined()) {
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
assertEquals("MySwiftClass(x: 20, y: 10)", c1.toString());
}
}

@Test
void toDebugStringTest() {
try (var arena = SwiftArena.ofConfined()) {
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
assertEquals("debug: MySwiftClass(x: 20, y: 10)", c1.toDebugString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,38 @@ extension JNISwift2JavaGenerator {
printer.println()
}

printToStringMethods(&printer, decl)
printer.println()

printTypeMetadataAddressFunction(&printer, decl)
printer.println()
printDestroyFunction(&printer, decl)
}
}


private func printToStringMethods(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
printer.printBraceBlock("public String toString()") { printer in
printer.print(
"""
return $toString(this.$memoryAddress());
"""
)
}
printer.print("private static native java.lang.String $toString(long selfPointer);")

printer.println()

printer.printBraceBlock("public String toDebugString()") { printer in
printer.print(
"""
return $toDebugString(this.$memoryAddress());
"""
)
}
printer.print("private static native java.lang.String $toDebugString(long selfPointer);")
}

private func printHeader(_ printer: inout CodePrinter) {
printer.print(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ extension JNISwift2JavaGenerator {
printer.println()
}


printToStringMethods(&printer, type)
printTypeMetadataAddressThunk(&printer, type)
printer.println()
printDestroyFunctionThunk(&printer, type)
Expand All @@ -267,6 +267,48 @@ extension JNISwift2JavaGenerator {
try printSwiftInterfaceWrapper(&printer, protocolWrapper)
}

private func printToStringMethods(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
let selfPointerParam = JavaParameter(name: "selfPointer", type: .long)
let parentName = type.qualifiedName

printCDecl(
&printer,
javaMethodName: "$toString",
parentName: type.swiftNominal.qualifiedName,
parameters: [
selfPointerParam
],
resultType: .javaLangString
) { printer in
let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam)

printer.print(
"""
return String(describing: \(selfVar).pointee).getJNIValue(in: environment)
"""
)
}

printer.println()

printCDecl(
&printer,
javaMethodName: "$toDebugString",
parentName: type.swiftNominal.qualifiedName,
parameters: [
selfPointerParam
],
resultType: .javaLangString
) { printer in
let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam)

printer.print(
"""
return String(reflecting: \(selfVar).pointee).getJNIValue(in: environment)
"""
)
}
}

private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
let selfPointerParam = JavaParameter(name: "selfPointer", type: .long)
Expand Down
9 changes: 6 additions & 3 deletions Sources/JExtractSwiftLib/Swift2JavaVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ final class Swift2JavaVisitor {
guard let importedNominalType = translator.importedNominalType(node.extendedType) else {
return
}

// Add any conforming protocols in the extension
importedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap {
try? SwiftType($0.type, lookupContext: translator.lookupContext)
} ?? []

for memberItem in node.memberBlock.members {
self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath)
}
Expand Down Expand Up @@ -374,9 +380,6 @@ final class Swift2JavaVisitor {
self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath)
}

// FIXME: why is this un-used
imported.variables.first?.signatureString

if !imported.initializers.contains(where: {
$0.functionSignature.parameters.count == 1
&& $0.functionSignature.parameters.first?.parameterName == "rawValue"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ class SwiftTypeLookupContext {
typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath)
case .protocolDecl(let node):
typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath)
case .extensionDecl(let node):
// For extensions, we have to perform a unqualified lookup,
// as the extentedType is just the identifier of the type.

guard case .identifierType(let id) = Syntax(node.extendedType).as(SyntaxEnum.self),
let lookupResult = try unqualifiedLookup(name: Identifier(id.name)!, from: node)
else {
throw TypeLookupError.notType(Syntax(node))
}

typeDecl = lookupResult
case .typeAliasDecl:
fatalError("typealias not implemented")
case .associatedTypeDecl:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S
| Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ |
| Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ |
| `@escaping` closures: `func callMe(_: @escaping () -> ())` | ❌ | ❌ |
| Swift type extensions: `extension String { func uppercased() }` | 🟡 | 🟡 |
| Swift type extensions: `extension String { func uppercased() }` | | |
| Swift macros (maybe) | ❌ | ❌ |
| Result builders | ❌ | ❌ |
| Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ |
Expand Down
65 changes: 65 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//===----------------------------------------------------------------------===//
//
// 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 JExtractSwiftLib
import Testing

@Suite
struct JNIExtensionTests {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an FFM test, I think this would either just work or require very minimal work. The README also has flipped both, so let's make the impl match that 😉

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thank you.

I guess we really have to start doing "in the same file" tests that do both modes (like in #475), it is getting hard to track what is covered 🤔

let interfaceFile =
"""
extension MyStruct {
public var variableInExtension: String { get }
public func methodInExtension() {}
}

public protocol MyProtocol {}
public struct MyStruct {}
extension MyStruct: MyProtocol {}
"""

@Test("Import extensions: Java methods")
func import_javaMethods() throws {
try assertOutput(
input: interfaceFile,
.jni, .java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public final class MyStruct implements JNISwiftInstance, MyProtocol {
...
public void methodInExtension() {
...
}
"""
])
}

@Test("Import extensions: Computed variables")
func import_computedVariables() throws {
try assertOutput(
input: interfaceFile,
.jni, .java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public final class MyStruct implements JNISwiftInstance, MyProtocol {
...
public java.lang.String getVariableInExtension() {
...
}
"""
])
}
}
96 changes: 96 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//===----------------------------------------------------------------------===//
//
// 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 JExtractSwiftLib
import Testing
import SwiftJavaConfigurationShared

@Suite
struct JNIToStringTests {
let source =
"""
public struct MyType {}
"""

@Test("JNI toString (Java)")
func toString_java() throws {
try assertOutput(
input: source,
.jni, .java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public String toString() {
return $toString(this.$memoryAddress());
}
""",
"""
private static native java.lang.String $toString(long selfPointer);
"""
]
)
}

@Test("JNI toString (Swift)")
func toString_swift() throws {
try assertOutput(
input: source,
.jni, .swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("Java_com_example_swift_MyType__00024toString__J")
func Java_com_example_swift_MyType__00024toString__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) -> jstring? {
...
return String(describing: self$.pointee).getJNIValue(in: environment)
}
""",
]
)
}

@Test("JNI toDebugString (Java)")
func toDebugString_java() throws {
try assertOutput(
input: source,
.jni, .java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public String toDebugString() {
return $toDebugString(this.$memoryAddress());
}
""",
]
)
}

@Test("JNI toDebugString (Swift)")
func toDebugString_swift() throws {
try assertOutput(
input: source,
.jni, .swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("Java_com_example_swift_MyType__00024toDebugString__J")
func Java_com_example_swift_MyType__00024toDebugString__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) -> jstring? {
...
return String(reflecting: self$.pointee).getJNIValue(in: environment)
}
""",
]
)
}
}
Loading