§ Kotlin & Scala
The published surface is Java, and both other JVM languages consume it directly, so there is no separate artifact for either.
Nullness is part of the contract.
The package is @NullMarked, so a Kotlin caller gets real non-null types across the whole boundary rather than platform types, and the compiler refuses a null where the API does not accept one. AutoCloseable is Kotlin’s use, and a coroutine waits on a Deferred with withContext(Dispatchers.IO):
val index: DenseIndex = DenseIndex.initRam(DenseOptions().dimensions(2).quantization(Scalar.BF16), intArrayOf())
index.use {
Arena(pool).use { arena ->
val matches = withContext(Dispatchers.IO) { index.deferred(pool).search(queries, 10, arena).use { it.join() } }
println(matches.key(0, 0)) // valid until the next call given `arena`, or its close
}
}Scala reads no nullness annotations, so what it gets instead is that the array-based batch verbs take Array[Key] and Array[Float] with no conversion, since both are JVM arrays already:
val keys: Array[Key] = Array(Key.of(1), Key.of(2))
index.on(pool).insert(keys, RowsView.of(Array(1.0f, 0.0f, 0.0f, 1.0f), 2))§ What This Binding Does Not Expose
The filtered search of the C interface stays unbound.
Its predicate runs on the engine’s own worker threads, which are not attached to the JVM, so every batch of candidates would pay an attach, an upcall, and an exception check.
A predicate that threw would also unwind through native frames that cannot handle it.
Filter the results of search instead, or narrow the index.
Progress handlers stay unbound for the same reason, which is what makes an insertion batch all-or-nothing here.
cursor and searchStream pull one batch at a time, blocking, so a DeferredOn lacks them, and constructors address no index yet, so no receiver offers them.
§ Design
- A thin layer over the C ABI. Every name follows
include/usearch/: a C verbusearch_dense_<verb>is a camel-cased<verb>here, a C type dropsusearch_and_t, and enums mirror C enums, never strings. Every wrapper’s documentation names the C symbol it calls, or says it is binding-only. - The receiver chooses, the verb stays. The index runs a verb on the calling thread,
on(executor)blocks on an executor, anddeferred(executor)submits it, all with the same arguments. - Deferred calls use the language’s own idioms.
statuspolls throughusearch_outcome_status,joinblocks throughusearch_outcome_wait, and aDeferredisAutoCloseable, so Java waits in try-with-resources or on a virtual thread, and Kotlin inwithContext(Dispatchers.IO). - No life of its own. The binding starts no threads and keeps no queues; releasing a
Deferredrides try-with-resources, whoseclosewaits for the call first, and the collector frees a dropped one only once it is final. - Arenas are lent and reused, never shared. Every output whose length scales with the call lands in an arena the caller passes; each call closes the FFM scope the previous call’s views read through, so a stale view throws instead of reading reused memory.
- Nothing costs anything until it is used. Submitting a call allocates nothing, and an arena allocates only when a call writes into it.
- Blocking calls stay as simple as the language allows.
index.search(queries, 10, arena)takes no executor, completion or callback and returns its result; nothing about asynchrony reaches a caller who never asks for it. - Argument structures live in FFM arenas. A blocking call fills its C argument structure in a confined arena freed on return and takes no lock; a deferred one keeps it in a shared arena until the call is final.