Skip to content
Closed
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
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
2 changes: 2 additions & 0 deletions Sources/JExtractSwiftLib/ExtractedDecls+JavaNaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion Sources/JExtractSwiftLib/JavaExtractDecider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions Sources/JExtractSwiftLib/JavaIdentifierFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -343,6 +344,7 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible {
case .function, .initializer: ""
case .subscriptGetter: "subscriptGetter:"
case .subscriptSetter: "subscriptSetter:"
case .operatorPlus: "plus:"
}

let context =
Expand Down
20 changes: 18 additions & 2 deletions Sources/SwiftExtract/SwiftAnalysisVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
117 changes: 117 additions & 0 deletions Tests/JExtractSwiftTests/OperationTests.swift
Original file line number Diff line number Diff line change
@@ -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<JNIEnv?>!, 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);
}
"""
]
)
}
}