Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ jobs:
'JavaSieve',
'SwiftAndJavaJarSampleLib',
'SwiftKitSampleApp',
'JExtractJNISampleApp'
]
container:
image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }}
Expand Down
79 changes: 79 additions & 0 deletions Samples/JExtractJNISampleApp/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription

import class Foundation.FileManager
import class Foundation.ProcessInfo

// Note: the JAVA_HOME environment variable must be set to point to where
// Java is installed, e.g.,
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
func findJavaHome() -> String {
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
return home
}

// This is a workaround for envs (some IDEs) which have trouble with
// picking up env variables during the build process
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
if let lastChar = home.last, lastChar.isNewline {
return String(home.dropLast())
}

return home
}

fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
}
let javaHome = findJavaHome()

let javaIncludePath = "\(javaHome)/include"
#if os(Linux)
let javaPlatformIncludePath = "\(javaIncludePath)/linux"
#elseif os(macOS)
let javaPlatformIncludePath = "\(javaIncludePath)/darwin"
#else
// TODO: Handle windows as well
#error("Currently only macOS and Linux platforms are supported, this may change in the future.")
#endif

let package = Package(
name: "JExtractJNISampleApp",
platforms: [
.macOS(.v15)
],
products: [
.library(
name: "MySwiftLibrary",
type: .dynamic,
targets: ["MySwiftLibrary"]
)

],
dependencies: [
.package(name: "swift-java", path: "../../")
],
targets: [
.target(
name: "MySwiftLibrary",
dependencies: [
.product(name: "JavaKit", package: "swift-java"),
.product(name: "JavaRuntime", package: "swift-java"),
.product(name: "SwiftKitSwift", package: "swift-java"),
],
exclude: [
"swift-java.config"
],
swiftSettings: [
.swiftLanguageMode(.v5),
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]),
],
plugins: [
.plugin(name: "JExtractSwiftPlugin", package: "swift-java")
]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

// This is a "plain Swift" file containing various types of declarations,
// that is exported to Java by using the `jextract-swift` tool.
//
// No annotations are necessary on the Swift side to perform the export.

#if os(Linux)
import Glibc
#else
import Darwin.C
#endif

public func helloWorld() {
p("\(#function)")
}

public func globalTakeInt(i: Int64) {
p("i:\(i)")
}

public func globalMakeInt() -> Int64 {
return 42
}

public func globalWriteString(string: String) -> Int64 {
return Int64(string.count)
}

public func globalTakeIntInt(i: Int64, j: Int64) {
p("i:\(i), j:\(j)")
}

// ==== Internal helpers

func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) {
print("[swift][\(file):\(line)](\(function)) \(msg)")
fflush(stdout)
}

#if os(Linux)
// FIXME: why do we need this workaround?
@_silgen_name("_objc_autoreleaseReturnValue")
public func _objc_autoreleaseReturnValue(a: Any) {}

@_silgen_name("objc_autoreleaseReturnValue")
public func objc_autoreleaseReturnValue(a: Any) {}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"javaPackage": "com.example.swift",
"mode": "jni"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄

}
195 changes: 195 additions & 0 deletions Samples/JExtractJNISampleApp/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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 groovy.json.JsonSlurper
import org.swift.swiftkit.gradle.BuildUtils

import java.nio.file.*
import kotlinx.serialization.json.*

plugins {
id("build-logic.java-application-conventions")
id("me.champeau.jmh") version "0.7.2"
}

group = "org.swift.swiftkit"
version = "1.0-SNAPSHOT"

repositories {
mavenCentral()
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(24))
}
}

def swiftProductsWithJExtractPlugin() {
def stdout = new ByteArrayOutputStream()
def stderr = new ByteArrayOutputStream()

def result = exec {
commandLine 'swift', 'package', 'describe', '--type', 'json'
standardOutput = stdout
errorOutput = stderr
ignoreExitValue = true
}

def jsonOutput = stdout.toString()

if (result.exitValue == 0) {
def json = new JsonSlurper().parseText(jsonOutput)
def products = json.targets
.findAll { target ->
target.product_dependencies?.contains("JExtractSwiftPlugin")
}
.collectMany { target ->
target.product_memberships ?: []
}
return products
} else {
logger.warn("Command failed: ${stderr.toString()}")
return []
}
}


def swiftCheckValid = tasks.register("swift-check-valid", Exec) {
commandLine "swift"
args("-version")
}

def jextract = tasks.register("jextract", Exec) {
description = "Generate Java wrappers for swift target"
dependsOn swiftCheckValid

// only because we depend on "live developing" the plugin while using this project to test it
inputs.file(new File(rootDir, "Package.swift"))
inputs.dir(new File(rootDir, "Sources"))

// If the package description changes, we should execute jextract again, maybe we added jextract to new targets
inputs.file(new File(projectDir, "Package.swift"))

// monitor all targets/products which depend on the JExtract plugin
swiftProductsWithJExtractPlugin().each {
logger.info("[swift-java:jextract (Gradle)] Swift input target: ${it}")
inputs.dir(new File(layout.projectDirectory.asFile, "Sources/${it}".toString()))
}
outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}"))

File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile
if (!baseSwiftPluginOutputsDir.exists()) {
baseSwiftPluginOutputsDir.mkdirs()
}
Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).each {
// Add any Java sources generated by the plugin to our sourceSet
if (it.endsWith("JExtractSwiftPlugin/src/generated/java")) {
outputs.dir(it)
}
}

workingDir = layout.projectDirectory
commandLine "swift"
args("build") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build
// If we wanted to execute a specific subcommand, we can like this:
// args("run",/*
// "swift-java", "jextract",
// "--swift-module", "MySwiftLibrary",
// // java.package is obtained from the swift-java.config in the swift module
// "--output-java", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/src/generated/java").get()}",
// "--output-swift", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/Sources").get()}",
// "--log-level", (logging.level <= LogLevel.INFO ? "debug" : */"info")
// )
}

// Add the java-swift generated Java sources
sourceSets {
main {
java {
srcDir(jextract)
}
}
test {
java {
srcDir(jextract)
}
}
jmh {
java {
srcDir(jextract)
}
}
}

tasks.build {
dependsOn("jextract")
}


def cleanSwift = tasks.register("cleanSwift", Exec) {
workingDir = layout.projectDirectory
commandLine "swift"
args("package", "clean")
}
tasks.clean {
dependsOn("cleanSwift")
}

dependencies {
implementation(project(':SwiftKit'))

testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}

tasks.named('test', Test) {
useJUnitPlatform()
}

application {
mainClass = "com.example.swift.HelloJava2Swift"

applicationDefaultJvmArgs = [
"--enable-native-access=ALL-UNNAMED",

// Include the library paths where our dylibs are that we want to load and call
"-Djava.library.path=" +
(BuildUtils.javaLibraryPaths(rootDir) +
BuildUtils.javaLibraryPaths(project.projectDir)).join(":"),


// Enable tracing downcalls (to Swift)
"-Djextract.trace.downcalls=true"
]
}

String jmhIncludes = findProperty("jmhIncludes")

jmh {
if (jmhIncludes != null) {
includes = [jmhIncludes]
}

jvmArgsAppend = [
"--enable-native-access=ALL-UNNAMED",

"-Djava.library.path=" +
(BuildUtils.javaLibraryPaths(rootDir) +
BuildUtils.javaLibraryPaths(project.projectDir)).join(":"),

// Enable tracing downcalls (to Swift)
"-Djextract.trace.downcalls=false"
]
}
6 changes: 6 additions & 0 deletions Samples/JExtractJNISampleApp/ci-validate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

set -x
set -e

./gradlew run
1 change: 1 addition & 0 deletions Samples/JExtractJNISampleApp/gradlew
1 change: 1 addition & 0 deletions Samples/JExtractJNISampleApp/gradlew.bat
Loading