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 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-06-13 16:54:31 +02:00
parent e36fe62033
commit d554443121
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
4 changed files with 64 additions and 79 deletions

View File

@ -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 <jni.h>
@ -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;

View File

@ -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)

View File

@ -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<String>
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")
}
}
}

View File

@ -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<String>
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")
}
}
}