Android
Overview
The Android target produces a Gradle android-library template that
combines a Kotlin wrapper, JNI C shims, and a CMake build for the JNI
shared library. The wrapper exposes idiomatic Kotlin types while the JNI
layer bridges them to the C ABI.
What gets generated
| File | Purpose |
|---|---|
generated/android/settings.gradle | Gradle settings for the library module |
generated/android/build.gradle | android-library plugin, NDK config |
generated/android/src/main/kotlin/com/weaveffi/WeaveFFI.kt | Kotlin wrapper (enums, struct classes, namespaced functions) |
generated/android/src/main/cpp/weaveffi_jni.c | JNI shims that call the C ABI and throw Java exceptions |
generated/android/src/main/cpp/CMakeLists.txt | NDK CMake build for the JNI shared library |
Type mapping
| IDL type | Kotlin type (external) | Kotlin type (wrapper) | JNI C type |
|---|---|---|---|
i32 | Int | Int | jint |
u32 | Long | Long | jlong |
i64 | Long | Long | jlong |
f64 | Double | Double | jdouble |
i8 | Byte | Byte | jbyte |
i16 | Short | Short | jshort |
u8 | Byte | Byte | jbyte |
u16 | Short | Short | jshort |
u64 | Long | Long | jlong |
f32 | Float | Float | jfloat |
bool | Boolean | Boolean | jboolean |
string | String | String | jstring |
bytes | ByteArray | ByteArray | jbyteArray |
handle | Long | Long | jlong |
StructName | Long | StructName | jlong |
EnumName (plain) | Int | EnumName | jint |
EnumName (rich) | Long | EnumName | jlong |
T? | T? | T? | jobject |
[i32] | IntArray | IntArray | jintArray |
[i64] | LongArray | LongArray | jlongArray |
[string] | Array<String> | Array<String> | jobjectArray |
iter<T> | Long (iterator handle) | Iterator<T> (lazy wrapper class) | jlong |
Example IDL → generated code
version: "0.5.0"
modules:
- name: contacts
enums:
- name: ContactType
variants:
- { name: Personal, value: 0 }
- { name: Work, value: 1 }
- { name: Other, value: 2 }
structs:
- name: Contact
fields:
- { name: name, type: string }
- { name: age, type: i32 }
functions:
- name: get_contact
params:
- { name: id, type: i32 }
return: Contact
- name: find_by_type
params:
- { name: contact_type, type: ContactType }
return: "[Contact]"
The Kotlin wrapper declares external fun entries inside a companion
object and loads the JNI library on first use. Function names are
lowerCamelCase with the module prefix stripped by default
(strip_module_prefix = false in [android] restores prefixed names).
Where a parameter or return value needs wrapping (enums, structs), the
external entry is a private ...Jni function with lowered types and a
public wrapper converts at the boundary. Struct returns come back as
handles and are wrapped in the struct class; [Contact] stays a
LongArray of handles:
package com.weaveffi
class WeaveFFI {
companion object {
init { System.loadLibrary("weaveffi") }
@JvmStatic private external fun getContactJni(id: Int): Long
@JvmStatic fun getContact(id: Int): Contact = Contact(getContactJni(id))
@JvmStatic private external fun findByTypeJni(contactType: Int): LongArray
@JvmStatic fun findByType(contactType: ContactType): LongArray = findByTypeJni(contactType.value)
}
}
Enums become Kotlin enum class with a fromValue factory:
enum class ContactType(val value: Int) {
Personal(0),
Work(1),
Other(2);
companion object {
fun fromValue(value: Int): ContactType = entries.first { it.value == value }
}
}
Structs are wrapped in a Kotlin class implementing Closeable, with a
finalize() safety net:
class Contact internal constructor(internal var handle: Long) : java.io.Closeable {
companion object {
init { System.loadLibrary("weaveffi") }
@JvmStatic external fun nativeCreate(name: String, age: Int): Long
@JvmStatic external fun nativeDestroy(handle: Long)
@JvmStatic external fun nativeGetName(handle: Long): String
@JvmStatic external fun nativeGetAge(handle: Long): Int
fun create(name: String, age: Int): Contact = Contact(nativeCreate(name, age))
}
val name: String get() = nativeGetName(handle)
val age: Int get() = nativeGetAge(handle)
override fun close() {
if (handle != 0L) {
nativeDestroy(handle)
handle = 0L
}
}
protected fun finalize() {
close()
}
}
The JNI shims (weaveffi_jni.c) bridge each Kotlin external fun into
the C ABI and route errors through a shared throw_weaveffi_error
helper that throws the generic WeaveFFIException:
static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) {
const char* msg = err->message ? err->message : "WeaveFFI error";
jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException");
if (exClass != NULL) {
jmethodID ctor = (*env)->GetMethodID(env, exClass, "<init>", "(ILjava/lang/String;)V");
jstring jmsg = (*env)->NewStringUTF(env, msg);
jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg);
if (ex != NULL) { (*env)->Throw(env, ex); }
}
weaveffi_error_clear(err);
}
JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_getContactJni(JNIEnv* env, jclass clazz, jint id) {
weaveffi_error err = {0, NULL};
weaveffi_contacts_Contact* rv = weaveffi_contacts_get_contact((int32_t)id, &err);
if (err.code != 0) {
throw_weaveffi_error(env, &err);
return 0;
}
return (jlong)(intptr_t)rv;
}
The CMake file links the JNI shim against the generated C header:
cmake_minimum_required(VERSION 3.22)
project(weaveffi)
add_library(weaveffi SHARED weaveffi_jni.c)
target_include_directories(weaveffi PRIVATE ../../../../c)
Typed errors
Every generated file carries the generic
open class WeaveFFIException(val code: Int, message: String). A module’s
error domain adds a sealed exception hierarchy
named after the domain with the trailing Error stem replaced by
Exception (KvError becomes KvException), one nested class per code,
and a fromCode mapper. From the kvstore sample:
/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */
open class WeaveFFIException(val code: Int, message: String) : Exception(message)
/** Typed error domain `KvError` declared by module `kv`. */
sealed class KvException(code: Int, message: String) : WeaveFFIException(code, message) {
class KeyNotFound(message: String = "key not found") : KvException(1001, message)
class Expired(message: String = "entry expired") : KvException(1002, message)
class StoreFull(message: String = "store has reached capacity") : KvException(1003, message)
class IoError(message: String = "I/O failure") : KvException(1004, message)
companion object {
fun fromCode(code: Int, message: String): WeaveFFIException = when (code) {
1001 -> KeyNotFound(message)
1002 -> Expired(message)
1003 -> StoreFull(message)
1004 -> IoError(message)
else -> WeaveFFIException(code, message)
}
}
}
A callable with throws: true throws the matching subclass from its JNI
shim (a per-domain throw_weaveffi_kv_KvError helper resolves
com/weaveffi/KvException$KeyNotFound and friends by code); catch the
specific class, the sealed domain, or the generic base:
try {
store.put("alpha", byteArrayOf(1), EntryKind.Volatile, null)
} catch (e: KvException.StoreFull) {
// typed case
} catch (e: KvException) {
// any kv domain error
}
A callable without throws keeps a plain signature; its only possible
failures are producer bugs (a panic or a marshalling failure), which
arrive as the generic WeaveFFIException. Unknown codes on the typed
path fall back to WeaveFFIException too.
Interfaces
An interfaces: entry becomes a Kotlin class holding a Long handle and
implementing java.io.Closeable, exactly like a struct wrapper. Its
members live on the class: constructors become companion factories (a
constructor named new becomes operator fun invoke, so ContactBook()
reads like a real constructor), methods are instance functions, statics
are companion functions, and close() calls the implicit destroy symbol.
From the kvstore sample’s Store (trimmed):
/** An embedded key-value store owning its entries */
class Store internal constructor(internal var handle: Long) : java.io.Closeable {
companion object {
init { System.loadLibrary("weaveffi") }
@JvmStatic private external fun nativeOpen(path: String): Long
@JvmStatic private external fun nativeDefaultCapacity(): Long
@JvmStatic private external fun nativeDelete(selfHandle: Long, key: String): Boolean
@JvmStatic private external fun nativeDestroy(handle: Long)
/** Open (or create) a store backed by the given filesystem path */
fun open(path: String): Store = Store(nativeOpen(path))
/** The largest number of live entries one store will hold */
fun defaultCapacity(): Long = nativeDefaultCapacity()
}
/** Remove the entry for the given key, returning true if it existed */
fun delete(key: String): Boolean = nativeDelete(handle, key)
/** Stream every key, optionally filtered by a prefix */
fun listKeys(prefix: String?): Iterator<String> = KvStoreListKeysIterator(nativeListKeys(handle, prefix))
/** Reclaim space asynchronously; returns the number of bytes reclaimed */
suspend fun compact(): Long = suspendCancellableCoroutine { cont ->
nativeCompactAsync(handle, 0L, WeaveContinuation(cont) { code, message -> KvException.fromCode(code, message) })
}
/** Legacy single-shot put kept for compatibility */
@Deprecated("use put() with explicit kind")
fun legacyPut(key: String, value: ByteArray): Boolean = nativeLegacyPut(handle, key, value)
override fun close() {
if (handle != 0L) {
nativeDestroy(handle)
handle = 0L
}
}
}
Store.open("/tmp/cache.kv").use { store ->
store.put("alpha", byteArrayOf(1), EntryKind.Volatile, null)
println(store.count())
}
The JNI externals pass the wrapper’s handle as the leading selfHandle
argument. An interface parameter elsewhere in the API takes the wrapper
class (WeaveFFI.getStats(store: Store) in the nested stats module);
an interface return wraps the new owned handle.
Rich (algebraic) enums
A rich (algebraic) enum, a sum type whose variants carry associated
data, lowers to an opaque object handle at the C ABI, exactly like a
struct, and shares the same ownership model as the struct wrappers above.
The Kotlin wrapper is a Closeable class holding a Long handle, with
one static factory per variant, a nested Tag discriminant enum class,
and per-variant field getters. (A plain C-style enum with no payloads
stays a Kotlin enum class backed by an Int; see above.)
For the shapes module’s Shape enum (Empty, Circle { radius: f64 },
Rectangle { width: f32, height: f32 }, and
Labeled { label: string, count: u8 }), the generator emits (abridged):
/** An algebraic shape (sum type with associated data) */
class Shape internal constructor(internal var handle: Long) : java.io.Closeable {
companion object {
init { System.loadLibrary("weaveffi") }
@JvmStatic external fun nativeTag(handle: Long): Int
@JvmStatic external fun nativeDestroy(handle: Long)
@JvmStatic external fun nativeNewEmpty(): Long
@JvmStatic external fun nativeNewCircle(radius: Double): Long
@JvmStatic external fun nativeNewRectangle(width: Float, height: Float): Long
@JvmStatic external fun nativeNewLabeled(label: String, count: Byte): Long
@JvmStatic external fun nativeGetCircleRadius(handle: Long): Double
@JvmStatic external fun nativeGetLabeledCount(handle: Long): Byte
/** The empty shape */
fun empty(): Shape = Shape(nativeNewEmpty())
/** A circle with a radius */
fun circle(radius: Double): Shape = Shape(nativeNewCircle(radius))
/** An axis-aligned rectangle */
fun rectangle(width: Float, height: Float): Shape = Shape(nativeNewRectangle(width, height))
/** A labeled shape with a small count */
fun labeled(label: String, count: Byte): Shape = Shape(nativeNewLabeled(label, count))
}
enum class Tag(val value: Int) {
Empty(0),
Circle(1),
Rectangle(2),
Labeled(3);
companion object {
fun fromValue(value: Int): Tag = entries.first { it.value == value }
}
}
val tag: Tag get() = Tag.fromValue(nativeTag(handle))
/** Radius in points */
val circleRadius: Double get() = nativeGetCircleRadius(handle)
val labeledCount: Byte get() = nativeGetLabeledCount(handle)
override fun close() {
if (handle != 0L) {
nativeDestroy(handle)
handle = 0L
}
}
protected fun finalize() {
close()
}
}
Each nativeNew* factory maps to a per-variant constructor
(weaveffi_shapes_Shape_<Variant>_new), nativeTag reads the
discriminant (weaveffi_shapes_Shape_tag), the nativeGet* getters read
one variant field (weaveffi_shapes_Shape_<Variant>_get_<field>), and
nativeDestroy frees the handle (weaveffi_shapes_Shape_destroy). The
JNI shims that back these external methods are named
Java_com_weaveffi_Shape_native*:
JNIEXPORT jlong JNICALL Java_com_weaveffi_Shape_nativeNewCircle(JNIEnv* env, jclass clazz, jdouble radius) {
weaveffi_error err = {0, NULL};
weaveffi_shapes_Shape* rv = weaveffi_shapes_Shape_Circle_new((double)radius, &err);
if (err.code != 0) {
throw_weaveffi_error(env, &err);
return 0;
}
return (jlong)(intptr_t)rv;
}
JNIEXPORT jint JNICALL Java_com_weaveffi_Shape_nativeTag(JNIEnv* env, jclass clazz, jlong handle) {
return (jint)weaveffi_shapes_Shape_tag((const weaveffi_shapes_Shape*)(intptr_t)handle);
}
JNIEXPORT void JNICALL Java_com_weaveffi_Shape_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) {
weaveffi_shapes_Shape_destroy((weaveffi_shapes_Shape*)(intptr_t)handle);
}
Free functions that take or return the enum pass the handle across the
boundary; on the WeaveFFI companion they are
describe(shape: Shape): String and
scale(shape: Shape, factor: Double): Shape:
Shape.circle(2.0).use { c ->
println(c.tag) // Tag.Circle
println(c.circleRadius) // 2.0
val bigger = WeaveFFI.scale(c, 3.0) // returns a new Shape
try {
println(WeaveFFI.describe(bigger))
} finally {
bigger.close()
}
}
Ownership: a Shape owns its native handle, so call close() (or use
use { ... }) on every Shape you construct or receive, including the
new Shape returned by scale. The finalize() safety net runs
during GC but is not a substitute for deterministic cleanup.
Build instructions
-
Install Android Studio (Giraffe or newer) plus the NDK.
-
Cross-compile the Rust cdylib for every Android ABI you support:
rustup target add aarch64-linux-android armv7-linux-androideabi \ x86_64-linux-android i686-linux-android export ANDROID_NDK_HOME=/path/to/ndk cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -t x86 \ build --release -p your_library -
Open
generated/androidin Android Studio, sync Gradle, and build the AAR (./gradlew :weaveffi:assemble). -
Add the resulting AAR as a dependency in your app module and ensure your
jniLibs/directory contains the Rust-built cdylib for each supported ABI.
Memory and ownership
- Struct and interface wrappers implement
Closeable; either call.close()explicitly or useuse { ... }. Thefinalize()safety net runs during GC but is not a substitute for deterministic cleanup. - Strings returned from JNI are fresh Java strings; the JNI shim frees
the underlying Rust pointer with
weaveffi_free_stringbefore returning. - Byte arrays returned from JNI are copied with
SetByteArrayRegion, then the Rust buffer is freed withweaveffi_free_bytes. - Returned string arrays and maps free each element with
weaveffi_free_stringafter copying, then release the array buffer (or both parallel key/value buffers) withweaveffi_free_bytes. - Optional values are passed as boxed wrappers (
Integer,Long,Double,Boolean); the JNI shim unboxes and forwards them to the C ABI. A returned boxed optional scalar is read and its box freed withweaveffi_free_bytes.
Async support
Async IDL functions (async: true) are exposed as Kotlin suspend fun
declarations built on suspendCancellableCoroutine. The public suspend
wrapper passes a WeaveContinuation (a small class with onSuccess /
onError methods) to a private external launcher; struct results
resume as raw handles and are re-wrapped after the await. From the
async-demo sample (WeaveFFI.kt):
@JvmStatic private external fun runTaskAsync(name: String, callback: Any)
@JvmStatic suspend fun runTask(name: String): TaskResult {
val raw: Long = suspendCancellableCoroutine { cont ->
runTaskAsync(name, WeaveContinuation(cont) { code, message -> TaskException.fromCode(code, message) })
}
return TaskResult(raw)
}
internal class WeaveContinuation<T>(
private val cont: kotlinx.coroutines.CancellableContinuation<T>,
private val mapError: (Int, String) -> Throwable
) {
@Suppress("UNCHECKED_CAST")
fun onSuccess(result: Any?) { cont.resume(result as T) }
fun onError(code: Int, message: String) { cont.resumeWithException(mapError(code, message)) }
}
run_task declares throws: true, so a failed suspend call resumes with
the typed TaskException; an async callable without throws maps its
(producer-bug-only) failures to the generic WeaveFFIException.
The JNI launcher allocates a per-call context holding the JavaVM and
a NewGlobalRef to the WeaveContinuation, then hands the C ABI a
completion callback. That callback attaches the producer’s thread to
the JVM if it is not already attached, calls onSuccess/onError,
deletes the global ref, frees the context exactly once, and detaches
the thread if it attached it:
typedef struct {
JavaVM* jvm;
jobject callback;
} weaveffi_jni_async_ctx;
JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_runTaskAsync(JNIEnv* env, jclass clazz, jstring name, jobject callback) {
weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx));
(*env)->GetJavaVM(env, &ctx->jvm);
ctx->callback = (*env)->NewGlobalRef(env, callback);
const char* name_chars = (*env)->GetStringUTFChars(env, name, NULL);
weaveffi_tasks_run_task_async(name_chars, weaveffi_tasks_run_task_jni_cb, ctx);
(*env)->ReleaseStringUTFChars(env, name, name_chars);
}
static void weaveffi_tasks_run_task_jni_cb(void* context, weaveffi_error* err, void* result) {
weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context;
JNIEnv* env = NULL;
int attached = 0;
if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; }
attached = 1;
}
/* ... calls callback.onError(int, String) or callback.onSuccess(Object) ... */
weaveffi_jni_handle_uncaught(env);
(*env)->DeleteGlobalRef(env, ctx->callback);
JavaVM* jvm = ctx->jvm;
free(ctx);
if (attached) (*jvm)->DetachCurrentThread(jvm);
}
The completion callback fires exactly once, on a producer thread.
Result buffers passed to it (strings, byte arrays, arrays) are
borrowed from the producer for the callback’s duration, so the shim
copies them into Java objects (NewStringUTF, SetByteArrayRegion)
inside the callback and never frees them. Owned-object results are the
exception: the callback receives ownership, resumes the continuation
with the raw handle, and the suspend wrapper adopts it into the
wrapper class (TaskResult(raw) above). An exception thrown by the
resumed coroutine goes through the same
weaveffi_jni_handle_uncaught path as listener exceptions (see
Callbacks and listeners).
The generated build.gradle does not declare a coroutines dependency;
add org.jetbrains.kotlinx:kotlinx-coroutines-android (or -core) to
the consuming project.
For callables marked cancellable: true, the C ABI takes an extra
weaveffi_cancel_token* parameter. The private external launcher
carries it as cancelToken: Long and the shim casts it to
weaveffi_cancel_token*, but the public suspend wrapper currently
passes 0L (no token); coroutine cancellation isn’t wired to the
native cancel token. From the kvstore sample’s async method
Store.compact:
@JvmStatic private external fun nativeCompactAsync(selfHandle: Long, cancelToken: Long, callback: Any)
suspend fun compact(): Long = suspendCancellableCoroutine { cont ->
nativeCompactAsync(handle, 0L, WeaveContinuation(cont) { code, message -> KvException.fromCode(code, message) })
}
Callbacks and listeners
IDL callbacks paired with listeners produce a register/unregister
pair. From the events sample:
modules:
- name: events
callbacks:
- name: OnMessage
params:
- { name: message, type: string }
listeners:
- name: message_listener
event_callback: OnMessage
The Kotlin surface takes a lambda and returns a Long subscription id;
pass that id back to unregister:
@JvmStatic external fun registerMessageListener(callback: (String) -> Unit): Long
@JvmStatic external fun unregisterMessageListener(id: Long)
The JNI shim keeps the lambda alive with a NewGlobalRef stored in a
mutex-guarded registry (a linked list of contexts holding the JavaVM,
the global ref, and the subscription id). When the producer fires, a C
trampoline attaches the producer’s thread to the JVM if needed and
invokes the lambda through its kotlin.jvm.functions.Function1
invoke(Object): Object method; unregistering removes the registry
entry, deletes the global ref, and frees the context:
static void weaveffi_events_OnMessage_fn_jni_tramp(const char* message, void* context) {
weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)context;
JNIEnv* env = NULL;
int attached = 0;
if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) return;
attached = 1;
}
if ((*env)->PushLocalFrame(env, 32) != 0) {
if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm);
return;
}
jobject _a0 = message ? (jobject)(*env)->NewStringUTF(env, message) : (jobject)(*env)->NewStringUTF(env, "");
jclass fn_cls = (*env)->GetObjectClass(env, ctx->callback);
jmethodID invoke = (*env)->GetMethodID(env, fn_cls, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;");
(*env)->CallObjectMethod(env, ctx->callback, invoke, _a0);
weaveffi_jni_handle_uncaught(env);
(*env)->PopLocalFrame(env, NULL);
if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm);
}
JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_registerMessageListener(JNIEnv* env, jclass clazz, jobject callback) {
weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)calloc(1, sizeof(weaveffi_jni_listener_ctx));
(*env)->GetJavaVM(env, &ctx->jvm);
ctx->callback = (*env)->NewGlobalRef(env, callback);
uint64_t id = weaveffi_events_register_message_listener(weaveffi_events_OnMessage_fn_jni_tramp, ctx);
/* ... stores ctx in the registry under id ... */
return (jlong)id;
}
The callback runs on the producer’s thread, whichever thread the
native side fires the event from. For UI work, hop to the main thread
yourself (e.g. withContext(Dispatchers.Main) or Handler.post).
An exception thrown by the Kotlin callback has no caller to propagate
to, since the frame below it is native producer code. The glue routes
it through weaveffi_jni_handle_uncaught, which delivers it to the
handler installed on the module companion:
/**
* Installs a handler for exceptions thrown by listener callbacks and
* async continuations on native producer threads. These exceptions have
* no Kotlin caller to propagate to; when no handler is installed, they
* are logged with their stack trace and dropped. Pass `null` to
* restore the default logging behavior.
*/
@JvmStatic fun setCallbackExceptionHandler(handler: ((Throwable) -> Unit)?) {
callbackExceptionHandler = handler
}
When a module declares listeners or async functions, the JNI glue also
defines JNI_OnLoad, which caches a global reference to the wrapper
class and the dispatchCallbackException method id so the producer
thread can deliver exceptions without an extra class lookup.
Iterators
iter<T> returns surface as Iterator<T> in Kotlin, backed by a
generated per-function wrapper class that is fully lazy: the external
launcher returns the raw iterator handle as a Long, and each
hasNext() lookahead issues exactly one nativeNext call, which maps
to one producer _next call. Nothing is drained into a hidden list.
From the events sample (get_messages returns iter<string>):
@JvmStatic private external fun getMessagesJni(): Long
@JvmStatic fun getMessages(): Iterator<String> = EventsGetMessagesIterator(getMessagesJni())
/**
* A lazy iterator over the `String` elements streamed by [getMessages]. Each step pulls
* exactly one element from the native producer. The native handle is
* released when the producer is exhausted, when [close] is called, or by
* the finalizer if the iterator is abandoned, whichever comes first.
*/
class EventsGetMessagesIterator internal constructor(private var handle: Long) : Iterator<String>, java.io.Closeable {
private var nextSlot: Array<Any?>? = null
override fun hasNext(): Boolean {
if (nextSlot != null) return true
if (handle == 0L) return false
val slot = nativeNext(handle)
if (slot == null) {
close()
return false
}
nextSlot = slot
return true
}
override fun next(): String {
if (!hasNext()) throw NoSuchElementException()
val raw = nextSlot!![0]
nextSlot = null
return raw as String
}
override fun close() {
if (handle != 0L) {
nativeDestroy(handle)
handle = 0L
}
}
protected fun finalize() {
close()
}
companion object {
init { System.loadLibrary("weaveffi") }
@JvmStatic private external fun nativeNext(handle: Long): Array<Any?>?
@JvmStatic private external fun nativeDestroy(handle: Long)
}
}
The JNI nativeNext shim pulls one element and returns it in a
one-slot Object[]; null means the stream is exhausted. Each string
element is freed with weaveffi_free_string right after NewStringUTF
copies it; when the element type is a struct, the raw handle is
returned instead and the Kotlin next() adopts it into the owning
wrapper class (Contact(raw as Long)), whose close() eventually
destroys it:
JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_EventsGetMessagesIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) {
weaveffi_events_GetMessagesIterator* _iter = (weaveffi_events_GetMessagesIterator*)(intptr_t)handle;
const char* _item = (const char*)0;
weaveffi_error err = {0, NULL};
int32_t _has = weaveffi_events_GetMessagesIterator_next(_iter, &_item, &err);
if (err.code != 0) {
throw_weaveffi_error(env, &err);
return NULL;
}
if (_has == 0) { return NULL; }
jstring _jitem = _item ? (*env)->NewStringUTF(env, _item) : (*env)->NewStringUTF(env, "");
weaveffi_free_string(_item);
jclass _obj_cls = (*env)->FindClass(env, "java/lang/Object");
jobjectArray _slot = (*env)->NewObjectArray(env, 1, _obj_cls, NULL);
(*env)->SetObjectArrayElement(env, _slot, 0, _jitem);
return _slot;
}
The native handle is destroyed exactly once: close() is called
eagerly when hasNext() sees exhaustion, callers can call close()
themselves (the class implements Closeable) when abandoning
iteration early, and the finalize() safety net covers abandoned
iterators during GC. Nulling the handle makes a double destroy
impossible.
Errors from the launcher and from each next follow the function’s
error strategy: the throwing kvstore sample’s Store.listKeys shim
throws the typed domain exception (throw_weaveffi_kv_KvError, so
KvException.KeyNotFound and friends) from the step that failed,
while the non-throwing getMessages throws the generic
WeaveFFIException only for producer bugs.
Troubleshooting
UnsatisfiedLinkError: Couldn't find libweaveffi.so: the Rust-built cdylib was not packaged inside the AAR. Place it undersrc/main/jniLibs/<abi>/and rebuild.UnsatisfiedLinkErrorfor the JNI symbol itself: Kotlin external function names must match the JNI signature, including the_1escape for underscores. Re-runweaveffi generateif you hand-edited either side.- Crashes when releasing strings: the JNI shim is responsible for
calling
ReleaseStringUTFCharson everyGetStringUTFChars. If you edit the shim, keep the pairing intact. - R8/ProGuard removes
WeaveFFIsymbols: keep the wrapper class with-keep class com.weaveffi.** { *; }in your ProGuard rules.