Skip to content

Commit

Permalink
JavaKit: add CustomJavaClassLoader protocol
Browse files Browse the repository at this point in the history
Swift projections of Java classes can conform to CustomJavaClassLoader, to
provide a custom class loader to be used when initializing new instances. This
is useful for platforms such as Android which modify class and class loader
visibility depending on the call stack.

Fixes: swiftlang#154
  • Loading branch information
lhoward committed Nov 7, 2024
1 parent a8a75c5 commit 5b1b461
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 57 deletions.
47 changes: 44 additions & 3 deletions Sources/JavaKit/AnyJavaObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ public protocol AnyJavaObject {
var javaHolder: JavaObjectHolder { get }
}

/// Protocol that allows Swift types to specify a custom Java class loader on
/// initialization. This is useful for platforms (e.g. Android) where the default
/// class loader does not make all application classes visible.
public protocol CustomJavaClassLoader: AnyJavaObject {
static func getJavaClassLoader(in environment: JNIEnvironment) throws -> JavaClassLoader!
}

/// Add getClassLoader() to JavaObject as it is otherwise recursively defined
extension JavaObject {
@JavaMethod
public func getClassLoader() throws -> JavaClassLoader!
}

extension AnyJavaObject {
/// Retrieve the underlying Java object.
public var javaThis: jobject {
Expand Down Expand Up @@ -81,13 +94,41 @@ extension AnyJavaObject {
JavaClass(javaThis: jniClass!, environment: javaEnvironment)
}

/// Retrieve the Java class for this type.
public static func getJNIClass(in environment: JNIEnvironment) throws -> jclass {
try environment.translatingJNIExceptions {
/// Retrieve the Java class for this type using the default class loader.
private static func _withJNIClassFromDefaultClassLoader<Result>(
in environment: JNIEnvironment,
_ body: (jclass) throws -> Result
) throws -> Result {
let resolvedClass = try environment.translatingJNIExceptions {
environment.interface.FindClass(
environment,
fullJavaClassNameWithSlashes
)
}!
return try body(resolvedClass)
}

/// Retrieve the Java class for this type using a specific class loader.
private static func _withJNIClassFromCustomClassLoader<Result>(
_ classLoader: JavaClassLoader,
in environment: JNIEnvironment,
_ body: (jclass) throws -> Result
) throws -> Result {
let resolvedClass = try classLoader.findClass(fullJavaClassName)
return try body(resolvedClass!.javaThis)
}

/// Retrieve the Java class for this type and execute body().
@_spi(Testing)
public static func withJNIClass<Result>(
in environment: JNIEnvironment,
_ body: (jclass) throws -> Result
) throws -> Result {
if let customJavaClassLoader = self as? CustomJavaClassLoader.Type,
let customClassLoader = try customJavaClassLoader.getJavaClassLoader(in: environment) {
try _withJNIClassFromCustomClassLoader(customClassLoader, in: environment, body)
} else {
try _withJNIClassFromDefaultClassLoader(in: environment, body)
}
}
}
8 changes: 3 additions & 5 deletions Sources/JavaKit/Exceptions/ExceptionHandling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ extension JNIEnvironment {
}

// Otherwise, create a exception with a message.
_ = interface.ThrowNew(
self,
try! JavaClass<Exception>.getJNIClass(in: self),
String(describing: error)
)
_ = try! JavaClass<Exception>.withJNIClass(in: self) { exceptionClass in
interface.ThrowNew(self, exceptionClass, String(describing: error))
}
}
}
1 change: 1 addition & 0 deletions Sources/JavaKit/Java2Swift.config
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"java.lang.Byte" : "JavaByte",
"java.lang.Character" : "JavaCharacter",
"java.lang.Class" : "JavaClass",
"java.lang.ClassLoader" : "JavaClassLoader",
"java.lang.Double" : "JavaDouble",
"java.lang.Error" : "JavaError",
"java.lang.Exception" : "Exception",
Expand Down
10 changes: 6 additions & 4 deletions Sources/JavaKit/JavaClass+Initialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ extension JavaClass {
@_nonoverride
public convenience init(environment: JNIEnvironment? = nil) throws {
let environment = try environment ?? JavaVirtualMachine.shared().environment()
self.init(
javaThis: try ObjectType.getJNIClass(in: environment),
environment: environment
)
var javaClassHolder: JavaObjectHolder!

javaClassHolder = try ObjectType.withJNIClass(in: environment) { javaClass in
JavaObjectHolder(object: javaClass, environment: environment)
}
self.init(javaHolder: javaClassHolder)
}
}
20 changes: 9 additions & 11 deletions Sources/JavaKit/JavaObject+Inheritance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,17 @@ extension AnyJavaObject {
private func isInstanceOf<OtherClass: AnyJavaObject>(
_ otherClass: OtherClass.Type
) -> jclass? {
guard let otherJavaClass = try? otherClass.getJNIClass(in: javaEnvironment) else {
return nil
}
try? otherClass.withJNIClass(in: javaEnvironment) { otherJavaClass in
if javaEnvironment.interface.IsInstanceOf(
javaEnvironment,
javaThis,
otherJavaClass
) == 0 {
return nil
}

if javaEnvironment.interface.IsInstanceOf(
javaEnvironment,
javaThis,
otherJavaClass
) == 0 {
return nil
return otherJavaClass
}

return otherJavaClass
}

/// Determine whether this object is an instance of a specific
Expand Down
32 changes: 16 additions & 16 deletions Sources/JavaKit/JavaObject+MethodCalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,23 +277,23 @@ extension AnyJavaObject {
in environment: JNIEnvironment,
arguments: repeat each Param
) throws -> jobject {
let thisClass = try Self.getJNIClass(in: environment)

// Compute the method signature so we can find the right method, then look up the
// method within the class.
let methodID = try Self.javaMethodLookup(
in: environment,
thisClass: thisClass,
methodName: javaConstructorName,
parameterTypes: repeat (each Param).self,
resultType: .void
)
try Self.withJNIClass(in: environment) { thisClass in
// Compute the method signature so we can find the right method, then look up the
// method within the class.
let methodID = try Self.javaMethodLookup(
in: environment,
thisClass: thisClass,
methodName: javaConstructorName,
parameterTypes: repeat (each Param).self,
resultType: .void
)

// Retrieve the constructor, then map the arguments and call it.
let jniArgs = getJValues(repeat each arguments, in: environment)
return try environment.translatingJNIExceptions {
environment.interface.NewObjectA!(environment, thisClass, methodID, jniArgs)
}!
// Retrieve the constructor, then map the arguments and call it.
let jniArgs = getJValues(repeat each arguments, in: environment)
return try environment.translatingJNIExceptions {
environment.interface.NewObjectA!(environment, thisClass, methodID, jniArgs)
}!
}
}

/// Retrieve the JNI field ID for a field with the given name and type.
Expand Down
5 changes: 3 additions & 2 deletions Sources/JavaKit/Optional+JavaObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ extension Optional: JavaValue where Wrapped: AnyJavaObject {

public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray {
return { environment, size in
let jniClass = try! Wrapped.getJNIClass(in: environment)
return environment.interface.NewObjectArray(environment, size, jniClass, nil)
try! Wrapped.withJNIClass(in: environment) { jniClass in
environment.interface.NewObjectArray(environment, size, jniClass, nil)
}
}
}

Expand Down
72 changes: 72 additions & 0 deletions Sources/JavaKit/generated/JavaClassLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Auto-generated by Java-to-Swift wrapper generator.
import JavaRuntime

@JavaClass("java.lang.ClassLoader")
open class JavaClassLoader: JavaObject {
@JavaMethod
open func getName() -> String

@JavaMethod
open func loadClass(_ arg0: String, _ arg1: Bool) throws -> JavaClass<JavaObject>!

@JavaMethod
open func loadClass(_ arg0: String) throws -> JavaClass<JavaObject>!

@JavaMethod
open func setSigners(_ arg0: JavaClass<JavaObject>?, _ arg1: [JavaObject?])

@JavaMethod
open func getClassLoadingLock(_ arg0: String) -> JavaObject!

@JavaMethod
open func findLoadedClass(_ arg0: String) -> JavaClass<JavaObject>!

@JavaMethod
open func findClass(_ arg0: String) throws -> JavaClass<JavaObject>!

@JavaMethod
open func findClass(_ arg0: String, _ arg1: String) -> JavaClass<JavaObject>!

@JavaMethod
open func resolveClass(_ arg0: JavaClass<JavaObject>?)

@JavaMethod
open func defineClass(_ arg0: [Int8], _ arg1: Int32, _ arg2: Int32) throws -> JavaClass<JavaObject>!

@JavaMethod
open func defineClass(_ arg0: String, _ arg1: [Int8], _ arg2: Int32, _ arg3: Int32) throws -> JavaClass<JavaObject>!

@JavaMethod
open func findLibrary(_ arg0: String) -> String

@JavaMethod
open func findSystemClass(_ arg0: String) throws -> JavaClass<JavaObject>!

@JavaMethod
open func isRegisteredAsParallelCapable() -> Bool

@JavaMethod
open func getParent() -> JavaClassLoader!

@JavaMethod
open func setDefaultAssertionStatus(_ arg0: Bool)

@JavaMethod
open func setPackageAssertionStatus(_ arg0: String, _ arg1: Bool)

@JavaMethod
open func setClassAssertionStatus(_ arg0: String, _ arg1: Bool)

@JavaMethod
open func clearAssertionStatus()
}
extension JavaClass<JavaClassLoader> {
@JavaStaticMethod
public func getPlatformClassLoader() -> JavaClassLoader!

@JavaStaticMethod
public func getSystemClassLoader() -> JavaClassLoader!

@JavaStaticMethod
public func registerAsParallelCapable() -> Bool
}
36 changes: 20 additions & 16 deletions Tests/Java2SwiftTests/Java2SwiftTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//

@_spi(Testing)
import JavaKit
import Java2SwiftLib
import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43
Expand Down Expand Up @@ -661,24 +662,27 @@ func assertTranslatedClass<JavaClassType: AnyJavaObject>(
translator.translatedClasses[javaType.fullJavaClassName] = (swiftTypeName, nil)
translator.nestedClasses = nestedClasses
translator.startNewFile()
let translatedDecls = try translator.translateClass(
JavaClass<JavaObject>(
javaThis: javaType.getJNIClass(in: environment),
environment: environment)
)
let importDecls = translator.getImportDecls()

let swiftFileText = """
// Auto-generated by Java-to-Swift wrapper generator.
\(importDecls.map { $0.description }.joined())
\(translatedDecls.map { $0.description }.joined(separator: "\n"))
"""
try javaType.withJNIClass(in: environment) { javaClass in
let translatedDecls = try translator.translateClass(
JavaClass<JavaObject>(
javaThis: javaClass,
environment: environment)
)
let importDecls = translator.getImportDecls()

for expectedChunk in expectedChunks {
if swiftFileText.contains(expectedChunk) {
continue
}
let swiftFileText = """
// Auto-generated by Java-to-Swift wrapper generator.
\(importDecls.map { $0.description }.joined())
\(translatedDecls.map { $0.description }.joined(separator: "\n"))
"""

XCTFail("Expected chunk '\(expectedChunk)' not found in '\(swiftFileText)'", file: file, line: line)
for expectedChunk in expectedChunks {
if swiftFileText.contains(expectedChunk) {
continue
}

XCTFail("Expected chunk '\(expectedChunk)' not found in '\(swiftFileText)'", file: file, line: line)
}
}
}

0 comments on commit 5b1b461

Please sign in to comment.