diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index e3fcea92..b536abc5 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -125,6 +125,33 @@ public func globalStringIdentity(string: String) -> String { string } +// ==== ----------------------------------------------------------------------- +// MARK: Throwing functions + +public struct SwiftExampleError: Error { + public let message: String +} + +public func globalThrowingVoid(doThrow: Bool) throws { + if doThrow { + throw SwiftExampleError(message: "expected error in globalThrowingVoid") + } +} + +public func globalThrowingReturn(doThrow: Bool) throws -> Int { + if doThrow { + throw SwiftExampleError(message: "expected error in globalThrowingReturn") + } + return 42 +} + +public func globalThrowingString(doThrow: Bool) throws -> String { + if doThrow { + throw SwiftExampleError(message: "expected error in globalThrowingString") + } + return "Hello from throwing Swift!" +} + // ==== ----------------------------------------------------------------------- // MARK: Overloaded functions diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index 35576ed2..e0decfb3 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.swift.swiftkit.ffm.generated.SwiftJavaErrorException; import java.util.concurrent.CountDownLatch; @@ -95,4 +96,56 @@ public void apply() { assertEquals(0, countDownLatch.getCount()); } + // ==== ---------------------------------------------------------------- + // Throwing functions + + @Test + void call_globalThrowingVoid_noThrow() throws SwiftJavaErrorException { + MySwiftLibrary.globalThrowingVoid(false); + } + + @Test + void call_globalThrowingVoid_throws() { + assertThrows(SwiftJavaErrorException.class, () -> { + MySwiftLibrary.globalThrowingVoid(true); + }); + } + + @Test + void call_globalThrowingReturn_noThrow() throws SwiftJavaErrorException { + long result = MySwiftLibrary.globalThrowingReturn(false); + assertEquals(42, result); + } + + @Test + void call_globalThrowingReturn_throws() { + assertThrows(SwiftJavaErrorException.class, () -> { + MySwiftLibrary.globalThrowingReturn(true); + }); + } + + @Test + void call_globalThrowingString_noThrow() throws SwiftJavaErrorException { + String result = MySwiftLibrary.globalThrowingString(false); + assertEquals("Hello from throwing Swift!", result); + } + + @Test + void call_globalThrowingString_throws() { + assertThrows(SwiftJavaErrorException.class, () -> { + MySwiftLibrary.globalThrowingString(true); + }); + } + + @Test + void call_globalThrowingString_throws_checkMessage() { + SwiftJavaErrorException error = assertThrows(SwiftJavaErrorException.class, () -> { + MySwiftLibrary.globalThrowingString(true); + }); + assertEquals( + "org.swift.swiftkit.ffm.generated.SwiftJavaErrorException: SwiftExampleError(message: \"expected error in globalThrowingString\")", + error.toString() + ); + } + } diff --git a/Sources/CodePrinting/CodePrinter.swift b/Sources/CodePrinting/CodePrinter.swift index 8a6f2e40..fc9cec38 100644 --- a/Sources/CodePrinting/CodePrinter.swift +++ b/Sources/CodePrinting/CodePrinter.swift @@ -107,6 +107,16 @@ public struct CodePrinter { print("}", .sloc, function: function, file: file, line: line) } + public mutating func printIfBlock( + _ condition: Any, + function: String = #function, + file: String = #fileID, + line: UInt = #line, + body: (inout CodePrinter) throws -> Void + ) rethrows { + try printBraceBlock("if (\(condition))", function: function, file: file, line: line, body: body) + } + public mutating func printParts( _ parts: String..., terminator: PrinterTerminator = .newLine, diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 52d12640..565c5c9d 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -22,12 +22,12 @@ extension FFMSwift2JavaGenerator { @_spi(Testing) public func lowerFunctionSignature( _ decl: FunctionDeclSyntax, - enclosingType: TypeSyntax? = nil + enclosingType: TypeSyntax? = nil, ) throws -> LoweredFunctionSignature { let signature = try SwiftFunctionSignature( decl, enclosingType: try enclosingType.map { try SwiftType($0, lookupContext: lookupContext) }, - lookupContext: lookupContext + lookupContext: lookupContext, ) return try CdeclLowering(symbolTable: lookupContext.symbolTable).lowerFunctionSignature(signature) } @@ -38,12 +38,12 @@ extension FFMSwift2JavaGenerator { @_spi(Testing) public func lowerFunctionSignature( _ decl: InitializerDeclSyntax, - enclosingType: TypeSyntax? = nil + enclosingType: TypeSyntax? = nil, ) throws -> LoweredFunctionSignature { let signature = try SwiftFunctionSignature( decl, enclosingType: try enclosingType.map { try SwiftType($0, lookupContext: lookupContext) }, - lookupContext: lookupContext + lookupContext: lookupContext, ) return try CdeclLowering(symbolTable: lookupContext.symbolTable).lowerFunctionSignature(signature) @@ -56,7 +56,7 @@ extension FFMSwift2JavaGenerator { public func lowerFunctionSignature( _ decl: VariableDeclSyntax, isSet: Bool, - enclosingType: TypeSyntax? = nil + enclosingType: TypeSyntax? = nil, ) throws -> LoweredFunctionSignature? { let supportedAccessors = decl.supportedAccessorKinds(binding: decl.bindings.first!) guard supportedAccessors.contains(isSet ? .set : .get) else { @@ -67,7 +67,7 @@ extension FFMSwift2JavaGenerator { decl, isSet: isSet, enclosingType: try enclosingType.map { try SwiftType($0, lookupContext: lookupContext) }, - lookupContext: lookupContext + lookupContext: lookupContext, ) return try CdeclLowering(symbolTable: lookupContext.symbolTable).lowerFunctionSignature(signature) } @@ -101,7 +101,7 @@ struct CdeclLowering { convention: convention, parameterName: "self", genericParameters: signature.genericParameters, - genericRequirements: signature.genericRequirements + genericRequirements: signature.genericRequirements, ) case nil, .initializer(_), .staticMethod(_): nil @@ -114,23 +114,56 @@ struct CdeclLowering { convention: param.convention, parameterName: param.parameterName ?? "_\(index)", genericParameters: signature.genericParameters, - genericRequirements: signature.genericRequirements + genericRequirements: signature.genericRequirements, ) } + var isThrowing = false for effect in signature.effectSpecifiers { - // Prohibit any effects for now. - throw LoweringError.effectNotSupported(effect) + switch effect { + case .throws: + isThrowing = true + case .async: + throw LoweringError.effectNotSupported(effect) + } } // Lower the result. let loweredResult = try lowerResult(signature.result.type) + // If the function throws, create an error out parameter + let errorOutParameter: LoweredParameter? = + if isThrowing { + LoweredParameter( + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: "result$throws", + type: knownTypes.unsafeMutablePointer(knownTypes.optionalSugar(knownTypes.unsafeMutableRawPointer)), + ) + ], + conversion: .placeholder, + ) + } else { + nil + } + + // When throwing with a non-void pointer return, make the return type + // optional so the catch block can return nil (nullable pointer in C) + let cdeclReturnTypeForThunk: SwiftType + if isThrowing && loweredResult.cdeclResultType.isPointer { + cdeclReturnTypeForThunk = knownTypes.optionalSugar(loweredResult.cdeclResultType) + } else { + cdeclReturnTypeForThunk = loweredResult.cdeclResultType + } + return LoweredFunctionSignature( original: signature, selfParameter: loweredSelf, parameters: loweredParameters, - result: loweredResult + result: loweredResult, + errorOutParameter: errorOutParameter, + cdeclReturnTypeForThunk: cdeclReturnTypeForThunk, ) } @@ -149,7 +182,7 @@ struct CdeclLowering { convention: SwiftParameterConvention, parameterName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> LoweredParameter { // If there is a 1:1 mapping between this Swift type and a C type, we just // return it. @@ -157,7 +190,7 @@ struct CdeclLowering { if convention != .inout { return LoweredParameter( cdeclParameters: [SwiftParameter(convention: .byValue, parameterName: parameterName, type: type)], - conversion: .placeholder + conversion: .placeholder, ) } } @@ -169,10 +202,10 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: knownTypes.unsafeRawPointer + type: knownTypes.unsafeRawPointer, ) ], - conversion: .unsafeCastPointer(.placeholder, swiftType: instanceType) + conversion: .unsafeCastPointer(.placeholder, swiftType: instanceType), ) case .nominal(let nominal): @@ -191,10 +224,10 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer + type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer, ) ], - conversion: .typedPointer(.placeholder, swiftType: pointee) + conversion: .typedPointer(.placeholder, swiftType: pointee), ) case .unsafeBufferPointer(let element), .unsafeMutableBufferPointer(let element): @@ -205,12 +238,12 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_pointer", - type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer + type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer, ), SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_count", - type: knownTypes.int + type: knownTypes.int, ), ], conversion: .initialize( @@ -220,15 +253,15 @@ struct CdeclLowering { label: "start", argument: .typedPointer( .explodedComponent(.placeholder, component: "pointer"), - swiftType: element - ) + swiftType: element, + ), ), LabeledArgument( label: "count", - argument: .explodedComponent(.placeholder, component: "count") + argument: .explodedComponent(.placeholder, component: "count"), ), - ] - ) + ], + ), ) case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: @@ -239,12 +272,12 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_pointer", - type: knownTypes.optionalSugar(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer) + type: knownTypes.optionalSugar(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer), ), SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_count", - type: knownTypes.int + type: knownTypes.int, ), ], conversion: .initialize( @@ -252,14 +285,14 @@ struct CdeclLowering { arguments: [ LabeledArgument( label: "start", - argument: .explodedComponent(.placeholder, component: "pointer") + argument: .explodedComponent(.placeholder, component: "pointer"), ), LabeledArgument( label: "count", - argument: .explodedComponent(.placeholder, component: "count") + argument: .explodedComponent(.placeholder, component: "count"), ), - ] - ) + ], + ), ) case .optional(let wrapped): @@ -268,7 +301,7 @@ struct CdeclLowering { convention: convention, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) case .string: @@ -278,15 +311,15 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: knownTypes.unsafePointer(knownTypes.int8) + type: knownTypes.unsafePointer(knownTypes.int8), ) ], conversion: .initialize( type, arguments: [ LabeledArgument(label: "cString", argument: .placeholder) - ] - ) + ], + ), ) case .array(let element) where element == knownTypes.uint8: @@ -295,12 +328,12 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_pointer", - type: knownTypes.unsafeRawPointer + type: knownTypes.unsafeRawPointer, ), SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_count", - type: knownTypes.int + type: knownTypes.int, ), ] @@ -309,28 +342,32 @@ struct CdeclLowering { arguments: [ LabeledArgument( label: "start", - argument: .explodedComponent(.placeholder, component: "pointer") + argument: .explodedComponent(.placeholder, component: "pointer"), ), LabeledArgument( label: "count", - argument: .explodedComponent(.placeholder, component: "count") + argument: .explodedComponent(.placeholder, component: "count"), ), - ] + ], ) let arrayInit = ConversionStep.initialize( type, - arguments: [LabeledArgument(argument: bufferPointerInit)] + arguments: [LabeledArgument(argument: bufferPointerInit)], ) return LoweredParameter( cdeclParameters: cdeclParameters, - conversion: arrayInit + conversion: arrayInit, ) case .foundationData, .essentialsData: break + case .swiftJavaError: + // SwiftJavaError is a class — treat as arbitrary nominal type below + break + default: // Unreachable? Should be handled by `CType(cdeclType:)` lowering above. throw LoweringError.unhandledType(type) @@ -344,10 +381,10 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer + type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer, ) ], - conversion: .pointee(.typedPointer(.placeholder, swiftType: type)) + conversion: .pointee(.typedPointer(.placeholder, swiftType: type)), ) case .tuple(let tuple): @@ -357,7 +394,7 @@ struct CdeclLowering { convention: convention, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } if convention == .inout { @@ -373,7 +410,7 @@ struct CdeclLowering { convention: convention, parameterName: cdeclName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) parameters.append(contentsOf: lowered.cdeclParameters) @@ -388,24 +425,24 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: loweredTy + type: loweredTy, ) ], - conversion: conversion + conversion: conversion, ) case .opaque, .existential, .genericParameter: if let concreteTy = type.representativeConcreteTypeIn( knownTypes: knownTypes, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) { return try lowerParameter( concreteTy, convention: convention, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } throw LoweringError.unhandledType(type) @@ -424,7 +461,7 @@ struct CdeclLowering { convention: SwiftParameterConvention, parameterName: String, genericParameters: [SwiftGenericParameterDeclaration], - genericRequirements: [SwiftGenericRequirement] + genericRequirements: [SwiftGenericRequirement], ) throws -> LoweredParameter { // If there is a 1:1 mapping between this Swift type and a C type, lower it to 'UnsafePointer?' if let _ = try? CType(cdeclType: wrappedType) { @@ -433,10 +470,10 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: knownTypes.optionalSugar(knownTypes.unsafePointer(wrappedType)) + type: knownTypes.optionalSugar(knownTypes.unsafePointer(wrappedType)), ) ], - conversion: .pointee(.optionalChain(.placeholder)) + conversion: .pointee(.optionalChain(.placeholder)), ) } @@ -470,24 +507,24 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: knownTypes.optionalSugar(knownTypes.unsafeRawPointer) + type: knownTypes.optionalSugar(knownTypes.unsafeRawPointer), ) ], - conversion: .pointee(.typedPointer(.optionalChain(.placeholder), swiftType: wrappedType)) + conversion: .pointee(.typedPointer(.optionalChain(.placeholder), swiftType: wrappedType)), ) case .existential, .opaque, .genericParameter: if let concreteTy = wrappedType.representativeConcreteTypeIn( knownTypes: knownTypes, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) { return try lowerOptionalParameter( concreteTy, convention: convention, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } throw LoweringError.unhandledType(knownTypes.optionalSugar(wrappedType)) @@ -499,7 +536,7 @@ struct CdeclLowering { convention: convention, parameterName: parameterName, genericParameters: genericParameters, - genericRequirements: genericRequirements + genericRequirements: genericRequirements, ) } throw LoweringError.unhandledType(knownTypes.optionalSugar(wrappedType)) @@ -524,7 +561,7 @@ struct CdeclLowering { let loweredParam = try lowerClosureParameter( parameter.type, convention: parameter.convention, - parameterName: parameterName + parameterName: parameterName, ) parameters.append(contentsOf: loweredParam.cdeclParameters) parameterConversions.append(loweredParam.conversion) @@ -545,14 +582,14 @@ struct CdeclLowering { return ( type: .function(SwiftFunctionType(convention: .c, parameters: parameters, resultType: resultType)), conversion: isCompatibleWithC - ? .placeholder : .closureLowering(parameters: parameterConversions, result: resultConversion) + ? .placeholder : .closureLowering(parameters: parameterConversions, result: resultConversion), ) } func lowerClosureParameter( _ type: SwiftType, convention: SwiftParameterConvention, - parameterName: String + parameterName: String, ) throws -> LoweredParameter { // If there is a 1:1 mapping between this Swift type and a C type, we just // return it. @@ -562,10 +599,10 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: parameterName, - type: type + type: type, ) ], - conversion: .placeholder + conversion: .placeholder, ) } @@ -581,18 +618,18 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_pointer", - type: knownTypes.optionalSugar(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer) + type: knownTypes.optionalSugar(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer), ), SwiftParameter( convention: .byValue, parameterName: "\(parameterName)_count", - type: knownTypes.int + type: knownTypes.int, ), ], conversion: .tuplify([ .member(.placeholder, member: "baseAddress"), .member(.placeholder, member: "count"), - ]) + ]), ) case .foundationData, .essentialsData: @@ -621,12 +658,12 @@ struct CdeclLowering { parameterName: "\(outParameterName)_pointer", type: knownTypes.unsafeMutablePointer( knownTypes.optionalSugar(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer) - ) + ), ), SwiftParameter( convention: .byValue, parameterName: "\(outParameterName)_count", - type: knownTypes.unsafeMutablePointer(knownTypes.int) + type: knownTypes.unsafeMutablePointer(knownTypes.int), ), ] } @@ -638,7 +675,7 @@ struct CdeclLowering { /// - outParameterName: If the type is lowered to a indirect return, this parameter name should be used. func lowerResult( _ type: SwiftType, - outParameterName: String = "_result" + outParameterName: String = "_result", ) throws -> LoweredResult { // If there is a 1:1 mapping between this Swift type and a C type, we just // return it. @@ -653,7 +690,7 @@ struct CdeclLowering { return LoweredResult( cdeclResultType: knownTypes.unsafeRawPointer, cdeclOutParameters: [], - conversion: .unsafeCastPointer(.placeholder, swiftType: knownTypes.unsafeRawPointer) + conversion: .unsafeCastPointer(.placeholder, swiftType: knownTypes.unsafeRawPointer), ) case .nominal(let nominal): @@ -667,7 +704,7 @@ struct CdeclLowering { return LoweredResult( cdeclResultType: resultType, cdeclOutParameters: [], - conversion: .initialize(resultType, arguments: [LabeledArgument(argument: .placeholder)]) + conversion: .initialize(resultType, arguments: [LabeledArgument(argument: .placeholder)]), ) case .unsafeBufferPointer, .unsafeMutableBufferPointer: @@ -678,7 +715,7 @@ struct CdeclLowering { SwiftTupleElement(label: nil, type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer), SwiftTupleElement(label: nil, type: knownTypes.int), ]), - outParameterName: outParameterName + outParameterName: outParameterName, ) case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: @@ -691,15 +728,15 @@ struct CdeclLowering { [ .populatePointer( name: "\(outParameterName)_pointer", - to: .member(.placeholder, member: "baseAddress") + to: .member(.placeholder, member: "baseAddress"), ), .populatePointer( name: "\(outParameterName)_count", - to: .member(.placeholder, member: "count") + to: .member(.placeholder, member: "count"), ), ], - name: outParameterName - ) + name: outParameterName, + ), ) case .void: @@ -716,8 +753,8 @@ struct CdeclLowering { conversion: .method( base: "_swiftjava_stringToCString", methodName: nil, - arguments: [.init(label: nil, argument: .placeholder)] - ) + arguments: [.init(label: nil, argument: .placeholder)], + ), ) case .optional: @@ -733,7 +770,7 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: "\(outParameterName)_initialize", - type: knownTypes.functionInitializeByteBuffer + type: knownTypes.functionInitializeByteBuffer, ) ], conversion: .aggregate( @@ -752,15 +789,15 @@ struct CdeclLowering { arguments: [ .init(label: nil, argument: .member(.constant("_0"), member: "baseAddress!")), .init(label: nil, argument: .member(.constant("_0"), member: "count")), - ] - ) + ], + ), ) ) - ] + ], ) ], - name: resultName - ) + name: resultName, + ), ) default: @@ -776,10 +813,10 @@ struct CdeclLowering { SwiftParameter( convention: .byValue, parameterName: outParameterName, - type: knownTypes.unsafeMutableRawPointer + type: knownTypes.unsafeMutableRawPointer, ) ], - conversion: .populatePointer(name: outParameterName, assumingType: type, to: .placeholder) + conversion: .populatePointer(name: outParameterName, assumingType: type, to: .placeholder), ) case .tuple(let tuple): @@ -800,13 +837,13 @@ struct CdeclLowering { let parameter = SwiftParameter( convention: .byValue, parameterName: parameterName, - type: knownTypes.unsafeMutablePointer(lowered.cdeclResultType) + type: knownTypes.unsafeMutablePointer(lowered.cdeclResultType), ) parameters.append(parameter) conversions.append( .populatePointer( name: parameterName, - to: lowered.conversion + to: lowered.conversion, ) ) } else { @@ -819,7 +856,7 @@ struct CdeclLowering { return LoweredResult( cdeclResultType: .void, cdeclOutParameters: parameters, - conversion: .tupleExplode(conversions, name: outParameterName) + conversion: .tupleExplode(conversions, name: outParameterName), ) case .genericParameter, .function, .existential, .opaque, .composite: @@ -834,7 +871,7 @@ struct CdeclLowering { @_spi(Testing) public func cdeclToCFunctionLowering( _ cdeclSignature: SwiftFunctionSignature, - cName: String + cName: String, ) -> CFunction { try! CFunction(cdeclSignature: cdeclSignature, cName: cName) } @@ -887,6 +924,14 @@ public struct LoweredFunctionSignature: Equatable { var selfParameter: LoweredParameter? var parameters: [LoweredParameter] var result: LoweredResult + var errorOutParameter: LoweredParameter? + + /// The cdecl return type for the thunk. When the function is throwing and + /// returns a pointer, this is the optional-wrapped version of + /// `result.cdeclResultType` so the catch block can return nil + var cdeclReturnTypeForThunk: SwiftType + + var isThrowing: Bool { errorOutParameter != nil } var allLoweredParameters: [SwiftParameter] { var all: [SwiftParameter] = [] @@ -900,6 +945,10 @@ public struct LoweredFunctionSignature: Equatable { } // Out parameters. all += result.cdeclOutParameters + // Error out parameter (always last) + if let errorOutParameter { + all += errorOutParameter.cdeclParameters + } return all } @@ -907,10 +956,10 @@ public struct LoweredFunctionSignature: Equatable { SwiftFunctionSignature( selfParameter: nil, parameters: allLoweredParameters, - result: SwiftResult(convention: .direct, type: result.cdeclResultType), + result: SwiftResult(convention: .direct, type: cdeclReturnTypeForThunk), effectSpecifiers: [], genericParameters: [], - genericRequirements: [] + genericRequirements: [], ) } } @@ -921,11 +970,12 @@ extension LoweredFunctionSignature { package func cdeclThunk( cName: String, swiftAPIName: String, - as apiKind: SwiftAPIKind + as apiKind: SwiftAPIKind, ) -> FunctionDeclSyntax { let cdeclParams = allLoweredParameters.map(\.description).joined(separator: ", ") - let returnClause = !result.cdeclResultType.isVoid ? " -> \(result.cdeclResultType.description)" : "" + let cdeclReturnType = cdeclReturnTypeForThunk + let returnClause = !cdeclReturnType.isVoid ? " -> \(cdeclReturnType.description)" : "" var loweredCDecl = try! FunctionDeclSyntax( """ @@ -943,7 +993,7 @@ extension LoweredFunctionSignature { // Raise the 'self' from cdecl parameters. selfExpr = self.selfParameter!.conversion.asExprSyntax( placeholder: "self", - bodyItems: &bodyItems + bodyItems: &bodyItems, ) case .staticMethod(let selfType), .initializer(let selfType): selfExpr = "\(raw: selfType.description)" @@ -955,7 +1005,7 @@ extension LoweredFunctionSignature { let paramExprs = parameters.enumerated().map { idx, param in param.conversion.asExprSyntax( placeholder: original.parameters[idx].parameterName ?? "_\(idx)", - bodyItems: &bodyItems + bodyItems: &bodyItems, )! } @@ -1009,17 +1059,44 @@ extension LoweredFunctionSignature { } // Lower the result. + let tryKeyword: String = isThrowing ? "try " : "" if !original.result.type.isVoid { let loweredResult: ExprSyntax? = result.conversion.asExprSyntax( placeholder: resultExpr.description, - bodyItems: &bodyItems + bodyItems: &bodyItems, ) if let loweredResult { - bodyItems.append(!result.cdeclResultType.isVoid ? "return \(loweredResult)" : "\(loweredResult)") + let returnKeyword = !result.cdeclResultType.isVoid ? "return " : "" + bodyItems.append("\(raw: returnKeyword)\(raw: tryKeyword)\(loweredResult)") } } else { - bodyItems.append("\(resultExpr)") + bodyItems.append("\(raw: tryKeyword)\(resultExpr)") + } + + // If throwing, wrap body in do/catch. + if isThrowing { + let doBody = bodyItems.map { item in + item.with(\.leadingTrivia, [.newlines(1), .spaces(4)]) + } + + let dummyReturnStmt: String + if !result.cdeclResultType.isVoid { + let dummyReturn = result.cdeclResultType.isPointer ? "nil" : "0" + dummyReturnStmt = "\n return \(dummyReturn)" + } else { + dummyReturnStmt = "" + } + let doStmt: StmtSyntax = """ + do {\(CodeBlockItemListSyntax(doBody)) + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque()\(raw: dummyReturnStmt) + } + """ + + bodyItems = [ + CodeBlockItemSyntax(item: .stmt(doStmt)) + ] } loweredCDecl.body!.statements = CodeBlockItemListSyntax { diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 21ac993f..c3b60943 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -45,7 +45,7 @@ extension FFMSwift2JavaGenerator { // 'try!' because we know 'loweredSignature' can be described with C. let cFunc = try! translated.loweredSignature.cFunctionDecl(cName: thunkName) - printJavaBindingDescriptorClass(&printer, cFunc) { printer in + printJavaBindingDescriptorClass(&printer, cFunc, symbolLookup: currentSymbolLookup) { printer in if let outCallback = translated.translatedSignature.result.outCallback { self.printUpcallParameterDescriptorClasses(&printer, outCallback) } else { // FIXME: not an "else" @@ -58,8 +58,10 @@ extension FFMSwift2JavaGenerator { package func printJavaBindingDescriptorClass( _ printer: inout CodePrinter, _ cFunc: CFunction, + symbolLookup: SymbolLookupTarget = .module, additionalContent: ((inout CodePrinter) -> Void)? = nil, ) { + let lookup = symbolLookup.javaClassName(moduleName: self.swiftModuleName) printer.printBraceBlock( """ /** @@ -74,7 +76,7 @@ extension FFMSwift2JavaGenerator { printer.print( """ private static final MemorySegment ADDR = - \(self.swiftModuleName).findOrThrow("\(cFunc.name)"); + \(lookup).findOrThrow("\(cFunc.name)"); private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); """ ) @@ -382,8 +384,16 @@ extension FFMSwift2JavaGenerator { paramDecls.append("AllocatingSwiftArena swiftArena") } - let needsThrows = translatedSignature.parameters.contains { $0.needs32BitIntOverflowCheck != .none } || translatedSignature.result.needs32BitIntOverflowCheck != .none - let throwsClause = needsThrows ? " throws SwiftIntegerOverflowException" : "" + var throwsClauses: [String] = [] + // If a Swift function is 'throws' we throw a checked error for the Java side + // TODO: When we support typed throws on Swift side we'll want to throw the right type here instead + if translatedSignature.isThrowing { + throwsClauses.append(JavaType.swiftJavaErrorException.simpleClassName) + } + if translatedSignature.canThrowSwiftIntegerOverflowException { + throwsClauses.append(JavaType.swiftIntegerOverflowException.simpleClassName) + } + let throwsClause = throwsClauses.isEmpty ? "" : " throws \(throwsClauses.joined(separator: ", "))" TranslatedDocumentation.printDocumentation( importedFunc: decl, @@ -455,7 +465,7 @@ extension FFMSwift2JavaGenerator { } // FIXME: use trailing$ convention - let varName = outParameter.name.isEmpty ? "_result" : "_result_" + outParameter.name + let varName = outParameter.name.isEmpty ? "result$" : "result$_" + outParameter.name printer.print( "MemorySegment \(varName) = \(arena).allocate(\(memoryLayout));" @@ -466,9 +476,7 @@ extension FFMSwift2JavaGenerator { let thunkName = thunkNameRegistry.functionThunkName(decl: decl) if let outCallback = translatedSignature.result.outCallback { - let funcName = outCallback.name - assert(funcName.first == "$", "OutCallback names must start with $") - let varName = funcName.dropFirst() + let varName = outCallback.name downCallArguments.append( """ \(thunkName).\(outCallback.name).toUpcallStub(\(varName), arena$) @@ -476,9 +484,16 @@ extension FFMSwift2JavaGenerator { ) } + // Error out parameter for throwing functions. + if translatedSignature.isThrowing { + printer.print("MemorySegment result$throws = arena$.allocate(ValueLayout.ADDRESS);") + printer.print("result$throws.set(ValueLayout.ADDRESS, 0, MemorySegment.NULL);") + downCallArguments.append("result$throws") + } + let hasOverflowChecks = translatedSignature.parameters.contains { $0.needs32BitIntOverflowCheck != .none } if hasOverflowChecks { - printer.printBraceBlock("if (SwiftValueLayout.has32bitSwiftInt)") { printer in + printer.printIfBlock("SwiftValueLayout.has32bitSwiftInt") { printer in for (i, parameter) in translatedSignature.parameters.enumerated() { switch parameter.needs32BitIntOverflowCheck { case .none: @@ -486,13 +501,13 @@ extension FFMSwift2JavaGenerator { case .signedInt: let original = decl.functionSignature.parameters[i] let parameterName = original.parameterName ?? "_\(i)" - printer.printBraceBlock("if (\(parameterName) < Integer.MIN_VALUE || \(parameterName) > Integer.MAX_VALUE)") { printer in + printer.printIfBlock("\(parameterName) < Integer.MIN_VALUE || \(parameterName) > Integer.MAX_VALUE") { printer in printer.print("throw new SwiftIntegerOverflowException(\"Parameter '\(parameterName)' overflow: \" + \(parameterName));") } case .unsignedInt: let original = decl.functionSignature.parameters[i] let parameterName = original.parameterName ?? "_\(i)" - printer.printBraceBlock("if (\(parameterName) < 0 || \(parameterName) > 0xFFFFFFFFL)") { printer in + printer.printIfBlock("\(parameterName) < 0 || \(parameterName) > 0xFFFFFFFFL") { printer in printer.print("throw new SwiftIntegerOverflowException(\"Parameter '\(parameterName)' overflow: \" + \(parameterName));") } } @@ -503,25 +518,45 @@ extension FFMSwift2JavaGenerator { //=== Part 3: Downcall. let downCall = "\(thunkName).call(\(downCallArguments.joined(separator: ", ")))" + /// Helper to emit the error check after a downcall + func printErrorCheck(_ printer: inout CodePrinter) { + guard translatedSignature.isThrowing else { return } + printer.printIfBlock("!result$throws.get(ValueLayout.ADDRESS, 0).equals(MemorySegment.NULL)") { printer in + printer.print("throw new \(JavaType.swiftJavaErrorException.simpleClassName)(result$throws.get(ValueLayout.ADDRESS, 0), AllocatingSwiftArena.ofAuto());") + } + } + //=== Part 4: Convert the return value. if translatedSignature.result.javaResultType == .void { // Trivial downcall with no conversion needed, no callback either printer.print("\(downCall);") + printErrorCheck(&printer) } else { let placeholder: String let placeholderForDowncall: String? if let outCallback = translatedSignature.result.outCallback { - placeholder = "\(outCallback.name)" // the result will be read out from the _result_initialize java class + placeholder = "\(outCallback.name)" // the result will be read out from the result$initialize java class placeholderForDowncall = "\(downCall)" } else if translatedSignature.result.outParameters.isEmpty { - placeholder = downCall + if translatedSignature.isThrowing { + // When throwing, we must separate the downcall from result conversion + // so we can check the error pointer between them. + let cResultType = self.translatedDecl(for: decl)!.loweredSignature.result.cdeclResultType + let javaResultType = (try? CType(cdeclType: cResultType))?.javaType ?? .javaForeignMemorySegment + printer.print("var result$ = (\(javaResultType)) \(downCall);") + printErrorCheck(&printer) + placeholder = "result$" + } else { + placeholder = downCall + } placeholderForDowncall = nil } else { // FIXME: Support cdecl thunk returning a value while populating the out parameters. printer.print("\(downCall);") + printErrorCheck(&printer) placeholderForDowncall = nil - placeholder = "_result" + placeholder = "result$" } let result = translatedSignature.result.conversion.render( &printer, @@ -559,21 +594,21 @@ extension FFMSwift2JavaGenerator { case .none: printer.print("return \(value);") case .signedInt: - let resultVar = "_result$checked" + let resultVar = "result$checked" printer.print("long \(resultVar) = \(value);") - printer.printBraceBlock("if (SwiftValueLayout.has32bitSwiftInt)") { printer in - printer.printBraceBlock("if (\(resultVar) < Integer.MIN_VALUE || \(resultVar) > Integer.MAX_VALUE)") { printer in + printer.printIfBlock("SwiftValueLayout.has32bitSwiftInt") { printer in + printer.printIfBlock("\(resultVar) < Integer.MIN_VALUE || \(resultVar) > Integer.MAX_VALUE") { printer in printer.print("throw new SwiftIntegerOverflowException(\"Return value overflow: \" + \(resultVar));") } } printer.print("return \(resultVar);") case .unsignedInt: - let resultVar = "_result$checked" + let resultVar = "result$checked" printer.print("long \(resultVar) = \(value);") - printer.printBraceBlock("if (SwiftValueLayout.has32bitSwiftInt)") { printer in - printer.printBraceBlock("if (\(resultVar) < 0 || \(resultVar) > 0xFFFFFFFFL)") { printer in + printer.printIfBlock("SwiftValueLayout.has32bitSwiftInt") { printer in + printer.printIfBlock("\(resultVar) < 0 || \(resultVar) > 0xFFFFFFFFL") { printer in printer.print("throw new SwiftIntegerOverflowException(\"Return value overflow: \" + \(resultVar));") } } @@ -775,7 +810,7 @@ extension FFMSwift2JavaGenerator.JavaConversionStep { element.elementConversion.render( &printer, element.outParamName, - placeholderForDowncall: placeholderForDowncall + placeholderForDowncall: placeholderForDowncall, ) } return "\(tupleClassName)(\(args.joined(separator: ", ")))" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 80543a20..bce460f6 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -73,10 +73,10 @@ extension FFMSwift2JavaGenerator { /// /// 'JavaParameter.name' is the suffix for the receiver variable names. For example /// - /// var _result_pointer = MemorySegment.allocate(...) - /// var _result_count = MemorySegment.allocate(...) - /// downCall(_result_pointer, _result_count) - /// return constructResult(_result_pointer, _result_count) + /// var result$_pointer = MemorySegment.allocate(...) + /// var result$_count = MemorySegment.allocate(...) + /// downCall(result$_pointer, result$_count) + /// return constructResult(result$_pointer, result$_count) /// /// This case, there're two out parameter, named '_pointer' and '_count'. var outParameters: [JavaParameter] @@ -88,9 +88,9 @@ extension FFMSwift2JavaGenerator { /// After the call is made, we may need to further extact the result from the called-back-into /// Java function class, for example: /// - /// var _result_initialize = new $result_initialize.Function(); - /// downCall($result_initialize.toUpcallHandle(_result_initialize, arena)) - /// return _result_initialize.result + /// var result$initialize = new result$initialize.Function(); + /// downCall(result$initialize.toUpcallHandle(result$initialize, arena)) + /// return result$initialize.result /// var outCallback: OutCallback? @@ -131,6 +131,14 @@ extension FFMSwift2JavaGenerator { var selfParameter: TranslatedParameter? var parameters: [TranslatedParameter] var result: TranslatedResult + var isThrowing: Bool = false + + /// Whether any parameter or the result requires a 32-bit integer overflow check, + /// which means the Java method must declare `throws SwiftIntegerOverflowException` + var canThrowSwiftIntegerOverflowException: Bool { + parameters.contains { $0.needs32BitIntOverflowCheck != .none } + || result.needs32BitIntOverflowCheck != .none + } // if the result type implied any annotations, // propagate them onto the function the result is returned from @@ -333,7 +341,8 @@ extension FFMSwift2JavaGenerator { return TranslatedFunctionSignature( selfParameter: selfParameter, parameters: parameters, - result: result + result: result, + isThrowing: loweredFunctionSignature.isThrowing ) } @@ -433,7 +442,7 @@ extension FFMSwift2JavaGenerator { type: .javaLangString ) ], - conversion: .call(.placeholder, function: "SwiftRuntime.toCString", withArena: true) + conversion: .call(.placeholder, function: "SwiftStrings.toCString", withArena: true) ) case .array(let element) where element == knownTypes.uint8: @@ -456,6 +465,10 @@ extension FFMSwift2JavaGenerator { case .foundationData, .essentialsData: break + case .swiftJavaError: + // SwiftJavaError is a class — treat as arbitrary nominal type below + break + default: throw JavaTranslationError.unhandledType(swiftType) } @@ -749,7 +762,7 @@ extension FFMSwift2JavaGenerator { javaResultType: .javaLangString, annotations: resultAnnotations, outParameters: [], - conversion: .call(.placeholder, function: "SwiftRuntime.fromCString", withArena: false) + conversion: .call(.placeholder, function: "SwiftStrings.fromCString", withArena: false) ) case .array(let element) where element == knownTypes.uint8: @@ -759,7 +772,7 @@ extension FFMSwift2JavaGenerator { annotations: [.unsigned], outParameters: [], // no out parameters, but we do an "out" callback outCallback: OutCallback( - name: "$_result_initialize", + name: "result$initialize", members: [ "byte[] result = null" ], @@ -782,14 +795,14 @@ extension FFMSwift2JavaGenerator { conversion: .initializeResultWithUpcall( [ .introduceVariable( - name: "_result_initialize", + name: "result$initialize", initializeWith: .javaNew( .commaSeparated( [ // We need to refer to the nested class that is created for this function. // The class that contains all the related functional interfaces is called the same // as the downcall function, so we use the thunk name to find this class/ - .placeholderForSwiftThunkName, .constant("$_result_initialize.Function$Impl()"), + .placeholderForSwiftThunkName, .constant("result$initialize.Function$Impl()"), ], separator: "." ) @@ -797,7 +810,7 @@ extension FFMSwift2JavaGenerator { ), .placeholderForDowncall, // perform the downcall here ], - extractResult: .property(.constant("_result_initialize"), propertyName: "result") + extractResult: .property(.constant("result$initialize"), propertyName: "result") ) ) @@ -853,7 +866,7 @@ extension FFMSwift2JavaGenerator { for (idx, element) in elements.enumerated() { let (javaType, elementConversion) = try translateTupleElementResult(type: element.type) outParameters.append(JavaParameter(name: "\(idx)", type: javaType)) - tupleElements.append((outParamName: "_result_\(idx)", elementConversion: elementConversion)) + tupleElements.append((outParamName: "result$_\(idx)", elementConversion: elementConversion)) elementJavaTypes.append(javaType) } @@ -933,7 +946,7 @@ extension FFMSwift2JavaGenerator { /// The result of the function will be initialized with a callback to Java (an upcall). /// /// The `extractResult` is used for the actual `return ...` statement, because we need to extract - /// the return value from the called back into class, e.g. `return _result_initialize.result`. + /// the return value from the called back into class, e.g. `return result$initialize.result`. indirect case initializeResultWithUpcall([JavaConversionStep], extractResult: JavaConversionStep) /// 'value.$memorySegment()' @@ -1007,6 +1020,9 @@ extension FFMSwift2JavaGenerator.TranslatedFunctionSignature { /// Whether or not if the down-calling requires temporary "Arena" which is /// only used during the down-calling. var requiresTemporaryArena: Bool { + if self.isThrowing { + return true + } if self.parameters.contains(where: { $0.conversion.requiresTemporaryArena }) { return true } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 1d21f658..64a67266 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -40,7 +40,7 @@ extension FFMSwift2JavaGenerator { _ = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: expectedFileName + filename: expectedFileName, ) } } @@ -56,7 +56,7 @@ extension FFMSwift2JavaGenerator { if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: moduleFilename + filename: moduleFilename, ) { log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))") self.expectedOutputSwiftFileNames.remove(moduleFilename) @@ -68,9 +68,11 @@ extension FFMSwift2JavaGenerator { // === All types // We have to write all types to their corresponding output file that matches the file they were declared in, // because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input. + let filteredTypes = self.analysis.importedTypes + for group: (key: String, value: [Dictionary.Element]) in Dictionary( - grouping: self.analysis.importedTypes, - by: { $0.value.sourceFilePath } + grouping: filteredTypes, + by: { $0.value.sourceFilePath }, ) { log.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))") @@ -81,6 +83,17 @@ extension FFMSwift2JavaGenerator { let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift") + // Print file header before all type thunks + printer.print( + """ + // Generated by swift-java + + import SwiftRuntimeFunctions + + """ + ) + self.lookupContext.symbolTable.printImportedModules(&printer) + for ty in importedTypesForThisFile { log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)") printer.printSeparator("Thunks for \(ty.qualifiedName)") @@ -98,7 +111,7 @@ extension FFMSwift2JavaGenerator { if let outputFile = try printer.writeContents( outputDirectory: self.swiftOutputDirectory, javaPackagePath: nil, - filename: filename + filename: filename, ) { log.info("Done writing Swift thunks to: \(outputFile.absoluteString)") self.expectedOutputSwiftFileNames.remove(filename) @@ -145,17 +158,6 @@ extension FFMSwift2JavaGenerator { package func printSwiftThunkSources(_ printer: inout CodePrinter, ty: ImportedNominalType) throws { let stt = SwiftThunkTranslator(self) - printer.print( - """ - // Generated by swift-java - - import SwiftRuntimeFunctions - - """ - ) - - self.lookupContext.symbolTable.printImportedModules(&printer) - self.currentJavaIdentifiers = JavaIdentifierFactory( ty.initializers + ty.variables + ty.methods ) @@ -223,7 +225,7 @@ struct SwiftThunkTranslator { func renderSwiftTypeAccessor(_ nominal: ImportedNominalType) -> DeclSyntax { let funcName = SwiftKitPrinting.Names.getType( module: st.swiftModuleName, - nominal: nominal + nominal: nominal, ) return @@ -246,7 +248,7 @@ struct SwiftThunkTranslator { let thunkFunc = translated.loweredSignature.cdeclThunk( cName: thunkName, swiftAPIName: decl.name, - as: decl.apiKind + as: decl.apiKind, ) return [DeclSyntax(thunkFunc)] } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index f9554bee..5d37c17a 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -42,6 +42,24 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { /// Duplicate identifier tracking for the current batch of methods being generated. var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() + /// Which Java class to use for `findOrThrow` native symbol lookup + package enum SymbolLookupTarget { + /// Use the generated module class (e.g. `MySwiftLibrary`) + case module + /// Use `SwiftRuntime` (for types whose symbols live in the runtime library) + case swiftRuntime + + func javaClassName(moduleName: String) -> String { + switch self { + case .module: moduleName + case .swiftRuntime: "SwiftRuntime" + } + } + } + + /// Override symbol lookup class for the current type being generated + var currentSymbolLookup: SymbolLookupTarget = .module + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. /// @@ -54,7 +72,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { translator: Swift2JavaTranslator, javaPackage: String, swiftOutputDirectory: String, - javaOutputDirectory: String + javaOutputDirectory: String, ) { self.log = Logger(label: "ffm-generator", logLevel: translator.log.logLevel) self.config = config @@ -118,6 +136,7 @@ extension FFMSwift2JavaGenerator { "org.swift.swiftkit.core.*", "org.swift.swiftkit.core.util.*", "org.swift.swiftkit.ffm.*", + "org.swift.swiftkit.ffm.generated.*", // NonNull, Unsigned and friends "org.swift.swiftkit.core.annotations.*", @@ -128,6 +147,14 @@ extension FFMSwift2JavaGenerator { "java.util.*", "java.nio.charset.StandardCharsets", ] + + /// Returns the Java class name for a nominal type, applying known-type overrides + func javaClassName(for decl: ImportedNominalType) -> String { + if decl.swiftNominal.knownTypeKind == .swiftJavaError { + return JavaType.swiftJavaErrorException.simpleClassName + } + return decl.swiftNominal.name + } } // ==== --------------------------------------------------------------------------------------------------------------- @@ -141,32 +168,34 @@ extension FFMSwift2JavaGenerator { /// Every imported public type becomes a public class in its own file in Java. 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" + let typesToExport = analysis.importedTypes + .sorted(by: { $0.key < $1.key }) + + for (_, ty) in typesToExport { + let javaName = javaClassName(for: ty) + let filename = "\(javaName).java" log.debug("Printing contents: \(filename)") printImportedNominal(&printer, ty) if let outputFile = try printer.writeContents( outputDirectory: javaOutputDirectory, javaPackagePath: javaPackagePath, - filename: filename + filename: filename, ) { - log.info("Generated: \((ty.swiftNominal.name.bold + ".java").bold) (at \(outputFile.absoluteString))") + log.info("Generated: \((javaName.bold + ".java").bold) (at \(outputFile.absoluteString))") } } - do { - let filename = "\(self.swiftModuleName).java" - log.debug("Printing contents: \(filename)") - printModule(&printer) + let filename = "\(self.swiftModuleName).java" + log.debug("Printing contents: \(filename)") + printModule(&printer) - if let outputFile = try printer.writeContents( - outputDirectory: javaOutputDirectory, - javaPackagePath: javaPackagePath, - filename: filename - ) { - log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))") - } + if let outputFile = try printer.writeContents( + outputDirectory: javaOutputDirectory, + javaPackagePath: javaPackagePath, + filename: filename, + ) { + log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))") } } } @@ -211,6 +240,9 @@ extension FFMSwift2JavaGenerator { decl.initializers + decl.variables + decl.methods ) + let isErrorType = decl.swiftNominal.knownTypeKind == .swiftJavaError + self.currentSymbolLookup = isErrorType ? .swiftRuntime : .module + printNominal(&printer, decl) { printer in // We use a static field to abuse the initialization order such that by the time we get type metadata, // we already have loaded the library where it will be obtained from. @@ -225,7 +257,13 @@ extension FFMSwift2JavaGenerator { SwiftLibraries.loadLibraryWithFallbacks(LIB_NAME); return true; } + """ + ) + printer.print("") + // Type metadata (common to all nominal types) + printer.printParts( + """ public static final SwiftAnyType TYPE_METADATA = new SwiftAnyType(\(SwiftKitPrinting.renderCallGetSwiftType(module: self.swiftModuleName, nominal: decl))); public final SwiftAnyType $swiftType() { @@ -235,31 +273,35 @@ extension FFMSwift2JavaGenerator { ) printer.print("") - // Layout of the class - printClassMemoryLayout(&printer, decl) + if let printSpecialExtras = self.getSpecialNominalConstructorPrinting(decl) { + printSpecialExtras(&printer) + } else { + // Layout of the class + printClassMemoryLayout(&printer, decl) - printer.print("") + printer.print("") - printer.print( - """ - private \(decl.swiftNominal.name)(MemorySegment segment, AllocatingSwiftArena arena) { - super(segment, arena); - } + printer.print( + """ + private \(self.javaClassName(for: decl))(MemorySegment segment, AllocatingSwiftArena arena) { + super(segment, arena); + } - /** - * Assume that the passed {@code MemorySegment} represents a memory address of a {@link \(decl.swiftNominal.name)}. - *

- * Warnings: - *

- */ - public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(MemorySegment selfPointer, AllocatingSwiftArena arena) { - return new \(decl.swiftNominal.name)(selfPointer, arena); - } - """ - ) + /** + * Assume that the passed {@code MemorySegment} represents a memory address of a {@link \(self.javaClassName(for: decl))}. + *

+ * Warnings: + *

+ */ + public static \(self.javaClassName(for: decl)) wrapMemoryAddressUnsafe(MemorySegment selfPointer, AllocatingSwiftArena arena) { + return new \(self.javaClassName(for: decl))(selfPointer, arena); + } + """ + ) + } // Initializers for initDecl in decl.initializers { @@ -279,8 +321,11 @@ extension FFMSwift2JavaGenerator { // Special helper methods for known types (e.g. Data) printSpecificTypeHelpers(&printer, decl) - // Helper methods and default implementations - printToStringMethod(&printer, decl) + if let printSpecialPostExtras = self.getSpecialNominalPostMembersPrinting(decl) { + printSpecialPostExtras(&printer) + } else { + printToStringMethod(&printer, decl) + } } } @@ -313,20 +358,32 @@ extension FFMSwift2JavaGenerator { func printNominal( _ printer: inout CodePrinter, _ decl: ImportedNominalType, - body: (inout CodePrinter) -> Void + body: (inout CodePrinter) -> Void, ) { + let isErrorType = decl.swiftNominal.knownTypeKind == .swiftJavaError + + let baseClass: String let parentProtocol: String - if decl.swiftNominal.isReferenceType { + if isErrorType { + baseClass = "FFMSwiftErrorInstance" + // Untyped throws wraps in SwiftJavaError which is a class (heap object) parentProtocol = "SwiftHeapObject" } else { - parentProtocol = "SwiftValue" + baseClass = "FFMSwiftInstance" + if decl.swiftNominal.isReferenceType { + parentProtocol = "SwiftHeapObject" + } else { + parentProtocol = "SwiftValue" + } } if decl.swiftNominal.isSendable { printer.print("@ThreadSafe // Sendable") } + + let implementsClause = parentProtocol.isEmpty ? "" : " implements \(parentProtocol)" printer.printBraceBlock( - "public final class \(decl.swiftNominal.name) extends FFMSwiftInstance implements \(parentProtocol)" + "public final class \(javaClassName(for: decl)) extends \(baseClass)\(implementsClause)" ) { printer in // Constants @@ -336,6 +393,42 @@ extension FFMSwift2JavaGenerator { } } + /// Returns a closure that prints the constructor and related extras for special nominal types + /// (e.g. error types), or `nil` for normal types that use the default layout + constructor + func getSpecialNominalConstructorPrinting(_ decl: ImportedNominalType) -> ((inout CodePrinter) -> Void)? { + if decl.swiftNominal.knownTypeKind == .swiftJavaError { + return { printer in + // Error constructor: wrap the opaque pointer so it becomes a pointer-to-reference + // (matching the convention used by normal class instance thunks) + printer.print( + """ + public \(self.javaClassName(for: decl))(MemorySegment errorPointer, AllocatingSwiftArena arena) { + super(fetchDescription(errorPointer), wrapPointer(errorPointer, arena), arena); + } + private static MemorySegment wrapPointer(MemorySegment errorPointer, AllocatingSwiftArena arena) { + MemorySegment wrapped = arena.allocate(ValueLayout.ADDRESS); + wrapped.set(ValueLayout.ADDRESS, 0, errorPointer); + return wrapped; + } + """ + ) + } + } + return nil + } + + /// Returns a closure that prints post-members extras for special nominal types + /// (e.g. `fetchDescription` for error types), or `nil` for normal types that use `toString()` + func getSpecialNominalPostMembersPrinting(_ decl: ImportedNominalType) -> ((inout CodePrinter) -> Void)? { + if decl.swiftNominal.knownTypeKind == .swiftJavaError { + return { printer in + // Error types inherit toString() from Exception; print fetchDescription helper instead + self.printSwiftJavaErrorFetchDescriptionMethod(&printer, decl) + } + } + return nil + } + func printModuleClass(_ printer: inout CodePrinter, body: (inout CodePrinter) -> Void) { printer.printBraceBlock("public final class \(swiftModuleName)") { printer in printPrivateConstructor(&printer, swiftModuleName) @@ -436,7 +529,7 @@ extension FFMSwift2JavaGenerator { func printToStringMethod( _ printer: inout CodePrinter, - _ decl: ImportedNominalType + _ decl: ImportedNominalType, ) { printer.print( """ @@ -466,4 +559,36 @@ extension FFMSwift2JavaGenerator { } } + /// Print the `fetchDescription` static helper for SwiftJavaError. + /// This calls the `errorDescription()` downcall to get the error message + /// for the super constructor + func printSwiftJavaErrorFetchDescriptionMethod(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + // Find the errorDescription method's thunk name + let errorDescMethod = decl.methods.first { $0.name == "errorDescription" } + guard let errorDescMethod, let _ = translatedDecl(for: errorDescMethod) else { + log.warning("SwiftJavaError: could not find errorDescription method for fetchDescription helper") + return + } + + let thunkName = thunkNameRegistry.functionThunkName(decl: errorDescMethod) + + // The descriptor class for errorDescription is already emitted by printFunctionDowncallMethods, + // so we just reference it here + printer.print( + """ + private static String fetchDescription(MemorySegment errorPointer) { + try (var arena$ = Arena.ofConfined()) { + // Wrap the raw opaque pointer into a pointer-to-reference for the thunk + MemorySegment selfPtr = arena$.allocate(ValueLayout.ADDRESS); + selfPtr.set(ValueLayout.ADDRESS, 0, errorPointer); + MemorySegment result$ = (MemorySegment) \(thunkName).HANDLE.invokeExact(selfPtr); + return SwiftStrings.fromCString(result$); + } catch (Throwable ex) { + return "Swift error (address: 0x" + Long.toHexString(errorPointer.address()) + ")"; + } + } + """ + ) + } + } diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index e20decb6..84fabeb7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -52,7 +52,8 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID: + .foundationUUID, .essentialsUUID, + .swiftJavaError: return nil } } @@ -78,7 +79,8 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID: + .foundationUUID, .essentialsUUID, + .swiftJavaError: nil } } @@ -104,7 +106,8 @@ enum JNIJavaTypeTranslator { .dictionary, .set, .foundationDate, .essentialsDate, - .foundationUUID, .essentialsUUID: + .foundationUUID, .essentialsUUID, + .swiftJavaError: nil } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 38e891f2..2f2c9362 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -1895,7 +1895,7 @@ extension JNISwift2JavaGenerator { case .ifStatement(let cond, let thenExp, let elseExp): let cond = cond.render(&printer, placeholder) - printer.printBraceBlock("if (\(cond))") { printer in + printer.printIfBlock("\(cond)") { printer in printer.print(thenExp.render(&printer, placeholder)) } if let elseExp { diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift index 68f8cc7f..87e77481 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift @@ -48,4 +48,25 @@ extension JavaType { static var _OutSwiftGenericInstance: JavaType { .class(package: "org.swift.swiftkit.core", name: "_OutSwiftGenericInstance") } + + // ==== ------------------------------------------------------------------- + // MARK: Exception types + + /// The Java exception type for the Swift error wrapper + static var swiftJavaErrorException: JavaType { + .class(package: "org.swift.swiftkit.ffm.generated", name: "SwiftJavaErrorException") + } + + /// The Java exception type for integer overflow checks + static var swiftIntegerOverflowException: JavaType { + .class(package: "org.swift.swiftkit.core", name: "SwiftIntegerOverflowException") + } + + /// Extract the simple class name from a `.class` JavaType + var simpleClassName: String { + switch self { + case .class(_, let name, _): name + default: fatalError("simpleClassName is only available for .class types, was: \(self)") + } + } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index c0c1eaad..5b1e2893 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -53,6 +53,9 @@ enum SwiftKnownType: Equatable { case foundationUUID case essentialsUUID + // SwiftRuntimeFunctions + case swiftJavaError + init?(kind: SwiftKnownTypeDeclKind, genericArguments: [SwiftType]?) { switch kind { case .bool: self = .bool @@ -106,6 +109,7 @@ enum SwiftKnownType: Equatable { case .essentialsDate: self = .essentialsDate case .foundationUUID: self = .foundationUUID case .essentialsUUID: self = .essentialsUUID + case .swiftJavaError: self = .swiftJavaError } } @@ -146,6 +150,7 @@ enum SwiftKnownType: Equatable { case .essentialsDate: .essentialsDate case .foundationUUID: .foundationUUID case .essentialsUUID: .essentialsUUID + case .swiftJavaError: .swiftJavaError } } } @@ -190,6 +195,9 @@ enum SwiftKnownTypeDeclKind: String, Hashable { case foundationUUID = "Foundation.UUID" case essentialsUUID = "FoundationEssentials.UUID" + // SwiftRuntimeFunctions + case swiftJavaError = "SwiftRuntimeFunctions.SwiftJavaError" + var moduleAndName: (module: String, name: String) { let qualified = self.rawValue let period = qualified.firstIndex(of: ".")! diff --git a/Sources/SwiftRuntimeFunctions/SwiftJavaError.swift b/Sources/SwiftRuntimeFunctions/SwiftJavaError.swift new file mode 100644 index 00000000..7889ebd9 --- /dev/null +++ b/Sources/SwiftRuntimeFunctions/SwiftJavaError.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Wraps a Swift `any Error` value so it can be passed across the FFI boundary +/// as a reference-counted heap object +public final class SwiftJavaError { + public let underlying: any Error + + public init(_ error: any Error) { + self.underlying = error + } + + /// Human-readable description of the underlying error + public func errorDescription() -> String { + String(describing: underlying) + } + + /// Metatype pointer for the underlying error's dynamic type + public func errorType() -> UnsafeRawPointer { + unsafeBitCast(type(of: underlying), to: UnsafeRawPointer.self) + } +} diff --git a/Sources/SwiftRuntimeFunctions/generated/SwiftJavaError+SwiftJava.swift b/Sources/SwiftRuntimeFunctions/generated/SwiftJavaError+SwiftJava.swift new file mode 100644 index 00000000..bdc2d849 --- /dev/null +++ b/Sources/SwiftRuntimeFunctions/generated/SwiftJavaError+SwiftJava.swift @@ -0,0 +1,21 @@ +// Generated by swift-java + +import SwiftRuntimeFunctions + +// ==== -------------------------------------------------- +// Thunks for SwiftJavaError + +@_cdecl("swiftjava_getType_SwiftRuntimeFunctions_SwiftJavaError") +public func swiftjava_getType_SwiftRuntimeFunctions_SwiftJavaError() -> UnsafeMutableRawPointer /* Any.Type */ { + unsafeBitCast(SwiftJavaError.self, to: UnsafeMutableRawPointer.self) +} + +@_cdecl("swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorDescription") +public func swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorDescription(_ self: UnsafeRawPointer) -> UnsafeMutablePointer { + _swiftjava_stringToCString(self.assumingMemoryBound(to: SwiftJavaError.self).pointee.errorDescription()) +} + +@_cdecl("swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorType") +public func swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorType(_ self: UnsafeRawPointer) -> UnsafeRawPointer { + self.assumingMemoryBound(to: SwiftJavaError.self).pointee.errorType() +} diff --git a/Sources/SwiftRuntimeFunctions/generated/SwiftRuntimeFunctionsModule+SwiftJava.swift b/Sources/SwiftRuntimeFunctions/generated/SwiftRuntimeFunctionsModule+SwiftJava.swift new file mode 100644 index 00000000..ff574c03 --- /dev/null +++ b/Sources/SwiftRuntimeFunctions/generated/SwiftRuntimeFunctionsModule+SwiftJava.swift @@ -0,0 +1,3 @@ +// Generated by swift-java + +import SwiftRuntimeFunctions diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftErrorInstance.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftErrorInstance.java new file mode 100644 index 00000000..350337c2 --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/FFMSwiftErrorInstance.java @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit.ffm; + +import org.swift.swiftkit.core.SwiftInstance; +import org.swift.swiftkit.core.SwiftInstanceCleanup; + +import java.lang.foreign.MemorySegment; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Base class for Swift errors passed across the FFM boundary. + * Extends {@link Exception} so it can be thrown in Java, and implements + * {@link SwiftInstance} for proper lifecycle management. + */ +public abstract class FFMSwiftErrorInstance extends Exception implements SwiftInstance { + private final MemorySegment memorySegment; + private final AtomicBoolean $state$destroyed = new AtomicBoolean(false); + + protected FFMSwiftErrorInstance(String message, MemorySegment segment, AllocatingSwiftArena arena) { + super(message); + this.memorySegment = segment; + arena.register(this); + } + + protected FFMSwiftErrorInstance(MemorySegment segment, AllocatingSwiftArena arena) { + super(); + this.memorySegment = segment; + arena.register(this); + } + + public final MemorySegment $memorySegment() { + return memorySegment; + } + + @Override + public long $memoryAddress() { + return $memorySegment().address(); + } + + public final AtomicBoolean $statusDestroyedFlag() { + return $state$destroyed; + } + + /** + * The Swift type metadata of this type. + */ + public abstract SwiftAnyType $swiftType(); + + @Override + public SwiftInstanceCleanup $createCleanup() { + var statusDestroyedFlag = $statusDestroyedFlag(); + Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true); + + return new FFMSwiftInstanceCleanup( + $memorySegment(), + $swiftType(), + markAsDestroyed + ); + } +} diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java index 889356f4..914efd81 100644 --- a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftRuntime.java @@ -72,7 +72,7 @@ private static SymbolLookup getSymbolLookup() { public SwiftRuntime() { } - static MemorySegment findOrThrow(String symbol) { + public static MemorySegment findOrThrow(String symbol) { return SYMBOL_LOOKUP.find(symbol) .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol: %s".formatted(symbol))); } @@ -398,22 +398,6 @@ public static MethodHandle upcallHandle(Class fi, String name, FunctionDescri } } - /** - * Convert String to a MemorySegment filled with the C string. - */ - public static MemorySegment toCString(String str, Arena arena) { - return arena.allocateFrom(str); - } - - /** - * Read a heap-allocated C string into a Java String, then free the native memory. - */ - public static String fromCString(MemorySegment cStr) { - if (cStr.equals(MemorySegment.NULL)) return null; - String result = cStr.reinterpret(Long.MAX_VALUE).getString(0); - cFree(cStr); - return result; - } public static MemorySegment toOptionalSegmentInt(OptionalInt opt, Arena arena) { return opt.isPresent() ? arena.allocateFrom(ValueLayout.JAVA_INT, opt.getAsInt()) : MemorySegment.NULL; diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftStrings.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftStrings.java new file mode 100644 index 00000000..cf5535f1 --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/SwiftStrings.java @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +import java.lang.foreign.*; + +/** + * Utility methods for converting between Java Strings and C strings (null-terminated UTF-8). + */ +public final class SwiftStrings { + + private SwiftStrings() { + // Not instantiable + } + + /** + * Convert String to a MemorySegment filled with the C string. + */ + public static MemorySegment toCString(String str, Arena arena) { + return arena.allocateFrom(str); + } + + /** + * Read a heap-allocated C string into a Java String, then free the native memory. + */ + public static String fromCString(MemorySegment cStr) { + if (cStr.equals(MemorySegment.NULL)) return null; + String result = cStr.reinterpret(Long.MAX_VALUE).getString(0); + SwiftRuntime.cFree(cStr); + return result; + } +} diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/generated/SwiftJavaErrorException.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/generated/SwiftJavaErrorException.java new file mode 100644 index 00000000..ce276caa --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/generated/SwiftJavaErrorException.java @@ -0,0 +1,130 @@ +// Generated by jextract-swift +// Swift module: SwiftRuntimeFunctions + +package org.swift.swiftkit.ffm.generated; + +import org.swift.swiftkit.core.*; +import org.swift.swiftkit.core.util.*; +import org.swift.swiftkit.ffm.*; +import org.swift.swiftkit.ffm.generated.*; +import org.swift.swiftkit.core.annotations.*; +import java.lang.foreign.*; +import java.lang.invoke.*; +import java.util.*; +import java.nio.charset.StandardCharsets; + +public final class SwiftJavaErrorException extends FFMSwiftErrorInstance implements SwiftHeapObject { + static final String LIB_NAME = "SwiftRuntimeFunctions"; + static final Arena LIBRARY_ARENA = Arena.ofAuto(); + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + SwiftLibraries.loadLibraryWithFallbacks(SwiftLibraries.LIB_NAME_SWIFT_CORE); + SwiftLibraries.loadLibraryWithFallbacks(SwiftLibraries.LIB_NAME_SWIFT_JAVA); + SwiftLibraries.loadLibraryWithFallbacks(SwiftLibraries.LIB_NAME_SWIFT_RUNTIME_FUNCTIONS); + SwiftLibraries.loadLibraryWithFallbacks(LIB_NAME); + return true; + } + + public static final SwiftAnyType TYPE_METADATA = + new SwiftAnyType(SwiftRuntime.swiftjava.getType("SwiftRuntimeFunctions", "SwiftJavaError")); + public final SwiftAnyType $swiftType() { + return TYPE_METADATA; + } + + public SwiftJavaErrorException(MemorySegment errorPointer, AllocatingSwiftArena arena) { + super(fetchDescription(errorPointer), wrapPointer(errorPointer, arena), arena); + } + private static MemorySegment wrapPointer(MemorySegment errorPointer, AllocatingSwiftArena arena) { + MemorySegment wrapped = arena.allocate(ValueLayout.ADDRESS); + wrapped.set(ValueLayout.ADDRESS, 0, errorPointer); + return wrapped; + } + + // ==== -------------------------------------------------- + // SwiftJavaError.errorDescription + + /** + * {@snippet lang=c : + * int8_t *swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorDescription(const void *self) + * } + */ + private static class swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorDescription { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_POINTER, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftRuntime.findOrThrow("swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorDescription"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static java.lang.foreign.MemorySegment call(java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); + } + return (java.lang.foreign.MemorySegment) HANDLE.invokeExact(self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } // printJavaBindingDescriptorClass(_:_:symbolLookup:additionalContent:) @ JExtractSwiftLib/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift:65 + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func errorDescription() -> String + * } + */ + public java.lang.String errorDescription() { + $ensureAlive(); + return SwiftStrings.fromCString(swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorDescription.call(this.$memorySegment())); + } // printJavaBindingWrapperMethod(_:_:) @ JExtractSwiftLib/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift:403 + + // ==== -------------------------------------------------- + // SwiftJavaError.errorType + + /** + * {@snippet lang=c : + * const void *swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorType(const void *self) + * } + */ + private static class swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorType { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_POINTER, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftRuntime.findOrThrow("swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorType"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static java.lang.foreign.MemorySegment call(java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); + } + return (java.lang.foreign.MemorySegment) HANDLE.invokeExact(self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } // printJavaBindingDescriptorClass(_:_:symbolLookup:additionalContent:) @ JExtractSwiftLib/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift:65 + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func errorType() -> UnsafeRawPointer + * } + */ + public java.lang.foreign.MemorySegment errorType() { + $ensureAlive(); + return swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorType.call(this.$memorySegment()); + } // printJavaBindingWrapperMethod(_:_:) @ JExtractSwiftLib/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift:403 + private static String fetchDescription(MemorySegment errorPointer) { + try (var arena$ = Arena.ofConfined()) { + // Wrap the raw opaque pointer into a pointer-to-reference for the thunk + MemorySegment selfPtr = arena$.allocate(ValueLayout.ADDRESS); + selfPtr.set(ValueLayout.ADDRESS, 0, errorPointer); + MemorySegment result$ = (MemorySegment) swiftjava_SwiftRuntimeFunctions_SwiftJavaError_errorDescription.HANDLE.invokeExact(selfPtr); + return SwiftStrings.fromCString(result$); + } catch (Throwable ex) { + return "Swift error (address: 0x" + Long.toHexString(errorPointer.address()) + ")"; + } + } +} // printNominal(_:_:body:) @ JExtractSwiftLib/FFMSwift2JavaGenerator.swift:385 diff --git a/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/generated/SwiftRuntimeFunctions.java b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/generated/SwiftRuntimeFunctions.java new file mode 100644 index 00000000..ec27f2a2 --- /dev/null +++ b/SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/generated/SwiftRuntimeFunctions.java @@ -0,0 +1,62 @@ +// Generated by jextract-swift +// Swift module: SwiftRuntimeFunctions + +package org.swift.swiftkit.ffm.generated; + +import org.swift.swiftkit.core.*; +import org.swift.swiftkit.core.util.*; +import org.swift.swiftkit.ffm.*; +import org.swift.swiftkit.ffm.generated.*; +import org.swift.swiftkit.core.annotations.*; +import java.lang.foreign.*; +import java.lang.invoke.*; +import java.util.*; +import java.nio.charset.StandardCharsets; + +public final class SwiftRuntimeFunctions { + private SwiftRuntimeFunctions() { + // Should not be called directly + } + + // Static enum to force initialization + private static enum Initializer { + FORCE; // Refer to this to force outer Class initialization (and static{} blocks to trigger) + } + static final String LIB_NAME = "SwiftRuntimeFunctions"; + static final Arena LIBRARY_ARENA = Arena.ofAuto(); + static MemorySegment findOrThrow(String symbol) { + return SYMBOL_LOOKUP.find(symbol) + .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol: %s".formatted(symbol))); + } + static MemoryLayout align(MemoryLayout layout, long align) { + return switch (layout) { + case PaddingLayout p -> p; + case ValueLayout v -> v.withByteAlignment(align); + case GroupLayout g -> { + MemoryLayout[] alignedMembers = g.memberLayouts().stream() + .map(m -> align(m, align)).toArray(MemoryLayout[]::new); + yield g instanceof StructLayout ? + MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers); + } + case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align)); + }; + } + static final SymbolLookup SYMBOL_LOOKUP = getSymbolLookup(); + private static SymbolLookup getSymbolLookup() { + if (SwiftLibraries.AUTO_LOAD_LIBS) { + SwiftLibraries.loadLibraryWithFallbacks(SwiftLibraries.LIB_NAME_SWIFT_CORE); + SwiftLibraries.loadLibraryWithFallbacks(SwiftLibraries.LIB_NAME_SWIFT_JAVA); + SwiftLibraries.loadLibraryWithFallbacks(SwiftLibraries.LIB_NAME_SWIFT_RUNTIME_FUNCTIONS); + SwiftLibraries.loadLibraryWithFallbacks(LIB_NAME); + } + + if (PlatformUtils.isMacOS()) { + return SymbolLookup.libraryLookup(System.mapLibraryName(LIB_NAME), LIBRARY_ARENA) + .or(SymbolLookup.loaderLookup()) + .or(Linker.nativeLinker().defaultLookup()); + } else { + return SymbolLookup.loaderLookup() + .or(Linker.nativeLinker().defaultLookup()); + } + } +} // printModuleClass(_:body:) @ JExtractSwiftLib/FFMSwift2JavaGenerator.swift:433 diff --git a/Tests/JExtractSwiftTests/ByteArrayTests.swift b/Tests/JExtractSwiftTests/ByteArrayTests.swift index e04444a5..79113ec3 100644 --- a/Tests/JExtractSwiftTests/ByteArrayTests.swift +++ b/Tests/JExtractSwiftTests/ByteArrayTests.swift @@ -103,7 +103,7 @@ final class ByteArrayTests { * void (void *, size_t) * } */ - private static class $_result_initialize { + private static class result$initialize { @FunctionalInterface public interface Function { void apply(java.lang.foreign.MemorySegment _0, long _1); @@ -134,9 +134,9 @@ final class ByteArrayTests { @Unsigned public static byte[] returnArray() { try(var arena$ = Arena.ofConfined()) { - var _result_initialize = new swiftjava_SwiftModule_returnArray.$_result_initialize.Function$Impl(); - swiftjava_SwiftModule_returnArray.call(swiftjava_SwiftModule_returnArray.$_result_initialize.toUpcallStub(_result_initialize, arena$)); - return _result_initialize.result; + var result$initialize = new swiftjava_SwiftModule_returnArray.result$initialize.Function$Impl(); + swiftjava_SwiftModule_returnArray.call(swiftjava_SwiftModule_returnArray.result$initialize.toUpcallStub(result$initialize, arena$)); + return result$initialize.result; } } """, diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index 08652aca..47d5a473 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -213,9 +213,9 @@ final class DataImportTests { * } */ public static Data returnData(AllocatingSwiftArena swiftArena) { - MemorySegment _result = swiftArena.allocate(Data.$LAYOUT); - swiftjava_SwiftModule_returnData.call(_result); - return Data.wrapMemoryAddressUnsafe(_result, swiftArena); + MemorySegment result$ = swiftArena.allocate(Data.$LAYOUT); + swiftjava_SwiftModule_returnData.call(result$); + return Data.wrapMemoryAddressUnsafe(result$, swiftArena); } """, @@ -255,14 +255,14 @@ final class DataImportTests { * } */ public static Data init(java.lang.foreign.MemorySegment bytes, long count, AllocatingSwiftArena swiftArena) throws SwiftIntegerOverflowException { - MemorySegment _result = swiftArena.allocate(Data.$LAYOUT); + MemorySegment result$ = swiftArena.allocate(Data.$LAYOUT); if (SwiftValueLayout.has32bitSwiftInt) { if (count < Integer.MIN_VALUE || count > Integer.MAX_VALUE) { throw new SwiftIntegerOverflowException("Parameter 'count' overflow: " + count); } } - swiftjava_SwiftModule_Data_init_bytes_count.call(bytes, count, _result); - return Data.wrapMemoryAddressUnsafe(_result, swiftArena); + swiftjava_SwiftModule_Data_init_bytes_count.call(bytes, count, result$); + return Data.wrapMemoryAddressUnsafe(result$, swiftArena); } """, @@ -302,13 +302,13 @@ final class DataImportTests { */ public long getCount() throws SwiftIntegerOverflowException { $ensureAlive(); - long _result$checked = swiftjava_SwiftModule_Data_count$get.call(this.$memorySegment()); + long result$checked = swiftjava_SwiftModule_Data_count$get.call(this.$memorySegment()); if (SwiftValueLayout.has32bitSwiftInt) { - if (_result$checked < Integer.MIN_VALUE || _result$checked > Integer.MAX_VALUE) { - throw new SwiftIntegerOverflowException("Return value overflow: " + _result$checked); + if (result$checked < Integer.MIN_VALUE || result$checked > Integer.MAX_VALUE) { + throw new SwiftIntegerOverflowException("Return value overflow: " + result$checked); } } - return _result$checked; + return result$checked; } """, diff --git a/Tests/JExtractSwiftTests/FFM/FFMThrowingTests.swift b/Tests/JExtractSwiftTests/FFM/FFMThrowingTests.swift new file mode 100644 index 00000000..cefca7b8 --- /dev/null +++ b/Tests/JExtractSwiftTests/FFM/FFMThrowingTests.swift @@ -0,0 +1,249 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct FFMThrowingTests { + let throwingSource = """ + public func throwingVoid() throws + public func throwingReturn(x: Int64) throws -> Int64 + """ + + @Test + func throwingVoid_swiftThunks() throws { + try assertOutput( + input: throwingSource, + .ffm, + .swift, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_throwingVoid") + public func swiftjava_SwiftModule_throwingVoid(_ result$throws: UnsafeMutablePointer) { + do { + try throwingVoid() + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + } + } + """ + ], + ) + } + + @Test + func throwingReturn_swiftThunks() throws { + try assertOutput( + input: throwingSource, + .ffm, + .swift, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_throwingReturn_x") + public func swiftjava_SwiftModule_throwingReturn_x(_ x: Int64, _ result$throws: UnsafeMutablePointer) -> Int64 { + do { + return try throwingReturn(x: x) + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + return 0 + } + } + """ + ], + ) + } + + @Test + func throwingVoid_javaBindings() throws { + try assertOutput( + input: throwingSource, + .ffm, + .java, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_throwingVoid(void **result$throws) + * } + */ + private static class swiftjava_SwiftModule_throwingVoid { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* result$throws: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_throwingVoid"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment result$throws) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(result$throws); + } + HANDLE.invokeExact(result$throws); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func throwingVoid() throws + * } + */ + public static void throwingVoid() throws SwiftJavaErrorException { + try(var arena$ = Arena.ofConfined()) { + MemorySegment result$throws = arena$.allocate(ValueLayout.ADDRESS); + result$throws.set(ValueLayout.ADDRESS, 0, MemorySegment.NULL); + swiftjava_SwiftModule_throwingVoid.call(result$throws); + if (!result$throws.get(ValueLayout.ADDRESS, 0).equals(MemorySegment.NULL)) { + throw new SwiftJavaErrorException(result$throws.get(ValueLayout.ADDRESS, 0), AllocatingSwiftArena.ofAuto()); + } + } + } + """, + ], + ) + } + + @Test + func throwingReturn_javaBindings() throws { + try assertOutput( + input: throwingSource, + .ffm, + .java, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * int64_t swiftjava_SwiftModule_throwingReturn_x(int64_t x, void **result$throws) + * } + */ + private static class swiftjava_SwiftModule_throwingReturn_x { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_INT64, + /* x: */SwiftValueLayout.SWIFT_INT64, + /* result$throws: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_throwingReturn_x"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static long call(long x, java.lang.foreign.MemorySegment result$throws) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(x, result$throws); + } + return (long) HANDLE.invokeExact(x, result$throws); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func throwingReturn(x: Int64) throws -> Int64 + * } + */ + public static long throwingReturn(long x) throws SwiftJavaErrorException { + try(var arena$ = Arena.ofConfined()) { + MemorySegment result$throws = arena$.allocate(ValueLayout.ADDRESS); + result$throws.set(ValueLayout.ADDRESS, 0, MemorySegment.NULL); + var result$ = (long) swiftjava_SwiftModule_throwingReturn_x.call(x, result$throws); + if (!result$throws.get(ValueLayout.ADDRESS, 0).equals(MemorySegment.NULL)) { + throw new SwiftJavaErrorException(result$throws.get(ValueLayout.ADDRESS, 0), AllocatingSwiftArena.ofAuto()); + } + return result$; + } + } + """, + ], + ) + } + + let stringReturnSource = """ + public func greeting() -> String + """ + + @Test + func stringReturn_swiftThunks() throws { + try assertOutput( + input: stringReturnSource, + .ffm, + .swift, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_greeting") + public func swiftjava_SwiftModule_greeting() -> UnsafeMutablePointer { + return _swiftjava_stringToCString(greeting()) + } + """ + ], + ) + } + + @Test + func stringReturn_javaBindings() throws { + try assertOutput( + input: stringReturnSource, + .ffm, + .java, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * int8_t *swiftjava_SwiftModule_greeting(void) + * } + */ + private static class swiftjava_SwiftModule_greeting { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_greeting"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static java.lang.foreign.MemorySegment call() { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(); + } + return (java.lang.foreign.MemorySegment) HANDLE.invokeExact(); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func greeting() -> String + * } + */ + public static java.lang.String greeting() { + return SwiftStrings.fromCString(swiftjava_SwiftModule_greeting.call()); + } + """, + ], + ) + } +} diff --git a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift index b83b04ad..1b0c09b5 100644 --- a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift +++ b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift @@ -33,10 +33,10 @@ struct FFMTupleTests { """ public static org.swift.swiftkit.core.tuple.Tuple2 returnPair() { try(var arena$ = Arena.ofConfined()) { - MemorySegment _result_0 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); - MemorySegment _result_1 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); - swiftjava_SwiftModule_returnPair.call(_result_0, _result_1); - return new org.swift.swiftkit.core.tuple.Tuple2(_result_0.get(SwiftValueLayout.SWIFT_INT64, 0), _result_1.get(SwiftValueLayout.SWIFT_INT64, 0)); + MemorySegment result$_0 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); + MemorySegment result$_1 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); + swiftjava_SwiftModule_returnPair.call(result$_0, result$_1); + return new org.swift.swiftkit.core.tuple.Tuple2(result$_0.get(SwiftValueLayout.SWIFT_INT64, 0), result$_1.get(SwiftValueLayout.SWIFT_INT64, 0)); } } """ @@ -72,7 +72,7 @@ struct FFMTupleTests { public static org.swift.swiftkit.core.tuple.Tuple2 labeledTuple() { """, """ - return new org.swift.swiftkit.core.tuple.Tuple2(_result_0.get(SwiftValueLayout.SWIFT_INT32, 0), _result_1.get(SwiftValueLayout.SWIFT_INT32, 0)); + return new org.swift.swiftkit.core.tuple.Tuple2(result$_0.get(SwiftValueLayout.SWIFT_INT32, 0), result$_1.get(SwiftValueLayout.SWIFT_INT32, 0)); """, ] ) diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index f56e3040..4cfb7bcf 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -30,7 +30,7 @@ final class FunctionLoweringTests { f(x: x, y: y, z: UnsafeBufferPointer(start: z_pointer.assumingMemoryBound(to: Bool.self), count: z_count)) } """, - expectedCFunction: "void c_f(ptrdiff_t x, float y, const void *z_pointer, ptrdiff_t z_count)" + expectedCFunction: "void c_f(ptrdiff_t x, float y, const void *z_pointer, ptrdiff_t z_count)", ) } @@ -46,7 +46,7 @@ final class FunctionLoweringTests { return f(t: (t_0, (t_1_0, t_1_1)), z: z) } """, - expectedCFunction: "ptrdiff_t c_f(ptrdiff_t t_0, float t_1_0, double t_1_1, const ptrdiff_t *z)" + expectedCFunction: "ptrdiff_t c_f(ptrdiff_t t_0, float t_1_0, double t_1_1, const ptrdiff_t *z)", ) } @@ -63,7 +63,7 @@ final class FunctionLoweringTests { """, expectedCFunction: """ void c_takeString(const int8_t *str) - """ + """, ) } @@ -82,7 +82,7 @@ final class FunctionLoweringTests { shift(point: &point.assumingMemoryBound(to: Point.self).pointee, by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(void *point, double delta_0, double delta_1)" + expectedCFunction: "void c_shift(void *point, double delta_0, double delta_1)", ) } @@ -102,7 +102,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1))) } """, - expectedCFunction: "void c_shifted(double delta_0, double delta_1, const void *self, void *_result)" + expectedCFunction: "void c_shifted(double delta_0, double delta_1, const void *self, void *_result)", ) } @@ -122,7 +122,7 @@ final class FunctionLoweringTests { self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(double delta_0, double delta_1, void *self)" + expectedCFunction: "void c_shift(double delta_0, double delta_1, void *self)", ) } @@ -142,7 +142,7 @@ final class FunctionLoweringTests { self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(double delta_0, double delta_1, const void *self)" + expectedCFunction: "void c_shift(double delta_0, double delta_1, const void *self)", ) } @@ -162,7 +162,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: Point.scaledUnit(by: value)) } """, - expectedCFunction: "void c_scaledUnit(double value, void *_result)" + expectedCFunction: "void c_scaledUnit(double value, void *_result)", ) try assertLoweredFunction( @@ -179,7 +179,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Person.self).initialize(to: Person.randomPerson(seed: seed)) } """, - expectedCFunction: "void c_randomPerson(double seed, void *_result)" + expectedCFunction: "void c_randomPerson(double seed, void *_result)", ) } @@ -199,7 +199,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: Point(scaledBy: value)) } """, - expectedCFunction: "void c_init(double value, void *_result)" + expectedCFunction: "void c_init(double value, void *_result)", ) try assertLoweredFunction( @@ -216,7 +216,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Person.self).initialize(to: Person(seed: seed)) } """, - expectedCFunction: "void c_init(double seed, void *_result)" + expectedCFunction: "void c_init(double seed, void *_result)", ) } @@ -232,7 +232,7 @@ final class FunctionLoweringTests { f(t: unsafeBitCast(t, to: Int.self)) } """, - expectedCFunction: "void c_f(const void *t)" + expectedCFunction: "void c_f(const void *t)", ) try assertLoweredFunction( @@ -245,7 +245,7 @@ final class FunctionLoweringTests { return unsafeBitCast(f(), to: UnsafeRawPointer.self) } """, - expectedCFunction: "const void *c_f(void)" + expectedCFunction: "const void *c_f(void)", ) } @@ -265,7 +265,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1))) } """, - expectedCFunction: "void c_shifted(double delta_0, double delta_1, const void *self, void *_result)" + expectedCFunction: "void c_shifted(double delta_0, double delta_1, const void *self, void *_result)", ) } @@ -284,7 +284,7 @@ final class FunctionLoweringTests { return UnsafeRawPointer(getPointer()) } """, - expectedCFunction: "const void *c_getPointer(void)" + expectedCFunction: "const void *c_getPointer(void)", ) } @@ -307,7 +307,7 @@ final class FunctionLoweringTests { _result_1_1.assumingMemoryBound(to: Point.self).initialize(to: _result_1.1) } """, - expectedCFunction: "void c_getTuple(ptrdiff_t *_result_0, float *_result_1_0, void *_result_1_1)" + expectedCFunction: "void c_getTuple(ptrdiff_t *_result_0, float *_result_1_0, void *_result_1_1)", ) } @@ -328,7 +328,7 @@ final class FunctionLoweringTests { _result_1.initialize(to: _result.1) } """, - expectedCFunction: "void c_getBufferPointer(void **_result_0, ptrdiff_t *_result_1)" + expectedCFunction: "void c_getBufferPointer(void **_result_0, ptrdiff_t *_result_1)", ) } @@ -347,7 +347,7 @@ final class FunctionLoweringTests { } """, expectedCFunction: - "void c_swapRawBufferPointer(const void *buffer_pointer, ptrdiff_t buffer_count, void **_result_pointer, ptrdiff_t *_result_count)" + "void c_swapRawBufferPointer(const void *buffer_pointer, ptrdiff_t buffer_count, void **_result_pointer, ptrdiff_t *_result_count)", ) } @@ -365,7 +365,7 @@ final class FunctionLoweringTests { }) } """, - expectedCFunction: "void c_withBuffer(ptrdiff_t (*body)(const void *, ptrdiff_t))" + expectedCFunction: "void c_withBuffer(ptrdiff_t (*body)(const void *, ptrdiff_t))", ) } @@ -381,7 +381,7 @@ final class FunctionLoweringTests { doSomething(body: body) } """, - expectedCFunction: "void c_doSomething(void (*body)(void))" + expectedCFunction: "void c_doSomething(void (*body)(void))", ) } @@ -399,7 +399,7 @@ final class FunctionLoweringTests { doSomething(body: body) } """, - expectedCFunction: "void c_doSomething(double (*body)(int32_t))" + expectedCFunction: "void c_doSomething(double (*body)(int32_t))", ) } @@ -421,7 +421,7 @@ final class FunctionLoweringTests { """, expectedCFunction: """ void c_fn(const ptrdiff_t *a1, const ptrdiff_t *a2, const void *a3, const void *a4) - """ + """, ) } @@ -442,7 +442,7 @@ final class FunctionLoweringTests { """, expectedCFunction: """ void c_fn(const void *x, const void *y) - """ + """, ) } @@ -464,7 +464,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: value) } """, - expectedCFunction: "void c_value(void *_result)" + expectedCFunction: "void c_value(void *_result)", ) } @@ -486,7 +486,7 @@ final class FunctionLoweringTests { value = newValue.assumingMemoryBound(to: Point.self).pointee } """, - expectedCFunction: "void c_value(const void *newValue)" + expectedCFunction: "void c_value(const void *newValue)", ) } @@ -509,7 +509,7 @@ final class FunctionLoweringTests { return self.assumingMemoryBound(to: Point.self).pointee.value } """, - expectedCFunction: "ptrdiff_t c_value(const void *self)" + expectedCFunction: "ptrdiff_t c_value(const void *self)", ) } @@ -532,7 +532,71 @@ final class FunctionLoweringTests { self.assumingMemoryBound(to: Point.self).pointee.value = newValue.assumingMemoryBound(to: Point.self).pointee } """, - expectedCFunction: "void c_value(const void *newValue, const void *self)" + expectedCFunction: "void c_value(const void *newValue, const void *self)", + ) + } + + @Test("Lowering throwing void function") + func lowerThrowingVoidFunction() throws { + try assertLoweredFunction( + """ + func foo() throws { } + """, + expectedCDecl: """ + @_cdecl("c_foo") + public func c_foo(_ result$throws: UnsafeMutablePointer) { + do { + try foo() + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + } + } + """, + expectedCFunction: "void c_foo(void **result$throws)", + ) + } + + @Test("Lowering throwing function with direct return") + func lowerThrowingDirectReturn() throws { + try assertLoweredFunction( + """ + func foo(x: Int) throws -> Int { } + """, + expectedCDecl: """ + @_cdecl("c_foo") + public func c_foo(_ x: Int, _ result$throws: UnsafeMutablePointer) -> Int { + do { + return try foo(x: x) + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + return 0 + } + } + """, + expectedCFunction: "ptrdiff_t c_foo(ptrdiff_t x, void **result$throws)", + ) + } + + @Test("Lowering throwing function with indirect return") + func lowerThrowingIndirectReturn() throws { + try assertLoweredFunction( + """ + func foo() throws -> MyStruct { } + """, + sourceFile: """ + struct MyStruct { } + """, + expectedCDecl: """ + @_cdecl("c_foo") + public func c_foo(_ _result: UnsafeMutableRawPointer, _ result$throws: UnsafeMutablePointer) { + do { + try _result.assumingMemoryBound(to: MyStruct.self).initialize(to: foo()) + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + } + } + """, + expectedCFunction: "void c_foo(void *_result, void **result$throws)", ) } @@ -548,7 +612,51 @@ final class FunctionLoweringTests { return _swiftjava_stringToCString(bar()) } """, - expectedCFunction: "int8_t *c_bar(void)" + expectedCFunction: "int8_t *c_bar(void)", + ) + } + + @Test("Lowering throwing function with class return") + func lowerThrowingClassReturn() throws { + try assertLoweredFunction( + """ + func foo() throws -> MyClass { } + """, + sourceFile: """ + class MyClass { } + """, + expectedCDecl: """ + @_cdecl("c_foo") + public func c_foo(_ _result: UnsafeMutableRawPointer, _ result$throws: UnsafeMutablePointer) { + do { + try _result.assumingMemoryBound(to: MyClass.self).initialize(to: foo()) + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + } + } + """, + expectedCFunction: "void c_foo(void *_result, void **result$throws)", + ) + } + + @Test("Lowering throwing function with String return") + func lowerThrowingStringReturn() throws { + try assertLoweredFunction( + """ + func foo() throws -> String { } + """, + expectedCDecl: """ + @_cdecl("c_foo") + public func c_foo(_ result$throws: UnsafeMutablePointer) -> UnsafeMutablePointer? { + do { + return try _swiftjava_stringToCString(foo()) + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + return nil + } + } + """, + expectedCFunction: "int8_t *c_foo(void **result$throws)", ) } } diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index c05e702a..aa546f3d 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -192,7 +192,7 @@ final class MethodImportTests { */ public static void globalTakeIntLongString(int i32, long l, java.lang.String s) { try(var arena$ = Arena.ofConfined()) { - swiftjava___FakeModule_globalTakeIntLongString_i32_l_s.call(i32, l, SwiftRuntime.toCString(s, arena$)); + swiftjava___FakeModule_globalTakeIntLongString_i32_l_s.call(i32, l, SwiftStrings.toCString(s, arena$)); } } """ @@ -235,9 +235,9 @@ final class MethodImportTests { * } */ public static MySwiftClass globalReturnClass(AllocatingSwiftArena swiftArena) { - MemorySegment _result = swiftArena.allocate(MySwiftClass.$LAYOUT); - swiftjava___FakeModule_globalReturnClass.call(_result); - return MySwiftClass.wrapMemoryAddressUnsafe(_result, swiftArena); + MemorySegment result$ = swiftArena.allocate(MySwiftClass.$LAYOUT); + swiftjava___FakeModule_globalReturnClass.call(result$); + return MySwiftClass.wrapMemoryAddressUnsafe(result$, swiftArena); } """ ) @@ -280,10 +280,10 @@ final class MethodImportTests { */ public static java.lang.foreign.MemorySegment swapRawBufferPointer(java.lang.foreign.MemorySegment buffer) { try(var arena$ = Arena.ofConfined()) { - MemorySegment _result_pointer = arena$.allocate(SwiftValueLayout.SWIFT_POINTER); - MemorySegment _result_count = arena$.allocate(SwiftValueLayout.SWIFT_INT64); - swiftjava___FakeModule_swapRawBufferPointer_buffer.call(buffer, buffer.byteSize(), _result_pointer, _result_count); - return _result_pointer.get(SwiftValueLayout.SWIFT_POINTER, 0).reinterpret(_result_count.get(SwiftValueLayout.SWIFT_INT64, 0)); + MemorySegment result$_pointer = arena$.allocate(SwiftValueLayout.SWIFT_POINTER); + MemorySegment result$_count = arena$.allocate(SwiftValueLayout.SWIFT_INT64); + swiftjava___FakeModule_swapRawBufferPointer_buffer.call(buffer, buffer.byteSize(), result$_pointer, result$_count); + return result$_pointer.get(SwiftValueLayout.SWIFT_POINTER, 0).reinterpret(result$_count.get(SwiftValueLayout.SWIFT_INT64, 0)); } } """ @@ -370,13 +370,13 @@ final class MethodImportTests { */ public long makeInt() throws SwiftIntegerOverflowException { $ensureAlive(); - long _result$checked = swiftjava___FakeModule_MySwiftClass_makeInt.call(this.$memorySegment()); + long result$checked = swiftjava___FakeModule_MySwiftClass_makeInt.call(this.$memorySegment()); if (SwiftValueLayout.has32bitSwiftInt) { - if (_result$checked < Integer.MIN_VALUE || _result$checked > Integer.MAX_VALUE) { - throw new SwiftIntegerOverflowException("Return value overflow: " + _result$checked); + if (result$checked < Integer.MIN_VALUE || result$checked > Integer.MAX_VALUE) { + throw new SwiftIntegerOverflowException("Return value overflow: " + result$checked); } } - return _result$checked; + return result$checked; } """ ) @@ -418,7 +418,7 @@ final class MethodImportTests { * } */ public static MySwiftClass init(long len, long cap, AllocatingSwiftArena swiftArena) throws SwiftIntegerOverflowException { - MemorySegment _result = swiftArena.allocate(MySwiftClass.$LAYOUT); + MemorySegment result$ = swiftArena.allocate(MySwiftClass.$LAYOUT); if (SwiftValueLayout.has32bitSwiftInt) { if (len < Integer.MIN_VALUE || len > Integer.MAX_VALUE) { throw new SwiftIntegerOverflowException("Parameter 'len' overflow: " + len); @@ -427,8 +427,8 @@ final class MethodImportTests { throw new SwiftIntegerOverflowException("Parameter 'cap' overflow: " + cap); } } - swiftjava___FakeModule_MySwiftClass_init_len_cap.call(len, cap, _result) - return MySwiftClass.wrapMemoryAddressUnsafe(_result, swiftArena); + swiftjava___FakeModule_MySwiftClass_init_len_cap.call(len, cap, result$) + return MySwiftClass.wrapMemoryAddressUnsafe(result$, swiftArena); } """ ) @@ -471,7 +471,7 @@ final class MethodImportTests { * } */ public static MySwiftStruct init(long len, long cap, AllocatingSwiftArena swiftArena) throws SwiftIntegerOverflowException { - MemorySegment _result = swiftArena.allocate(MySwiftStruct.$LAYOUT); + MemorySegment result$ = swiftArena.allocate(MySwiftStruct.$LAYOUT); if (SwiftValueLayout.has32bitSwiftInt) { if (len < Integer.MIN_VALUE || len > Integer.MAX_VALUE) { throw new SwiftIntegerOverflowException("Parameter 'len' overflow: " + len); @@ -480,8 +480,8 @@ final class MethodImportTests { throw new SwiftIntegerOverflowException("Parameter 'cap' overflow: " + cap); } } - swiftjava___FakeModule_MySwiftStruct_init_len_cap.call(len, cap, _result) - return MySwiftStruct.wrapMemoryAddressUnsafe(_result, swiftArena); + swiftjava___FakeModule_MySwiftStruct_init_len_cap.call(len, cap, result$) + return MySwiftStruct.wrapMemoryAddressUnsafe(result$, swiftArena); } """ ) diff --git a/Tests/JExtractSwiftTests/StringPassingTests.swift b/Tests/JExtractSwiftTests/StringPassingTests.swift index 58ceca9f..c3b780ed 100644 --- a/Tests/JExtractSwiftTests/StringPassingTests.swift +++ b/Tests/JExtractSwiftTests/StringPassingTests.swift @@ -66,13 +66,13 @@ final class StringPassingTests { */ public static long writeString(java.lang.String string) throws SwiftIntegerOverflowException { try(var arena$ = Arena.ofConfined()) { - long _result$checked = swiftjava___FakeModule_writeString_string.call(SwiftRuntime.toCString(string, arena$)); + long result$checked = swiftjava___FakeModule_writeString_string.call(SwiftStrings.toCString(string, arena$)); if (SwiftValueLayout.has32bitSwiftInt) { - if (_result$checked < Integer.MIN_VALUE || _result$checked > Integer.MAX_VALUE) { - throw new SwiftIntegerOverflowException("Return value overflow: " + _result$checked); + if (result$checked < Integer.MIN_VALUE || result$checked > Integer.MAX_VALUE) { + throw new SwiftIntegerOverflowException("Return value overflow: " + result$checked); } } - return _result$checked; + return result$checked; } } """, diff --git a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift index 1d6afea3..c0e98971 100644 --- a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift +++ b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift @@ -351,13 +351,13 @@ final class UnsignedNumberTests { throw new SwiftIntegerOverflowException("Parameter 'second' overflow: " + second); } } - long _result$checked = swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); + long result$checked = swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); if (SwiftValueLayout.has32bitSwiftInt) { - if (_result$checked < 0 || _result$checked > 0xFFFFFFFFL) { - throw new SwiftIntegerOverflowException("Return value overflow: " + _result$checked); + if (result$checked < 0 || result$checked > 0xFFFFFFFFL) { + throw new SwiftIntegerOverflowException("Return value overflow: " + result$checked); } } - return _result$checked; + return result$checked; } """, ] diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index 3b81ea9d..408cb4f4 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -72,13 +72,13 @@ final class VariableImportTests { */ public long getCounterInt() throws SwiftIntegerOverflowException { $ensureAlive(); - long _result$checked = swiftjava_FakeModule_MySwiftClass_counterInt$get.call(this.$memorySegment()); + long result$checked = swiftjava_FakeModule_MySwiftClass_counterInt$get.call(this.$memorySegment()); if (SwiftValueLayout.has32bitSwiftInt) { - if (_result$checked < Integer.MIN_VALUE || _result$checked > Integer.MAX_VALUE) { - throw new SwiftIntegerOverflowException("Return value overflow: " + _result$checked); + if (result$checked < Integer.MIN_VALUE || result$checked > Integer.MAX_VALUE) { + throw new SwiftIntegerOverflowException("Return value overflow: " + result$checked); } } - return _result$checked; + return result$checked; } """, """ diff --git a/scripts/swiftkit-ffm-generate-bindings.sh b/scripts/swiftkit-ffm-generate-bindings.sh new file mode 100755 index 00000000..9cc0317c --- /dev/null +++ b/scripts/swiftkit-ffm-generate-bindings.sh @@ -0,0 +1,51 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2026 Apple Inc. and the Swift.org project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of Swift.org project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +# Regenerate FFM bindings for types in SwiftKitFFM/src/main/java/org/swift/swiftkit/ffm/generated/ +# +# Run from the swift-java repository root: +# ./scripts/swiftkit-ffm-generate-bindings.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +JAVA_OUTPUT="${REPO_ROOT}/SwiftKitFFM/src/main/java" +JAVA_PACKAGE="org.swift.swiftkit.ffm.generated" + +# Declare types to generate: SWIFT_MODULE FILTER_INCLUDE INPUT_SWIFT_DIR OUTPUT_SWIFT_DIR +TYPES=( + "SwiftRuntimeFunctions SwiftJavaError Sources/SwiftRuntimeFunctions Sources/SwiftRuntimeFunctions/generated" +) + +for entry in "${TYPES[@]}"; do + read -r MODULE FILTER INPUT_SWIFT OUTPUT_SWIFT <<< "$entry" + + echo "==> Generating ${FILTER} (module: ${MODULE})..." + + xcrun swift run swift-java jextract \ + --mode ffm \ + --filter-include "$FILTER" \ + --swift-module "$MODULE" \ + --input-swift "${REPO_ROOT}/${INPUT_SWIFT}" \ + --output-swift "${REPO_ROOT}/${OUTPUT_SWIFT}" \ + --output-java "$JAVA_OUTPUT" \ + --java-package "$JAVA_PACKAGE" + + echo " Swift thunks: ${OUTPUT_SWIFT}/" + echo " Java output: SwiftKitFFM/src/main/java/$(echo "$JAVA_PACKAGE" | tr '.' '/')/" +done + +echo "==> Done."