diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index 9bd9a7d8..fa380b12 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -39,6 +39,8 @@ enum JNIJavaTypeTranslator { case .int64: return .long case .uint64: return .long + + case .int, .uint: return .long case .float: return .float case .double: return .double @@ -46,8 +48,7 @@ enum JNIJavaTypeTranslator { case .string: return .javaLangString - case .int, .uint, // FIXME: why not supported int/uint? - .unsafeRawPointer, .unsafeMutableRawPointer, + case .unsafeRawPointer, .unsafeMutableRawPointer, .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, @@ -55,4 +56,38 @@ enum JNIJavaTypeTranslator { return nil } } + + static func indirectConversionSetepSwiftType(for knownKind: SwiftKnownTypeDeclKind, from knownTypes: SwiftKnownTypes) -> SwiftType? { + switch knownKind { + case .int: knownTypes.int64 + case .uint: knownTypes.uint64 + + case .bool, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, + .float, .double, .void, .string, + .unsafeRawPointer, .unsafeMutableRawPointer, + .unsafePointer, .unsafeMutablePointer, + .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, + .unsafeBufferPointer, .unsafeMutableBufferPointer, + .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, + .array: + nil + } + } + + static func checkStep(for knownKind: SwiftKnownTypeDeclKind, from knownTypes: SwiftKnownTypes) -> JNISwift2JavaGenerator.NativeSwiftConversionCheck? { + switch knownKind { + case .int: .check32BitIntOverflow(typeWithMinAndMax: knownTypes.int32) + case .uint: .check32BitIntOverflow(typeWithMinAndMax: knownTypes.uint32) + + case .bool, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, + .float, .double, .void, .string, + .unsafeRawPointer, .unsafeMutableRawPointer, + .unsafePointer, .unsafeMutablePointer, + .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, + .unsafeBufferPointer, .unsafeMutableBufferPointer, + .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, + .array: + nil + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 07d23dc0..46d72213 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -445,7 +445,7 @@ extension JNISwift2JavaGenerator { let translatedSignature = translatedDecl.translatedFunctionSignature let resultType = translatedSignature.resultType.javaType var parameters = translatedDecl.translatedFunctionSignature.parameters.map { $0.parameter.renderParameter() } - let throwsClause = translatedDecl.isThrowing && !translatedDecl.isAsync ? " throws Exception" : "" + let throwsClause = translatedDecl.throwsClause() let generics = translatedDecl.translatedFunctionSignature.parameters.reduce(into: [(String, [JavaType])]()) { generics, parameter in guard case .generic(let name, let extends) = parameter.parameter.type else { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 9c3cdbf1..32f1c438 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -114,6 +114,12 @@ extension JNISwift2JavaGenerator { .class(package: nil,name: caseName) ) ]) + var exceptions: [JavaException] = [] + + if enumCase.parameters.contains(where: \.type.isArchDependingInteger) { + exceptions.append(.integerOverflow) + } + let getAsCaseFunction = TranslatedFunctionDecl( name: getAsCaseName, isStatic: false, @@ -137,12 +143,15 @@ extension JNISwift2JavaGenerator { javaType: .class(package: nil, name: "Optional<\(caseName)>"), outParameters: conversions.flatMap(\.translated.outParameters), conversion: enumCase.parameters.isEmpty ? constructRecordConversion : .aggregate(variable: ("$nativeParameters", nativeParametersType), [constructRecordConversion]) - ) + ), + exceptions: exceptions ), nativeFunctionSignature: NativeFunctionSignature( selfParameter: NativeParameter( parameters: [JavaParameter(name: "self", type: .long)], - conversion: .extractSwiftValue(.placeholder, swiftType: .nominal(enumCase.enumType), allowNil: false) + conversion: .extractSwiftValue(.placeholder, swiftType: .nominal(enumCase.enumType), allowNil: false), + indirectConversion: nil, + conversionCheck: nil ), parameters: [], result: NativeResult( @@ -291,12 +300,19 @@ extension JNISwift2JavaGenerator { genericRequirements: functionSignature.genericRequirements ) + var exceptions: [JavaException] = [] + + if functionSignature.parameters.contains(where: \.type.isArchDependingInteger) { + exceptions.append(.integerOverflow) + } + let resultType = try translate(swiftResult: functionSignature.result) return TranslatedFunctionSignature( selfParameter: selfParameter, parameters: parameters, - resultType: resultType + resultType: resultType, + exceptions: exceptions ) } @@ -955,12 +971,22 @@ extension JNISwift2JavaGenerator { var annotations: [JavaAnnotation] { self.translatedFunctionSignature.annotations } + + func throwsClause() -> String { + guard !translatedFunctionSignature.exceptions.isEmpty else { + return isThrowing && !isAsync ? " throws Exception" : "" + } + + let signatureExceptions = translatedFunctionSignature.exceptions.compactMap(\.type.className).joined(separator: ", ") + return " throws \(signatureExceptions)\(isThrowing ? ", Exception" : "")" + } } struct TranslatedFunctionSignature { var selfParameter: TranslatedParameter? var parameters: [TranslatedParameter] var resultType: TranslatedResult + var exceptions: [JavaException] // if the result type implied any annotations, // propagate them onto the function the result is returned from diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 1994dce0..494207f0 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -103,11 +103,16 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(type) } + let indirectStepType = JNIJavaTypeTranslator.indirectConversionSetepSwiftType(for: knownType, from: knownTypes) + let indirectCheck = JNIJavaTypeTranslator.checkStep(for: knownType, from: knownTypes) + return NativeParameter( parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .initFromJNI(.placeholder, swiftType: type) + conversion: indirectStepType != nil ? .labelessAssignmentOfVariable(.placeholder, swiftType: type) : .initFromJNI(.placeholder, swiftType: type), + indirectConversion: indirectStepType.flatMap { .initFromJNI(.placeholder, swiftType: $0) }, + conversionCheck: indirectCheck ) } @@ -129,7 +134,9 @@ extension JNISwift2JavaGenerator { fatalErrorMessage: "\(parameterName) was null in call to \\(#function), but Swift requires non-optional!" ), wrapperName: nominalTypeName - ) + ), + indirectConversion: nil, + conversionCheck: nil ) } @@ -138,7 +145,9 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: .long) ], - conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: type)) + conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: type)), + indirectConversion: nil, + conversionCheck: nil ) case .tuple([]): @@ -146,7 +155,9 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: .void) ], - conversion: .placeholder + conversion: .placeholder, + indirectConversion: nil, + conversionCheck: nil ) case .function(let fn): @@ -169,7 +180,9 @@ extension JNISwift2JavaGenerator { conversion: .closureLowering( parameters: parameters, result: result - ) + ), + indirectConversion: nil, + conversionCheck: nil ) case .optional(let wrapped): @@ -242,7 +255,9 @@ extension JNISwift2JavaGenerator { .placeholder, typeMetadataVariableName: .combinedName(component: "typeMetadataAddress"), protocolNames: protocolNames - ) + ), + indirectConversion: nil, + conversionCheck: nil ) } @@ -272,7 +287,9 @@ extension JNISwift2JavaGenerator { .initFromJNI(.placeholder, swiftType: swiftType), discriminatorName: discriminatorName, valueName: valueName - ) + ), + indirectConversion: nil, + conversionCheck: nil ) } @@ -285,7 +302,9 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .optionalMap(.initializeJavaKitWrapper(.placeholder, wrapperName: nominalTypeName)) + conversion: .optionalMap(.initializeJavaKitWrapper(.placeholder, wrapperName: nominalTypeName)), + indirectConversion: nil, + conversionCheck: nil ) } @@ -300,7 +319,9 @@ extension JNISwift2JavaGenerator { allowNil: true ) ) - ) + ), + indirectConversion: nil, + conversionCheck: nil ) default: @@ -434,7 +455,9 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .getJValue(.placeholder) + conversion: .getJValue(.placeholder), + indirectConversion: nil, + conversionCheck: nil ) } @@ -570,7 +593,9 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: .array(javaType)), ], - conversion: .initFromJNI(.placeholder, swiftType: .array(elementType)) + conversion: .initFromJNI(.placeholder, swiftType: .array(elementType)), + indirectConversion: nil, + conversionCheck: nil ) } @@ -594,7 +619,9 @@ extension JNISwift2JavaGenerator { convertLongFromJNI: false )))) ] - ) + ), + indirectConversion: nil, + conversionCheck: nil ) default: @@ -616,6 +643,12 @@ extension JNISwift2JavaGenerator { /// Represents how to convert the JNI parameter to a Swift parameter let conversion: NativeSwiftConversionStep + + /// Represents swift type for indirect variable used in required checks, e.g Int64 for Int overflow check on 32-bit platforms + let indirectConversion: NativeSwiftConversionStep? + + /// Represents check operations executed in if/guard conditional block for check during conversion + let conversionCheck: NativeSwiftConversionCheck? } struct NativeResult { @@ -695,6 +728,8 @@ extension JNISwift2JavaGenerator { /// `{ (args) -> return body }` indirect case closure(args: [String] = [], body: NativeSwiftConversionStep) + indirect case labelessAssignmentOfVariable(NativeSwiftConversionStep, swiftType: SwiftType) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -1025,6 +1060,20 @@ extension JNISwift2JavaGenerator { } } return printer.finalize() + case .labelessAssignmentOfVariable(let name, let swiftType): + return "\(swiftType)(indirect_\(name.render(&printer, placeholder)))" + } + } + } + + enum NativeSwiftConversionCheck { + case check32BitIntOverflow(typeWithMinAndMax: SwiftType) + + // Returns the check string + func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { + switch self { + case .check32BitIntOverflow(let minMaxSource): + return "\(placeholder) >= \(minMaxSource).min && \(placeholder) <= \(minMaxSource).max" } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 91a0b0e7..d6e6ad39 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -293,11 +293,43 @@ extension JNISwift2JavaGenerator { let tryClause: String = decl.isThrowing ? "try " : "" // Regular parameters. - var arguments = [String]() + var arguments: [String] = [String]() + var indirectVariables: [(name: String, lowered: String)] = [] + var int32OverflowChecks: [String] = [] + for (idx, parameter) in nativeSignature.parameters.enumerated() { let javaParameterName = translatedDecl.translatedFunctionSignature.parameters[idx].parameter.name let lowered = parameter.conversion.render(&printer, javaParameterName) arguments.append(lowered) + + parameter.indirectConversion.flatMap { + indirectVariables.append((javaParameterName, $0.render(&printer, javaParameterName))) + } + + switch parameter.conversionCheck { + case .check32BitIntOverflow: + int32OverflowChecks.append(parameter.conversionCheck!.render(&printer, "indirect_\(javaParameterName)")) + case nil: + break + } + } + + // Make indirect variables + for (name, lowered) in indirectVariables { + printer.print("let indirect_\(name) = \(lowered)") + } + + if !int32OverflowChecks.isEmpty { + printer.print("#if _pointerBitWidth(_32)") + + for check in int32OverflowChecks { + printer.printBraceBlock("guard \(check) else") { printer in + let dummyReturn = dummyReturn(for: nativeSignature) + printer.print("environment.throwJavaException(javaException: .integerOverflow)") + printer.print(dummyReturn) + } + } + printer.print("#endif") } // Callee @@ -362,14 +394,7 @@ extension JNISwift2JavaGenerator { } if decl.isThrowing, !decl.isAsync { - let dummyReturn: String - - if nativeSignature.result.javaType.isVoid { - dummyReturn = "" - } else { - // We assume it is something that implements JavaValue - dummyReturn = "return \(nativeSignature.result.javaType.swiftTypeName(resolver: { _ in "" })).jniPlaceholderValue" - } + let dummyReturn = dummyReturn(for: nativeSignature) printer.print("do {") printer.indent() @@ -390,6 +415,15 @@ extension JNISwift2JavaGenerator { } } + private func dummyReturn(for nativeSignature: NativeFunctionSignature) -> String { + return if nativeSignature.result.javaType.isVoid { + "return" + } else { + // We assume it is something that implements JavaValue + "return \(nativeSignature.result.javaType.swiftTypeName(resolver: { _ in "" })).jniPlaceholderValue" + } + } + private func printCDecl( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index b2f8d6ea..5852446a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -103,13 +103,24 @@ enum SwiftType: Equatable { switch self { case .nominal(let nominal): switch nominal.nominalTypeDecl.knownTypeKind { - case .uint8, .uint16, .uint32, .uint64: true + case .uint8, .uint16, .uint32, .uint64, .uint: true default: false } default: false } } + var isArchDependingInteger: Bool { + switch self { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .int, .uint: true + default: false + } + default: false + } + } + var isRawTypeCompatible: Bool { switch self { case .nominal(let nominal): diff --git a/Sources/JavaTypes/JavaException.swift b/Sources/JavaTypes/JavaException.swift new file mode 100644 index 00000000..266ff9ab --- /dev/null +++ b/Sources/JavaTypes/JavaException.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +/// Describes a Java exception class (e.g. `SwiftIntegerOverflowException`) +public struct JavaException: Equatable, Hashable { + public let type: JavaType + public let message: String? + + public init(className name: some StringProtocol, message: String? = nil) { + self.type = JavaType(className: name) + self.message = message + } +} + +extension JavaException { + public static var integerOverflow: JavaException { + JavaException(className: "org.swift.swiftkit.core.SwiftIntegerOverflowException") + } +} diff --git a/Sources/SwiftJava/Exceptions/ExceptionHandling.swift b/Sources/SwiftJava/Exceptions/ExceptionHandling.swift index d5ca1107..447b82fe 100644 --- a/Sources/SwiftJava/Exceptions/ExceptionHandling.swift +++ b/Sources/SwiftJava/Exceptions/ExceptionHandling.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import struct JavaTypes.JavaException + extension JNIEnvironment { /// Execute a JNI call and check for an exception at the end. Translate /// any Java exception into an error. @@ -43,4 +45,16 @@ extension JNIEnvironment { interface.ThrowNew(self, exceptionClass, String(describing: error)) } } + + public func throwJavaException(javaException: JavaException) { + guard let exceptionClass = self.interface.FindClass(self, javaException.type.className!) else { + // Otherwise, create a exception with a message. + _ = try! Exception.withJNIClass(in: self) { exceptionClass in + interface.ThrowNew(self, exceptionClass, "An exception(\(String(describing: javaException))) occured!") + } + return + } + + _ = interface.ThrowNew(self, exceptionClass, javaException.message ?? "") + } } diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftIntegerOverflowException.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftIntegerOverflowException.java new file mode 100644 index 00000000..529721b5 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftIntegerOverflowException.java @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 org.swift.swiftkit.core; + +public class SwiftIntegerOverflowException extends RuntimeException { + public SwiftIntegerOverflowException() { + super("Swift runtime has detected IntegerOverflow! Most probably you are running 32-bit application while using Swift's Int type."); + } +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/JNI/JNIConversionChecksTests.swift b/Tests/JExtractSwiftTests/JNI/JNIConversionChecksTests.swift new file mode 100644 index 00000000..d30226d5 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIConversionChecksTests.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@testable import JExtractSwiftLib +import Testing + +struct JNIConversionChecksTests { + private let signedSource = """ + public struct MyStruct { + public var normalInt: Int = 0 + + public init(normalInt: Int) { + self.normalInt = normalInt + } + } + """ + private let unsignedSource = """ + public struct MyStruct { + public var unsignedInt: UInt = 0 + + public init(unsignedInt: UInt) { + self.unsignedInt = unsignedInt + } + } + """ + + @Test func generatesUnsignedSetterWithCheck() throws { + try assertOutput(input: unsignedSource, .jni, .swift, expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setUnsignedInt__JJ") + func Java_com_example_swift_MyStruct__00024setUnsignedInt__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jlong, self: jlong) { + let indirect_newValue = UInt64(fromJNI: newValue, in: environment) + #if _pointerBitWidth(_32) + guard indirect_newValue >= UInt32.min && indirect_newValue <= UInt32.max else { + environment.throwJavaException(javaException: .integerOverflow) + return + """, + """ + #endif + 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.unsignedInt = UInt(indirect_newValue) + """ + ]) + } + + @Test func generatesUnsignedGetterWithoutCheck() throws { + try assertOutput(input: unsignedSource, .jni, .swift, expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getUnsignedInt__J") + func Java_com_example_swift_MyStruct__00024getUnsignedInt__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> 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)!") + } + return self$.pointee.unsignedInt.getJNIValue(in: environment) + """ + ]) + } + + @Test func generatesSignedSetterWithCheck() throws { + try assertOutput(input: signedSource, .jni, .swift, expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setNormalInt__JJ") + func Java_com_example_swift_MyStruct__00024setNormalInt__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jlong, self: jlong) { + let indirect_newValue = Int64(fromJNI: newValue, in: environment) + #if _pointerBitWidth(_32) + guard indirect_newValue >= Int32.min && indirect_newValue <= Int32.max else { + environment.throwJavaException(javaException: .integerOverflow) + return + """, + """ + #endif + 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.normalInt = Int(indirect_newValue) + """ + ]) + } +}