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
2 changes: 2 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ jobs:
'SwiftAndJavaJarFFMSampleLib',
'SwiftJavaExtractFFMSampleApp',
'SwiftJavaExtractJNISampleApp',
'SwiftJavaExtractJNIKotlinSampleApp',
]
container:
image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }}
Expand Down Expand Up @@ -315,6 +316,7 @@ jobs:
'SwiftAndJavaJarFFMSampleLib',
'SwiftJavaExtractFFMSampleApp',
'SwiftJavaExtractJNISampleApp',
'SwiftJavaExtractJNIKotlinSampleApp',
]
steps:
- uses: actions/checkout@v7
Expand Down
53 changes: 53 additions & 0 deletions Samples/SwiftJavaExtractJNIKotlinSampleApp/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

// Swift 6 language mode, with the generated bindings used from Kotlin (src/test/kotlin).
// KotlinSampleModels is a separate module so types and protocols cross module boundaries.
let package = Package(
name: "SwiftJavaExtractJNIKotlinSampleApp",
platforms: [
.macOS(.v15)
],
products: [
.library(name: "KotlinSampleModels", type: .dynamic, targets: ["KotlinSampleModels"]),
.library(name: "KotlinSampleLibrary", type: .dynamic, targets: ["KotlinSampleLibrary"]),
],
dependencies: [
.package(name: "swift-java", path: "../../")
],
targets: [
.target(
name: "KotlinSampleModels",
dependencies: [
.product(name: "SwiftJava", package: "swift-java")
],
exclude: [
"swift-java.config"
],
swiftSettings: [
.swiftLanguageMode(.v6)
],
plugins: [
.plugin(name: "JExtractSwiftPlugin", package: "swift-java")
]
),
.target(
name: "KotlinSampleLibrary",
dependencies: [
.product(name: "SwiftJava", package: "swift-java"),
"KotlinSampleModels",
],
exclude: [
"swift-java.config"
],
swiftSettings: [
.swiftLanguageMode(.v6)
],
plugins: [
.plugin(name: "JExtractSwiftPlugin", package: "swift-java")
]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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 KotlinSampleModels

public struct Item: Hashable, Sendable {
public var id: String
public var count: Int64

public init(id: String, count: Int64) {
self.id = id
self.count = count
}
}

public enum Route: Sendable {
case home
case detail(Item)
}

public enum StoreError: Error {
case notFound
}

public final class Store: Sendable {
public init() {}

public func item(id: String) throws -> Item {
if id.isEmpty { throw StoreError.notFound }
return Item(id: id, count: 1)
}

public func load(id: String) async throws -> Item {
try item(id: id)
}

public func route(for item: Item) -> Route {
.detail(item)
}

/// Takes a type from another module.
public func label(_ item: Item, tag: Tag) -> String {
"\(tag.name):\(item.id)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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
//
//===----------------------------------------------------------------------===//

/// View models are main-actor isolated. Their Java bindings stay synchronous and must be called
/// on the main thread (Compose's UI thread on Android).
@MainActor
public final class CounterViewModel {
public private(set) var count: Int64 = 0
public var title: String

public init(title: String) {
self.title = title
}

public func increment() {
count += 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"javaPackage": "com.example.kotlin",
"mode": "jni",
"memoryManagementMode": "allowGlobalAutomatic"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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
//
//===----------------------------------------------------------------------===//

/// A value type declared in its own module and used from another.
public struct Tag: Sendable {
public var name: String

public init(name: String) {
self.name = name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"javaPackage": "com.example.kotlin.models",
"mode": "jni",
"memoryManagementMode": "allowGlobalAutomatic"
}
91 changes: 91 additions & 0 deletions Samples/SwiftJavaExtractJNIKotlinSampleApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// 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 java.time.Duration
import utilities.javaLibraryPaths
import utilities.jextractGeneratedJavaDirs
import utilities.registerJextractTask

// The generated Java bindings, used from Kotlin: src/test/kotlin has one test file per feature.
plugins {
id("build-logic.java-common-conventions")
kotlin("jvm") version "2.4.21"
}

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

repositories {
mavenCentral()
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of((gradle.extra.properties["swiftJavaJdk"] as? Int) ?: 17))
}
}

val jextract = registerJextractTask {
// FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418
val cmdArgs = mutableListOf("build", "--disable-experimental-prebuilts", "--disable-sandbox")
if (project.hasProperty("swiftSdk")) {
cmdArgs.add("--swift-sdk")
cmdArgs.add(project.property("swiftSdk").toString())
}
cmdArgs
}

sourceSets {
main {
java {
srcDir(jextract)
jextractGeneratedJavaDirs().forEach { srcDir(it) }
}
}
}

tasks.build {
dependsOn(jextract)
}

dependencies {
implementation(projects.swiftKitCore)

testImplementation(kotlin("test-junit5"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testRuntimeOnly(libs.junit.platform.launcher) // TODO: workaround for not finding junit: https://github.com/gradle/gradle/issues/34512
}

tasks.named<Test>("test") {
useJUnitPlatform()
// A deadlocked test fails instead of stalling the build.
timeout.set(Duration.ofMinutes(2))

jvmArgs(
"--enable-native-access=ALL-UNNAMED",
// Include the library paths where our dylibs are that we want to load and call
"-Djava.library.path=" + javaLibraryPaths(project.projectDir).joinToString(File.pathSeparator),
)

// Run tests on the process's first thread, which Swift treats as the main actor's thread (as on
// Android's UI thread), so main-actor isolated APIs can be called. macOS only.
if (System.getProperty("os.name").startsWith("Mac")) {
jvmArgs("-XstartOnFirstThread")
}

testLogging {
events("failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}
16 changes: 16 additions & 0 deletions Samples/SwiftJavaExtractJNIKotlinSampleApp/ci-validate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

set -x
set -e

# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418
if [ "$(uname)" = "Darwin" ]; then
DISABLE_EXPERIMENTAL_PREBUILTS=''
else
DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts'
fi

# FIXME: disable prebuilts until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418
swift build $DISABLE_EXPERIMENTAL_PREBUILTS --disable-sandbox

./gradlew test
1 change: 1 addition & 0 deletions Samples/SwiftJavaExtractJNIKotlinSampleApp/gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Samples/SwiftJavaExtractJNIKotlinSampleApp/gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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) 2026 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
//
//===----------------------------------------------------------------------===//

package com.example.kotlin

import com.example.kotlin.models.Tag
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import org.swift.swiftkit.core.SwiftArena
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertIs

class BaselineTest {
@Test
fun structRoundTrip() {
val item = Item.init("a", 2)
assertEquals("a", item.id)
assertEquals(2, item.count)
assertEquals(Item.init("a", 2), item)
assertEquals(Item.init("a", 2).hashCode(), item.hashCode())
}

@Test
fun throwsBecomesAnException() {
val store = Store.init()
assertEquals("x", store.item("x").id)
assertFailsWith<Exception> { store.item("") }
}

@Test
fun asyncIsAFuture() = runBlocking {
assertEquals("x", Store.init().load("x").await().id)
}

@Test
fun enumWithPayload() {
val route = Store.init().route(Item.init("a", 1))
val case = route.getCase(SwiftArena.ofAuto())
assertIs<Route.Case.Detail>(case)
assertEquals("a", case.arg0.id)
}

@Test
fun typeFromAnotherModule() {
assertEquals("tag:a", Store.init().label(Item.init("a", 1), Tag.init("tag")))
}
}
Loading
Loading