Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//

enum MySwiftError: Error {
public enum MySwiftError: Error {
case swiftError
case invalidInput(reason: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,30 @@ public func throwString(input: String) throws -> String {
return input
}
// snippet.end

public func throwTyped(input: String) throws(MySwiftError) -> String {
if input.isEmpty {
throw .swiftError
}
if input == "invalid" {
throw .invalidInput(reason: "input was 'invalid'")
}
return input
}

// Not public, so it is not extracted and surfaces in Java as `SwiftErrorException`.
struct InternalOnlyError: Error {
let code: Int
}

public func throwInternalError() throws {
throw InternalOnlyError(code: 42)
}

public func asyncThrowTyped(input: String) async throws -> String {
try await Task.sleep(for: .milliseconds(10))
if input.isEmpty {
throw MySwiftError.invalidInput(reason: "empty input")
}
return input
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ void asyncThrows() {

Throwable cause = ex.getCause();
assertNotNull(cause);
assertEquals(Exception.class, cause.getClass());
assertEquals(MySwiftError.class, cause.getClass());
assertEquals("swiftError", cause.getMessage());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void incrementThrows() throws Exception {

Throwable cause = ex.getCause();
assertNotNull(cause);
assertEquals(Exception.class, cause.getClass());
assertEquals(MySwiftError.class, cause.getClass());
assertEquals("swiftError", cause.getMessage());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

import com.example.swift.MySwiftLibrary;
import org.junit.jupiter.api.Test;
import org.swift.swiftkit.core.SwiftErrorException;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import static org.junit.jupiter.api.Assertions.*;

Expand All @@ -36,4 +40,63 @@ void throwStringActuallyThrows() {
assertTrue(exception.getMessage().contains("swiftError"));
// snippet.end
}

@Test
void catchSwiftErrorEnum() {
MySwiftError error = assertThrows(MySwiftError.class, () -> {
MySwiftLibrary.throwString("");
});
switch (error.getCase()) {
case MySwiftError.Case.SwiftError _ -> {}
case MySwiftError.Case.InvalidInput _ -> fail("unexpected case");
}
}

@Test
void typedThrowsIsCheckedException() {
// Only `MySwiftError` is caught here: this compiles because the
// Java signature is `throws MySwiftError`, not `throws Exception`.
try {
MySwiftLibrary.throwTyped("");
fail("expected MySwiftError");
} catch (MySwiftError e) {
assertTrue(e.getMessage().contains("swiftError"));
}
}

@Test
void typedThrowsPayload() {
try {
MySwiftLibrary.throwTyped("invalid");
fail("expected MySwiftError");
} catch (MySwiftError e) {
switch (e.getCase()) {
case MySwiftError.Case.InvalidInput(var reason) -> assertEquals("input was 'invalid'", reason);
default -> fail("unexpected case");
}
}
}

@Test
void typedThrowsSuccess() throws MySwiftError {
assertEquals("ok", MySwiftLibrary.throwTyped("ok"));
}

@Test
void nonExtractedErrorFallsBackToSwiftErrorException() {
SwiftErrorException e = assertThrows(SwiftErrorException.class, () -> {
MySwiftLibrary.throwInternalError();
});
assertNotNull(e.getMessage());
assertTrue(e.getMessage().contains("InternalOnlyError"));
assertTrue(e.as(MySwiftError.class).isEmpty());
}

@Test
void asyncThrowsTypedCause() {
Future<String> future = MySwiftLibrary.asyncThrowTyped("");

ExecutionException ex = assertThrows(ExecutionException.class, future::get);
assertInstanceOf(MySwiftError.class, ex.getCause());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ extension JNISwift2JavaGenerator {
printer.println()
}

printSwiftInstanceObjectMethods(&printer)
printSwiftInstanceObjectMethods(&printer, decl)
printer.println()
}
}
Expand Down Expand Up @@ -434,7 +434,7 @@ extension JNISwift2JavaGenerator {
printTypeMetadataAddressFunction(&printer, decl)
printer.println()

printSwiftInstanceObjectMethods(&printer)
printSwiftInstanceObjectMethods(&printer, decl)
printer.println()
}
}
Expand Down Expand Up @@ -509,7 +509,7 @@ extension JNISwift2JavaGenerator {
}

/// Prints common Swift object methods such as `equals`, `hashCode` etc.
private func printSwiftInstanceObjectMethods(_ printer: inout JavaPrinter) {
private func printSwiftInstanceObjectMethods(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) {
printer.print(
"""
public boolean equals(Object obj) {
Expand All @@ -523,10 +523,21 @@ extension JNISwift2JavaGenerator {
return SwiftObjects.hashCode(this.$memoryAddress(), this.$typeMetadataAddress());
}

public java.lang.String toString() {
return SwiftObjects.toString(this.$memoryAddress(), this.$typeMetadataAddress());
}
"""
)
// Error types inherit `Throwable.toString()`, which prints the class name followed by the Swift description.
if !self.isSwiftErrorType(decl) {
printer.print(
"""
public java.lang.String toString() {
return SwiftObjects.toString(this.$memoryAddress(), this.$typeMetadataAddress());
}

"""
)
}
printer.print(
"""
public java.lang.String toDebugString() {
return SwiftObjects.toDebugString(this.$memoryAddress(), this.$typeMetadataAddress());
}
Expand Down Expand Up @@ -589,14 +600,21 @@ extension JNISwift2JavaGenerator {
modifiers.append("static")
}
modifiers.append("final")
var implements = ["JNISwiftInstance"]
let isErrorType = self.isSwiftErrorType(decl)
// Error types get `JNISwiftInstance` from their `SwiftError` base class.
var implements = isErrorType ? [] : ["JNISwiftInstance"]
// Only protocols that were actually extracted have a generated Java interface to implement.
implements += self.inheritedProtocols(of: decl).map(\.effectiveJavaSimpleName)
let implementsClause = implements.joined(separator: .comma)
// Fully qualified so a user type named `SwiftError` cannot shadow it.
let extendsClause = isErrorType ? " extends org.swift.swiftkit.core.SwiftError" : ""
let implementsClause = implements.isEmpty ? "" : " implements \(implements.joined(separator: .comma))"
if isErrorType {
printer.print("@SuppressWarnings(\"serial\")")
}
// Specialized types are concrete — no generic clause on the Java side
let genericClause = decl.javaGenericClause
printer.printBraceBlock(
"\(modifiers.joined(separator: " ")) class \(decl.effectiveJavaSimpleName)\(genericClause) implements \(implementsClause)"
"\(modifiers.joined(separator: " ")) class \(decl.effectiveJavaSimpleName)\(genericClause)\(extendsClause)\(implementsClause)"
) { printer in
body(&printer)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ extension JNISwift2JavaGenerator {
knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable),
protocolWrappers: self.interfaceProtocolWrappers,
logger: self.logger,
errorTypes: self.swiftErrorTypes,
javaIdentifiers: self.currentJavaIdentifiers
)
}
Expand All @@ -40,18 +41,53 @@ extension JNISwift2JavaGenerator {
return cached
}

let translated: TranslatedFunctionDecl?
var translated: TranslatedFunctionDecl?
do {
translated = try self.javaTranslator.translate(decl)
} catch {
self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)")
translated = nil
}

if let candidate = translated, collidesWithThrowableMember(candidate, of: decl) {
translated = nil
}

translatedDecls[decl] = translated
return translated
}

/// Zero-argument `Throwable` members that a generated `SwiftError` subclass cannot redeclare with another meaning.
static let throwableMemberNames: Set<String> = [
"getMessage", "getLocalizedMessage", "getCause", "fillInStackTrace", "printStackTrace", "getStackTrace",
"getSuppressed",
]

/// Whether `translated`, a member of an error type, would clash with a `java.lang.Throwable` method
/// on the generated exception class, in which case it is skipped with a warning.
private func collidesWithThrowableMember(_ translated: TranslatedFunctionDecl, of decl: ExtractedFunc) -> Bool {
guard let parent = decl.parentType?.asNominalTypeDeclaration,
self.swiftErrorTypes.contains(parent.identity),
translated.translatedFunctionSignature.parameters.isEmpty,
Self.throwableMemberNames.contains(translated.name)
else {
return false
}

// `getMessage()`/`getLocalizedMessage()` returning a String naturally override the Throwable ones.
let isStringOverride =
["getMessage", "getLocalizedMessage"].contains(translated.name)
&& translated.translatedFunctionSignature.result.javaType == .javaLangString
if isStringOverride {
return false
}

self.logger.warning(
"Skipping '\(parent.qualifiedName).\(translated.name)': it collides with 'java.lang.Throwable.\(translated.name)()' on the generated exception class."
)
return true
}

func translatedEnumCase(
for decl: ExtractedEnumCase
) -> TranslatedEnumCase? {
Expand All @@ -70,6 +106,7 @@ extension JNISwift2JavaGenerator {
knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable),
protocolWrappers: self.interfaceProtocolWrappers,
logger: self.logger,
errorTypes: self.swiftErrorTypes,
javaIdentifiers: self.currentJavaIdentifiers,
)
translated = try translation.translate(enumCase: decl)
Expand All @@ -91,6 +128,8 @@ extension JNISwift2JavaGenerator {
var knownTypes: SwiftKnownTypes
let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper]
let logger: Logger
/// Module-qualified identities of extracted types that are surfaced to Java as `SwiftError` exceptions.
let errorTypes: Set<SwiftNominalIdentity>
var javaIdentifiers: JavaIdentifierFactory

func translate(enumCase: ExtractedEnumCase) throws -> TranslatedEnumCase {
Expand Down Expand Up @@ -227,10 +266,23 @@ extension JNISwift2JavaGenerator {
)
}

var thrownJavaType: JavaType?
if case .nominal(let thrown) = decl.functionSignature.thrownTypedError,
self.errorTypes.contains(thrown.nominalTypeDecl.identity)
{
// Only qualify the package when the error type lives in another Java package
let thrownPackage = moduleJavaPackages[thrown.nominalTypeDecl.moduleName]
thrownJavaType = .class(
package: thrownPackage == self.javaPackage ? nil : thrownPackage,
name: thrown.nominalTypeDecl.qualifiedName
)
}

return TranslatedFunctionDecl(
name: javaName,
isStatic: decl.isStatic || decl.isClass || !decl.hasParent || decl.isInitializer,
isThrowing: decl.isThrowing,
thrownJavaType: thrownJavaType,
isAsync: decl.isAsync,
isIsolated: decl.isIsolated,
isImplicitlyAsync: decl.functionSignature.isImplicitlyAsync,
Expand Down Expand Up @@ -1697,6 +1749,9 @@ extension JNISwift2JavaGenerator {

var isThrowing: Bool

/// The Java exception type for functions with typed throws of an extracted error type, `nil` otherwise.
var thrownJavaType: JavaType?

var isAsync: Bool

var isIsolated: Bool
Expand Down Expand Up @@ -1724,14 +1779,16 @@ extension JNISwift2JavaGenerator {
}

func throwsClause() -> String {
let isSyncThrowing = !(isAsync || isIsolated || isImplicitlyAsync)
let thrownException = (isSyncThrowing ? thrownJavaType?.fullyQualifiedClassName : nil) ?? "Exception"
guard !translatedFunctionSignature.exceptions.isEmpty else {
return isThrowing && !(isAsync || isIsolated || isImplicitlyAsync) ? " throws Exception" : ""
return isThrowing && isSyncThrowing ? " throws \(thrownException)" : ""
}

let signatureExceptions = translatedFunctionSignature.exceptions.compactMap(\.type.className).joined(
separator: ", "
)
return " throws \(signatureExceptions)\(isThrowing ? ", Exception" : "")"
return " throws \(signatureExceptions)\(isThrowing ? ", \(thrownException)" : "")"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2038,7 +2038,7 @@ extension JNISwift2JavaGenerator {
printer.print(
"""
let catchEnvironment = try! JavaVirtualMachine.shared().environment()
let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)])
let exception = catchEnvironment.makeJavaThrowable(for: error)
_ = catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, \(completeExceptionallyMethodID), [jvalue(l: exception)])
"""
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ extension JNISwift2JavaGenerator {
printer.println()
printNominalJavaBridge(&printer, type)
printer.println()

if self.isSwiftErrorType(type) && !self.inheritsThrowableErrorConformance(type) {
printThrowableErrorConformance(&printer, type)
printer.println()
}
}

printSpecificTypeThunks(&printer, type)
Expand Down Expand Up @@ -781,7 +786,7 @@ extension JNISwift2JavaGenerator {
printer.indent()
printer.print(
"""
environment.throwAsException(error)
environment.throwSwiftErrorAsJava(error)
\(dummyReturn(for: nativeSignature))
"""
)
Expand Down Expand Up @@ -951,6 +956,16 @@ extension JNISwift2JavaGenerator {
}
}

/// Prints the `_JNIThrowableError` conformance that lets the runtime throw this error as its generated Java class.
private func printThrowableErrorConformance(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) {
let bridgeName = JNICaching.bridgeName(for: type)
printer.printBraceBlock("extension \(type.effectiveSwiftTypeName): _JNIThrowableError") { printer in
printer.printBraceBlock("public func _makeJavaThrowable(in environment: JNIEnvironment) -> jthrowable?") { printer in
printer.print("\(bridgeName).toJavaObject(self, in: environment)")
}
}
}

private func printNominalJavaBridge(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) {
let bridgeName = JNICaching.bridgeName(for: type)
let cacheName = JNICaching.cacheName(for: type)
Expand Down
Loading
Loading