diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 363e0683..5b5c2d32 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -16,10 +16,14 @@ public struct MySwiftStruct { private var cap: Int private var len: Int + private var subscriptValue: Int + private var subscriptArray: [Int] public init(cap: Int, len: Int) { self.cap = cap self.len = len + self.subscriptValue = 0 + self.subscriptArray = [10, 20, 15, 75] } public func voidMethod() { @@ -61,4 +65,22 @@ public struct MySwiftStruct { public func makeRandomIntMethod() -> Int { return Int.random(in: 1..<256) } + + public func getSubscriptValue() -> Int { + return self.subscriptValue + } + + public func getSubscriptArrayValue(index: Int) -> Int { + return self.subscriptArray[index] + } + + public subscript() -> Int { + get { return subscriptValue } + set { subscriptValue = newValue } + } + + public subscript(index: Int) -> Int { + get { return subscriptArray[index] } + set { subscriptArray[index] = newValue } + } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java index 6b994137..d904f7e8 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java @@ -33,4 +33,26 @@ void create_struct() { assertEquals(len, struct.getLength()); } } + + @Test + void testSubscript() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(); + s.setSubscript(66); + assertEquals(0, currentValue); + assertEquals(66, s.getSubscriptValue()); + } + } + + @Test + void testSubscriptWithParams() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(1); + s.setSubscript(1, 66); + assertEquals(20, currentValue); + assertEquals(66, s.getSubscriptArrayValue(1)); + } + } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index ddd77132..34686f41 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -15,10 +15,14 @@ public struct MySwiftStruct { private var cap: Int64 public var len: Int64 + private var subscriptValue: Int64 + private var subscriptArray: [Int64] public init(cap: Int64, len: Int64) { self.cap = cap self.len = len + self.subscriptValue = 0 + self.subscriptArray = [10, 20, 15, 75] } public init?(doInit: Bool) { @@ -38,4 +42,22 @@ public struct MySwiftStruct { self.cap += value return self.cap } + + public func getSubscriptValue() -> Int64 { + return self.subscriptValue + } + + public func getSubscriptArrayValue(index: Int64) -> Int64 { + return self.subscriptArray[Int(index)] + } + + public subscript() -> Int64 { + get { return subscriptValue } + set { subscriptValue = newValue } + } + + public subscript(index: Int64) -> Int64 { + get { return subscriptArray[Int(index)] } + set { subscriptArray[Int(index)] = newValue } + } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java index e52e1959..24b1fdbf 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java @@ -61,4 +61,26 @@ void increaseCap() { assertEquals(1347, s.getCapacity()); } } + + @Test + void testSubscript() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(); + s.setSubscript(66); + assertEquals(0, currentValue); + assertEquals(66, s.getSubscriptValue()); + } + } + + @Test + void testSubscriptWithParams() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(1); + s.setSubscript(1, 66); + assertEquals(20, currentValue); + assertEquals(66, s.getSubscriptArrayValue(1)); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 832aff26..a7da370b 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -812,11 +812,10 @@ extension LoweredFunctionSignature { // Build callee expression. let callee: ExprSyntax = if let selfExpr { - if case .initializer = apiKind { + switch apiKind { // Don't bother to create explicit ${Self}.init expression. - selfExpr - } else { - ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) + case .initializer, .subscriptGetter, .subscriptSetter: selfExpr + default: ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) } } else { ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(swiftAPIName))) @@ -845,6 +844,18 @@ extension LoweredFunctionSignature { case .enumCase: // This should not be called, but let's fatalError. fatalError("Enum cases are not supported with FFM.") + + case .subscriptGetter: + let parameters = paramExprs.map { $0.description }.joined(separator: ", ") + resultExpr = "\(callee)[\(raw: parameters)]" + case .subscriptSetter: + assert(paramExprs.count >= 1) + + var argumentsWithoutNewValue = paramExprs + let newValueArgument = argumentsWithoutNewValue.removeLast() + + let parameters = argumentsWithoutNewValue.map { $0.description }.joined(separator: ", ") + resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)" } // Lower the result. diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 72da323c..76284b78 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -144,8 +144,8 @@ extension FFMSwift2JavaGenerator { // Name. let javaName = switch decl.apiKind { - case .getter: decl.javaGetterName - case .setter: decl.javaSetterName + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName case .function, .initializer, .enumCase: decl.name } diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index 9ba1cd6f..b9cc2d49 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -23,6 +23,8 @@ package enum SwiftAPIKind { case getter case setter case enumCase + case subscriptGetter + case subscriptSetter } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been @@ -179,6 +181,8 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { case .setter: "setter:" case .enumCase: "case:" case .function, .initializer: "" + case .subscriptGetter: "subscriptGetter:" + case .subscriptSetter: "subscriptSetter:" } let context = if let parentType { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 9c3cdbf1..c8e4bbf3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -176,8 +176,8 @@ extension JNISwift2JavaGenerator { // Name. let javaName = switch decl.apiKind { - case .getter: decl.javaGetterName - case .setter: decl.javaSetterName + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName case .function, .initializer, .enumCase: decl.name } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 91a0b0e7..d3b47732 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -348,6 +348,19 @@ extension JNISwift2JavaGenerator { } result = "\(callee).\(decl.name) = \(newValueArgument)" + case .subscriptGetter: + let parameters = arguments.joined(separator: ", ") + result = "\(callee)[\(parameters)]" + case .subscriptSetter: + guard let newValueArgument = arguments.last else { + fatalError("Setter did not contain newValue parameter: \(decl)") + } + + var argumentsWithoutNewValue = arguments + argumentsWithoutNewValue.removeLast() + + let parameters = argumentsWithoutNewValue.joined(separator: ", ") + result = "\(callee)[\(parameters)] = \(newValueArgument)" } // Lower the result. diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 247b2662..ab1ce32f 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -13,9 +13,9 @@ //===----------------------------------------------------------------------===// import Foundation +import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -import SwiftJavaConfigurationShared final class Swift2JavaVisitor { let translator: Swift2JavaTranslator @@ -53,9 +53,9 @@ final class Swift2JavaVisitor { case .extensionDecl(let node): self.visit(extensionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .typeAliasDecl: - break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 + break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 case .associatedTypeDecl: - break // TODO: Implement associated types + break // TODO: Implement associated types case .initializerDecl(let node): self.visit(initializerDecl: node, in: parent) @@ -63,9 +63,8 @@ final class Swift2JavaVisitor { self.visit(functionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .variableDecl(let node): self.visit(variableDecl: node, in: parent, sourceFilePath: sourceFilePath) - case .subscriptDecl: - // TODO: Implement subscripts - break + case .subscriptDecl(let node): + self.visit(subscriptDecl: node, in: parent) case .enumCaseDecl(let node): self.visit(enumCaseDecl: node, in: parent) @@ -75,7 +74,8 @@ final class Swift2JavaVisitor { } func visit( - nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, + nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax + & WithAttributesSyntax & WithModifiersSyntax, in parent: ImportedNominalType?, sourceFilePath: String ) { @@ -115,7 +115,7 @@ final class Swift2JavaVisitor { } func visit( - functionDecl node: FunctionDeclSyntax, + functionDecl node: FunctionDeclSyntax, in typeContext: ImportedNominalType?, sourceFilePath: String ) { @@ -154,7 +154,7 @@ final class Swift2JavaVisitor { } func visit( - enumCaseDecl node: EnumCaseDeclSyntax, + enumCaseDecl node: EnumCaseDeclSyntax, in typeContext: ImportedNominalType? ) { guard let typeContext else { @@ -200,7 +200,7 @@ final class Swift2JavaVisitor { } func visit( - variableDecl node: VariableDeclSyntax, + variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?, sourceFilePath: String ) { @@ -216,37 +216,21 @@ final class Swift2JavaVisitor { self.log.debug("Import variable: \(node.kind) '\(node.qualifiedNameForDebug)'") - func importAccessor(kind: SwiftAPIKind) throws { - let signature = try SwiftFunctionSignature( - node, - isSet: kind == .setter, - enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext - ) - - let imported = ImportedFunc( - module: translator.swiftModuleName, - swiftDecl: node, - name: varName, - apiKind: kind, - functionSignature: signature - ) - - log.debug("Record imported variable accessor \(kind == .getter ? "getter" : "setter"):\(node.qualifiedNameForDebug)") - if let typeContext { - typeContext.variables.append(imported) - } else { - translator.importedGlobalVariables.append(imported) - } - } - do { let supportedAccessors = node.supportedAccessorKinds(binding: binding) if supportedAccessors.contains(.get) { - try importAccessor(kind: .getter) + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .getter, + name: varName) } if supportedAccessors.contains(.set) { - try importAccessor(kind: .setter) + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .setter, + name: varName) } } catch { self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") @@ -289,10 +273,89 @@ final class Swift2JavaVisitor { typeContext.initializers.append(imported) } + private func visit( + subscriptDecl node: SubscriptDeclSyntax, + in typeContext: ImportedNominalType?, + ) { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { + return + } + + guard let accessorBlock = node.accessorBlock else { + return + } + + let name = "subscript" + let accessors = accessorBlock.supportedAccessorKinds() + + do { + if accessors.contains(.get) { + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .subscriptGetter, + name: name) + } + if accessors.contains(.set) { + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .subscriptSetter, + name: name) + } + } catch { + self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") + } + } + + private func importAccessor( + from node: DeclSyntax, + in typeContext: ImportedNominalType?, + kind: SwiftAPIKind, + name: String + ) throws { + let signature: SwiftFunctionSignature + + switch node.as(DeclSyntaxEnum.self) { + case .variableDecl(let varNode): + signature = try SwiftFunctionSignature( + varNode, + isSet: kind == .setter, + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext) + case .subscriptDecl(let subscriptNode): + signature = try SwiftFunctionSignature( + subscriptNode, + isSet: kind == .subscriptSetter, + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext) + default: + log.warning("Not supported declaration type \(node.kind) while calling importAccessor!") + return + } + + let imported = ImportedFunc( + module: translator.swiftModuleName, + swiftDecl: node, + name: name, + apiKind: kind, + functionSignature: signature + ) + + log.debug( + "Record imported variable accessor \(kind == .getter || kind == .subscriptGetter ? "getter" : "setter"):\(node.qualifiedNameForDebug)" + ) + if let typeContext { + typeContext.variables.append(imported) + } else { + translator.importedGlobalVariables.append(imported) + } + } + private func synthesizeRawRepresentableConformance( enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType? - ) { + ) { guard let imported = translator.importedNominalType(node, parent: parent) else { return } @@ -304,7 +367,9 @@ final class Swift2JavaVisitor { ), inheritanceType.isRawTypeCompatible { - if !imported.variables.contains(where: { $0.name == "rawValue" && $0.functionSignature.result.type != inheritanceType }) { + if !imported.variables.contains(where: { + $0.name == "rawValue" && $0.functionSignature.result.type != inheritanceType + }) { let decl: DeclSyntax = "public var rawValue: \(raw: inheritanceType.description) { get }" self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) } @@ -312,7 +377,11 @@ final class Swift2JavaVisitor { // 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" && $0.functionSignature.parameters.first?.type == inheritanceType }) { + if !imported.initializers.contains(where: { + $0.functionSignature.parameters.count == 1 + && $0.functionSignature.parameters.first?.parameterName == "rawValue" + && $0.functionSignature.parameters.first?.type == inheritanceType + }) { let decl: DeclSyntax = "public init?(rawValue: \(raw: inheritanceType))" self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) } @@ -330,7 +399,9 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn } guard meetsRequiredAccessLevel else { - log.debug("Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)") + log.debug( + "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)" + ) return false } guard !attributes.contains(where: { $0.isJava }) else { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift index 1f2fbd36..804769e5 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift @@ -271,31 +271,10 @@ extension SwiftFunctionSignature { lookupContext: SwiftTypeLookupContext ) throws { - // If this is a member of a type, so we will have a self parameter. Figure out the - // type and convention for the self parameter. - if let enclosingType { - var isStatic = false - for modifier in varNode.modifiers { - switch modifier.name.tokenKind { - case .keyword(.static): isStatic = true - case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) - default: break - } - } - - if isStatic { - self.selfParameter = .staticMethod(enclosingType) - } else { - self.selfParameter = .instance( - SwiftParameter( - convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue, - type: enclosingType - ) - ) - } - } else { - self.selfParameter = nil - } + self.selfParameter = try Self.variableSelfParameter( + for: DeclSyntax(varNode), + enclosingType: enclosingType, + isSet: isSet) guard let binding = varNode.bindings.first, varNode.bindings.count == 1 else { throw SwiftFunctionTranslationError.multipleBindings(varNode) @@ -323,7 +302,9 @@ extension SwiftFunctionSignature { self.effectSpecifiers = effectSpecifiers ?? [] if isSet { - self.parameters = [SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)] + self.parameters = [ + SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType) + ] self.result = .void } else { self.parameters = [] @@ -333,6 +314,50 @@ extension SwiftFunctionSignature { self.genericRequirements = [] } + init( + _ subscriptNode: SubscriptDeclSyntax, + isSet: Bool, + enclosingType: SwiftType?, + lookupContext: SwiftTypeLookupContext + ) throws { + self.selfParameter = try Self.variableSelfParameter( + for: DeclSyntax(subscriptNode), + enclosingType: enclosingType, + isSet: isSet) + + let valueType: SwiftType = try SwiftType(subscriptNode.returnClause.type, lookupContext: lookupContext) + var nodeParameters = try subscriptNode.parameterClause.parameters.map { param in + try SwiftParameter(param, lookupContext: lookupContext) + } + + var effectSpecifiers: [SwiftEffectSpecifier]? = nil + switch subscriptNode.accessorBlock?.accessors { + case .getter(let getter): + if let getter = getter.as(AccessorDeclSyntax.self) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + case .accessors(let accessors): + if let getter = accessors.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + default: + break + } + + self.effectSpecifiers = effectSpecifiers ?? [] + + if isSet { + nodeParameters.append(SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)) + self.result = .void + } else { + self.result = .init(convention: .direct, type: valueType) + } + + self.parameters = nodeParameters + self.genericParameters = [] + self.genericRequirements = [] + } + private static func effectSpecifiers(from decl: AccessorDeclSyntax) throws -> [SwiftEffectSpecifier] { var effectSpecifiers = [SwiftEffectSpecifier]() if decl.effectSpecifiers?.throwsClause != nil { @@ -343,27 +368,80 @@ extension SwiftFunctionSignature { } return effectSpecifiers } -} -extension VariableDeclSyntax { - struct SupportedAccessorKinds: OptionSet { - var rawValue: UInt8 + private static func variableSelfParameter( + for decl: DeclSyntax, + enclosingType: SwiftType?, + isSet: Bool + ) throws -> SwiftSelfParameter? { + let modifiers: DeclModifierListSyntax? = + switch decl.as(DeclSyntaxEnum.self) { + case .variableDecl(let varDecl): varDecl.modifiers + case .subscriptDecl(let subscriptDecl): subscriptDecl.modifiers + default: nil + } - static var get: Self = .init(rawValue: 1 << 0) - static var set: Self = .init(rawValue: 1 << 1) + guard let modifiers else { + return nil + } + + // If this is a member of a type, so we will have a self parameter. Figure out the + // type and convention for the self parameter. + if let enclosingType { + var isStatic = false + for modifier in modifiers { + switch modifier.name.tokenKind { + case .keyword(.static): isStatic = true + case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) + default: break + } + } + + if isStatic { + return .staticMethod(enclosingType) + } else { + return .instance( + SwiftParameter( + convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue, + type: enclosingType + ) + ) + } + } else { + return nil + } } +} +extension VariableDeclSyntax { /// Determine what operations (i.e. get and/or set) supported in this `VariableDeclSyntax` /// /// - Parameters: /// - binding the pattern binding in this declaration. - func supportedAccessorKinds(binding: PatternBindingSyntax) -> SupportedAccessorKinds { + func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { if self.bindingSpecifier.tokenKind == .keyword(.let) { return [.get] } if let accessorBlock = binding.accessorBlock { - switch accessorBlock.accessors { + return accessorBlock.supportedAccessorKinds() + } + + return [.get, .set] + } +} + +extension AccessorBlockSyntax { + struct SupportedAccessorKinds: OptionSet { + var rawValue: UInt8 + + static var get: Self = .init(rawValue: 1 << 0) + static var set: Self = .init(rawValue: 1 << 1) + } + + /// Determine what operations (i.e. get and/or set) supported in this `AccessorBlockSyntax` + func supportedAccessorKinds() -> SupportedAccessorKinds { + switch self.accessors { case .getter: return [.get] case .accessors(let accessors): @@ -379,9 +457,6 @@ extension VariableDeclSyntax { } return [.get] } - } - - return [.get, .set] } } diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index 3369ec62..da2e95c1 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -32,9 +32,9 @@ package struct ThunkNameRegistry { let suffix: String switch decl.apiKind { - case .getter: + case .getter, .subscriptGetter: suffix = "$get" - case .setter: + case .setter, .subscriptSetter: suffix = "$set" default: suffix = decl.functionSignature.parameters diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 7e8c66d3..849b7f01 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -83,7 +83,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Ownership modifiers: `inout`, `borrowing`, `consuming` | ❌ | ❌ | | Default parameter values: `func p(name: String = "")` | ❌ | ❌ | | Operators: `+`, `-`, user defined | ❌ | ❌ | -| Subscripts: `subscript()` | ❌ | ❌ | +| Subscripts: `subscript()` | ✅ | ✅ | | Equatable | ❌ | ❌ | | Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | | Nested types: `struct Hello { struct World {} }` | ❌ | ✅ | diff --git a/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift new file mode 100644 index 00000000..89029dd7 --- /dev/null +++ b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// 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 JExtractSwiftLib +import Testing + +@Suite +struct FFMSubscriptsTests { + private let noParamsSubscriptSource = """ + public struct MyStruct { + private var testVariable: Double = 0 + + public subscript() -> Double { + get { return testVariable } + set { testVariable = newValue } + } + } + """ + + private let subscriptWithParamsSource = """ + public struct MyStruct { + private var testVariable: [Int32] = [] + + public subscript(index: Int32) -> Int32 { + get { return testVariable[Int(index)] } + set { testVariable[Int(index)] = newValue } + } + } + """ + + @Test("Test generation of JavaClass for subscript with no parameters") + func generatesJavaClassForNoParams() throws { + try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$get { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_DOUBLE, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + """, + """ + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); + """, + """ + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static double call(java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); + } + return (double) HANDLE.invokeExact(self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public double getSubscript() { + $ensureAlive(); + return swiftjava_SwiftModule_MyStruct_subscript$get.call(this.$memorySegment()); + """, + ]) + try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$set { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* newValue: */SwiftValueLayout.SWIFT_DOUBLE, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(double newValue, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(newValue, self); + } + HANDLE.invokeExact(newValue, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public void setSubscript(double newValue) { + $ensureAlive(); + swiftjava_SwiftModule_MyStruct_subscript$set.call(newValue, this.$memorySegment()); + """ + ]) + } + + @Test("Test generation of JavaClass for subscript with parameters") + func generatesJavaClassForParameters() throws { + try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$get { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_INT32, + /* index: */SwiftValueLayout.SWIFT_INT32, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static int call(int index, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(index, self); + } + return (int) HANDLE.invokeExact(index, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public int getSubscript(int index) { + $ensureAlive(); + return swiftjava_SwiftModule_MyStruct_subscript$get.call(index, this.$memorySegment()); + """, + ]) + try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$set { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* index: */SwiftValueLayout.SWIFT_INT32, + /* newValue: */SwiftValueLayout.SWIFT_INT32, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(int index, int newValue, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(index, newValue, self); + } + HANDLE.invokeExact(index, newValue, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public void setSubscript(int index, int newValue) { + $ensureAlive(); + swiftjava_SwiftModule_MyStruct_subscript$set.call(index, newValue, this.$memorySegment()); + """, + ]) + } + + @Test("Test generation of Swift thunks for subscript without parameters") + func subscriptWithoutParamsMethodSwiftThunk() throws { + try assertOutput( + input: noParamsSubscriptSource, + .ffm, + .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") + public func swiftjava_SwiftModule_MyStruct_subscript$get(_ self: UnsafeRawPointer) -> Double { + return self.assumingMemoryBound(to: MyStruct.self).pointee[] + } + """, + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") + public func swiftjava_SwiftModule_MyStruct_subscript$set(_ newValue: Double, _ self: UnsafeMutableRawPointer) { + self.assumingMemoryBound(to: MyStruct.self).pointee[] = newValue + } + """ + ] + ) + } + + @Test("Test generation of Swift thunks for subscript with parameters") + func subscriptWithParamsMethodSwiftThunk() throws { + try assertOutput( + input: subscriptWithParamsSource, + .ffm, + .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") + public func swiftjava_SwiftModule_MyStruct_subscript$get(_ index: Int32, _ self: UnsafeRawPointer) -> Int32 { + return self.assumingMemoryBound(to: MyStruct.self).pointee[index] + } + """, + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") + public func swiftjava_SwiftModule_MyStruct_subscript$set(_ index: Int32, _ newValue: Int32, _ self: UnsafeMutableRawPointer) { + self.assumingMemoryBound(to: MyStruct.self).pointee[index] = newValue + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift new file mode 100644 index 00000000..0f7b131d --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift @@ -0,0 +1,155 @@ +//===----------------------------------------------------------------------===// +// +// 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 JNISubscriptsTests { + private let noParamsSubscriptSource = """ + public struct MyStruct { + private var testVariable: Double = 0 + + public subscript() -> Double { + get { return testVariable } + set { testVariable = newValue } + } + } + """ + + private let subscriptWithParamsSource = """ + public struct MyStruct { + private var testVariable: [Int32] = [] + + public subscript(index: Int32) -> Int32 { + get { return testVariable[Int(index)] } + set { testVariable[Int(index)] = newValue } + } + } + """ + + @Test("Test generation of JavaClass for subscript with no parameters") + func generatesJavaClassForNoParams() throws { + try assertOutput(input: noParamsSubscriptSource, .jni, .java, expectedChunks: [ + """ + public double getSubscript() { + return MyStruct.$getSubscript(this.$memoryAddress()); + """, + """ + private static native double $getSubscript(long self); + """, + """ + public void setSubscript(double newValue) { + MyStruct.$setSubscript(newValue, this.$memoryAddress()); + """, + """ + private static native void $setSubscript(double newValue, long self); + """ + ]) + try assertOutput( + input: noParamsSubscriptSource, .jni, .java, + expectedChunks: [ + """ + private static native void $destroy(long selfPointer); + """ + ]) + } + + @Test("Test generation of JavaClass for subscript with parameters") + func generatesJavaClassForParameters() throws { + try assertOutput(input: subscriptWithParamsSource, .jni, .java, expectedChunks: [ + """ + public int getSubscript(int index) { + return MyStruct.$getSubscript(index, this.$memoryAddress()); + + """, + """ + private static native int $getSubscript(int index, long self); + """, + """ + public void setSubscript(int index, int newValue) { + MyStruct.$setSubscript(index, newValue, this.$memoryAddress()); + """, + """ + private static native void $setSubscript(int index, int newValue, long self); + """ + ]) + } + + @Test("Test generation of Swift thunks for subscript without parameters") + func subscriptWithoutParamsMethodSwiftThunk() throws { + try assertOutput( + input: noParamsSubscriptSource, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__J") + func Java_com_example_swift_MyStruct__00024getSubscript__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jdouble { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + return self$.pointee[].getJNIValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__DJ") + func Java_com_example_swift_MyStruct__00024setSubscript__DJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jdouble, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee[] = Double(fromJNI: newValue, in: environment) + """ + ] + ) + } + + @Test("Test generation of Swift thunks for subscript with parameters") + func subscriptWithParamsMethodSwiftThunk() throws { + try assertOutput( + input: subscriptWithParamsSource, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__IJ") + func Java_com_example_swift_MyStruct__00024getSubscript__IJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, self: jlong) -> jint { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + return self$.pointee[Int32(fromJNI: index, in: environment)].getJNIValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__IIJ") + func Java_com_example_swift_MyStruct__00024setSubscript__IIJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, newValue: jint, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee[Int32(fromJNI: index, in: environment)] = Int32(fromJNI: newValue, in: environment) + """ + ] + ) + } +}