Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ UTF-8, so 👍 reached C as two surrogates on HotSpot, and `NewStringUTF` given
CheckJNI aborts (#14). A `const char*` crosses as a `ByteArray` of UTF-8 behind a Kotlin wrapper, around a
private `external` named `<name>$kni`: `$` cannot occur in a C identifier, so it clashes with no binding.

**A pointer to a named record is a typed handle** (`@JvmInline value class <Tag>Pointer(val address: Long)`), and
again only the Kotlin wrapper sees it: a function with a value-class parameter gets a mangled JVM name, so the
`external` keeps the `Long` under `<name>$kni`. The class is named by the record's tag (`node_t*` and `struct node*`
are one `NodePointer`); results are nullable, parameters are not.

Bindings are `internal` by default and each carries **`@JvmName`**. JNI resolves the C symbol from the
unmangled method name, so a mangled internal name compiles, links, and dies on the first call.

Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,23 @@ plugin writes both sides of each JNI bridge from the C declarations:
| struct by value (argument or result) | `ByteArray` | the struct's bytes |
| pointer to pointer (`handle_t**` out-parameter) | `LongArray?` | addresses, copied into a C pointer array and back unless `const` |
| `void*`, `const void*` argument | `java.nio.Buffer?` | a direct buffer's start address (`GetDirectBufferAddress`); a heap buffer throws |
| opaque handle, typedef'd `void*`, struct or function pointer, any pointer result | `Long` | the address |
| pointer to a named struct or union (`tsf*`, `struct node*`, opaque or not) | `TsfPointer`, `NodePointer`, …; `…?` as a result | the address, through a generated wrapper |
| typedef'd `void*`, function pointer, other pointer results | `Long` | the address |

A pointer to a struct or union gets a typed handle named after the record's tag (or, for an anonymous
one, its typedef), so `tsf*` and `const tsf*` are both a `TsfPointer`:

```kotlin
@JvmInline internal value class TsfPointer(val address: Long)

internal fun tsf_load_filename(filename: String?): TsfPointer? // NULL is null
internal fun tsf_note_on(f: TsfPointer, preset_index: Int, key: Int, vel: Float): Int
```

Results are nullable, so a failed call cannot be passed on unchecked; parameters take a non-null handle,
so a stray `Long` does not compile. `address` is the raw pointer for other native APIs, and
`TsfPointer(0L)` passes NULL deliberately. A handle says nothing about who owns the memory: freeing it
(`tsf_close`) and not using it afterwards stays the caller's job. Pointers to pointers stay `LongArray?`.

Enum constants and integer, floating-point and string macros become `const val`s. Functions that
cannot be bound — variadic ones, `long double` — are listed with the reason at the end of the generated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import java.io.File
* - structs by value: a `ByteArray` holding the struct's bytes;
* - `void*` and `const void*` arguments, spelled as such: a direct `java.nio.Buffer?`, whose start
* address — not its position — is passed; a heap buffer is an `IllegalArgumentException`;
* - every other pointer (opaque handles, typedef'd `void*`, structs, function pointers, results): its
* - pointers to a named struct or union (`tsf*`, `struct node*`, opaque or not): a typed handle,
* `@JvmInline value class <Name>Pointer(val address: Long)`, named after the record. Parameters take it
* non-null, results are `null` for NULL, so neither an unrelated `Long` nor a failed call's NULL reaches
* C unnoticed. The `external` still takes the `Long`, behind a wrapper: Kotlin mangles the JVM name of
* a function with value-class parameters, which JNI could not resolve;
* - every other pointer (typedef'd `void*`, function pointers, pointers to primitives as results): its
* address as a `Long`.
*
* Enum constants and the integer, floating-point and string macro constants become `const val`s.
Expand Down Expand Up @@ -64,14 +69,17 @@ object JniGenerator : BindingGenerator<GeneratorRequest.Jni> {

/** One C function, decided: how each parameter and the result cross. */
private class Binding(val function: FunctionDecl, val params: List<Pair<String, Crossing>>, val result: Crossing) {
/** Strings cross as bytes, so the `external` takes and returns those behind a Kotlin wrapper. */
val crossesStrings get() = result == Crossing.CString || params.any { it.second == Crossing.CString }
/**
* Strings cross as bytes and handles as their address, so the `external` takes and returns those
* behind a Kotlin wrapper.
*/
val needsWrapper get() = (params.map { it.second } + result).any { it == Crossing.CString || it is Crossing.Handle }

/**
* The JVM name of the `external`: the C function's, or with a wrapper `<name>$kni` — `$` cannot occur
* in a C identifier, so it names no other binding.
*/
val nativeName get() = if (crossesStrings) "${function.name}\$kni" else function.name
val nativeName get() = if (needsWrapper) "${function.name}\$kni" else function.name
}

/** How one value crosses the JNI boundary. */
Expand Down Expand Up @@ -102,7 +110,17 @@ object JniGenerator : BindingGenerator<GeneratorRequest.Jni> {
class Primitive(val primitive: JvmPrimitive) : Crossing(primitive.kotlinName, primitive.jniName)

/** A pointer as an address; [cType] is how C spells the pointer, for the cast. */
class Address(val cType: String) : Crossing(JvmPrimitive.LONG.kotlinName, JvmPrimitive.LONG.jniName)
open class Address(val cType: String, kotlinType: String = JvmPrimitive.LONG.kotlinName) :
Crossing(kotlinType, JvmPrimitive.LONG.jniName, JvmPrimitive.LONG.kotlinName)

/**
* A pointer to the record [record] as its typed handle, [className]; `null` for NULL when [nullable]
* (results). Crosses JNI as an [Address].
*/
class Handle(cType: String, val record: String, val nullable: Boolean) :
Address(cType, handleClassName(record) + if (nullable) "?" else "") {
val className get() = handleClassName(record)
}

/** `const char*` as a `String?`, through its NUL-terminated UTF-8 bytes. */
object CString : Crossing("String?", "jbyteArray", "${JvmPrimitive.BYTE.kotlinName}Array?")
Expand Down Expand Up @@ -151,18 +169,23 @@ object JniGenerator : BindingGenerator<GeneratorRequest.Jni> {

private fun result(type: Type): Crossing = when (val t = resolved(type)) {
VoidType -> Crossing.Void
is PointerType ->
if (resolved(t.pointeeType) == CharType && t.pointeeIsConst) Crossing.CString else Crossing.Address(spelling(type))
is PointerType -> when {
resolved(t.pointeeType) == CharType && t.pointeeIsConst -> Crossing.CString
else -> handleRecord(t.pointeeType)?.let { Crossing.Handle(spelling(type), it, nullable = true) }
?: Crossing.Address(spelling(type))
}
is RecordType -> Crossing.StructBytes(spelling(type))
else -> primitive(t)
}

private fun pointer(declared: Type, pointer: Type): Crossing {
val (pointee, isConst) = when (pointer) {
is PointerType -> resolved(pointer.pointeeType) to pointer.pointeeIsConst
is ArrayType -> resolved(pointer.elemType) to false
val declaredPointee = when (pointer) {
is PointerType -> pointer.pointeeType
is ArrayType -> pointer.elemType
else -> error("not a pointer")
}
val pointee = resolved(declaredPointee)
val isConst = pointer is PointerType && pointer.pointeeIsConst
if (pointee == CharType && isConst) return Crossing.CString
val cType = spelling(declared)
val element = when (pointee) {
Expand All @@ -175,10 +198,20 @@ object JniGenerator : BindingGenerator<GeneratorRequest.Jni> {
element != null -> Crossing.PrimitiveArray(element, cType, isConst)
pointee == VoidType && declared !is Typedef -> Crossing.DirectBuffer(cType)
pointee is PointerType -> Crossing.AddressArray(cType, isConst)
else -> Crossing.Address(cType)
else -> handleRecord(declaredPointee)?.let { Crossing.Handle(cType, it, nullable = false) } ?: Crossing.Address(cType)
}
}

/**
* The name a pointer to [pointee] is typed by: its struct or union's tag, else — an anonymous record —
* the typedef naming it. `null` for anything else, which stays a `Long`.
*/
private fun handleRecord(pointee: Type): String? {
val record = resolved(pointee) as? RecordType ?: return null
if (!record.decl.isAnonymous) return record.decl.spelling.removePrefix("struct ").removePrefix("union ")
return (pointee as? Typedef)?.def?.name
}

private fun primitive(type: Type): Crossing.Primitive = when (type) {
CharType -> Crossing.Primitive(JvmPrimitive.BYTE)
is BoolType -> Crossing.Primitive(JvmPrimitive.BOOLEAN)
Expand Down Expand Up @@ -234,23 +267,38 @@ object JniGenerator : BindingGenerator<GeneratorRequest.Jni> {
appendLine("/** A string as the NUL-terminated UTF-8 C reads; JNI's own string functions speak modified UTF-8. */")
appendLine("private fun kniUtf8(s: String): ByteArray = (s + '\\u0000').encodeToByteArray()")
}
val visibilityName = visibility.name.lowercase()
bound.flatMap { b -> b.params.map { it.second } + b.result }.filterIsInstance<Crossing.Handle>()
.distinctBy { it.record }.sortedBy { it.className }.forEach { h ->
appendLine()
appendLine("/** The address of a C `${h.record}`; a function that may return NULL returns `null` instead. */")
appendLine("@JvmInline")
appendLine("$visibilityName value class ${h.className}(val address: Long)")
}
bound.forEach { b ->
val params = b.params.joinToString { (name, crossing) -> "${kotlinIdentifier(name)}: ${crossing.kotlinType}" }
val visibilityName = visibility.name.lowercase()
appendLine()
appendLine("/** C: `${cSignature(b.function)}` */")
// JNI resolves the C symbol from the JVM method name; without @JvmName an internal function
// could be mangled.
appendLine("""@JvmName("${b.function.name}")""")
if (!b.crossesStrings) {
if (!b.needsWrapper) {
appendLine("$visibilityName external fun ${kotlinIdentifier(b.function.name)}($params): ${b.result.kotlinType}")
return@forEach
}
val native = "`${b.nativeName}`"
val arguments = b.params.joinToString { (name, crossing) ->
if (crossing == Crossing.CString) "${kotlinIdentifier(name)}?.let(::kniUtf8)" else kotlinIdentifier(name)
when (crossing) {
Crossing.CString -> "${kotlinIdentifier(name)}?.let(::kniUtf8)"
is Crossing.Handle -> "${kotlinIdentifier(name)}.address"
else -> kotlinIdentifier(name)
}
}
val decode = when (val result = b.result) {
Crossing.CString -> "?.decodeToString()"
is Crossing.Handle -> ".let { if (it == 0L) null else ${result.className}(it) }"
else -> ""
}
val decode = if (b.result == Crossing.CString) "?.decodeToString()" else ""
appendLine("$visibilityName fun ${kotlinIdentifier(b.function.name)}($params): ${b.result.kotlinType} = $native($arguments)$decode")
appendLine()
val externalParams = b.params.joinToString { (name, crossing) -> "${kotlinIdentifier(name)}: ${crossing.externalType}" }
Expand Down Expand Up @@ -450,6 +498,9 @@ object JniGenerator : BindingGenerator<GeneratorRequest.Jni> {
private fun kotlinName(cName: String?, index: Int): String =
cName?.takeIf { it.isNotEmpty() && it.all { c -> c == '_' || c.isLetterOrDigit() } && !it.first().isDigit() } ?: "p$index"

/** `tsf` → `TsfPointer`: the record's name, capitalized as a Kotlin class, and `Pointer`. */
private fun handleClassName(record: String) = record.replaceFirstChar { it.uppercaseChar() } + "Pointer"

private fun kotlinIdentifier(name: String) = if (name in KOTLIN_KEYWORDS) "`$name`" else name

private val KOTLIN_KEYWORDS = setOf(
Expand Down
49 changes: 49 additions & 0 deletions src/test/kotlin/io/github/lemcoder/KonanPluginFunctionalTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,55 @@ class KonanPluginFunctionalTest {
assertContains(output, "echo=true")
}

@Test
fun `pointers to records cross JNI as typed handles, NULL as null`() {
val home = realKonanHome()
File(projectDir, "native").mkdirs()
File(projectDir, "native/counter.c").writeText(
"""
#include <stdlib.h>
struct counter { int n; };
typedef struct counter counter_t;
struct counter* counter_new(int start) {
if (start < 0) return 0;
struct counter* c = malloc(sizeof *c);
c->n = start;
return c;
}
int counter_bump(counter_t* c) { return ++c->n; }
void counter_free(struct counter* c) { free(c); }
""".trimIndent()
)
File(projectDir, "src/main/kotlin").mkdirs()
// Each call goes through a wrapper around a `$kni` external: a JNI name that does not resolve fails here.
File(projectDir, "src/main/kotlin/Main.kt").writeText(
"""
import example.CounterPointer
import example.counter_bump
import example.counter_free
import example.counter_new
fun main() {
val c: CounterPointer = counter_new(41) ?: error("counter_new returned NULL")
println("bump=" + counter_bump(c))
println("null=" + (counter_new(-1) == null))
counter_free(c)
}
""".trimIndent()
)
write("""kotlin("jvm")${"\n"}id("io.github.lemcoder.konanplugin")${"\n"}application""", "config.konanHome.set(file(\"$home\"))\n$mymath")
File(projectDir, "mymath.h").writeText(
"struct counter;\ntypedef struct counter counter_t;\n" +
"struct counter* counter_new(int start);\nint counter_bump(counter_t* c);\nvoid counter_free(struct counter* c);\n"
)
File(projectDir, "build.gradle.kts").appendText("repositories { mavenCentral() }\napplication { mainClass.set(\"MainKt\") }\n")

val output = runner("run", "-q").withPluginClasspathAndKgp().build().output

// counter_t* and struct counter* are one CounterPointer; the handle reached C as the address it came from.
assertContains(output, "bump=42")
assertContains(output, "null=true")
}

@Test
fun `Android libraries compile and link with the NDK, its C++ runtime inside`() {
val home = realKonanHome()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ class GenerateFromNativeIndexTest {
assertContains(kotlin, "internal fun sample_name(): String? = `sample_name\$kni`()?.decodeToString()")
assertContains(kotlin, "@JvmName(\"sample_name\\\$kni\")\nprivate external fun `sample_name\$kni`(): ByteArray?")
assertContains(kotlin, "internal fun sample_echo(text: String?): String? = `sample_echo\$kni`(text?.let(::kniUtf8))?.decodeToString()")
assertContains(kotlin, "internal external fun sample_buffer(handle: Long, c: Int, s: Int): ByteArray")
// Pointers to records are typed handles, unwrapped for the external (Kotlin would mangle its name).
assertContains(kotlin, "@JvmInline\ninternal value class OpaquePointer(val address: Long)")
assertContains(kotlin, "internal fun sample_buffer(handle: OpaquePointer, c: Int, s: Int): ByteArray = `sample_buffer\$kni`(handle.address, c, s)")
assertContains(kotlin, "@JvmName(\"sample_buffer\\\$kni\")\nprivate external fun `sample_buffer\$kni`(handle: Long, c: Int, s: Int): ByteArray")
// Named by the struct's tag whichever way it is spelled: node_t is struct node.
assertContains(kotlin, "internal fun sample_scale(node: NodePointer, factor: Double): Double = `sample_scale\$kni`(node.address, factor)")
assertContains(kotlin, "internal fun sample_packet_size(p: PacketPointer): Long")
// NULL comes back as null.
assertContains(kotlin, "internal fun sample_create(): OpaquePointer? = `sample_create\$kni`().let { if (it == 0L) null else OpaquePointer(it) }")
assertContains(kotlin, "internal external fun sample_bits(f: ByteArray): ByteArray")
assertContains(kotlin, "internal external fun sample_sort(base: java.nio.Buffer?, n: Long, cmp: Long): Unit")
// A typedef'd void* is a handle, not memory.
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/nativeindex/sample.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct buffer sample_buffer(struct opaque *handle, enum color c, size_class s);
int sample_log(const char *format, ...);
size_t sample_packet_size(const struct packet *p);
bits_t sample_bits(struct flags f);
struct opaque *sample_create(void);
int sample_open(struct opaque **out);
int sample_peek(const struct opaque *const *handles, size_t n);
typedef void *sample_handle;
Expand Down
Loading