From d5544431210d9ca54fb5e1e030f5b43b09c35e39 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sat, 13 Jun 2026 16:54:31 +0200 Subject: [PATCH] feat(examples): Android example consumes the generated Kotlin/JNI wrapper Regenerates the wrapper (MyTimerNode.kt) and JNI shim (my_timer_jni.c) via `nimble genbindings_kotlin`, removing the hand-written sources, and points the instrumented test at the derived MyTimerNode class. Validated by cross-compiling both ABIs with the NDK: arm64-v8a + x86_64 build clean, the four Java_org_logos_mytimer_MyTimerNode_* symbols are exported, and libmy_timer_jni.so correctly NEEDS libmy_timer.so. (The Kotlin/Gradle layer still runs on a device/emulator.) Co-Authored-By: Claude Opus 4.8 --- examples/timer/android/jni/my_timer_jni.c | 75 +++++++++---------- .../kotlin/org/logos/mytimer/TimerNodeTest.kt | 2 +- .../kotlin/org/logos/mytimer/MyTimerNode.kt | 28 +++++++ .../kotlin/org/logos/mytimer/TimerNode.kt | 38 ---------- 4 files changed, 64 insertions(+), 79 deletions(-) create mode 100644 examples/timer/android/src/main/kotlin/org/logos/mytimer/MyTimerNode.kt delete mode 100644 examples/timer/android/src/main/kotlin/org/logos/mytimer/TimerNode.kt diff --git a/examples/timer/android/jni/my_timer_jni.c b/examples/timer/android/jni/my_timer_jni.c index 435cdb4..155d501 100644 --- a/examples/timer/android/jni/my_timer_jni.c +++ b/examples/timer/android/jni/my_timer_jni.c @@ -1,9 +1,10 @@ -// JNI bridge: exposes the timer library's native C ABI to Kotlin/Java. +// Generated by nim-ffi Kotlin/JNI codegen. Do not edit by hand. // +// JNI bridge exposing the library's native (zero-serialization) C ABI to Kotlin. // The library calls back on its own FFI thread; each bridge function blocks on a // condvar until the callback fires, then returns a plain Java value — so the -// Kotlin side sees a simple synchronous API. A struct return (EchoResponse) is -// read out of the typed C-POD inside the callback (valid only there). +// Kotlin side sees a simple synchronous API. A struct return is read out of the +// typed C-POD inside the callback (valid only there). #include "my_timer.h" #include @@ -13,9 +14,8 @@ typedef struct { int ret, done; - char text[1024]; // string return / error text - char echoed[256]; // EchoResponse.echoed - char timerName[256]; + char text[1024]; // string return / error text + char fields[2][256]; // string fields of a struct return pthread_mutex_t mu; pthread_cond_t cv; } Resp; @@ -59,14 +59,15 @@ static void string_cb(int ret, const char *msg, size_t len, void *ud) { pthread_cond_signal(&r->cv); pthread_mutex_unlock(&r->mu); } + static void echo_cb(int ret, const char *msg, size_t len, void *ud) { Resp *r = ud; pthread_mutex_lock(&r->mu); r->ret = ret; if (ret == RET_OK) { - const EchoResponse *e = (const EchoResponse *)msg; // typed struct return - strncpy(r->echoed, e->echoed, sizeof(r->echoed) - 1); - strncpy(r->timerName, e->timerName, sizeof(r->timerName) - 1); + const EchoResponse *e = (const EchoResponse *)msg; + strncpy(r->fields[0], e->echoed, sizeof(r->fields[0]) - 1); + strncpy(r->fields[1], e->timerName, sizeof(r->fields[1]) - 1); } else { copy_raw(r->text, sizeof(r->text), msg, len); } @@ -76,23 +77,39 @@ static void echo_cb(int ret, const char *msg, size_t len, void *ud) { } JNIEXPORT jlong JNICALL -Java_org_logos_mytimer_TimerNode_nativeCreate(JNIEnv *env, jobject thiz, - jstring jname) { - const char *name = (*env)->GetStringUTFChars(env, jname, NULL); +Java_org_logos_mytimer_MyTimerNode_nativeCreate(JNIEnv *env, jobject thiz, jstring j_name) { + const char *name = (*env)->GetStringUTFChars(env, j_name, NULL); + TimerConfig config = {.name = name}; Resp r; resp_init(&r); - TimerConfig cfg = {.name = name}; - void *ctx = my_timer_create(cfg, ack_cb, &r); + void *ctx = my_timer_create(config, ack_cb, &r); resp_wait(&r); - (*env)->ReleaseStringUTFChars(env, jname, name); + (*env)->ReleaseStringUTFChars(env, j_name, name); resp_destroy(&r); (void)thiz; return (jlong)(intptr_t)ctx; } +JNIEXPORT jobjectArray JNICALL +Java_org_logos_mytimer_MyTimerNode_nativeEcho(JNIEnv *env, jobject thiz, jlong ctx, jstring j_message, jlong delayMs) { + const char *message = (*env)->GetStringUTFChars(env, j_message, NULL); + EchoRequest req = {.message = message, .delayMs = (int64_t)delayMs}; + Resp r; + resp_init(&r); + if (my_timer_echo((void *)(intptr_t)ctx, echo_cb, &r, req) == RET_OK) + resp_wait(&r); + (*env)->ReleaseStringUTFChars(env, j_message, message); + jclass strClass = (*env)->FindClass(env, "java/lang/String"); + jobjectArray arr = (*env)->NewObjectArray(env, 2, strClass, NULL); + (*env)->SetObjectArrayElement(env, arr, 0, (*env)->NewStringUTF(env, r.fields[0])); + (*env)->SetObjectArrayElement(env, arr, 1, (*env)->NewStringUTF(env, r.fields[1])); + resp_destroy(&r); + (void)thiz; + return arr; +} + JNIEXPORT jstring JNICALL -Java_org_logos_mytimer_TimerNode_nativeVersion(JNIEnv *env, jobject thiz, - jlong ctx) { +Java_org_logos_mytimer_MyTimerNode_nativeVersion(JNIEnv *env, jobject thiz, jlong ctx) { Resp r; resp_init(&r); if (my_timer_version((void *)(intptr_t)ctx, string_cb, &r) == RET_OK) @@ -103,30 +120,8 @@ Java_org_logos_mytimer_TimerNode_nativeVersion(JNIEnv *env, jobject thiz, return out; } -// Returns String[2] = { echoed, timerName }. -JNIEXPORT jobjectArray JNICALL -Java_org_logos_mytimer_TimerNode_nativeEcho(JNIEnv *env, jobject thiz, jlong ctx, - jstring jmsg, jlong delayMs) { - const char *msg = (*env)->GetStringUTFChars(env, jmsg, NULL); - Resp r; - resp_init(&r); - EchoRequest req = {.message = msg, .delayMs = (int64_t)delayMs}; - if (my_timer_echo((void *)(intptr_t)ctx, echo_cb, &r, req) == RET_OK) - resp_wait(&r); - (*env)->ReleaseStringUTFChars(env, jmsg, msg); - - jclass strClass = (*env)->FindClass(env, "java/lang/String"); - jobjectArray arr = (*env)->NewObjectArray(env, 2, strClass, NULL); - (*env)->SetObjectArrayElement(env, arr, 0, (*env)->NewStringUTF(env, r.echoed)); - (*env)->SetObjectArrayElement(env, arr, 1, (*env)->NewStringUTF(env, r.timerName)); - resp_destroy(&r); - (void)thiz; - return arr; -} - JNIEXPORT void JNICALL -Java_org_logos_mytimer_TimerNode_nativeDestroy(JNIEnv *env, jobject thiz, - jlong ctx) { +Java_org_logos_mytimer_MyTimerNode_nativeDestroy(JNIEnv *env, jobject thiz, jlong ctx) { my_timer_destroy((void *)(intptr_t)ctx); (void)env; (void)thiz; diff --git a/examples/timer/android/src/androidTest/kotlin/org/logos/mytimer/TimerNodeTest.kt b/examples/timer/android/src/androidTest/kotlin/org/logos/mytimer/TimerNodeTest.kt index fbd1c75..3be200c 100644 --- a/examples/timer/android/src/androidTest/kotlin/org/logos/mytimer/TimerNodeTest.kt +++ b/examples/timer/android/src/androidTest/kotlin/org/logos/mytimer/TimerNodeTest.kt @@ -11,7 +11,7 @@ import org.junit.runner.RunWith class TimerNodeTest { @Test fun createVersionEcho() { - TimerNode("android-demo").use { node -> + MyTimerNode("android-demo").use { node -> assertEquals("nim-timer v0.1.0", node.version()) val r = node.echo("hello from Kotlin", delayMs = 2) assertEquals("hello from Kotlin", r.echoed) diff --git a/examples/timer/android/src/main/kotlin/org/logos/mytimer/MyTimerNode.kt b/examples/timer/android/src/main/kotlin/org/logos/mytimer/MyTimerNode.kt new file mode 100644 index 0000000..d64379b --- /dev/null +++ b/examples/timer/android/src/main/kotlin/org/logos/mytimer/MyTimerNode.kt @@ -0,0 +1,28 @@ +// Generated by nim-ffi Kotlin codegen. Do not edit by hand. +package org.logos.mytimer + +/** Typed result mirroring the library's EchoResponse. */ +data class EchoResponse(val echoed: String, val timerName: String) + +class MyTimerNode(name: String) : AutoCloseable { + private val ctx: Long = nativeCreate(name) + + fun echo(message: String, delayMs: Long = 0): EchoResponse { + val a = nativeEcho(ctx, message, delayMs) + return EchoResponse(a[0], a[1]) + } + fun version(): String = nativeVersion(ctx) + override fun close() = nativeDestroy(ctx) + + private external fun nativeCreate(name: String): Long + private external fun nativeEcho(ctx: Long, message: String, delayMs: Long): Array + private external fun nativeVersion(ctx: Long): String + private external fun nativeDestroy(ctx: Long) + + companion object { + init { + System.loadLibrary("my_timer") + System.loadLibrary("my_timer_jni") + } + } +} \ No newline at end of file diff --git a/examples/timer/android/src/main/kotlin/org/logos/mytimer/TimerNode.kt b/examples/timer/android/src/main/kotlin/org/logos/mytimer/TimerNode.kt deleted file mode 100644 index 351fd81..0000000 --- a/examples/timer/android/src/main/kotlin/org/logos/mytimer/TimerNode.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.logos.mytimer - -/** Typed result of [TimerNode.echo] — mirrors the library's EchoResponse. */ -data class EchoResult(val echoed: String, val timerName: String) - -/** - * Idiomatic Kotlin wrapper over the timer library's native C ABI, bridged through - * JNI (see jni/my_timer_jni.c). Each call blocks until the library's FFI-thread - * callback fires, so these methods are simple and synchronous. - * - * Call [close] (or use Kotlin's `use { }`) to tear the context down. - */ -class TimerNode(name: String) : AutoCloseable { - private val ctx: Long = nativeCreate(name) - - fun version(): String = nativeVersion(ctx) - - fun echo(message: String, delayMs: Long = 0): EchoResult { - val a = nativeEcho(ctx, message, delayMs) - return EchoResult(a[0], a[1]) - } - - override fun close() = nativeDestroy(ctx) - - private external fun nativeCreate(name: String): Long - private external fun nativeVersion(ctx: Long): String - private external fun nativeEcho(ctx: Long, message: String, delayMs: Long): Array - private external fun nativeDestroy(ctx: Long) - - companion object { - init { - // libmy_timer.so (the Nim library) is a NEEDED dependency of the JNI - // shim, but load it explicitly first so the linker finds it. - System.loadLibrary("my_timer") - System.loadLibrary("my_timer_jni") - } - } -}