···11+# panama-exploration
22+33+This is an example configuration of Project Panama:
44+55+- `rust-src` cdylib crate connected as a library
66+- Dynamically generated bindings in `interop-panama`
77+- `app` Kotlin consumer module
88+- A Nix flake that includes everything you need to reproduce the results everywhere Nix is available
99+1010+I **strongly advise** you to use what Nix flake provides through `nix develop` or `direnv allow`.
1111+It’ll set up Cargo, glibc, and Azul Zulu Community JDK. That's all you need.
1212+1313+To run, simply execute `./gradlew run`. If you want to learn more about how it works, dive into `src-rust` crate
1414+and `interop-panama` Gradle module, especially buildscript files.
+50
app/build.gradle.kts
···11+plugins {
22+ // Apply the shared build logic from a convention plugin.
33+ // The shared code is located in `buildSrc/src/main/kotlin/kotlin-jvm.gradle.kts`.
44+ id("buildsrc.convention.kotlin-jvm")
55+66+ // Apply the Application plugin to add support for building an executable JVM application.
77+ application
88+}
99+1010+dependencies {
1111+ // Project "app" depends on project "utils". (Project paths are separated with ":", so ":utils" refers to the top-level "utils" project.)
1212+ implementation(project(":interop-panama"))
1313+}
1414+1515+application {
1616+ // Define the Fully Qualified Name for the application main class
1717+ // (Note that Kotlin compiles `App.kt` to a class with FQN `com.example.app.AppKt`.)
1818+ mainClass = "org.example.app.AppKt"
1919+2020+ applicationDefaultJvmArgs = listOf(
2121+ "--enable-native-access=ALL-UNNAMED",
2222+ )
2323+}
2424+2525+val rustLibName = providers.provider {
2626+ when {
2727+ org.gradle.internal.os.OperatingSystem.current().isLinux -> "libsrc_rust.so"
2828+ else -> error("Unsupported operating system")
2929+ }
3030+}
3131+3232+tasks.named<JavaExec>("run") {
3333+ dependsOn(":interop-panama:copyRustLib")
3434+ val nativeDir = project(":interop-panama").layout.buildDirectory.dir("native")
3535+ jvmArgs(
3636+ "--enable-native-access=ALL-UNNAMED",
3737+ "--illegal-native-access=deny",
3838+ "-Drust.library.path=${nativeDir.get().file(rustLibName.get()).asFile.absolutePath}"
3939+ )
4040+}
4141+4242+tasks.withType<Test>().configureEach {
4343+ dependsOn(":interop-panama:copyRustLib")
4444+ val nativeDir = project(":interop-panama").layout.buildDirectory.dir("native")
4545+ jvmArgs(
4646+ "--enable-native-access=ALL-UNNAMED",
4747+ "--illegal-native-access=deny",
4848+ "-Drust.library.path=${nativeDir.get().file(rustLibName.get()).asFile.absolutePath}"
4949+ )
5050+}
+20
app/src/main/kotlin/App.kt
···11+package org.example.app
22+33+import org.example.interop.ExternStruct
44+import org.example.interop.RustApi
55+import org.example.interop.jvm_interop_h
66+77+// if IDEA or Kotlin LSP is scared, run ./gradlew :interop-panama:generateJextractBindings.
88+// jvm_interop_h is a jextract-generated class.
99+fun main() {
1010+ println("Hello from Kotlin!")
1111+ println("Heavy Rust calculation: 40 + 2 = ${RustApi.add(40, 2)}")
1212+ println("Testing generated Java code: 40 + 2 = ${jvm_interop_h.add(40, 2)}")
1313+1414+ // operating on Rust/C-defined structs
1515+ val struct = jvm_interop_h.extern_struct_new(60, 9) // this returns MemorySegment!
1616+ val xFromRust = ExternStruct.x(struct) // accessor methods - they read from MemorySegment
1717+ val yFromRust = ExternStruct.y(struct)
1818+ println("Rust owns these. x: $xFromRust, y: $yFromRust. also, xy = nice!")
1919+ jvm_interop_h.extern_struct_free(struct) // remember to clean after yourself!
2020+}
+16
buildSrc/build.gradle.kts
···11+plugins {
22+ // The Kotlin DSL plugin provides a convenient way to develop convention plugins.
33+ // Convention plugins are located in `src/main/kotlin`, with the file extension `.gradle.kts`,
44+ // and are applied in the project's `build.gradle.kts` files as required.
55+ `kotlin-dsl`
66+}
77+88+kotlin {
99+ // Keep the build logic on a Kotlin-supported JDK even if runtime is newer.
1010+ jvmToolchain(24)
1111+}
1212+1313+dependencies {
1414+ // Add a dependency on the Kotlin Gradle plugin, so that convention plugins can apply it.
1515+ implementation(libs.kotlinGradlePlugin)
1616+}
+17
buildSrc/settings.gradle.kts
···11+dependencyResolutionManagement {
22+33+ // Use Maven Central and the Gradle Plugin Portal for resolving dependencies in the shared build logic (`buildSrc`) project.
44+ @Suppress("UnstableApiUsage")
55+ repositories {
66+ mavenCentral()
77+ }
88+99+ // Reuse the version catalog from the main build.
1010+ versionCatalogs {
1111+ create("libs") {
1212+ from(files("../gradle/libs.versions.toml"))
1313+ }
1414+ }
1515+}
1616+1717+rootProject.name = "buildSrc"
+29
buildSrc/src/main/kotlin/kotlin-jvm.gradle.kts
···11+// The code in this file is a convention plugin - a Gradle mechanism for sharing reusable build logic.
22+// `buildSrc` is a Gradle-recognized directory and every plugin there will be easily available in the rest of the build.
33+package buildsrc.convention
44+55+import org.gradle.api.tasks.testing.logging.TestLogEvent
66+77+plugins {
88+ // Apply the Kotlin JVM plugin to add support for Kotlin in JVM projects.
99+ kotlin("jvm")
1010+}
1111+1212+kotlin {
1313+ // Use a specific Java version to make it easier to work in different environments.
1414+ jvmToolchain(24)
1515+}
1616+1717+tasks.withType<Test>().configureEach {
1818+ // Configure all test Gradle tasks to use JUnitPlatform.
1919+ useJUnitPlatform()
2020+2121+ // Log information about all test results, not only the failed ones.
2222+ testLogging {
2323+ events(
2424+ TestLogEvent.FAILED,
2525+ TestLogEvent.PASSED,
2626+ TestLogEvent.SKIPPED
2727+ )
2828+ }
2929+}
···11+# Enable the build cache to save time by reusing outputs produced by other successful builds.
22+# https://docs.gradle.org/current/userguide/build_cache.html
33+org.gradle.caching=true
44+# Enable the configuration cache to reuse the build configuration and enable parallel task execution.
55+# (Note that some plugins may not yet be compatible with the configuration cache.)
66+# https://docs.gradle.org/current/userguide/configuration_cache.html
77+org.gradle.configuration-cache=true
88+# Prefer the JDK provided by JAVA_HOME (e.g. the dev shell) for toolchains.
99+org.gradle.java.installations.fromEnv=JAVA_HOME
+22
gradle/libs.versions.toml
···11+# Version catalog is a central place for you to declare and version dependencies
22+# https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog
33+# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
44+55+[versions]
66+kotlin = "2.3.0"
77+kotlinxDatetime = "0.7.1"
88+kotlinxSerializationJSON = "1.9.0"
99+kotlinxCoroutines = "1.10.2"
1010+1111+[libraries]
1212+kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
1313+kotlinxDatetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
1414+kotlinxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJSON" }
1515+kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
1616+1717+# Libraries can be bundled together for easier import
1818+[bundles]
1919+kotlinxEcosystem = ["kotlinxDatetime", "kotlinxSerialization", "kotlinxCoroutines"]
2020+2121+[plugins]
2222+kotlinPluginSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
···11+@rem
22+@rem Copyright 2015 the original author or authors.
33+@rem
44+@rem Licensed under the Apache License, Version 2.0 (the "License");
55+@rem you may not use this file except in compliance with the License.
66+@rem You may obtain a copy of the License at
77+@rem
88+@rem https://www.apache.org/licenses/LICENSE-2.0
99+@rem
1010+@rem Unless required by applicable law or agreed to in writing, software
1111+@rem distributed under the License is distributed on an "AS IS" BASIS,
1212+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+@rem See the License for the specific language governing permissions and
1414+@rem limitations under the License.
1515+@rem
1616+1717+@if "%DEBUG%" == "" @echo off
1818+@rem ##########################################################################
1919+@rem
2020+@rem Gradle startup script for Windows
2121+@rem
2222+@rem ##########################################################################
2323+2424+@rem Set local scope for the variables with windows NT shell
2525+if "%OS%"=="Windows_NT" setlocal
2626+2727+set DIRNAME=%~dp0
2828+if "%DIRNAME%" == "" set DIRNAME=.
2929+set APP_BASE_NAME=%~n0
3030+set APP_HOME=%DIRNAME%
3131+3232+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
3333+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
3434+3535+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
3636+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
3737+3838+@rem Find java.exe
3939+if defined JAVA_HOME goto findJavaFromJavaHome
4040+4141+set JAVA_EXE=java.exe
4242+%JAVA_EXE% -version >NUL 2>&1
4343+if "%ERRORLEVEL%" == "0" goto execute
4444+4545+echo.
4646+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
4747+echo.
4848+echo Please set the JAVA_HOME variable in your environment to match the
4949+echo location of your Java installation.
5050+5151+goto fail
5252+5353+:findJavaFromJavaHome
5454+set JAVA_HOME=%JAVA_HOME:"=%
5555+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
5656+5757+if exist "%JAVA_EXE%" goto execute
5858+5959+echo.
6060+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
6161+echo.
6262+echo Please set the JAVA_HOME variable in your environment to match the
6363+echo location of your Java installation.
6464+6565+goto fail
6666+6767+:execute
6868+@rem Setup the command line
6969+7070+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
7171+7272+7373+@rem Execute Gradle
7474+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
7575+7676+:end
7777+@rem End local scope for the variables with windows NT shell
7878+if "%ERRORLEVEL%"=="0" goto mainEnd
7979+8080+:fail
8181+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
8282+rem the _cmd.exe /c_ return code!
8383+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
8484+exit /b 1
8585+8686+:mainEnd
8787+if "%OS%"=="Windows_NT" endlocal
8888+8989+:omega
+82
interop-panama/build.gradle.kts
···11+import org.gradle.internal.os.OperatingSystem
22+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
33+44+plugins {
55+ // Apply the shared build logic from a convention plugin.
66+ // The shared code is located in `buildSrc/src/main/kotlin/kotlin-jvm.gradle.kts`.
77+ id("buildsrc.convention.kotlin-jvm")
88+ id("java")
99+}
1010+1111+val rustProjectDir = rootProject.layout.projectDirectory.dir("src-rust")
1212+val rustTargetDir = rustProjectDir.dir("target/debug")
1313+val rustHeadersDir = rustProjectDir.dir("bindings")
1414+val jextractOutputDir = layout.buildDirectory.dir("generated/jextract")
1515+1616+val rustLibFileName = providers.provider {
1717+ when {
1818+ OperatingSystem.current().isLinux -> "libsrc_rust.so"
1919+ else -> error("Unsupported operating system: ${OperatingSystem.current()}")
2020+ }
2121+}
2222+2323+val buildRustCdylib by tasks.registering(Exec::class) {
2424+ group = "interop"
2525+ description = "Build src-rust cdylib with Cargo"
2626+ workingDir = rustProjectDir.asFile
2727+ commandLine("cargo", "build")
2828+}
2929+3030+val copyRustLib by tasks.registering(Copy::class) {
3131+ group = "interop"
3232+ description = "Copy Rust shared library into interop build output"
3333+ dependsOn(buildRustCdylib)
3434+ from(rustTargetDir.file(rustLibFileName))
3535+ into(layout.buildDirectory.dir("native"))
3636+}
3737+3838+val copyHeaders by tasks.registering(Copy::class) {
3939+ group = "interop"
4040+ description = "Copy generated headers into interop build output"
4141+ dependsOn(buildRustCdylib)
4242+ from(rustHeadersDir.file("jvm_interop.h"))
4343+ into(layout.buildDirectory.dir("native"))
4444+}
4545+4646+// HACK: I'd rather interact with jextract as a build-time dependency, much like
4747+// Rust's cbindgen crate in rust-src/build.rs
4848+val generateJextractBindings by tasks.registering(Exec::class) {
4949+ group = "interop"
5050+ description = "Generate bindings from headers"
5151+ workingDir = layout.projectDirectory.asFile
5252+ dependsOn(copyRustLib, copyHeaders)
5353+ val nativeDir = layout.buildDirectory.dir("native").get().asFile
5454+ val nativeLibPath = layout.buildDirectory.dir("native").get().file(rustLibFileName).get().asFile.absolutePath
5555+ outputs.dir(jextractOutputDir)
5656+ commandLine(
5757+ "jextract",
5858+ "--include-dir", nativeDir.absolutePath,
5959+ "--output", jextractOutputDir.get().asFile.absolutePath,
6060+ "--target-package", "org.example.interop",
6161+ "--library", ":$nativeLibPath",
6262+ layout.buildDirectory.dir("native").get().file("jvm_interop.h").asFile.absolutePath
6363+ )
6464+}
6565+6666+tasks.named("classes") {
6767+ dependsOn(generateJextractBindings)
6868+}
6969+7070+sourceSets {
7171+ named("main") {
7272+ java.srcDir(jextractOutputDir)
7373+ }
7474+}
7575+7676+tasks.withType<KotlinCompile>().configureEach {
7777+ dependsOn(generateJextractBindings)
7878+}
7979+8080+dependencies {
8181+ testImplementation(kotlin("test"))
8282+}
+30
interop-panama/src/main/kotlin/interop/Main.kt
···11+package org.example.interop
22+33+import java.lang.foreign.Arena
44+import java.lang.foreign.FunctionDescriptor
55+import java.lang.foreign.Linker
66+import java.lang.foreign.SymbolLookup
77+import java.lang.foreign.ValueLayout.JAVA_LONG
88+import java.nio.file.Path
99+1010+object RustApi {
1111+ // Most of the time, you do not need to interact with these low-level APIs,
1212+ // as jextract generates bindings for you.
1313+ private val addHandle by lazy {
1414+ val libPath = System.getProperty("rust.library.path")
1515+ ?: error("Missing -Drust.library.path JVM property")
1616+1717+ val arena = Arena.ofAuto()
1818+ val symbols = SymbolLookup.libraryLookup(Path.of(libPath), arena)
1919+ val symbol = symbols.find("add")
2020+ .orElseThrow { IllegalStateException("Symbol 'add' not found in $libPath") }
2121+2222+ Linker.nativeLinker().downcallHandle(
2323+ symbol,
2424+ FunctionDescriptor.of(JAVA_LONG, JAVA_LONG, JAVA_LONG)
2525+ )
2626+ }
2727+2828+ fun add(left: Long, right: Long): Long =
2929+ addHandle.invoke(left, right) as Long
3030+}
+25
settings.gradle.kts
···11+// The settings file is the entry point of every Gradle build.
22+// Its primary purpose is to define the subprojects.
33+// It is also used for some aspects of project-wide configuration, like managing plugins, dependencies, etc.
44+// https://docs.gradle.org/current/userguide/settings_file_basics.html
55+66+dependencyResolutionManagement {
77+ // Use Maven Central as the default repository (where Gradle will download dependencies) in all subprojects.
88+ @Suppress("UnstableApiUsage")
99+ repositories {
1010+ mavenCentral()
1111+ }
1212+}
1313+1414+plugins {
1515+ // Use the Foojay Toolchains plugin to automatically download JDKs required by subprojects.
1616+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
1717+}
1818+1919+// Include the `app` and `utils` subprojects in the build.
2020+// If there are changes in only one of the projects, Gradle will rebuild only the one that has changed.
2121+// Learn more about structuring projects with Gradle - https://docs.gradle.org/8.7/userguide/multi_project_builds.html
2222+include(":app")
2323+include(":interop-panama")
2424+2525+rootProject.name = "panama-exploration"
···11+Signature: 8a477f597d28d172789f06886806bc55
22+# This file is a cache directory tag created by cargo.
33+# For information about cache directory tags see https://bford.info/cachedir/