Skip to content

Commit 8c38d19

Browse files
madsodgaardktoso
andauthored
Add JExtract JNI sample app (#295)
Co-authored-by: Konrad `ktoso` Malawski <[email protected]>
1 parent df03e23 commit 8c38d19

File tree

12 files changed

+482
-27
lines changed

12 files changed

+482
-27
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ jobs:
7878
'JavaSieve',
7979
'SwiftAndJavaJarSampleLib',
8080
'SwiftKitSampleApp',
81+
'JExtractJNISampleApp'
8182
]
8283
container:
8384
image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import CompilerPluginSupport
5+
import PackageDescription
6+
7+
import class Foundation.FileManager
8+
import class Foundation.ProcessInfo
9+
10+
// Note: the JAVA_HOME environment variable must be set to point to where
11+
// Java is installed, e.g.,
12+
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
13+
func findJavaHome() -> String {
14+
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
15+
return home
16+
}
17+
18+
// This is a workaround for envs (some IDEs) which have trouble with
19+
// picking up env variables during the build process
20+
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
21+
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
22+
if let lastChar = home.last, lastChar.isNewline {
23+
return String(home.dropLast())
24+
}
25+
26+
return home
27+
}
28+
29+
fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
30+
}
31+
let javaHome = findJavaHome()
32+
33+
let javaIncludePath = "\(javaHome)/include"
34+
#if os(Linux)
35+
let javaPlatformIncludePath = "\(javaIncludePath)/linux"
36+
#elseif os(macOS)
37+
let javaPlatformIncludePath = "\(javaIncludePath)/darwin"
38+
#else
39+
// TODO: Handle windows as well
40+
#error("Currently only macOS and Linux platforms are supported, this may change in the future.")
41+
#endif
42+
43+
let package = Package(
44+
name: "JExtractJNISampleApp",
45+
platforms: [
46+
.macOS(.v15)
47+
],
48+
products: [
49+
.library(
50+
name: "MySwiftLibrary",
51+
type: .dynamic,
52+
targets: ["MySwiftLibrary"]
53+
)
54+
55+
],
56+
dependencies: [
57+
.package(name: "swift-java", path: "../../")
58+
],
59+
targets: [
60+
.target(
61+
name: "MySwiftLibrary",
62+
dependencies: [
63+
.product(name: "JavaKit", package: "swift-java"),
64+
.product(name: "JavaRuntime", package: "swift-java"),
65+
.product(name: "SwiftKitSwift", package: "swift-java"),
66+
],
67+
exclude: [
68+
"swift-java.config"
69+
],
70+
swiftSettings: [
71+
.swiftLanguageMode(.v5),
72+
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]),
73+
],
74+
plugins: [
75+
.plugin(name: "JExtractSwiftPlugin", package: "swift-java")
76+
]
77+
)
78+
]
79+
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
// This is a "plain Swift" file containing various types of declarations,
16+
// that is exported to Java by using the `jextract-swift` tool.
17+
//
18+
// No annotations are necessary on the Swift side to perform the export.
19+
20+
#if os(Linux)
21+
import Glibc
22+
#else
23+
import Darwin.C
24+
#endif
25+
26+
public func helloWorld() {
27+
p("\(#function)")
28+
}
29+
30+
public func globalTakeInt(i: Int64) {
31+
p("i:\(i)")
32+
}
33+
34+
public func globalMakeInt() -> Int64 {
35+
return 42
36+
}
37+
38+
public func globalWriteString(string: String) -> Int64 {
39+
return Int64(string.count)
40+
}
41+
42+
public func globalTakeIntInt(i: Int64, j: Int64) {
43+
p("i:\(i), j:\(j)")
44+
}
45+
46+
// ==== Internal helpers
47+
48+
func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) {
49+
print("[swift][\(file):\(line)](\(function)) \(msg)")
50+
fflush(stdout)
51+
}
52+
53+
#if os(Linux)
54+
// FIXME: why do we need this workaround?
55+
@_silgen_name("_objc_autoreleaseReturnValue")
56+
public func _objc_autoreleaseReturnValue(a: Any) {}
57+
58+
@_silgen_name("objc_autoreleaseReturnValue")
59+
public func objc_autoreleaseReturnValue(a: Any) {}
60+
#endif
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"javaPackage": "com.example.swift",
3+
"mode": "jni"
4+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import groovy.json.JsonSlurper
16+
import org.swift.swiftkit.gradle.BuildUtils
17+
18+
import java.nio.file.*
19+
import kotlinx.serialization.json.*
20+
21+
plugins {
22+
id("build-logic.java-application-conventions")
23+
id("me.champeau.jmh") version "0.7.2"
24+
}
25+
26+
group = "org.swift.swiftkit"
27+
version = "1.0-SNAPSHOT"
28+
29+
repositories {
30+
mavenCentral()
31+
}
32+
33+
java {
34+
toolchain {
35+
languageVersion.set(JavaLanguageVersion.of(24))
36+
}
37+
}
38+
39+
def swiftProductsWithJExtractPlugin() {
40+
def stdout = new ByteArrayOutputStream()
41+
def stderr = new ByteArrayOutputStream()
42+
43+
def result = exec {
44+
commandLine 'swift', 'package', 'describe', '--type', 'json'
45+
standardOutput = stdout
46+
errorOutput = stderr
47+
ignoreExitValue = true
48+
}
49+
50+
def jsonOutput = stdout.toString()
51+
52+
if (result.exitValue == 0) {
53+
def json = new JsonSlurper().parseText(jsonOutput)
54+
def products = json.targets
55+
.findAll { target ->
56+
target.product_dependencies?.contains("JExtractSwiftPlugin")
57+
}
58+
.collectMany { target ->
59+
target.product_memberships ?: []
60+
}
61+
return products
62+
} else {
63+
logger.warn("Command failed: ${stderr.toString()}")
64+
return []
65+
}
66+
}
67+
68+
69+
def swiftCheckValid = tasks.register("swift-check-valid", Exec) {
70+
commandLine "swift"
71+
args("-version")
72+
}
73+
74+
def jextract = tasks.register("jextract", Exec) {
75+
description = "Generate Java wrappers for swift target"
76+
dependsOn swiftCheckValid
77+
78+
// only because we depend on "live developing" the plugin while using this project to test it
79+
inputs.file(new File(rootDir, "Package.swift"))
80+
inputs.dir(new File(rootDir, "Sources"))
81+
82+
// If the package description changes, we should execute jextract again, maybe we added jextract to new targets
83+
inputs.file(new File(projectDir, "Package.swift"))
84+
85+
// monitor all targets/products which depend on the JExtract plugin
86+
swiftProductsWithJExtractPlugin().each {
87+
logger.info("[swift-java:jextract (Gradle)] Swift input target: ${it}")
88+
inputs.dir(new File(layout.projectDirectory.asFile, "Sources/${it}".toString()))
89+
}
90+
outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}"))
91+
92+
File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile
93+
if (!baseSwiftPluginOutputsDir.exists()) {
94+
baseSwiftPluginOutputsDir.mkdirs()
95+
}
96+
Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).each {
97+
// Add any Java sources generated by the plugin to our sourceSet
98+
if (it.endsWith("JExtractSwiftPlugin/src/generated/java")) {
99+
outputs.dir(it)
100+
}
101+
}
102+
103+
workingDir = layout.projectDirectory
104+
commandLine "swift"
105+
args("build") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build
106+
// If we wanted to execute a specific subcommand, we can like this:
107+
// args("run",/*
108+
// "swift-java", "jextract",
109+
// "--swift-module", "MySwiftLibrary",
110+
// // java.package is obtained from the swift-java.config in the swift module
111+
// "--output-java", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/src/generated/java").get()}",
112+
// "--output-swift", "${layout.buildDirectory.dir(".build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}/JExtractSwiftPlugin/Sources").get()}",
113+
// "--log-level", (logging.level <= LogLevel.INFO ? "debug" : */"info")
114+
// )
115+
}
116+
117+
// Add the java-swift generated Java sources
118+
sourceSets {
119+
main {
120+
java {
121+
srcDir(jextract)
122+
}
123+
}
124+
test {
125+
java {
126+
srcDir(jextract)
127+
}
128+
}
129+
jmh {
130+
java {
131+
srcDir(jextract)
132+
}
133+
}
134+
}
135+
136+
tasks.build {
137+
dependsOn("jextract")
138+
}
139+
140+
141+
def cleanSwift = tasks.register("cleanSwift", Exec) {
142+
workingDir = layout.projectDirectory
143+
commandLine "swift"
144+
args("package", "clean")
145+
}
146+
tasks.clean {
147+
dependsOn("cleanSwift")
148+
}
149+
150+
dependencies {
151+
implementation(project(':SwiftKit'))
152+
153+
testImplementation(platform("org.junit:junit-bom:5.10.0"))
154+
testImplementation("org.junit.jupiter:junit-jupiter")
155+
}
156+
157+
tasks.named('test', Test) {
158+
useJUnitPlatform()
159+
}
160+
161+
application {
162+
mainClass = "com.example.swift.HelloJava2SwiftJNI"
163+
164+
applicationDefaultJvmArgs = [
165+
"--enable-native-access=ALL-UNNAMED",
166+
167+
// Include the library paths where our dylibs are that we want to load and call
168+
"-Djava.library.path=" +
169+
(BuildUtils.javaLibraryPaths(rootDir) +
170+
BuildUtils.javaLibraryPaths(project.projectDir)).join(":"),
171+
172+
173+
// Enable tracing downcalls (to Swift)
174+
"-Djextract.trace.downcalls=true"
175+
]
176+
}
177+
178+
String jmhIncludes = findProperty("jmhIncludes")
179+
180+
jmh {
181+
if (jmhIncludes != null) {
182+
includes = [jmhIncludes]
183+
}
184+
185+
jvmArgsAppend = [
186+
"--enable-native-access=ALL-UNNAMED",
187+
188+
"-Djava.library.path=" +
189+
(BuildUtils.javaLibraryPaths(rootDir) +
190+
BuildUtils.javaLibraryPaths(project.projectDir)).join(":"),
191+
192+
// Enable tracing downcalls (to Swift)
193+
"-Djextract.trace.downcalls=false"
194+
]
195+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
set -x
4+
set -e
5+
6+
./gradlew run

Samples/JExtractJNISampleApp/gradlew

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../gradlew
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../gradlew.bat

0 commit comments

Comments
 (0)