diff --git a/.unacceptablelanguageignore b/.unacceptablelanguageignore index 92a1d726e..c3f97d3d4 100644 --- a/.unacceptablelanguageignore +++ b/.unacceptablelanguageignore @@ -3,4 +3,5 @@ Sources/_Subprocess/Platforms/Subprocess+Darwin.swift Sources/_Subprocess/Platforms/Subprocess+Linux.swift Sources/_Subprocess/Platforms/Subprocess+Unix.swift Sources/_Subprocess/Teardown.swift -Sources/_Subprocess/Subprocess.swift \ No newline at end of file +Sources/_Subprocess/Subprocess.swift +NOTICE.txt \ No newline at end of file diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 1dd845470..09903638e 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -45,6 +45,11 @@ public func globalTakeIntInt(i: Int64, j: Int64) { p("i:\(i), j:\(j)") } +public func echoUnsignedInt(i: UInt32, j: UInt64) -> UInt64 { + p("i:\(i), j:\(j)") + return UInt64(i) + j +} + // ==== Internal helpers func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index 5c9c2358d..6da2fd4b0 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -61,4 +61,11 @@ void globalVariable() { MySwiftLibrary.setGlobalVariable(100); assertEquals(100, MySwiftLibrary.getGlobalVariable()); } + + @Test + void globalUnsignedIntEcho() { + int i = 12; + long l = 1200; + assertEquals(1212, MySwiftLibrary.echoUnsignedInt(12, 1200)); + } } \ No newline at end of file diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 306610456..e1139c2b3 100644 --- a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -60,4 +60,16 @@ public class MySwiftClass { public func makeRandomIntMethod() -> Int { return Int.random(in: 1..<256) } + + public func takeUnsignedChar(arg: UInt16) { + p("\(UInt32.self) = \(arg)") + } + + public func takeUnsignedInt(arg: UInt32) { + p("\(UInt32.self) = \(arg)") + } + + public func takeUnsignedLong(arg: UInt64) { + p("\(UInt64.self) = \(arg)") + } } diff --git a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/UnsignedNumbersTest.java b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/UnsignedNumbersTest.java new file mode 100644 index 000000000..beb0f817f --- /dev/null +++ b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/UnsignedNumbersTest.java @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.AllocatingSwiftArena; + +public class UnsignedNumbersTest { + @Test + void take_uint32() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var c = MySwiftClass.init(1, 2, arena); + c.takeUnsignedInt(128); + } + } + + @Test + void take_uint64() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + var c = MySwiftClass.init(1, 2, arena); + c.takeUnsignedLong(Long.MAX_VALUE); + } + } +} diff --git a/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift b/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift new file mode 100644 index 000000000..70a86e820 --- /dev/null +++ b/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaTypes +import JavaKitConfigurationShared + +/// Determine if the given type needs any extra annotations that should be included +/// in Java sources when the corresponding Java type is rendered. +func getTypeAnnotations(swiftType: SwiftType, config: Configuration) -> [JavaAnnotation] { + if swiftType.isUnsignedInteger, config.effectiveUnsignedNumbersMode == .annotate { + return [JavaAnnotation.unsigned] + } + + return [] +} diff --git a/Sources/JExtractSwiftLib/Configuration+Extensions.swift b/Sources/JExtractSwiftLib/Configuration+Extensions.swift new file mode 100644 index 000000000..d85cf4472 --- /dev/null +++ b/Sources/JExtractSwiftLib/Configuration+Extensions.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JavaKitConfigurationShared // TODO: this should become SwiftJavaConfigurationShared +import JavaTypes // TODO: this should become SwiftJavaConfigurationShared + +extension Configuration { + public var effectiveUnsignedNumericsMode: UnsignedNumericsMode { + switch effectiveUnsignedNumbersMode { + case .annotate: .ignoreSign + case .wrapGuava: .wrapUnsignedGuava + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 28b3aba12..b87492999 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -105,9 +105,16 @@ extension FFMSwift2JavaGenerator { var params: [String] = [] var args: [String] = [] for param in cFunc.parameters { - // ! unwrapping because cdecl lowering guarantees the parameter named. - params.append("\(param.type.javaType) \(param.name!)") - args.append(param.name!) + let name = param.name! // !-safe, because cdecl lowering guarantees the parameter named. + + let annotationsStr = + if param.type.javaType.parameterAnnotations.isEmpty { + "" + } else { + param.type.javaType.parameterAnnotations.map({$0.render()}).joined(separator: " ") + " " + } + params.append("\(annotationsStr)\(param.type.javaType) \(name)") + args.append(name) } let paramsStr = params.joined(separator: ", ") let argsStr = args.joined(separator: ", ") @@ -316,23 +323,21 @@ extension FFMSwift2JavaGenerator { let translatedSignature = translated.translatedSignature let returnTy = translatedSignature.result.javaResultType + var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") + if !annotationsStr.isEmpty { annotationsStr += "\n" } + var paramDecls = translatedSignature.parameters .flatMap(\.javaParameters) - .map { "\($0.type) \($0.name)" } + .map { $0.renderParameter() } if translatedSignature.requiresSwiftArena { paramDecls.append("AllocatingSwiftArena swiftArena$") } // TODO: we could copy the Swift method's documentation over here, that'd be great UX + printDeclDocumentation(&printer, decl) printer.printBraceBlock( """ - /** - * Downcall to Swift: - * {@snippet lang=swift : - * \(decl.signatureString) - * } - */ - \(modifiers) \(returnTy) \(methodName)(\(paramDecls.joined(separator: ", "))) + \(annotationsStr)\(modifiers) \(returnTy) \(methodName)(\(paramDecls.joined(separator: ", "))) """ ) { printer in if case .instance(_) = decl.functionSignature.selfParameter { @@ -344,6 +349,19 @@ extension FFMSwift2JavaGenerator { } } + private func printDeclDocumentation(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + printer.print( + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * \(decl.signatureString) + * } + */ + """ + ) + } + /// Print the actual downcall to the Swift API. /// /// This assumes that all the parameters are passed-in with appropriate names. diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index b29ca5d9e..f8b2e7e84 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JavaTypes +import JavaKitConfigurationShared extension FFMSwift2JavaGenerator { func translatedDecl( @@ -24,7 +25,9 @@ extension FFMSwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - let translation = JavaTranslation(knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable)) + let translation = JavaTranslation( + config: self.config, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable)) translated = try translation.translate(decl) } catch { self.log.info("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") @@ -52,6 +55,9 @@ extension FFMSwift2JavaGenerator { /// Java type that represents the Swift result type. var javaResultType: JavaType + /// Java annotations that should be propagated from the result type onto the method + var annotations: [JavaAnnotation] = [] + /// Required indirect return receivers for receiving the result. /// /// 'JavaParameter.name' is the suffix for the receiver variable names. For example @@ -85,8 +91,13 @@ extension FFMSwift2JavaGenerator { /// Function signature. let translatedSignature: TranslatedFunctionSignature - /// Cdecl lowerd signature. + /// Cdecl lowered signature. let loweredSignature: LoweredFunctionSignature + + /// Annotations to include on the Java function declaration + var annotations: [JavaAnnotation] { + self.translatedSignature.annotations + } } /// Function signature for a Java API. @@ -94,6 +105,12 @@ extension FFMSwift2JavaGenerator { var selfParameter: TranslatedParameter? var parameters: [TranslatedParameter] var result: TranslatedResult + + // if the result type implied any annotations, + // propagate them onto the function the result is returned from + var annotations: [JavaAnnotation] { + self.result.annotations + } } /// Represent a Swift closure type in the user facing Java API. @@ -113,15 +130,15 @@ extension FFMSwift2JavaGenerator { } struct JavaTranslation { + let config: Configuration var knownTypes: SwiftKnownTypes - init(knownTypes: SwiftKnownTypes) { + init(config: Configuration, knownTypes: SwiftKnownTypes) { + self.config = config self.knownTypes = knownTypes } - func translate( - _ decl: ImportedFunc - ) throws -> TranslatedFunctionDecl { + func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { let lowering = CdeclLowering(knownTypes: knownTypes) let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) @@ -303,6 +320,18 @@ extension FFMSwift2JavaGenerator { genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement] ) throws -> TranslatedParameter { + // If the result type should cause any annotations on the method, include them here. + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + // If we need to handle unsigned integers do so here + if config.effectiveUnsignedNumbersMode.needsConversion { + if let unsignedWrapperType = JavaType.unsignedWrapper(for: swiftType) { + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: unsignedWrapperType, annotations: parameterAnnotations) + ], conversion: .call(.placeholder, function: "UnsignedNumbers.toPrimitive", withArena: false)) + } + } // If there is a 1:1 mapping between this Swift type and a C type, that can // be expressed as a Java primitive type. @@ -311,8 +340,9 @@ extension FFMSwift2JavaGenerator { return TranslatedParameter( javaParameters: [ JavaParameter( - name: parameterName, type: javaType - ) + name: parameterName, + type: javaType, + annotations: parameterAnnotations) ], conversion: .placeholder ) @@ -324,7 +354,10 @@ extension FFMSwift2JavaGenerator { return TranslatedParameter( javaParameters: [ JavaParameter( - name: parameterName, type: JavaType.class(package: "org.swift.swiftkit.ffm", name: "SwiftAnyType")) + name: parameterName, + type: JavaType.class(package: "org.swift.swiftkit.ffm", name: "SwiftAnyType"), + annotations: parameterAnnotations + ) ], conversion: .swiftValueSelfSegment(.placeholder) ) @@ -524,6 +557,37 @@ extension FFMSwift2JavaGenerator { } } + func unsignedResultConversion(_ from: SwiftType, to javaType: JavaType, + mode: JExtractUnsignedIntegerMode) -> JavaConversionStep { + switch mode { + case .annotate: + return .placeholder // no conversions + + case .wrapGuava: + guard let typeName = javaType.fullyQualifiedClassName else { + fatalError("Missing target class name for result conversion step from \(from) to \(javaType)") + } + + switch from { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .uint8: + return .call(.placeholder, function: "\(typeName).fromIntBits", withArena: false) + case .uint16: + return .placeholder // no conversion, UInt16 can be returned as-is and will be seen as char by Java + case .uint32: + return .call(.placeholder, function: "\(typeName).fromIntBits", withArena: false) + case .uint64: + return .call(.placeholder, function: "\(typeName).fromLongBits", withArena: false) + default: + fatalError("unsignedResultConversion: Unsupported conversion from \(from) to \(javaType)") + } + default: + fatalError("unsignedResultConversion: Unsupported conversion from \(from) to \(javaType)") + } + } + } + /// Translate a Swift API result to the user-facing Java API result. func translate( swiftResult: SwiftResult, @@ -531,12 +595,29 @@ extension FFMSwift2JavaGenerator { ) throws -> TranslatedResult { let swiftType = swiftResult.type + // If we need to handle unsigned integers do so here + if config.effectiveUnsignedNumbersMode.needsConversion { + if let unsignedWrapperType = JavaType.unsignedWrapper(for: swiftType) /* and we're in safe wrapper mode */ { + return TranslatedResult( + javaResultType: unsignedWrapperType, + outParameters: [], + conversion: unsignedResultConversion( + swiftType, to: unsignedWrapperType, + mode: self.config.effectiveUnsignedNumbersMode) + ) + } + } + + // If the result type should cause any annotations on the method, include them here. + let resultAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + // If there is a 1:1 mapping between this Swift type and a C type, that can // be expressed as a Java primitive type. if let cType = try? CType(cdeclType: swiftType) { let javaType = cType.javaType return TranslatedResult( javaResultType: javaType, + annotations: resultAnnotations, outParameters: [], conversion: .placeholder ) @@ -548,6 +629,7 @@ extension FFMSwift2JavaGenerator { let javaType = JavaType.class(package: "org.swift.swiftkit.ffm", name: "SwiftAnyType") return TranslatedResult( javaResultType: javaType, + annotations: resultAnnotations, outParameters: [], conversion: .construct(.placeholder, javaType) ) @@ -558,6 +640,7 @@ extension FFMSwift2JavaGenerator { case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: return TranslatedResult( javaResultType: .javaForeignMemorySegment, + annotations: resultAnnotations, outParameters: [ JavaParameter(name: "pointer", type: .javaForeignMemorySegment), JavaParameter(name: "count", type: .long), @@ -597,6 +680,7 @@ extension FFMSwift2JavaGenerator { let javaType: JavaType = .class(package: nil, name: swiftNominalType.nominalTypeDecl.name) return TranslatedResult( javaResultType: javaType, + annotations: resultAnnotations, outParameters: [ JavaParameter(name: "", type: javaType) ], @@ -702,7 +786,7 @@ extension CType { case .integral(.signed(bits: 32)): return .int case .integral(.signed(bits: 64)): return .long case .integral(.unsigned(bits: 8)): return .byte - case .integral(.unsigned(bits: 16)): return .short + case .integral(.unsigned(bits: 16)): return .char // char is Java's only unsigned primitive, we can use it! case .integral(.unsigned(bits: 32)): return .int case .integral(.unsigned(bits: 64)): return .long @@ -739,10 +823,10 @@ extension CType { case .integral(.signed(bits: 32)): return .SwiftInt32 case .integral(.signed(bits: 64)): return .SwiftInt64 - case .integral(.unsigned(bits: 8)): return .SwiftInt8 - case .integral(.unsigned(bits: 16)): return .SwiftInt16 - case .integral(.unsigned(bits: 32)): return .SwiftInt32 - case .integral(.unsigned(bits: 64)): return .SwiftInt64 + case .integral(.unsigned(bits: 8)): return .SwiftUInt8 + case .integral(.unsigned(bits: 16)): return .SwiftUInt16 + case .integral(.unsigned(bits: 32)): return .SwiftUInt32 + case .integral(.unsigned(bits: 64)): return .SwiftUInt64 case .floating(.double): return .SwiftDouble case .floating(.float): return .SwiftFloat diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 1e32d1e59..f35973eb9 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -38,7 +38,7 @@ extension FFMSwift2JavaGenerator { let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" let moduleFilename = "\(moduleFilenameBase).swift" do { - log.info("Printing contents: \(moduleFilename)") + log.debug("Printing contents: \(moduleFilename)") try printGlobalSwiftThunkSources(&printer) @@ -58,7 +58,7 @@ extension FFMSwift2JavaGenerator { for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava" let filename = "\(fileNameBase).swift" - log.info("Printing contents: \(filename)") + log.debug("Printing contents: \(filename)") do { try printSwiftThunkSources(&printer, ty: ty) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index d30d3e749..299626213 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -15,10 +15,12 @@ import JavaTypes import SwiftSyntax import SwiftSyntaxBuilder +import JavaKitConfigurationShared import struct Foundation.URL package class FFMSwift2JavaGenerator: Swift2JavaGenerator { let log: Logger + let config: Configuration let analysis: AnalysisResult let swiftModuleName: String let javaPackage: String @@ -40,12 +42,14 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { var expectedOutputSwiftFiles: Set package init( + config: Configuration, translator: Swift2JavaTranslator, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String ) { self.log = Logger(label: "ffm-generator", logLevel: translator.log.logLevel) + self.config = config self.analysis = translator.result self.swiftModuleName = translator.swiftModuleName self.javaPackage = javaPackage @@ -96,7 +100,9 @@ extension FFMSwift2JavaGenerator { "org.swift.swiftkit.core.*", "org.swift.swiftkit.core.util.*", "org.swift.swiftkit.ffm.*", - "org.swift.swiftkit.ffm.SwiftRuntime", + + // NonNull, Unsigned and friends + "org.swift.swiftkit.core.annotations.*", // Necessary for native calls and type mapping "java.lang.foreign.*", @@ -120,7 +126,7 @@ extension FFMSwift2JavaGenerator { package func writeExportedJavaSources(printer: inout CodePrinter) throws { for (_, ty) in analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let filename = "\(ty.swiftNominal.name).java" - log.info("Printing contents: \(filename)") + log.debug("Printing contents: \(filename)") printImportedNominal(&printer, ty) if let outputFile = try printer.writeContents( @@ -134,7 +140,7 @@ extension FFMSwift2JavaGenerator { do { let filename = "\(self.swiftModuleName).java" - log.info("Printing contents: \(filename)") + log.debug("Printing contents: \(filename)") printModule(&printer) if let outputFile = try printer.writeContents( @@ -178,7 +184,7 @@ extension FFMSwift2JavaGenerator { func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printHeader(&printer) printPackage(&printer) - printImports(&printer) + printImports(&printer) // TODO: we could have some imports be driven from types used in the generated decl printNominal(&printer, decl) { printer in // We use a static field to abuse the initialization order such that by the time we get type metadata, diff --git a/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift b/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift index 3784a75a0..329efaad3 100644 --- a/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift +++ b/Sources/JExtractSwiftLib/FFM/ForeignValueLayouts.swift @@ -67,11 +67,19 @@ extension ForeignValueLayout { public static let SwiftBool = Self(javaConstant: "SWIFT_BOOL") public static let SwiftInt = Self(javaConstant: "SWIFT_INT") + public static let SwiftUInt = Self(javaConstant: "SWIFT_UINT") + public static let SwiftInt64 = Self(javaConstant: "SWIFT_INT64") + public static let SwiftUInt64 = Self(javaConstant: "SWIFT_UINT64") + public static let SwiftInt32 = Self(javaConstant: "SWIFT_INT32") + public static let SwiftUInt32 = Self(javaConstant: "SWIFT_UINT32") + public static let SwiftInt16 = Self(javaConstant: "SWIFT_INT16") public static let SwiftUInt16 = Self(javaConstant: "SWIFT_UINT16") + public static let SwiftInt8 = Self(javaConstant: "SWIFT_INT8") + public static let SwiftUInt8 = Self(javaConstant: "SWIFT_UINT8") public static let SwiftFloat = Self(javaConstant: "SWIFT_FLOAT") public static let SwiftDouble = Self(javaConstant: "SWIFT_DOUBLE") diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index dd7e9c10e..4c4d9c0f4 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -78,6 +78,15 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { } } + /// If this function type uses types that require any additional `import` statements, + /// these would be exported here. + var additionalJavaImports: Set { + var imports: Set = [] +// imports += self.functionSignature.parameters.flatMap { $0.additionalJavaImports } +// imports += self.functionSignature.result.additionalJavaImports + return imports + } + var isStatic: Bool { if case .staticMethod = functionSignature.selfParameter { return true diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift new file mode 100644 index 000000000..fe10ef72d --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-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 JavaTypes +import JavaKitConfigurationShared + +enum JNIJavaTypeTranslator { + + static func translate(knownType: SwiftKnownTypeDeclKind, config: Configuration) -> JavaType? { + let unsigned = config.effectiveUnsignedNumbersMode + guard unsigned == .annotate else { + // We do not support wrap mode in JNI mode currently; + // In the future this is where it would be interesting to implement Kotlin UInt support. + return nil + } + + switch knownType { + case .bool: return .boolean + + case .int8: return .byte + case .uint8: return .byte + + case .int16: return .short + case .uint16: return .char + + case .int32: return .int + case .uint32: return .int + + case .int64: return .long + case .uint64: return .long + + case .float: return .float + case .double: return .double + case .void: return .void + + case .string: return .javaLangString + case .int, .uint, // FIXME: why not supported int/uint? + .unsafeRawPointer, .unsafeMutableRawPointer, + .unsafePointer, .unsafeMutablePointer, + .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, + .unsafeBufferPointer, .unsafeMutableBufferPointer, + .optional, .data, .dataProtocol: + return nil + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index e0e5cb701..63b9dcd1e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -20,6 +20,9 @@ extension JNISwift2JavaGenerator { static let defaultJavaImports: Array = [ "org.swift.swiftkit.core.*", "org.swift.swiftkit.core.util.*", + + // NonNull, Unsigned and friends + "org.swift.swiftkit.core.annotations.*", ] } @@ -32,9 +35,11 @@ extension JNISwift2JavaGenerator { } package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { - for (_, ty) in analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { + let importedTypes = analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) + + for (_, ty) in importedTypes { let filename = "\(ty.swiftNominal.name).java" - logger.info("Printing contents: \(filename)") + logger.debug("Printing contents: \(filename)") printImportedNominal(&printer, ty) if let outputFile = try printer.writeContents( @@ -62,6 +67,7 @@ extension JNISwift2JavaGenerator { private func printModule(_ printer: inout CodePrinter) { printHeader(&printer) printPackage(&printer) + printImports(&printer) printModuleClass(&printer) { printer in printer.print( @@ -81,6 +87,7 @@ extension JNISwift2JavaGenerator { } for decl in analysis.importedGlobalVariables { + self.logger.trace("Print global variable: \(decl)") printFunctionDowncallMethods(&printer, decl) printer.println() } @@ -219,7 +226,7 @@ extension JNISwift2JavaGenerator { _ printer: inout CodePrinter, _ functionType: TranslatedFunctionType ) { - let apiParams = functionType.parameters.map(\.parameter.asParameter) + let apiParams = functionType.parameters.map({ $0.parameter.renderParameter() }) printer.print( """ @@ -243,18 +250,21 @@ extension JNISwift2JavaGenerator { let translatedSignature = translatedDecl.translatedFunctionSignature let resultType = translatedSignature.resultType.javaType - var parameters = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.asParameter) + var parameters = translatedDecl.translatedFunctionSignature.parameters.map({ $0.parameter.renderParameter() }) if translatedSignature.requiresSwiftArena { parameters.append("SwiftArena swiftArena$") } let throwsClause = decl.isThrowing ? " throws Exception" : "" + var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") + if !annotationsStr.isEmpty { annotationsStr += "\n" } + let modifiersStr = modifiers.joined(separator: " ") let parametersStr = parameters.joined(separator: ", ") printDeclDocumentation(&printer, decl) printer.printBraceBlock( - "\(modifiersStr) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" + "\(annotationsStr)\(modifiersStr) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" ) { printer in printDowncall(&printer, decl) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 8d18e6aef..5908cfb12 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JavaTypes +import JavaKitConfigurationShared extension JNISwift2JavaGenerator { func translatedDecl( @@ -25,6 +26,7 @@ extension JNISwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { let translation = JavaTranslation( + config: config, swiftModuleName: swiftModuleName, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable @@ -40,12 +42,14 @@ extension JNISwift2JavaGenerator { } struct JavaTranslation { + let config: Configuration let swiftModuleName: String let javaPackage: String let javaClassLookupTable: JavaClassLookupTable func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { let nativeTranslation = NativeJavaTranslation( + config: self.config, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable ) @@ -117,12 +121,12 @@ extension JNISwift2JavaGenerator { ) } - let transltedResult = try translate(swiftResult: SwiftResult(convention: .direct, type: swiftType.resultType)) + let translatedResult = try translate(swiftResult: SwiftResult(convention: .direct, type: swiftType.resultType)) return TranslatedFunctionType( name: name, parameters: translatedParams, - result: transltedResult, + result: translatedResult, swiftType: swiftType ) } @@ -150,10 +154,12 @@ extension JNISwift2JavaGenerator { selfParameter = nil } - return try TranslatedFunctionSignature( + let resultType = try translate(swiftResult: functionSignature.result) + + return TranslatedFunctionSignature( selfParameter: selfParameter, parameters: parameters, - resultType: translate(swiftResult: functionSignature.result) + resultType: resultType ) } @@ -163,17 +169,33 @@ extension JNISwift2JavaGenerator { methodName: String, parentName: String ) throws -> TranslatedParameter { + + // If the result type should cause any annotations on the method, include them here. + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + // If we need to handle unsigned integers do so here + if config.effectiveUnsignedNumbersMode.needsConversion { + if let unsignedWrapperType = JavaType.unsignedWrapper(for: swiftType) { + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: unsignedWrapperType, annotations: parameterAnnotations), + conversion: unsignedResultConversion( + swiftType, to: unsignedWrapperType, + mode: self.config.effectiveUnsignedNumbersMode) + ) + } + } + switch swiftType { case .nominal(let nominalType): let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } return TranslatedParameter( - parameter: JavaParameter(name: parameterName, type: javaType), + parameter: JavaParameter(name: parameterName, type: javaType, annotations: parameterAnnotations), conversion: .placeholder ) } @@ -184,10 +206,7 @@ extension JNISwift2JavaGenerator { } return TranslatedParameter( - parameter: JavaParameter( - name: parameterName, - type: javaType - ), + parameter: JavaParameter(name: parameterName, type: javaType, annotations: parameterAnnotations), conversion: .placeholder ) } @@ -196,14 +215,15 @@ extension JNISwift2JavaGenerator { return TranslatedParameter( parameter: JavaParameter( name: parameterName, - type: .class(package: nil, name: nominalTypeName) + type: .class(package: nil, name: nominalTypeName), + annotations: parameterAnnotations ), conversion: .valueMemoryAddress(.placeholder) ) case .tuple([]): return TranslatedParameter( - parameter: JavaParameter(name: parameterName, type: .void), + parameter: JavaParameter(name: parameterName, type: .void, annotations: parameterAnnotations), conversion: .placeholder ) @@ -211,7 +231,8 @@ extension JNISwift2JavaGenerator { return TranslatedParameter( parameter: JavaParameter( name: parameterName, - type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)") + type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"), + annotations: parameterAnnotations ), conversion: .placeholder ) @@ -221,30 +242,46 @@ extension JNISwift2JavaGenerator { } } - func translate( - swiftResult: SwiftResult - ) throws -> TranslatedResult { - switch swiftResult.type { + func unsignedResultConversion(_ from: SwiftType, to javaType: JavaType, + mode: JExtractUnsignedIntegerMode) -> JavaNativeConversionStep { + switch mode { + case .annotate: + return .placeholder // no conversions + + case .wrapGuava: + fatalError("JExtract in JNI mode does not support the \(JExtractUnsignedIntegerMode.wrapGuava) unsigned numerics mode") + } + } + + func translate(swiftResult: SwiftResult) throws -> TranslatedResult { + let swiftType = swiftResult.type + + // If the result type should cause any annotations on the method, include them here. + let resultAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + switch swiftType { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { - throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) } return TranslatedResult( javaType: javaType, + annotations: resultAnnotations, conversion: .placeholder ) } if nominalType.isJavaKitWrapper { - throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + throw JavaTranslationError.unsupportedSwiftType(swiftType) } // We assume this is a JExtract class. let javaType = JavaType.class(package: nil, name: nominalType.nominalTypeDecl.name) return TranslatedResult( javaType: javaType, + annotations: resultAnnotations, conversion: .constructSwiftValue(.placeholder, javaType) ) @@ -252,7 +289,7 @@ extension JNISwift2JavaGenerator { return TranslatedResult(javaType: .void, conversion: .placeholder) case .metatype, .optional, .tuple, .function, .existential, .opaque, .genericParameter: - throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + throw JavaTranslationError.unsupportedSwiftType(swiftType) } } } @@ -275,33 +312,23 @@ extension JNISwift2JavaGenerator { /// Function signature of the native function that will be implemented by Swift let nativeFunctionSignature: NativeFunctionSignature - } - static func translate(knownType: SwiftKnownTypeDeclKind) -> JavaType? { - switch knownType { - case .bool: .boolean - case .int8: .byte - case .uint16: .char - case .int16: .short - case .int32: .int - case .int64: .long - case .float: .float - case .double: .double - case .void: .void - case .string: .javaLangString - case .int, .uint, .uint8, .uint32, .uint64, - .unsafeRawPointer, .unsafeMutableRawPointer, - .unsafePointer, .unsafeMutablePointer, - .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, - .unsafeBufferPointer, .unsafeMutableBufferPointer, .optional, .data, .dataProtocol: - nil + /// Annotations to include on the Java function declaration + var annotations: [JavaAnnotation] { + self.translatedFunctionSignature.annotations } } struct TranslatedFunctionSignature { - let selfParameter: TranslatedParameter? - let parameters: [TranslatedParameter] - let resultType: TranslatedResult + var selfParameter: TranslatedParameter? + var parameters: [TranslatedParameter] + var resultType: TranslatedResult + + // if the result type implied any annotations, + // propagate them onto the function the result is returned from + var annotations: [JavaAnnotation] { + self.resultType.annotations + } var requiresSwiftArena: Bool { return self.resultType.conversion.requiresSwiftArena @@ -318,6 +345,9 @@ extension JNISwift2JavaGenerator { struct TranslatedResult { let javaType: JavaType + /// Java annotations that should be propagated from the result type onto the method + var annotations: [JavaAnnotation] = [] + /// Represents how to convert the Java native result into a user-facing result. let conversion: JavaNativeConversionStep } @@ -343,6 +373,8 @@ extension JNISwift2JavaGenerator { /// Call `new \(Type)(\(placeholder), swiftArena$)` indirect case constructSwiftValue(JavaNativeConversionStep, JavaType) + indirect case call(JavaNativeConversionStep, function: String) + /// 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. @@ -350,14 +382,18 @@ extension JNISwift2JavaGenerator { switch self { case .placeholder: return placeholder - + case .valueMemoryAddress: return "\(placeholder).$memoryAddress()" - + case .constructSwiftValue(let inner, let javaType): let inner = inner.render(&printer, placeholder) return "new \(javaType.className!)(\(inner), swiftArena$)" - + + case .call(let inner, let function): + let inner = inner.render(&printer, placeholder) + return "\(function)(\(inner))" + } } @@ -372,12 +408,18 @@ extension JNISwift2JavaGenerator { case .valueMemoryAddress(let inner): return inner.requiresSwiftArena + + case .call(let inner, _): + return inner.requiresSwiftArena } } } enum JavaTranslationError: Error { - case unsupportedSwiftType(SwiftType) + case unsupportedSwiftType(SwiftType, fileID: String, line: Int) + static func unsupportedSwiftType(_ type: SwiftType, _fileID: String = #fileID, _line: Int = #line) -> JavaTranslationError { + .unsupportedSwiftType(type, fileID: _fileID, line: _line) + } /// The user has not supplied a mapping from `SwiftType` to /// a java class. diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 48a6e8d45..9f1113fc6 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -13,10 +13,12 @@ //===----------------------------------------------------------------------===// import JavaTypes +import JavaKitConfigurationShared extension JNISwift2JavaGenerator { struct NativeJavaTranslation { + let config: Configuration let javaPackage: String let javaClassLookupTable: JavaClassLookupTable @@ -70,7 +72,8 @@ extension JNISwift2JavaGenerator { let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) } @@ -140,7 +143,8 @@ extension JNISwift2JavaGenerator { switch type { case .nominal(let nominal): if let knownType = nominal.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(type) } @@ -172,7 +176,8 @@ extension JNISwift2JavaGenerator { switch type { case .nominal(let nominal): if let knownType = nominal.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(type) } @@ -198,7 +203,10 @@ extension JNISwift2JavaGenerator { switch swiftResult.type { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + guard javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 35d4dbef1..8b910ffb3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -59,7 +59,7 @@ extension JNISwift2JavaGenerator { for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava" let filename = "\(fileNameBase).swift" - logger.info("Printing contents: \(filename)") + logger.debug("Printing contents: \(filename)") do { try printNominalTypeThunks(&printer, ty) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 79f546aca..c5f3a5b67 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -13,16 +13,19 @@ //===----------------------------------------------------------------------===// import JavaTypes +import JavaKitConfigurationShared /// A table that where keys are Swift class names and the values are /// the fully qualified canoical names. package typealias JavaClassLookupTable = [String: String] package class JNISwift2JavaGenerator: Swift2JavaGenerator { + + let logger: Logger + let config: Configuration let analysis: AnalysisResult let swiftModuleName: String let javaPackage: String - let logger: Logger let swiftOutputDirectory: String let javaOutputDirectory: String @@ -42,12 +45,14 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { var expectedOutputSwiftFiles: Set package init( + config: Configuration, translator: Swift2JavaTranslator, javaPackage: String, swiftOutputDirectory: String, javaOutputDirectory: String, javaClassLookupTable: JavaClassLookupTable ) { + self.config = config self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel) self.analysis = translator.result self.swiftModuleName = translator.swiftModuleName diff --git a/Sources/JExtractSwiftLib/JNI/JNIType.swift b/Sources/JExtractSwiftLib/JNI/JNIType.swift index feb8a5458..cdedb0a17 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIType.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIType.swift @@ -88,7 +88,7 @@ extension JavaType { /// Returns whether this type returns `JavaValue` from JavaKit var implementsJavaValue: Bool { - switch self { + return switch self { case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString: true default: diff --git a/Sources/JExtractSwiftLib/JavaParameter.swift b/Sources/JExtractSwiftLib/JavaParameter.swift index 728964199..a12b13b28 100644 --- a/Sources/JExtractSwiftLib/JavaParameter.swift +++ b/Sources/JExtractSwiftLib/JavaParameter.swift @@ -18,8 +18,21 @@ import JavaTypes struct JavaParameter { let name: String let type: JavaType + /// Parameter annotations are used in parameter declarations like this: `@Annotation int example` + let annotations: [JavaAnnotation] - var asParameter: String { - "\(type) \(name)" + init(name: String, type: JavaType, annotations: [JavaAnnotation] = []) { + self.name = name + self.type = type + self.annotations = annotations + } + + func renderParameter() -> String { + if annotations.isEmpty { + return "\(type) \(name)" + } + + let annotationsStr = annotations.map({$0.render()}).joined(separator: "") + return "\(annotationsStr) \(type) \(name)" } } diff --git a/Sources/JExtractSwiftLib/JavaConstants/JavaTypes.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift similarity index 100% rename from Sources/JExtractSwiftLib/JavaConstants/JavaTypes.swift rename to Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift new file mode 100644 index 000000000..2ab9c0a22 --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JavaTypes + +extension JavaType { + + /// Try to map a Swift type name (e.g., from the module Swift) over to a + /// primitive Java type, or fail otherwise. + public init?(swiftTypeName: String, WHT_unsigned unsigned: UnsignedNumericsMode) { + switch swiftTypeName { + case "Bool": self = .boolean + + case "Int8": self = .byte + case "UInt8": + self = switch unsigned { + case .ignoreSign: .byte + case .wrapUnsignedGuava: JavaType.guava.primitives.UnsignedInteger + } + + case "Int16": self = .short + case "UInt16": self = .char + + case "Int32": self = .int + case "UInt32": + self = switch unsigned { + case .ignoreSign: .int + case .wrapUnsignedGuava: JavaType.guava.primitives.UnsignedInteger + } + + case "Int64": self = .long + case "UInt64": + self = switch unsigned { + case .ignoreSign: .long + case .wrapUnsignedGuava: JavaType.guava.primitives.UnsignedLong + } + + case "Float": self = .float + case "Double": self = .double + case "Void": self = .void + default: return nil + } + } +} + +extension JavaType { + + static func unsignedWrapper(for swiftType: SwiftType) -> JavaType? { + switch swiftType { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .uint8: return guava.primitives.UnsignedInteger + case .uint16: return .char // no wrapper necessary, we can express it as 'char' natively in Java + case .uint32: return guava.primitives.UnsignedInteger + case .uint64: return guava.primitives.UnsignedLong + default: return nil + } + default: return nil + } + } + + /// Known types from the Google Guava library + enum guava { + enum primitives { + static let package = "com.google.common.primitives" + + static var UnsignedInteger: JavaType { + .class(package: primitives.package, name: "UnsignedInteger") + } + + static var UnsignedLong: JavaType { + .class(package: primitives.package, name: "UnsignedLong") + } + } + } + +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 6473cea32..5a73e3288 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -98,6 +98,7 @@ public struct SwiftToJava { switch config.mode { case .some(.ffm), .none: let generator = FFMSwift2JavaGenerator( + config: self.config, translator: translator, javaPackage: config.javaPackage ?? "", swiftOutputDirectory: outputSwiftDirectory, @@ -108,6 +109,7 @@ public struct SwiftToJava { case .jni: let generator = JNISwift2JavaGenerator( + config: self.config, translator: translator, javaPackage: config.javaPackage ?? "", swiftOutputDirectory: outputSwiftDirectory, diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index da738d39b..3cc14406a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -92,6 +92,17 @@ enum SwiftType: Equatable { return false } } + + var isUnsignedInteger: Bool { + switch self { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .uint8, .uint16, .uint32, .uint64: true + default: false + } + default: false + } + } } extension SwiftType: CustomStringConvertible { diff --git a/Sources/JavaKit/BridgedValues/JavaValue+Integers.swift b/Sources/JavaKit/BridgedValues/JavaValue+Integers.swift index 1af1b5494..005753eb6 100644 --- a/Sources/JavaKit/BridgedValues/JavaValue+Integers.swift +++ b/Sources/JavaKit/BridgedValues/JavaValue+Integers.swift @@ -14,6 +14,66 @@ import JavaTypes +extension UInt8: JavaValue { + public typealias JNIType = jbyte + + public static var jvalueKeyPath: WritableKeyPath { \.b } + + public static var javaType: JavaType { .byte } + + /// Retrieve the JNI value. + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + /// Initialize from a JNI value. + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self(value) + } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallByteMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetByteField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetByteField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticByteMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticByteField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticByteField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewByteArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetByteArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetByteArrayRegion + } + + public static var jniPlaceholderValue: jbyte { + 0 + } +} + extension Int8: JavaValue { public typealias JNIType = jbyte @@ -170,6 +230,66 @@ extension Int16: JavaValue { } } +extension UInt32: JavaValue { + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public static var javaType: JavaType { .int } + + /// Retrieve the JNI value. + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + /// Initialize from a JNI value. + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self(value) + } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} + extension Int32: JavaValue { public typealias JNIType = jint @@ -228,6 +348,64 @@ extension Int32: JavaValue { } } +extension UInt64: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = UInt64(value) + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} + extension Int64: JavaValue { public typealias JNIType = jlong diff --git a/Sources/JavaKitConfigurationShared/Configuration.swift b/Sources/JavaKitConfigurationShared/Configuration.swift index 5c57082eb..c1ae7dd2b 100644 --- a/Sources/JavaKitConfigurationShared/Configuration.swift +++ b/Sources/JavaKitConfigurationShared/Configuration.swift @@ -42,6 +42,11 @@ public struct Configuration: Codable { public var writeEmptyFiles: Bool? // FIXME: default it to false, but that plays not nice with Codable + public var unsignedNumbersMode: JExtractUnsignedIntegerMode? + public var effectiveUnsignedNumbersMode: JExtractUnsignedIntegerMode { + unsignedNumbersMode ?? .default + } + // ==== java 2 swift --------------------------------------------------------- /// The Java class path that should be passed along to the swift-java tool. diff --git a/Sources/JavaKitConfigurationShared/GenerationMode.swift b/Sources/JavaKitConfigurationShared/GenerationMode.swift index b4a964769..190323a05 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Sources/JavaKitConfigurationShared/GenerationMode.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +/// Determines which source generation mode JExtract should be using: JNI or Foreign Function and Memory. public enum JExtractGenerationMode: String, Codable { /// Foreign Value and Memory API case ffm @@ -19,3 +20,46 @@ public enum JExtractGenerationMode: String, Codable { /// Java Native Interface case jni } + +/// Configures how Swift unsigned integers should be extracted by jextract. +public enum JExtractUnsignedIntegerMode: String, Codable { + /// Treat unsigned Swift integers as their signed equivalents in Java signatures, + /// however annotate them using the `@Unsigned` annotation which serves as a hint + /// to users of APIs with unsigned integers that a given parameter or return type + /// is actually unsigned, and must be treated carefully. + /// + /// Specifically negative values of a `@Unchecked long` must be interpreted carefully as + /// a value larger than the Long.MAX_VALUE can represent in Java. + case annotate + + /// Wrap any unsigned Swift integer values in an explicit `Unsigned...` wrapper types. + /// + /// This mode trades off performance, due to needing to allocate the type-safe wrapper objects around + /// primitive values, however allows to retain static type information about the unsignedness of + /// unsigned number types in the Java side of generated bindings. + case wrapGuava + +// /// If possible, use a wider Java signed integer type to represent an Unsigned Swift integer type. +// /// For example, represent a Swift `UInt32` (width equivalent to Java `int`) as a Java signed `long`, +// /// because UInt32's max value is possible to be stored in a signed Java long (64bit). +// /// +// /// Since it is not possible to widen a value beyond 64bits (Java `long`), the Long type would be wrapped +// case widenOrWrap +// +// /// Similar to `widenOrWrap`, however instead of wrapping `UInt64` as an `UnsignedLong` in Java, +// /// only annotate it as `@Unsigned long`. +// case widenOrAnnotate +} + +extension JExtractUnsignedIntegerMode { + public var needsConversion: Bool { + switch self { + case .annotate: false + case .wrapGuava: true + } + } + + public static var `default`: JExtractUnsignedIntegerMode { + .annotate + } +} diff --git a/Sources/JavaTypes/JavaAnnotation.swift b/Sources/JavaTypes/JavaAnnotation.swift new file mode 100644 index 000000000..a643c2987 --- /dev/null +++ b/Sources/JavaTypes/JavaAnnotation.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// 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 annotation (e.g. `@Deprecated` or `@Unsigned`) +public struct JavaAnnotation: Equatable, Hashable { + public let type: JavaType + public let arguments: [String] + + public init(className name: some StringProtocol, arguments: [String] = []) { + type = JavaType(className: name) + self.arguments = arguments + } + + public func render() -> String { + guard let className = type.className else { + fatalError("Java annotation must have a className") + } + + var res = "@\(className)" + guard !arguments.isEmpty else { + return res + } + + res += "(" + res += arguments.joined(separator: ",") + res += ")" + return res + } + +} + +extension JavaAnnotation { + public static var unsigned: JavaAnnotation { + JavaAnnotation(className: "Unsigned") + } +} \ No newline at end of file diff --git a/Sources/JavaTypes/JavaType+JavaSource.swift b/Sources/JavaTypes/JavaType+JavaSource.swift index ccb4e96b5..f9e2e0cdc 100644 --- a/Sources/JavaTypes/JavaType+JavaSource.swift +++ b/Sources/JavaTypes/JavaType+JavaSource.swift @@ -60,7 +60,7 @@ extension JavaType: CustomStringConvertible { } } - /// Returns the a class name if this java type was a class, + /// Returns the class name if this java type was a class, /// and nil otherwise. public var className: String? { switch self { @@ -70,4 +70,17 @@ extension JavaType: CustomStringConvertible { return nil } } + + /// Returns the fully qualified class name if this java type was a class, + /// and nil otherwise. + public var fullyQualifiedClassName: String? { + switch self { + case .class(.some(let package), let name): + return "\(package).\(name)" + case .class(nil, let name): + return name + default: + return nil + } + } } diff --git a/Sources/JavaTypes/JavaType+SwiftNames.swift b/Sources/JavaTypes/JavaType+SwiftNames.swift index 492ff4595..20de73fc8 100644 --- a/Sources/JavaTypes/JavaType+SwiftNames.swift +++ b/Sources/JavaTypes/JavaType+SwiftNames.swift @@ -85,20 +85,32 @@ extension JavaType { } } - /// Try to map a Swift type name (e.g., from the module Swift) over to a - /// primitive Java type, or fail otherwise. - public init?(swiftTypeName: String) { - switch swiftTypeName { - case "Bool": self = .boolean - case "Int8": self = .byte - case "UInt16": self = .char - case "Int16": self = .short - case "Int32": self = .int - case "Int64": self = .long - case "Float": self = .float - case "Double": self = .double - case "Void": self = .void - default: return nil - } - } +} + +/// Determines how type conversion should deal with Swift's unsigned numeric types. +/// +/// When `ignoreSign` is used, unsigned Swift types are imported directly as their corresponding bit-width types, +/// which may yield surprising values when an unsigned Swift value is interpreted as a signed Java type: +/// - `UInt8` is imported as `byte` +/// - `UInt16` is imported as `char` (this is always correct, since `char` is unsigned in Java) +/// - `UInt32` is imported as `int` +/// - `UInt64` is imported as `long` +/// +/// When `wrapUnsignedGuava` is used, unsigned Swift types are imported as safe "wrapper" types from the popular Guava +/// library on the Java side. SwiftJava does not include these types, so you would have to make sure your project depends +/// on Guava for such generated code to be able to compile. +/// +/// These make the Unsigned nature of the types explicit in Java, however they come at a cost of allocating the wrapper +/// object, and indirection when accessing the underlying numeric value. These are often useful as a signal to watch out +/// when dealing with a specific API, however in high performance use-cases, one may want to choose using the primitive +/// values directly, and interact with them using {@code UnsignedIntegers} SwiftKit helper classes on the Java side. +/// +/// The type mappings in this mode are as follows: +/// - `UInt8` is imported as `com.google.common.primitives.UnsignedInteger` +/// - `UInt16` is imported as `char` (this is always correct, since `char` is unsigned in Java) +/// - `UInt32` is imported as `com.google.common.primitives.UnsignedInteger` +/// - `UInt64` is imported as `com.google.common.primitives.UnsignedLong` +public enum UnsignedNumericsMode { + case ignoreSign + case wrapUnsignedGuava } diff --git a/Sources/JavaTypes/JavaType.swift b/Sources/JavaTypes/JavaType.swift index 6c5f5357c..2a2d901fe 100644 --- a/Sources/JavaTypes/JavaType.swift +++ b/Sources/JavaTypes/JavaType.swift @@ -13,13 +13,15 @@ //===----------------------------------------------------------------------===// /// Describes the Java type system. +/// +/// Some types may need to be annotated when in parameter position, public enum JavaType: Equatable, Hashable { case boolean - case byte - case char - case short - case int - case long + case byte(parameterAnnotations: [JavaAnnotation]) + case char(parameterAnnotations: [JavaAnnotation]) + case short(parameterAnnotations: [JavaAnnotation]) + case int(parameterAnnotations: [JavaAnnotation]) + case long(parameterAnnotations: [JavaAnnotation]) case float case double case void @@ -31,6 +33,12 @@ public enum JavaType: Equatable, Hashable { /// A Java array. indirect case array(JavaType) + public static var byte: JavaType { .byte(parameterAnnotations: []) } + public static var char: JavaType { .char(parameterAnnotations: []) } + public static var short: JavaType { .short(parameterAnnotations: []) } + public static var int: JavaType { .int(parameterAnnotations: []) } + public static var long: JavaType { .long(parameterAnnotations: []) } + /// Given a class name such as "java.lang.Object", split it into /// its package and class name to form a class instance. public init(className name: some StringProtocol) { @@ -45,6 +53,21 @@ public enum JavaType: Equatable, Hashable { } } +extension JavaType { + /// List of Java annotations this type should have include in parameter position, + /// e.g. `void example(@Unsigned long num)` + public var parameterAnnotations: [JavaAnnotation] { + switch self { + case .byte(let parameterAnnotations): return parameterAnnotations + case .char(let parameterAnnotations): return parameterAnnotations + case .short(let parameterAnnotations): return parameterAnnotations + case .int(let parameterAnnotations): return parameterAnnotations + case .long(let parameterAnnotations): return parameterAnnotations + default: return [] + } + } +} + extension JavaType { /// Whether this is a primitive Java type. public var isPrimitive: Bool { @@ -57,3 +80,4 @@ extension JavaType { } } } + diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index e7acb3eb9..b3d02276f 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -45,7 +45,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Swift Feature | FFM | JNI | -|--------------------------------------------------------------------------------------| -------- |-----| +|--------------------------------------------------------------------------------------|----------|-----| | Initializers: `class`, `struct` | ✅ | ✅ | | Optional Initializers / Throwing Initializers | ❌ | ❌ | | Deinitializers: `class`, `struct` | ✅ | ✅ | @@ -67,7 +67,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | | Parameters: JavaKit wrapped types `JavaLong`, `JavaInteger` | ❌ | ✅ | | Return values: JavaKit wrapped types `JavaLong`, `JavaInteger` | ❌ | ❌ | -| Unsigned primitive types: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` | ❌ | ❌ | +| Unsigned primitive types: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` | ✅ * | ✅ * | | String (with copying data) | ✅ | ✅ | | Variadic parameters: `T...` | ❌ | ❌ | | Parametrer packs / Variadic generics | ❌ | ❌ | @@ -76,14 +76,14 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Operators: `+`, `-`, user defined | ❌ | ❌ | | Subscripts: `subscript()` | ❌ | ❌ | | Equatable | ❌ | ❌ | -| Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | +| Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | | Nested types: `struct Hello { struct World {} }` | ❌ | ❌ | | Inheritance: `class Caplin: Capybara` | ❌ | ❌ | | Non-escaping `Void` closures: `func callMe(maybe: () -> ())` | ✅ | ✅ | | 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 | ✅ | ✅ | @@ -94,3 +94,65 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | | | | > tip: The list of features may be incomplete, please file an issue if something is unclear or should be clarified in this table. + +## Detailed feature support discussion + +### Unsigned integers + +### Java <-> Swift Type mapping + +Java does not support unsigned numbers (other than the 16-bit wide `char`), and therefore mapping Swift's (and C) +unsigned integer types is somewhat problematic. + +SwiftJava's jextract mode, similar to OpenJDK jextract, does extract unsigned types from native code to Java +as their bit-width equivalents. This is potentially dangerous because values larger than the `MAX_VALUE` of a given +*signed* type in Java, e.g. `200` stored in an `UInt8` in Swift, would be interpreted as a `byte` of value `-56`, +because Java's `byte` type is _signed_. + +#### Unsigned numbers mode: annotate (default) + +Because in many situations the data represented by such numbers is merely passed along, and not interpreted by Java, +this may be safe to pass along. However, interpreting unsigned values incorrectly like this can lead to subtle mistakes +on the Java side. + +| Swift type | Java type | +|------------|-----------| +| `Int8` | `byte` | +| `UInt8` | `byte` ⚠️ | +| `Int16` | `short` | +| `UInt16` | `char` | +| `Int32` | `int` | +| `UInt32` | `int` ⚠️ | +| `Int64` | `long` | +| `UInt64` | `long` ⚠️ | +| `Float` | `float` | +| `Double` | `double` | + +#### Unsigned numbers mode: wrap-guava + +You can configure `jextract` (in FFM mode) to instead import unsigned values as their unsigned type-safe representations +as offered by the Guava library: `UnsignedLong` or `UnsignedInt`. To enable this mode pass the `--unsigned-numbers wrap-guava` +command line option, or set the corresponding configuration value in `swift-java.config` (TODO). + +This approach is type-safe, however it incurs a performance penalty for allocating a wrapper class for every +unsigned integer parameter passed to and from native Swift functions. + +SwiftJava _does not_ vendor or provide the Guava library as a dependency, and when using this mode +you are expected to add a Guava dependency to your Java project. + +> You can read more about the unsigned integers support + +| Swift type | Java type | +|------------|--------------------------------------------------------| +| `Int8` | `byte` | +| `UInt8` | `com.google.common.primitives.UnsignedInteger` (class) | +| `Int16` | `short` | +| `UInt16` | `char` | +| `Int32` | `int` | +| `UInt32` | `com.google.common.primitives.UnsignedInteger` (class)️ | +| `Int64` | `long` | +| `UInt64` | `com.google.common.primitives.UnsignedLong` (class) | +| `Float` | `float` | +| `Double` | `double` | + +> Note: The `wrap-guava` mode is currently only available in FFM mode of jextract. diff --git a/Sources/SwiftJavaLib/JavaClassTranslator.swift b/Sources/SwiftJavaLib/JavaClassTranslator.swift index bfd1657f5..ea6ca4810 100644 --- a/Sources/SwiftJavaLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaLib/JavaClassTranslator.swift @@ -517,7 +517,7 @@ extension JavaClassTranslator { package func renderConstructor( _ javaConstructor: Constructor ) throws -> DeclSyntax { - let parameters = try translateParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment? = nil"] + let parameters = try translateJavaParameters(javaConstructor.getParameters()) + ["environment: JNIEnvironment? = nil"] let parametersStr = parameters.map { $0.description }.joined(separator: ", ") let throwsStr = javaConstructor.throwsCheckedException ? "throws" : "" let accessModifier = javaConstructor.isPublic ? "public " : "" @@ -537,7 +537,7 @@ extension JavaClassTranslator { whereClause: String = "" ) throws -> DeclSyntax { // Map the parameters. - let parameters = try translateParameters(javaMethod.getParameters()) + let parameters = try translateJavaParameters(javaMethod.getParameters()) let parametersStr = parameters.map { $0.description }.joined(separator: ", ") @@ -700,7 +700,7 @@ extension JavaClassTranslator { } // Translate a Java parameter list into Swift parameters. - private func translateParameters(_ parameters: [Parameter?]) throws -> [FunctionParameterSyntax] { + private func translateJavaParameters(_ parameters: [Parameter?]) throws -> [FunctionParameterSyntax] { return try parameters.compactMap { javaParameter in guard let javaParameter else { return nil } diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index e4901bc2e..03ca0cd71 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -61,6 +61,9 @@ extension SwiftJava { @Flag(help: "Some build systems require an output to be present when it was 'expected', even if empty. This is used by the JExtractSwiftPlugin build plugin, but otherwise should not be necessary.") var writeEmptyFiles: Bool = false + @Option(help: "The mode of generation to use for the output files. Used with jextract mode. By default, unsigned Swift types are imported as their bit-width compatible signed Java counterparts, and annotated using the '@Unsigned' annotation. You may choose the 'wrap-guava' mode in order to import types as class wrapper types (`UnsignedInteger` et al) defined by the Google Guava library's `com.google.common.primitives' package. that ensure complete type-safety with regards to unsigned values, however they incur an allocation and performance overhead.") + var unsignedNumbers: JExtractUnsignedIntegerMode = .default + @Option( help: """ A swift-java configuration file for a given Swift module name on which this module depends, @@ -81,6 +84,12 @@ extension SwiftJava.JExtractCommand { config.outputJavaDirectory = outputJava config.outputSwiftDirectory = outputSwift config.writeEmptyFiles = writeEmptyFiles + config.unsignedNumbersMode = unsignedNumbers + + guard checkModeCompatibility() else { + // check would have logged the reason for early exit. + return + } if let inputSwift = commonOptions.inputSwift { config.inputSwiftDirectory = inputSwift @@ -97,6 +106,28 @@ extension SwiftJava.JExtractCommand { try jextractSwift(config: config, dependentConfigs: dependentConfigs.map(\.1)) } + + /// Check if the configured modes are compatible, and fail if not + func checkModeCompatibility() -> Bool { + if self.mode == .jni { + switch self.unsignedNumbers { + case .annotate: + print("Error: JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage)") + return false + case .wrapGuava: + () // OK + } + } + + return true + } +} + +struct IncompatibleModeError: Error { + let message: String + init(_ message: String) { + self.message = message + } } extension SwiftJava.JExtractCommand { @@ -110,3 +141,4 @@ extension SwiftJava.JExtractCommand { } extension JExtractGenerationMode: ExpressibleByArgument {} +extension JExtractUnsignedIntegerMode: ExpressibleByArgument {} diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index 4f022d717..3eb01fe75 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -148,7 +148,7 @@ extension SwiftJava.ResolveCommand { } else { let suggestDisablingSandbox = "It may be that the Sandbox has prevented dependency fetching, please re-run with '--disable-sandbox'." fatalError("Gradle output had no SWIFT_JAVA_CLASSPATH! \(suggestDisablingSandbox). \n" + - "Output was:<<<\(outString ?? "")>>>; Err was:<<<\(errString ?? "")>>>") + "Output was:<<<\(outString)>>>; Err was:<<<\(errString ?? "")>>>") } return String(classpathOutput.dropFirst(SwiftJavaClasspathPrefix.count)) diff --git a/Sources/_Subprocess/Configuration.swift b/Sources/_Subprocess/Configuration.swift index 4dad1a479..ba6f15bab 100644 --- a/Sources/_Subprocess/Configuration.swift +++ b/Sources/_Subprocess/Configuration.swift @@ -40,7 +40,7 @@ public struct Configuration: Sendable { public var environment: Environment /// The working directory to use when running the executable. public var workingDirectory: FilePath - /// The platform specifc options to use when + /// The platform specific options to use when /// running the subprocess. public var platformOptions: PlatformOptions diff --git a/SwiftKitCore/build.gradle b/SwiftKitCore/build.gradle index 7bab76e03..82874e20c 100644 --- a/SwiftKitCore/build.gradle +++ b/SwiftKitCore/build.gradle @@ -14,6 +14,7 @@ plugins { id("build-logic.java-application-conventions") + id("me.champeau.jmh") version "0.7.2" id("maven-publish") } @@ -45,12 +46,21 @@ java { languageVersion.set(JavaLanguageVersion.of(17)) } // Support Android 6+ (Java 7) - sourceCompatibility = JavaVersion.VERSION_1_7 - targetCompatibility = JavaVersion.VERSION_1_7 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { - testImplementation 'junit:junit:4.13.2' + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +testing { + suites { + test { + useJUnitJupiter('5.10.3') + } + } } tasks.test { diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/NotImplementedError.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/NotImplementedError.java new file mode 100644 index 000000000..6956eaca4 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/NotImplementedError.java @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// 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 NotImplementedError extends AssertionError { + + private static final long serialVersionUID = 1L; + + public NotImplementedError(String message) { + super(message); + } +} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/Preconditions.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/Preconditions.java new file mode 100644 index 000000000..bd1e52b07 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/Preconditions.java @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package org.swift.swiftkit.core; + +import org.swift.swiftkit.core.annotations.Nullable; + +/** + * Collection of convenience functions to check argument preconditions. + *

+ * Partially based on {@code com.google.common.base.Preconditions}. + */ +public final class Preconditions { + private Preconditions() { + } + + public static void checkArgument(boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + public static void checkArgument(boolean expression, @Nullable String format) { + if (!expression) { + throw new IllegalArgumentException(format); + } + } + + public static void checkArgument(boolean expression, @Nullable String format, + @Nullable Object arg1) { + if (!expression) { + throw new IllegalArgumentException(String.format(format, arg1)); + } + } + + public static void checkArgument(boolean expression, @Nullable String format, + @Nullable Object arg1, + @Nullable Object arg2) { + if (!expression) { + throw new IllegalArgumentException(String.format(format, arg1, arg2)); + } + } + + public static T checkNotNull(@Nullable T reference) { + if (reference == null) { + throw new NullPointerException(); + } + + return reference; + } + + public static T checkNotNull(@Nullable T reference, @Nullable String message) { + if (reference == null) { + throw new NullPointerException(message); + } + + return reference; + } + + /* + * All recent hotspots (as of 2009) *really* like to have the natural code + * + * if (guardExpression) { + * throw new BadException(messageExpression); + * } + * + * refactored so that messageExpression is moved to a separate String-returning method. + * + * if (guardExpression) { + * throw new BadException(badMsg(...)); + * } + * + * The alternative natural refactorings into void or Exception-returning methods are much slower. + * This is a big deal - we're talking factors of 2-8 in microbenchmarks, not just 10-20%. (This is + * a hotspot optimizer bug, which should be fixed, but that's a separate, big project). + * + * The coding pattern above is heavily used in java.util, e.g. in ArrayList. There is a + * RangeCheckMicroBenchmark in the JDK that was used to test this. + * + * But the methods in this class want to throw different exceptions, depending on the args, so it + * appears that this pattern is not directly applicable. But we can use the ridiculous, devious + * trick of throwing an exception in the middle of the construction of another exception. Hotspot + * is fine with that. + */ + + /** + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list or string + * @param size the size of that array, list or string + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkElementIndex(int index, int size) { + return checkElementIndex(index, size, "index"); + } + + /** + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. + * + * @param index a user-supplied index identifying an element of an array, list or string + * @param size the size of that array, list or string + * @param desc the text to use to describe this index in an error message + * @return the value of {@code index} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} + * @throws IllegalArgumentException if {@code size} is negative + */ + public static int checkElementIndex(int index, int size, String desc) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException( + String.format("%s, index:%d, size:%d", desc, index, size)); + } + return index; + } + + public static void checkPositionIndexes(int start, int end, int size) { + // Carefully optimized for execution by hotspot (explanatory comment above) + if (start < 0 || end < start || end > size) { + throw new IndexOutOfBoundsException( + String.format("Start index:%d, end index:%d, size: %d", start, end, size)); + } + } + +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/NonNull.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/NonNull.java new file mode 100644 index 000000000..cad6cd8bf --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/NonNull.java @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +// TODO: Consider depending on jspecify instead +@Documented +@Target(TYPE_USE) +@Retention(RUNTIME) +public @interface NonNull {} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Nullable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Nullable.java new file mode 100644 index 000000000..c20ad884c --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Nullable.java @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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.annotations; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +// TODO: Consider depending on jspecify instead +@Documented +@Target(TYPE_USE) +@Retention(RUNTIME) +public @interface Nullable {} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java new file mode 100644 index 000000000..4bf8e3544 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// 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.annotations; + +import jdk.jfr.Description; +import jdk.jfr.Label; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +/** + * Value is of an unsigned numeric type. + *

+ * This annotation is used to annotate java integer primitives when their + * corresponding Swift type was actually unsigned, e.g. an {@code @Unsigned long} + * in a method signature corresponds to a Swift {@code UInt64} type, and therefore + * negative values reported by the signed {@code long} should instead be interpreted positive values, + * larger than {@code Long.MAX_VALUE} that are just not representable using a signed {@code long}. + */ +@Documented +@Label("Unsigned integer type") +@Description("Value should be interpreted as unsigned data type") +@Target({TYPE_USE, PARAMETER, FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Unsigned { +} diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueLayout.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueLayout.java index c0a751442..be883e051 100644 --- a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueLayout.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftValueLayout.java @@ -34,11 +34,19 @@ public static long addressByteSize() { } public static final ValueLayout.OfBoolean SWIFT_BOOL = ValueLayout.JAVA_BOOLEAN; + public static final ValueLayout.OfByte SWIFT_INT8 = ValueLayout.JAVA_BYTE; - public static final ValueLayout.OfChar SWIFT_UINT16 = ValueLayout.JAVA_CHAR; + public static final ValueLayout.OfByte SWIFT_UINT8 = SWIFT_INT8; + public static final ValueLayout.OfShort SWIFT_INT16 = ValueLayout.JAVA_SHORT; + public static final ValueLayout.OfChar SWIFT_UINT16 = ValueLayout.JAVA_CHAR; + public static final ValueLayout.OfInt SWIFT_INT32 = ValueLayout.JAVA_INT; + public static final ValueLayout.OfInt SWIFT_UINT32 = SWIFT_INT32; + public static final ValueLayout.OfLong SWIFT_INT64 = ValueLayout.JAVA_LONG; + public static final ValueLayout.OfLong SWIFT_UINT64 = SWIFT_INT64; + public static final ValueLayout.OfFloat SWIFT_FLOAT = ValueLayout.JAVA_FLOAT; public static final ValueLayout.OfDouble SWIFT_DOUBLE = ValueLayout.JAVA_DOUBLE; diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index eebfdf4a9..0b8ca1d33 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -43,6 +43,7 @@ func assertLoweredFunction( translator.prepareForTranslation() let generator = FFMSwift2JavaGenerator( + config: config, translator: translator, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -128,6 +129,7 @@ func assertLoweredVariableAccessor( translator.prepareForTranslation() let generator = FFMSwift2JavaGenerator( + config: config, translator: translator, javaPackage: javaPackage, swiftOutputDirectory: "/fake", diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index ffefe9078..e975d2239 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -25,6 +25,7 @@ enum RenderKind { func assertOutput( dump: Bool = false, input: String, + config: Configuration? = nil, _ mode: JExtractGenerationMode, _ renderKind: RenderKind, swiftModuleName: String = "SwiftModule", @@ -36,8 +37,7 @@ func assertOutput( line: Int = #line, column: Int = #column ) throws { - var config = Configuration() - config.logLevel = .trace + var config = config ?? Configuration() config.swiftModule = swiftModuleName let translator = Swift2JavaTranslator(config: config) translator.dependenciesClasses = Array(javaClassLookupTable.keys) @@ -49,6 +49,7 @@ func assertOutput( switch mode { case .ffm: let generator = FFMSwift2JavaGenerator( + config: config, translator: translator, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -64,6 +65,7 @@ func assertOutput( case .jni: let generator = JNISwift2JavaGenerator( + config: config, translator: translator, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 1678f91f7..a87294b05 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -47,6 +47,7 @@ final class FuncCallbackImportTests { let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMe" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -129,13 +130,13 @@ final class FuncCallbackImportTests { var config = Configuration() config.swiftModule = "__FakeModule" let st = Swift2JavaTranslator(config: config) - st.log.logLevel = .error try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.name == "callMeMore" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -251,6 +252,7 @@ final class FuncCallbackImportTests { let funcDecl = st.importedGlobalFuncs.first { $0.name == "withBuffer" }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index 6854b2603..a8d83a2a4 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -246,6 +246,7 @@ extension FunctionDescriptorTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: javaPackage, swiftOutputDirectory: "/fake", @@ -275,6 +276,7 @@ extension FunctionDescriptorTests { try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: javaPackage, swiftOutputDirectory: "/fake", diff --git a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift index cfe78fbd6..483d53f5a 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift @@ -18,78 +18,92 @@ import Testing @Suite struct JNIClassTests { let source = """ - public class MyClass { - let x: Int64 - let y: Int64 - - public static func method() {} - - public init(x: Int64, y: Int64) { - self.x = y - self.y = y - } - - public init() { - self.x = 0 - self.y = 0 + public class MyClass { + let x: Int64 + let y: Int64 + + public static func method() {} + + public init(x: Int64, y: Int64) { + self.x = y + self.y = y + } + + public init() { + self.x = 0 + self.y = 0 + } + + public func doSomething(x: Int64) {} + + public func copy() -> MyClass {} + public func isEqual(to other: MyClass) -> Bool {} } - - public func doSomething(x: Int64) {} - - public func copy() -> MyClass {} - public func isEqual(to other: MyClass) -> Bool {} - } - """ + """ @Test func generatesJavaClass() throws { - try assertOutput(input: source, .jni, .java, expectedChunks: [ - """ - // Generated by jextract-swift - // Swift module: SwiftModule + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule - package com.example.swift; - - import org.swift.swiftkit.core.*; - import org.swift.swiftkit.core.util.*; + package com.example.swift; - public final class MyClass extends JNISwiftInstance { - static final String LIB_NAME = "SwiftModule"; - - @SuppressWarnings("unused") - private static final boolean INITIALIZED_LIBS = initializeLibs(); - static boolean initializeLibs() { - System.loadLibrary(LIB_NAME); - return true; - } - - public MyClass(long selfPointer, SwiftArena swiftArena) { - super(selfPointer, swiftArena); - } - """, - """ - private static native void $destroy(long selfPointer); - """, - """ - @Override - protected Runnable $createDestroyFunction() { - long self$ = this.$memoryAddress(); - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("MyClass.$createDestroyFunction", - "this", this, - "self", self$); - } - return new Runnable() { - @Override - public void run() { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("MyClass.$destroy", "self", self$); - } - MyClass.$destroy(self$); + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + """, + """ + public final class MyClass extends JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(LIB_NAME); + return true; + } + + public MyClass(long selfPointer, SwiftArena swiftArena) { + super(selfPointer, swiftArena); + } + """, + ]) + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + private static native void $destroy(long selfPointer); + """ + ]) + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + @Override + protected Runnable $createDestroyFunction() { + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyClass.$createDestroyFunction", + "this", this, + "self", self$); } - }; - """ - ]) + return new Runnable() { + @Override + public void run() { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyClass.$destroy", "self", self$); + } + MyClass.$destroy(self$); + } + }; + """ + ]) } @Test @@ -101,18 +115,18 @@ struct JNIClassTests { expectedChunks: [ """ /** - * Downcall to Swift: - * {@snippet lang=swift : - * public static func method() - * } - */ + * Downcall to Swift: + * {@snippet lang=swift : + * public static func method() + * } + */ public static void method() { MyClass.$method(); } """, """ private static native void $method(); - """ + """, ] ) } @@ -169,7 +183,7 @@ struct JNIClassTests { """, """ private static native long $init(); - """ + """, ] ) } @@ -199,7 +213,7 @@ struct JNIClassTests { let resultBits$ = Int64(Int(bitPattern: result$)) return resultBits$.getJNIValue(in: environment!) } - """ + """, ] ) } @@ -251,7 +265,7 @@ struct JNIClassTests { """, """ private static native void $doSomething(long x, long self); - """ + """, ] ) } @@ -274,7 +288,7 @@ struct JNIClassTests { } self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment!)) } - """, + """ ] ) } @@ -299,7 +313,7 @@ struct JNIClassTests { """, """ private static native long $copy(long self); - """ + """, ] ) } @@ -325,7 +339,7 @@ struct JNIClassTests { let resultBits$ = Int64(Int(bitPattern: result$)) return resultBits$.getJNIValue(in: environment!) } - """, + """ ] ) } @@ -350,7 +364,7 @@ struct JNIClassTests { """, """ private static native boolean $isEqual(long other, long self); - """ + """, ] ) } @@ -378,7 +392,7 @@ struct JNIClassTests { } return self$.pointee.isEqual(to: other$.pointee).getJNIValue(in: environment!) } - """, + """ ] ) } diff --git a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift index 4696253cf..be9cf0ce6 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift @@ -43,6 +43,10 @@ struct JNIModuleTests { package com.example.swift; + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + import org.swift.swiftkit.core.annotations.*; + public final class SwiftModule { static final String LIB_NAME = "SwiftModule"; @@ -81,6 +85,7 @@ struct JNIModuleTests { * public func takeIntegers(i1: Int8, i2: Int16, i3: Int32, i4: Int64) -> UInt16 * } */ + @Unsigned public static char takeIntegers(byte i1, short i2, int i3, long i4) { return SwiftModule.$takeIntegers(i1, i2, i3, i4); } diff --git a/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift index a4084654c..09a8626d3 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift @@ -18,69 +18,81 @@ import Testing @Suite struct JNIStructTests { let source = """ - public struct MyStruct { - let x: Int64 - let y: Int64 - - public init(x: Int64, y: Int64) { - self.x = y - self.y = y + public struct MyStruct { + let x: Int64 + let y: Int64 + + public init(x: Int64, y: Int64) { + self.x = y + self.y = y + } + + public func doSomething(x: Int64) {} } - - public func doSomething(x: Int64) {} - } - """ + """ @Test func generatesJavaClass() throws { - try assertOutput(input: source, .jni, .java, expectedChunks: [ - """ - // Generated by jextract-swift - // Swift module: SwiftModule + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule - package com.example.swift; - - import org.swift.swiftkit.core.*; - import org.swift.swiftkit.core.util.*; + package com.example.swift; - public final class MyStruct extends JNISwiftInstance { - static final String LIB_NAME = "SwiftModule"; - - @SuppressWarnings("unused") - private static final boolean INITIALIZED_LIBS = initializeLibs(); - static boolean initializeLibs() { - System.loadLibrary(LIB_NAME); - return true; - } - - public MyStruct(long selfPointer, SwiftArena swiftArena) { - super(selfPointer, swiftArena); - } - """, - """ - private static native void $destroy(long selfPointer); - """, - """ - @Override - protected Runnable $createDestroyFunction() { - long self$ = this.$memoryAddress(); - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("MyStruct.$createDestroyFunction", - "this", this, - "self", self$); - } - return new Runnable() { - @Override - public void run() { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall("MyStruct.$destroy", "self", self$); - } - MyStruct.$destroy(self$); + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + """,]) + try assertOutput(input: source, .jni, .java, expectedChunks: [ + """ + public final class MyStruct extends JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(LIB_NAME); + return true; } - }; - } - """ - ]) + + public MyStruct(long selfPointer, SwiftArena swiftArena) { + super(selfPointer, swiftArena); + } + """ + ]) + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + private static native void $destroy(long selfPointer); + """ + ]) + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + @Override + protected Runnable $createDestroyFunction() { + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyStruct.$createDestroyFunction", + "this", this, + "self", self$); + } + return new Runnable() { + @Override + public void run() { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyStruct.$destroy", "self", self$); + } + MyStruct.$destroy(self$); + } + }; + } + """ + ]) } @Test @@ -175,7 +187,7 @@ struct JNIStructTests { """, """ private static native void $doSomething(long x, long self); - """ + """, ] ) } @@ -198,7 +210,7 @@ struct JNIStructTests { } self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment!)) } - """, + """ ] ) } diff --git a/Tests/JExtractSwiftTests/JNI/JNIUnsignedNumberTests.swift b/Tests/JExtractSwiftTests/JNI/JNIUnsignedNumberTests.swift new file mode 100644 index 000000000..f4dbffed3 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIUnsignedNumberTests.swift @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import JavaKitConfigurationShared +import Testing + +final class JNIUnsignedNumberTests { + + @Test("Import: UInt16 (char)") + func jni_unsignedChar() throws { + try assertOutput( + input: "public func unsignedChar(_ arg: UInt16)", + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func unsignedChar(_ arg: UInt16) + * } + */ + public static void unsignedChar(@Unsigned char arg) { + SwiftModule.$unsignedChar(arg); + } + """, + """ + private static native void $unsignedChar(char arg); + """, + ] + ) + } + + @Test("Import: UInt32 (annotate)") + func jni_unsignedInt_annotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + config.logLevel = .trace + + try assertOutput( + input: "public func unsignedInt(_ arg: UInt32)", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func unsignedInt(_ arg: UInt32) + * } + */ + public static void unsignedInt(@Unsigned int arg) { + SwiftModule.$unsignedInt(arg); + } + private static native void $unsignedInt(int arg); + """, + ] + ) + } + + @Test("Import: return UInt32 (default)") + func jni_returnUnsignedIntDefault() throws { + let config = Configuration() + + try assertOutput( + input: "public func returnUnsignedInt() -> UInt32", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func returnUnsignedInt() -> UInt32 + * } + */ + @Unsigned + public static int returnUnsignedInt() { + return SwiftModule.$returnUnsignedInt(); + } + private static native int $returnUnsignedInt(); + """, + ] + ) + } + + @Test("Import: return UInt64 (wrap, unsupported)") + func jni_return_unsignedLongWrap() throws { + var config = Configuration() + config.unsignedNumbersMode = .wrapGuava + + try assertOutput( + input: "public func returnUnsignedLong() -> UInt64", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + // we do not import in wrap mode + """ + public final class SwiftModule { + static final String LIB_NAME = "SwiftModule"; + + static { + System.loadLibrary(LIB_NAME); + } + + } + """, + ] + ) + } + + @Test("Import: take UInt64 return UInt32 (annotate)") + func jni_echo_unsignedLong_annotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func unsignedLong(first: UInt64, second: UInt32) -> UInt32", + config: config, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func unsignedLong(first: UInt64, second: UInt32) -> UInt32 + * } + */ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return SwiftModule.$unsignedLong(first, second); + } + private static native int $unsignedLong(long first, int second); + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift b/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift index 5757e8daa..9d2fcb227 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift @@ -20,6 +20,7 @@ struct JNIVariablesTests { let membersSource = """ public class MyClass { + public let someByte: UInt8 public let constant: Int64 public var mutable: Int64 public var computed: Int64 { diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 73357d6bd..fd885f1b5 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -73,6 +73,7 @@ final class MethodImportTests { try st.analyze(file: "Fake.swift", text: class_interfaceFile) let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -116,6 +117,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -157,6 +159,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -200,6 +203,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -243,6 +247,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -289,6 +294,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -331,6 +337,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -373,6 +380,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", @@ -417,6 +425,7 @@ final class MethodImportTests { }! let generator = FFMSwift2JavaGenerator( + config: config, translator: st, javaPackage: "com.example.swift", swiftOutputDirectory: "/fake", diff --git a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift new file mode 100644 index 000000000..11b91e53d --- /dev/null +++ b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift @@ -0,0 +1,268 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import JavaKitConfigurationShared +import Testing + +final class UnsignedNumberTests { + + @Test("Import: UInt16 (char)") + func unsignedChar() throws { + try assertOutput( + input: "public func unsignedChar(_ arg: UInt16)", + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedChar__(uint16_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedChar__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT16 + ); + """, + """ + public static void unsignedChar(@Unsigned char arg) { + swiftjava_SwiftModule_unsignedChar__.call(arg); + } + """, + ] + ) + } + + @Test("Import: UInt32 (wrap)") + func unsignedInt() throws { + var config = Configuration() + config.unsignedNumbersMode = .wrapGuava + + try assertOutput( + input: "public func unsignedInt(_ arg: UInt32)", + config: config, + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(com.google.common.primitives.UnsignedInteger arg) { + swiftjava_SwiftModule_unsignedInt__.call(UnsignedNumbers.toPrimitive(arg)); + } + """, + ] + ) + } + + @Test("Import: UInt32 (annotate)") + func unsignedIntAnnotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func unsignedInt(_ arg: UInt32)", + config: config, + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(@Unsigned int arg) { + swiftjava_SwiftModule_unsignedInt__.call(arg); + } + """, + ] + ) + } + + @Test("Import: return UInt32 (default)") + func returnUnsignedIntDefault() throws { + let config = Configuration() + + try assertOutput( + input: "public func returnUnsignedInt() -> UInt32", + config: config, + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_returnUnsignedInt(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedInt { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int returnUnsignedInt() { + return swiftjava_SwiftModule_returnUnsignedInt.call(); + } + """, + ] + ) + } + + @Test("Import: return UInt64 (wrap)") + func return_unsignedLongWrap() throws { + var config = Configuration() + config.unsignedNumbersMode = .wrapGuava + + try assertOutput( + input: "public func returnUnsignedLong() -> UInt64", + config: config, + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static com.google.common.primitives.UnsignedLong returnUnsignedLong() { + return com.google.common.primitives.UnsignedLong.fromLongBits(swiftjava_SwiftModule_returnUnsignedLong.call()); + } + """, + ] + ) + } + + @Test("Import: return UInt64 (annotate)") + func return_unsignedLong_annotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func returnUnsignedLong() -> UInt64", + config: config, + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + @Unsigned + public static long returnUnsignedLong() { + return swiftjava_SwiftModule_returnUnsignedLong.call(); + } + """, + ] + ) + } + + @Test("Import: take UInt64 (annotate)") + func take_unsignedLong_annotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func takeUnsignedLong(arg: UInt64)", + config: config, + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_takeUnsignedLong_arg(uint64_t arg) + * } + */ + private static class swiftjava_SwiftModule_takeUnsignedLong_arg { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static void takeUnsignedLong(@Unsigned long arg) { + swiftjava_SwiftModule_takeUnsignedLong_arg.call(arg); + } + """, + ] + ) + } + + @Test("Import: take UInt64 return UInt32 (annotate)") + func echo_unsignedLong_annotate() throws { + var config = Configuration() + config.unsignedNumbersMode = .annotate + + try assertOutput( + input: "public func unsignedLong(first: UInt64, second: UInt32) -> UInt32", + config: config, + .ffm, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_unsignedLong_first_second(uint64_t first, uint32_t second) + * } + */ + private static class swiftjava_SwiftModule_unsignedLong_first_second { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + /* first: */SwiftValueLayout.SWIFT_UINT64 + /* second: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); + } + """, + ] + ) + } +}