diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Operations.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Operations.swift new file mode 100644 index 000000000..e517eb651 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Operations.swift @@ -0,0 +1,21 @@ +public struct MyVector2 { + public var x: Int + public var y: Int + + public init(x: Int, y: Int) { + self.x = x + self.y = y + } + + public func getX() -> Int { + self.x + } + + public func getY() -> Int { + self.y + } + + public static func + (lhs: MyVector2, rhs: MyVector2) -> MyVector2 { + MyVector2(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OperationsTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OperationsTest.java new file mode 100644 index 000000000..36f28f35b --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/OperationsTest.java @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class OperationsTest { + + @Test + void operatorAdd() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + MyVector2 a = MyVector2.init(1, 2, arena); + MyVector2 b = MyVector2.init(3, 4, arena); + MyVector2 c = MyVector2.plus(a, b, arena); + assertEquals(c.getX(), 4); + assertEquals(c.getY(), 6); + } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Operators.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Operators.swift new file mode 100644 index 000000000..e517eb651 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Operators.swift @@ -0,0 +1,21 @@ +public struct MyVector2 { + public var x: Int + public var y: Int + + public init(x: Int, y: Int) { + self.x = x + self.y = y + } + + public func getX() -> Int { + self.x + } + + public func getY() -> Int { + self.y + } + + public static func + (lhs: MyVector2, rhs: MyVector2) -> MyVector2 { + MyVector2(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OperationsTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OperationsTest.java new file mode 100644 index 000000000..cd6104fa7 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OperationsTest.java @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class OperationsTest { + + @Test + void operatorAdd() { + try (var arena = SwiftArena.ofConfined()) { + MyVector2 a = MyVector2.init(1, 2, arena); + MyVector2 b = MyVector2.init(3, 4, arena); + MyVector2 c = MyVector2.plus(a, b, arena); + assertEquals(c.getX(), 4); + assertEquals(c.getY(), 6); + } + } +} diff --git a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift index 68965ce14..97a8c7584 100644 --- a/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift +++ b/Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift @@ -58,6 +58,7 @@ extension ExtractedFunc { switch apiKind { case .getter, .subscriptGetter: break case .setter, .subscriptSetter, .function, .initializer, .enumCase: return nil + case .operatorPlus: return nil } let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool @@ -82,6 +83,7 @@ extension ExtractedFunc { switch apiKind { case .setter, .subscriptSetter: break case .getter, .subscriptGetter, .function, .initializer, .enumCase: return nil + case .operatorPlus: return nil } let isBooleanSetter = self.functionSignature.parameters.first?.type.asNominalTypeDeclaration?.knownTypeKind == .bool diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index ad8f0a6f4..29a5af6a0 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -1039,6 +1039,9 @@ extension LoweredFunctionSignature { .joined(separator: .comma) resultExpr = "\(callee)(\(raw: arguments))" + case .operatorPlus: + resultExpr = "\(paramExprs[0]) + \(paramExprs[1])" + case .getter: assert(paramExprs.isEmpty) resultExpr = callee diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index db8c66320..ee76e873e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -704,6 +704,9 @@ extension JNISwift2JavaGenerator { .joined(separator: .comma) result = "\(tryClause)\(callee).\(decl.name)(\(downcallArguments))" + case .operatorPlus: + result = "\(arguments[0]) + \(arguments[1])" + case .enumCase: let downcallArguments = zip( decl.functionSignature.parameters, diff --git a/Sources/JExtractSwiftLib/JavaExtractDecider.swift b/Sources/JExtractSwiftLib/JavaExtractDecider.swift index 34e2e164a..de2c90b5f 100644 --- a/Sources/JExtractSwiftLib/JavaExtractDecider.swift +++ b/Sources/JExtractSwiftLib/JavaExtractDecider.swift @@ -74,7 +74,14 @@ public struct JavaExtractDecider: ExtractDecider { // Swift operators have no Java mapping if let fn = decl.as(FunctionDeclSyntax.self) { switch fn.name.tokenKind { - case .binaryOperator, .prefixOperator, .postfixOperator: + case .binaryOperator(let symbol): + // Allow only supported operators + if symbol == "+" { + break + } + log.trace("Skip '\(decl.qualifiedNameForDebug)': operators are not supported on Java") + return false + case .prefixOperator, .postfixOperator: log.trace("Skip '\(decl.qualifiedNameForDebug)': operators are not supported on Java") return false default: diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift index 823246305..c0d620806 100644 --- a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -38,6 +38,7 @@ package struct JavaIdentifierFactory { case .getter, .subscriptGetter: method.javaGetterName! case .setter, .subscriptSetter: method.javaSetterName! case .function, .initializer, .enumCase: method.name + case .operatorPlus: "plus" } methodsByBaseName[baseName, default: []].append(method) } @@ -69,6 +70,7 @@ package struct JavaIdentifierFactory { case .getter, .subscriptGetter: decl.javaGetterName! case .setter, .subscriptSetter: decl.javaSetterName! case .function, .initializer, .enumCase: decl.name + case .operatorPlus: "plus" } var methodName = baseName + paramsSuffix(decl, baseName: baseName) if Self.javaKeywords.contains(methodName) { diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 580bddb47..37c3d4129 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -25,6 +25,7 @@ public enum SwiftAPIKind: Equatable { case enumCase case subscriptGetter case subscriptSetter + case operatorPlus } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been @@ -343,6 +344,7 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { case .function, .initializer: "" case .subscriptGetter: "subscriptGetter:" case .subscriptSetter: "subscriptSetter:" + case .operatorPlus: "plus:" } let context = diff --git a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift index 76de54d70..b6a66a2dd 100644 --- a/Sources/SwiftExtract/SwiftAnalysisVisitor.swift +++ b/Sources/SwiftExtract/SwiftAnalysisVisitor.swift @@ -201,11 +201,27 @@ final class SwiftAnalysisVisitor { return } + let abiKind = + switch node.name.text.unescapedSwiftName { + case "+": + SwiftAPIKind.operatorPlus + default: + SwiftAPIKind.function + } + + let name = + switch node.name.text.unescapedSwiftName { + case "+": + "plus" + default: + node.name.text.unescapedSwiftName + } + let extracted = ExtractedFunc( module: analyzer.swiftModuleName, swiftDecl: node, - name: node.name.text.unescapedSwiftName, - apiKind: .function, + name: name, + apiKind: abiKind, functionSignature: signature, ) diff --git a/Tests/JExtractSwiftTests/OperationTests.swift b/Tests/JExtractSwiftTests/OperationTests.swift new file mode 100644 index 000000000..085af17bb --- /dev/null +++ b/Tests/JExtractSwiftTests/OperationTests.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 Testing + +@Suite +final class OperationsOverloadingTests { + let input = + """ + public struct MyVector2 { + var x: Int + var y: Int + + public static func + (lhs: MyVector2, rhs: MyVector2) -> MyVector2 { + MyVector2(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + } + """ + + @Test + func operatorPlus_ffm_swiftThunks() throws { + try assertOutput( + input: input, + .ffm, + .swift, + swiftModuleName: "FakeModule", + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("swiftjava_FakeModule_MyVector2_plus_lhs_rhs") + public func swiftjava_FakeModule_MyVector2_plus_lhs_rhs(_ lhs: UnsafeRawPointer, _ rhs: UnsafeRawPointer, _ _result: UnsafeMutableRawPointer) { + _result.assumingMemoryBound(to: MyVector2.self).initialize(to: lhs.assumingMemoryBound(to: MyVector2.self).pointee + rhs.assumingMemoryBound(to: MyVector2.self).pointee) + } + """ + ] + ) + } + + @Test + func operatorPlus_ffm_javaBindings() throws { + try assertOutput( + input: input, + .ffm, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public static func + (lhs: MyVector2, rhs: MyVector2) -> MyVector2 + * } + */ + public static MyVector2 plus(MyVector2 lhs, MyVector2 rhs, AllocatingSwiftArena swiftArena) { + MemorySegment result$ = swiftArena.allocate(MyVector2.$LAYOUT); + swiftjava_SwiftModule_MyVector2_plus_lhs_rhs.call(lhs.$memorySegment(), rhs.$memorySegment(), result$); + return MyVector2.wrapMemoryAddressUnsafe(result$, swiftArena); + } + """ + ] + ) + } + + @Test + func operatorPlus_jni_swiftThunks() throws { + try assertOutput( + input: input, + .jni, + .swift, + swiftModuleName: "FakeModule", + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyVector2__00024plus__JJ") + public func Java_com_example_swift_MyVector2__00024plus__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, lhs: jlong, rhs: jlong) -> jlong + """, + """ + result$.initialize(to: lhs$.pointee + rhs$.pointee) + let resultBits$ = Int64(Int(bitPattern: result$)) + """, + ] + ) + } + + @Test + func operatorPlus_jni_javaBindings() throws { + try assertOutput( + input: input, + .jni, + .java, + swiftModuleName: "FakeModule", + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public static func + (lhs: MyVector2, rhs: MyVector2) -> MyVector2 + * } + */ + public static MyVector2 plus(MyVector2 lhs, MyVector2 rhs, SwiftArena swiftArena) { + return MyVector2.wrapMemoryAddressUnsafe(MyVector2.$plus(lhs.$memoryAddress(), rhs.$memoryAddress()), swiftArena); + } + """ + ] + ) + } +}