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
35 changes: 27 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
// swift-tools-version:4.0
// swift-tools-version:6.0

import PackageDescription

let package = Package(
name: "JavaCoder",
products:[
products: [
.library(
name: "JavaCoder",
targets:["JavaCoder"]
)
name: "JavaCoder",
targets: ["JavaCoder"]
),
.library(
name: "java_swift",
targets: ["java_swift"]
),
],
dependencies: [
.package(url: "https://github.com/readdle/java_swift.git", .upToNextMinor(from: "2.2.0")),
.package(url: "https://github.com/swiftlang/swift-java-jni-core.git", .upToNextMinor(from: "0.5.1")),
.package(url: "https://github.com/readdle/swift-anycodable.git", .upToNextMinor(from: "1.0.2")),
],
targets: [
.target(name: "JavaCoder", dependencies: ["java_swift", "AnyCodable"], path: "Sources"),
// compatibility shim exposing the historical `java_swift` API on top of swiftlang's SwiftJavaJNICore (jni-core).
.target(
name: "java_swift",
dependencies: [
.product(name: "SwiftJavaJNICore", package: "swift-java-jni-core"),
],
path: "Sources/java_swift"
),
.target(
name: "JavaCoder",
dependencies: [
"java_swift",
.product(name: "AnyCodable", package: "swift-anycodable"),
],
path: "Sources/JavaCoder"
),
],
swiftLanguageVersions: [5, 4]
swiftLanguageModes: [.v5]
)
244 changes: 244 additions & 0 deletions Sources/JavaCoder/JavaBridgeable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//
// JavaBridgeable.swift
//
// Ported from readdle/swift-java's `Java` module (JavaBridgeable.swift) and
// readdle/java_swift (Throwable.swift) so that the JavaCoder package provides
// the non-primitive bridging the SwiftJava codegen relies on, without the
// readdle `Java`/`java_swift` runtime dependencies.
//
// This lives in the JavaCoder target (rather than the `java_swift` shim)
// because the default `from(javaObject:)`/`javaObject()` implementations are
// built on `JavaDecoder`/`JavaEncoder`. The shim cannot import JavaCoder
// without forming a dependency cycle.
//

import Foundation
import java_swift

public protocol JavaBridgeable: Codable {

static func from(javaObject: jobject) throws -> Self

func javaObject() throws -> jobject
}

extension JavaBridgeable {

// Decoding SwiftValue type with JavaCoder
public static func from(javaObject: jobject) throws -> Self {
// ignore forPackage for basic impl
return try JavaDecoder(forPackage: "").decode(Self.self, from: javaObject)
}

// Encoding SwiftValue type with JavaCoder
public func javaObject() throws -> jobject {
// ignore forPackage for basic impl
return try JavaEncoder(forPackage: "").encode(self)
}

}

extension Bool: JavaBridgeable {}
extension Int: JavaBridgeable {}
extension Int8: JavaBridgeable {}
extension Int16: JavaBridgeable {}
extension Int32: JavaBridgeable {}
extension Int64: JavaBridgeable {}
extension UInt: JavaBridgeable {}
extension UInt8: JavaBridgeable {}
extension UInt16: JavaBridgeable {}
extension UInt32: JavaBridgeable {}
extension UInt64: JavaBridgeable {}
extension Float: JavaBridgeable {}
extension Double: JavaBridgeable {}
extension Dictionary: JavaBridgeable where Key: Codable, Value: Codable {}
extension Set: JavaBridgeable where Element: Codable {}
extension Date: JavaBridgeable {}
extension Data: JavaBridgeable {}
extension URL: JavaBridgeable {}

extension String: JavaBridgeable {

public static func from(javaObject: jobject) throws -> String {
var isCopy: jboolean = 0
guard let env = JNI.env,
let chars = JNI.api.GetStringChars(env, javaObject, &isCopy) else {
throw JavaCodingError.cantCreateObject("String")
}
defer {
JNI.api.ReleaseStringChars(env, javaObject, chars)
}
return String(utf16CodeUnits: chars, count: Int(JNI.api.GetStringLength(env, javaObject)))
}

public func javaObject() throws -> jobject {
// Use NewString with UTF-16 code units rather than NewStringUTF, which
// expects Java's "modified UTF-8" and mangles supplementary (4-byte)
// characters. Matches the GetStringChars decode path above.
guard let env = JNI.env else {
throw JavaCodingError.cantCreateObject("String")
}
let utf16 = Array(self.utf16)
guard let javaObject: jstring = utf16.withUnsafeBufferPointer({ buffer in
JNI.api.NewString(env, buffer.baseAddress, jsize(buffer.count))
}) else {
throw JavaCodingError.cantCreateObject("String")
}
return javaObject
}

}

/// Minimal `java.lang.Throwable` wrapper, ported from readdle/java_swift, used
/// to bridge Java exceptions into `NSError`.
open class Throwable {

private let javaObject: jobject

public required init(javaObject: jobject) {
self.javaObject = javaObject
}

open func getMessage() -> String! {
guard let methodID = try? JNI.getJavaMethod(forClass: "java/lang/Throwable",
method: "getMessage",
sig: "()Ljava/lang/String;"),
let result = JNI.CallObjectMethod(javaObject, methodID: methodID) else {
return nil
}
defer {
JNI.DeleteLocalRef(result)
}
return String(javaObject: result)
}

open func printStackTrace() {
guard let methodID = try? JNI.getJavaMethod(forClass: "java/lang/Throwable",
method: "printStackTrace",
sig: "()V") else {
return
}
JNI.CallVoidMethod(javaObject, methodID)
}

public func className() -> String {
guard let env = JNI.env,
let cls = JNI.api.GetObjectClass(env, javaObject) else {
return "java/lang/Throwable"
}
defer {
JNI.DeleteLocalRef(cls)
}
guard let methodID = try? JNI.getJavaMethod(forClass: "java/lang/Class",
method: "getName",
sig: "()Ljava/lang/String;"),
let javaClassName = JNI.CallObjectMethod(cls, methodID: methodID) else {
return "java/lang/Throwable"
}
defer {
JNI.DeleteLocalRef(javaClassName)
}
return String(javaObject: javaClassName)
}

public func stackTraceString() -> String {
return "" // no stack trace here
}

public func lastStackTraceString() -> String? {
return nil
}

}

// Error can't implement JavaBridgeable protocol
fileprivate let javaExceptionClass = JNI.GlobalFindClass("java/lang/Exception")!
fileprivate let javaExceptionConstructor = try! JNI.getJavaMethod(forClass: "java/lang/Exception",
method: "<init>",
sig: "(Ljava/lang/String;)V")

fileprivate let JavaErrorMessageKey = "JavaErrorMessageKey"
fileprivate let JavaErrorStackTrace = "JavaErrorStackTrace"

extension Error {

public static var javaErrorMessageKey: String {
return JavaErrorMessageKey
}

public static var javaErrorStackTrace: String {
return JavaErrorStackTrace
}

public static func from(javaObject: jobject) throws -> Error {
let throwable = Throwable(javaObject: javaObject)
let className = throwable.className()
let message = throwable.getMessage()
let lastStackTrace = throwable.lastStackTraceString()
let userInfo: [String: Any] = [javaErrorMessageKey: message ?? "unavailable",
javaErrorStackTrace: lastStackTrace ?? "unavailable"]

// Try extract error according to Error.javaObject()
if let javaMessage = message {
let parts = javaMessage.split(separator: ":")
if parts.count > 1 {
let domain = String(parts[0])
let codeString = String(parts[1])
if let code = Int(codeString) {
return NSError(domain: domain, code: code, userInfo: userInfo)
}
}
}

// Plan B
let domain = className
let code = 0
return NSError(domain: domain, code: code, userInfo: userInfo)
}

}

#if !HIDE_ERROR_JAVA_BRIDGEABLE
extension Error {
public func javaObject() throws -> jobject {
let nsError = self as NSError
let message = "\(nsError.domain):\(nsError.code)"
guard let javaObject = JNI.NewObject(javaExceptionClass,
methodID: javaExceptionConstructor,
args: [jvalue(l: try message.javaObject())]) else {
throw JavaCodingError.cantCreateObject("java/lang/Exception")
}
return javaObject
}
}

extension Error where Self: RawRepresentable, Self.RawValue: SignedInteger {

public func javaObject() throws -> jobject {
let domain = String(reflecting: type(of: self))
let code: Int = numericCast(self.rawValue)
let message = try "\(domain):\(code)".javaObject()
guard let javaObject = JNI.NewObject(javaExceptionClass,
methodID: javaExceptionConstructor,
args: [jvalue(l: message)]) else {
throw JavaCodingError.cantCreateObject("java/lang/Exception")
}
return javaObject
}
}

extension Error where Self: RawRepresentable, Self.RawValue: UnsignedInteger {

public func javaObject() throws -> jobject {
let domain = String(reflecting: type(of: self))
let code: Int = numericCast(self.rawValue)
let message = try "\(domain):\(code)".javaObject()
guard let javaObject = JNI.NewObject(javaExceptionClass,
methodID: javaExceptionConstructor,
args: [jvalue(l: message)]) else {
throw JavaCodingError.cantCreateObject("java/lang/Exception")
}
return javaObject
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import Foundation
import java_swift
import CAndroidNDK

public typealias JavaEncodableClosure = (Any, [CodingKey]) throws -> jobject
public typealias JavaDecodableClosure = (jobject, [CodingKey]) throws -> Decodable
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import Foundation
import java_swift
import CAndroidNDK

public typealias JavaBoolean = jboolean
public typealias JavaByte = jbyte
Expand Down
76 changes: 76 additions & 0 deletions Sources/java_swift/JNIBootstrap.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// JNIBootstrap.swift
// java_swift (compatibility shim)
//
// JVM bootstrap glue.
//
// IMPORTANT: This shim intentionally does NOT define `@_cdecl("JNI_OnLoad")`.
// A dynamic library can export only one `JNI_OnLoad`, and when this package is
// linked alongside swiftlang's `SwiftJava` (which defines its own `JNI_OnLoad`
// that calls `JavaVirtualMachine.setSharedJVM`), defining a second one here
// would be a duplicate-symbol conflict.
//
// Registering the JVM is the application's responsibility. Define your own
// `@_cdecl("JNI_OnLoad")` (or rely on SwiftJava's, when linked in) to register
// the shared JVM via `JavaVirtualMachine.setShared(...)` /
// `JavaVirtualMachine.setSharedJVM(...)`. Once a shared JVM is registered,
// `JNI.env` / `JavaVirtualMachine.shared()` work out of the box.
//
// The only thing this shim needs beyond that is the context class loader, used
// to resolve *application* classes from threads not started by the JVM
// (notably on Android). Call `JNIBootstrap.captureContextClassLoader()` once
// from a Java-originated thread (e.g. your native init method) to capture it.
//
// On desktop where Swift drives the JVM, no bootstrap is required;
// `JavaVirtualMachine.shared()` lazily creates or adopts a JVM, and raw
// `FindClass` resolves classpath classes without a context loader.
//

import Foundation
@_exported import SwiftJavaJNICore

public enum JNIBootstrap {

/// Capture the current thread's context class loader into ``JNICore``'s
/// ``JNICore/classLoader`` so that ``JNICore/FindClass(_:_:_:)`` can resolve
/// application classes from threads not started by the JVM.
///
/// Call this exactly once, early, from a thread that originated in Java
/// (so the context class loader is the application loader, not the system
/// loader). Safe to use whether or not SwiftJava is present; it relies only
/// on the shared JVM already being registered.
///
/// - Returns: `true` if a class loader was captured.
@discardableResult
public static func captureContextClassLoader() -> Bool {
guard let env = JNI.env else {
return false
}
guard let threadClass = JNI.api.FindClass(env, "java/lang/Thread") else {
return false
}
defer { env.deleteLocalRef(threadClass) }

guard let currentThreadMethod = JNI.api.GetStaticMethodID(env, threadClass,
"currentThread",
"()Ljava/lang/Thread;"),
let getContextClassLoaderMethod = JNI.api.GetMethodID(env, threadClass,
"getContextClassLoader",
"()Ljava/lang/ClassLoader;") else {
return false
}

guard let currentThread = JNI.api.CallStaticObjectMethodA(env, threadClass, currentThreadMethod, nil) else {
return false
}
defer { env.deleteLocalRef(currentThread) }

guard let loader = JNI.api.CallObjectMethodA(env, currentThread, getContextClassLoaderMethod, nil) else {
return false
}
defer { env.deleteLocalRef(loader) }

JNI.classLoader = JNI.api.NewGlobalRef(env, loader)
return JNI.classLoader != nil
}
}
Loading