Skip to content
Merged
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
64 changes: 63 additions & 1 deletion Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import Foundation
import PackagePlugin

fileprivate let SwiftJavaConfigFileName = "swift-java.config"

@main
struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {

Expand Down Expand Up @@ -51,12 +53,14 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
log("Skipping jextract step, no 'javaPackage' configuration in \(getSwiftJavaConfigPath(target: target) ?? "")")
return []
}

// We use the the usual maven-style structure of "src/[generated|main|test]/java/..."
// that is common in JVM ecosystem
let outputJavaDirectory = context.outputJavaDirectory
let outputSwiftDirectory = context.outputSwiftDirectory

let dependentConfigFiles = searchForDependentConfigFiles(in: target)

var arguments: [String] = [
/*subcommand=*/"jextract",
"--swift-module", sourceModule.name,
Expand All @@ -71,6 +75,16 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
// as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin.
// We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc.
]

let dependentConfigFilesArguments = dependentConfigFiles.flatMap { moduleAndConfigFile in
let (moduleName, configFile) = moduleAndConfigFile
return [
"--depends-on",
"\(configFile.path(percentEncoded: false))"
]
}
arguments += dependentConfigFilesArguments

if !javaPackage.isEmpty {
arguments += ["--java-package", javaPackage]
}
Expand Down Expand Up @@ -117,5 +131,53 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
)
]
}

/// Find the manifest files from other swift-java executions in any targets
/// this target depends on.
func searchForDependentConfigFiles(in target: any Target) -> [(String, URL)] {
var dependentConfigFiles = [(String, URL)]()

func _searchForConfigFiles(in target: any Target) {
// log("Search for config files in target: \(target.name)")
let dependencyURL = URL(filePath: target.directory.string)

// Look for a config file within this target.
let dependencyConfigURL = dependencyURL
.appending(path: SwiftJavaConfigFileName)
let dependencyConfigString = dependencyConfigURL
.path(percentEncoded: false)

if FileManager.default.fileExists(atPath: dependencyConfigString) {
dependentConfigFiles.append((target.name, dependencyConfigURL))
}
}

// Process direct dependencies of this target.
for dependency in target.dependencies {
switch dependency {
case .target(let target):
// log("Dependency target: \(target.name)")
_searchForConfigFiles(in: target)

case .product(let product):
// log("Dependency product: \(product.name)")
for target in product.targets {
// log("Dependency product: \(product.name), target: \(target.name)")
_searchForConfigFiles(in: target)
}

@unknown default:
break
}
}

// Process indirect target dependencies.
for dependency in target.recursiveTargetDependencies {
// log("Recursive dependency target: \(dependency.name)")
_searchForConfigFiles(in: dependency)
}

return dependentConfigFiles
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

import JavaKit

public class MySwiftClass {
public let x: Int64
public let y: Int64
Expand Down Expand Up @@ -84,4 +86,8 @@ public class MySwiftClass {
public func copy() -> MySwiftClass {
return MySwiftClass(x: self.x, y: self.y)
}

public func addXWithJavaLong(_ other: JavaLong) -> Int64 {
return self.x + other.longValue()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,13 @@ void copy() {
assertNotEquals(c1.$memoryAddress(), c2.$memoryAddress());
}
}

@Test
void addXWithJavaLong() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
Long javaLong = 50L;
assertEquals(70, c1.addXWithJavaLong(javaLong));
}
}
}
39 changes: 39 additions & 0 deletions Sources/JExtractSwiftLib/Convenience/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

import JavaTypes

extension String {

var firstCharacterUppercased: String {
Expand All @@ -31,4 +33,41 @@ extension String {
let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2)
return self[thirdCharacterIndex].isUppercase
}

/// Returns a version of the string correctly escaped for a JNI
var escapedJNIIdentifier: String {
self.map {
if $0 == "_" {
return "_1"
} else if $0 == "/" {
return "_"
} else if $0 == ";" {
return "_2"
} else if $0 == "[" {
return "_3"
} else if $0.isASCII && ($0.isLetter || $0.isNumber) {
return String($0)
} else if let utf16 = $0.utf16.first {
// Escape any non-alphanumeric to their UTF16 hex encoding
let utf16Hex = String(format: "%04x", utf16)
return "_0\(utf16Hex)"
} else {
fatalError("Invalid JNI character: \($0)")
}
}
.joined()
}

/// Looks up self as a JavaKit wrapped class name and converts it
/// into a `JavaType.class` if it exists in `lookupTable`.
func parseJavaClassFromJavaKitName(in lookupTable: [String: String]) -> JavaType? {
guard let canonicalJavaName = lookupTable[self] else {
return nil
}
let nameParts = canonicalJavaName.components(separatedBy: ".")
let javaPackageName = nameParts.dropLast().joined(separator: ".")
let javaClassName = nameParts.last!

return .class(package: javaPackageName, name: javaClassName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ extension JNISwift2JavaGenerator {

let translated: TranslatedFunctionDecl?
do {
let translation = JavaTranslation(swiftModuleName: swiftModuleName, javaPackage: self.javaPackage)
let translation = JavaTranslation(
swiftModuleName: swiftModuleName,
javaPackage: self.javaPackage,
javaClassLookupTable: self.javaClassLookupTable
)
translated = try translation.translate(decl)
} catch {
self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)")
Expand All @@ -38,9 +42,13 @@ extension JNISwift2JavaGenerator {
struct JavaTranslation {
let swiftModuleName: String
let javaPackage: String
let javaClassLookupTable: JavaClassLookupTable

func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl {
let nativeTranslation = NativeJavaTranslation(javaPackage: self.javaPackage)
let nativeTranslation = NativeJavaTranslation(
javaPackage: self.javaPackage,
javaClassLookupTable: self.javaClassLookupTable
)

// Types with no parent will be outputted inside a "module" class.
let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName
Expand Down Expand Up @@ -157,6 +165,8 @@ extension JNISwift2JavaGenerator {
) throws -> TranslatedParameter {
switch swiftType {
case .nominal(let nominalType):
let nominalTypeName = nominalType.nominalTypeDecl.name

if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else {
throw JavaTranslationError.unsupportedSwiftType(swiftType)
Expand All @@ -168,11 +178,25 @@ extension JNISwift2JavaGenerator {
)
}

// For now, we assume this is a JExtract class.
if nominalType.isJavaKitWrapper {
guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else {
throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType)
}

return TranslatedParameter(
parameter: JavaParameter(
name: parameterName,
type: javaType
),
conversion: .placeholder
)
}

// We assume this is a JExtract class.
return TranslatedParameter(
parameter: JavaParameter(
name: parameterName,
type: .class(package: nil, name: nominalType.nominalTypeDecl.name)
type: .class(package: nil, name: nominalTypeName)
),
conversion: .valueMemoryAddress(.placeholder)
)
Expand Down Expand Up @@ -213,7 +237,11 @@ extension JNISwift2JavaGenerator {
)
}

// For now, we assume this is a JExtract class.
if nominalType.isJavaKitWrapper {
throw JavaTranslationError.unsupportedSwiftType(swiftResult.type)
}

// We assume this is a JExtract class.
let javaType = JavaType.class(package: nil, name: nominalType.nominalTypeDecl.name)
return TranslatedResult(
javaType: javaType,
Expand Down Expand Up @@ -350,5 +378,9 @@ extension JNISwift2JavaGenerator {

enum JavaTranslationError: Error {
case unsupportedSwiftType(SwiftType)

/// The user has not supplied a mapping from `SwiftType` to
/// a java class.
case wrappedJavaClassTranslationNotProvided(SwiftType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extension JNISwift2JavaGenerator {

struct NativeJavaTranslation {
let javaPackage: String
let javaClassLookupTable: JavaClassLookupTable

/// Translates a Swift function into the native JNI method signature.
func translate(
Expand Down Expand Up @@ -62,10 +63,12 @@ extension JNISwift2JavaGenerator {
swiftParameter: SwiftParameter,
parameterName: String,
methodName: String,
parentName: String,
parentName: String
) throws -> NativeParameter {
switch swiftParameter.type {
case .nominal(let nominalType):
let nominalTypeName = nominalType.nominalTypeDecl.name

if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else {
throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type)
Expand All @@ -78,6 +81,25 @@ extension JNISwift2JavaGenerator {
)
}

if nominalType.isJavaKitWrapper {
guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else {
throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftParameter.type)
}

return NativeParameter(
name: parameterName,
javaType: javaType,
conversion: .initializeJavaKitWrapper(wrapperName: nominalTypeName)
)
}

// JExtract classes are passed as the pointer.
return NativeParameter(
name: parameterName,
javaType: .long,
conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type))
)

case .tuple([]):
return NativeParameter(
name: parameterName,
Expand Down Expand Up @@ -110,13 +132,6 @@ extension JNISwift2JavaGenerator {
case .metatype, .optional, .tuple, .existential, .opaque:
throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type)
}

// Classes are passed as the pointer.
return NativeParameter(
name: parameterName,
javaType: .long,
conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type))
)
}

func translateClosureResult(
Expand Down Expand Up @@ -193,6 +208,15 @@ extension JNISwift2JavaGenerator {
)
}

if nominalType.isJavaKitWrapper {
throw JavaTranslationError.unsupportedSwiftType(swiftResult.type)
}

return NativeResult(
javaType: .long,
conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type))
)

case .tuple([]):
return NativeResult(
javaType: .void,
Expand All @@ -203,13 +227,7 @@ extension JNISwift2JavaGenerator {
throw JavaTranslationError.unsupportedSwiftType(swiftResult.type)
}

// TODO: Handle other classes, for example from JavaKit macros.
// for now we assume all passed in classes are JExtract generated
// so we pass the pointer.
return NativeResult(
javaType: .long,
conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type))
)

}
}

Expand Down Expand Up @@ -262,6 +280,8 @@ extension JNISwift2JavaGenerator {

indirect case closureLowering(parameters: [NativeParameter], result: NativeResult)

case initializeJavaKitWrapper(wrapperName: String)

/// Returns the conversion string applied to the placeholder.
func render(_ printer: inout CodePrinter, _ placeholder: String) -> String {
// NOTE: 'printer' is used if the conversion wants to cause side-effects.
Expand Down Expand Up @@ -348,6 +368,9 @@ extension JNISwift2JavaGenerator {
printer.print("}")

return printer.finalize()

case .initializeJavaKitWrapper(let wrapperName):
return "\(wrapperName)(javaThis: \(placeholder), environment: environment!)"
}
}
}
Expand Down
Loading
Loading