commit e31489275629cb9f98e346df9a147144bdee90e5
parent 11b6337eba8948a2f01635cbf63b8cfdd59a3cf8
Author: triesap <tyson@radroots.org>
Date: Sun, 2 Aug 2026 19:44:56 +0000
build(desktop): generate UniFFI Kotlin bindings
- run the pinned UniFFI generator from the Rust workspace
- place generated Kotlin under ignored desktop build output
- compile the generated package with its pinned JNA runtime
- avoid generated close and exception-name collisions
Diffstat:
10 files changed, 125 insertions(+), 16 deletions(-)
diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts
@@ -47,9 +47,41 @@ val buildRustCore by tasks.registering(Exec::class) {
outputs.file(rustDebugLibrary)
}
+val generatedUniFfiKotlin = layout.buildDirectory.dir("generated/uniffi/kotlin")
+val generateUniFfiKotlin by tasks.registering(Exec::class) {
+ dependsOn(buildRustCore)
+ workingDir(rootProject.projectDir)
+ commandLine(
+ "cargo",
+ "run",
+ "--manifest-path",
+ rustManifest.asFile.absolutePath,
+ "-p",
+ "radroots-studio-uniffi-bindgen",
+ "--",
+ "generate",
+ "--library",
+ "--language",
+ "kotlin",
+ "--metadata-no-deps",
+ "--no-format",
+ "--config",
+ rootProject.layout.projectDirectory
+ .file("core/crates/ffi/uniffi.toml")
+ .asFile.absolutePath,
+ "--out-dir",
+ generatedUniFfiKotlin.get().asFile.absolutePath,
+ rustDebugLibrary.asFile.absolutePath,
+ )
+ inputs.file(rustDebugLibrary)
+ inputs.file(rootProject.layout.projectDirectory.file("core/crates/ffi/uniffi.toml"))
+ outputs.dir(generatedUniFfiKotlin)
+}
+
dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.compose.foundation)
+ implementation(libs.jna)
testImplementation(kotlin("test-junit"))
testImplementation(libs.compose.ui.test.junit4)
@@ -87,6 +119,10 @@ tasks.withType<Test>().configureEach {
}
kotlin {
+ sourceSets.main {
+ kotlin.srcDir(generatedUniFfiKotlin)
+ }
+
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
@@ -94,6 +130,10 @@ kotlin {
jvmToolchain(21)
}
+tasks.named("compileKotlin") {
+ dependsOn(generateUniFfiKotlin)
+}
+
compose.desktop {
application {
mainClass = "org.radroots.studio.desktop.MainKt"
diff --git a/core/Cargo.lock b/core/Cargo.lock
@@ -48,6 +48,12 @@ dependencies = [
]
[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -542,6 +548,45 @@ dependencies = [
]
[[package]]
+name = "clap"
+version = "4.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078"
+dependencies = [
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1719,6 +1764,9 @@ dependencies = [
[[package]]
name = "radroots-studio-uniffi-bindgen"
version = "0.1.0-alpha.0"
+dependencies = [
+ "uniffi",
+]
[[package]]
name = "rand"
@@ -2255,6 +2303,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2617,7 +2671,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a782a48d72cfd7a2d65cfc7c691dbf5375c43104b3c195f7eccc716dcc3540c8"
dependencies = [
"anyhow",
+ "camino",
"cargo_metadata",
+ "clap",
"uniffi_bindgen",
"uniffi_core",
"uniffi_macros",
diff --git a/core/crates/ffi/src/commands.rs b/core/crates/ffi/src/commands.rs
@@ -17,13 +17,13 @@ use crate::{AccountDto, AppSnapshotDto};
#[derive(Debug, uniffi::Error)]
pub enum StudioError {
- Failure { code: String, message: String },
+ Failure { code: String, safe_message: String },
}
impl Display for StudioError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
- Self::Failure { message, .. } => formatter.write_str(message),
+ Self::Failure { safe_message, .. } => formatter.write_str(safe_message),
}
}
}
@@ -34,7 +34,7 @@ impl From<SafeError> for StudioError {
fn from(error: SafeError) -> Self {
Self::Failure {
code: format!("{:?}", error.code()),
- message: error.message().as_str().to_owned(),
+ safe_message: error.message().as_str().to_owned(),
}
}
}
@@ -339,21 +339,21 @@ fn runtime() -> &'static tokio::runtime::Runtime {
fn path_unavailable() -> StudioError {
StudioError::Failure {
code: "StorageUnavailable".to_owned(),
- message: "The application data directory is unavailable.".to_owned(),
+ safe_message: "The application data directory is unavailable.".to_owned(),
}
}
fn runtime_unavailable() -> StudioError {
StudioError::Failure {
code: "InvalidApplicationState".to_owned(),
- message: "The application runtime is unavailable.".to_owned(),
+ safe_message: "The application runtime is unavailable.".to_owned(),
}
}
fn confirmation_expired() -> StudioError {
StudioError::Failure {
code: "InvalidApplicationState".to_owned(),
- message: "The account removal confirmation is no longer valid.".to_owned(),
+ safe_message: "The account removal confirmation is no longer valid.".to_owned(),
}
}
diff --git a/core/crates/ffi/src/observer.rs b/core/crates/ffi/src/observer.rs
@@ -29,7 +29,7 @@ pub struct ObserverSubscription {
#[uniffi::export]
impl ObserverSubscription {
- pub fn close(&self) {
+ pub fn unsubscribe(&self) {
let handle = self
.handle
.lock()
@@ -48,7 +48,7 @@ impl ObserverSubscription {
impl Drop for ObserverSubscription {
fn drop(&mut self) {
- self.close();
+ self.unsubscribe();
}
}
@@ -86,7 +86,7 @@ impl StudioAppCore {
}))
}
- pub fn close(&self) {
+ pub fn shutdown(&self) {
if self.inner.closed.swap(true, Ordering::AcqRel) {
return;
}
@@ -107,7 +107,7 @@ impl StudioAppCore {
fn closed_error() -> StudioError {
StudioError::Failure {
code: "InvalidApplicationState".to_owned(),
- message: "The application runtime is closed.".to_owned(),
+ safe_message: "The application runtime is closed.".to_owned(),
}
}
@@ -168,7 +168,7 @@ mod tests {
.bootstrap()
.expect("idempotent bootstrap");
assert_eq!(observer.snapshots.lock().expect("snapshots").len(), 1);
- subscription.close();
+ subscription.unsubscribe();
core.inner.adapter.core().sign_out().expect("sign out");
assert_eq!(observer.snapshots.lock().expect("snapshots").len(), 1);
}
@@ -182,8 +182,8 @@ mod tests {
.subscribe(Box::new(ArcObserver(observer.clone())))
.expect("subscribe");
- core.close();
- core.close();
+ core.shutdown();
+ core.shutdown();
assert!(core.subscribe(Box::new(ArcObserver(observer))).is_err());
assert!(core.inner.observers.lock().expect("observers").is_empty());
diff --git a/core/crates/ffi/uniffi.toml b/core/crates/ffi/uniffi.toml
@@ -1,3 +1,3 @@
-[bindings.kotlin]
+[crates.radroots_studio_ffi.bindings.kotlin]
package_name = "org.radroots.studio.ffi"
cdylib_name = "radroots_studio_ffi"
diff --git a/core/tools/uniffi-bindgen/Cargo.toml b/core/tools/uniffi-bindgen/Cargo.toml
@@ -7,5 +7,8 @@ license.workspace = true
repository.workspace = true
publish = false
+[dependencies]
+uniffi = { workspace = true, features = ["cli"] }
+
[lints]
workspace = true
diff --git a/core/tools/uniffi-bindgen/src/main.rs b/core/tools/uniffi-bindgen/src/main.rs
@@ -1,6 +1,8 @@
#![doc = "Pinned `UniFFI` binding generator entry point."]
-fn main() {}
+fn main() {
+ uniffi::uniffi_bindgen_main();
+}
#[cfg(test)]
mod tests {
diff --git a/docs/architecture/rust-ffi-boundary.md b/docs/architecture/rust-ffi-boundary.md
@@ -44,3 +44,9 @@ Gradle exposes the same operation as `:app:desktop:buildRustCore`. The task
tracks Rust manifests, lockfile, sources, and migrations as inputs and the
current-host debug dynamic library as its output. It does not redirect Cargo's
target directory.
+
+`:app:desktop:generateUniFfiKotlin` runs the repository-pinned UniFFI 0.32
+generator against that dynamic library and writes Kotlin to
+`app/desktop/build/generated/uniffi/kotlin`. The main Kotlin source set reads
+that generated directory, and `compileKotlin` depends on generation. Generated
+bindings are ignored build output and are never committed.
diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md
@@ -526,7 +526,7 @@ handoff commit sequence.
- [x] 43. Expose observer callback and deregistration through UniFFI.
- [x] 44. Build native library artifacts from Cargo.
- [x] 45. Add Gradle task for Cargo build.
-- [ ] 46. Add Gradle UniFFI binding generation and generated source set.
+- [x] 46. Add Gradle UniFFI binding generation and generated source set.
- [ ] 47. Stage native library for development, tests, and packaged app.
### RCLD-10
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
@@ -1,10 +1,12 @@
[versions]
kotlin = "2.4.10"
compose = "1.11.1"
+jna = "5.17.0"
[libraries]
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" }
compose-ui-test-junit4 = { module = "org.jetbrains.compose.ui:ui-test-junit4", version.ref = "compose" }
+jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }