Skip to content

feat: pointers to records cross JNI as typed handles - #17

Merged
lemcoder merged 2 commits into
feature/konan-dslfrom
feat/jni-typed-handles
Nov 3, 2026
Merged

lemcoder merged 2 commits into
feature/konan-dslfrom
feat/jni-typed-handles

Conversation

@lemcoder

@lemcoder lemcoder commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Problem

The JNI generator passed every pointer to a struct as a bare Long. Any Long, even a channel number, compiled where a tsf* was expected. A failed call's NULL came back as 0L, ready to be dereferenced by the next call, which crashes the process.

Change

Each named struct or union reached by a pointer gets a typed handle:

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

internal fun tsf_load_filename(filename: String?): TsfPointer?            // NULL → null
internal fun tsf_note_on(f: TsfPointer, preset_index: Int, key: Int, vel: Float): Int
  • Naming: by the record's tag, else (anonymous record) its typedef. node_t* and struct node* are one NodePointer; const is ignored.
  • Nullability: parameters are non-null, results are nullable.
  • JNI: the external keeps the Long behind the <name>$kni wrapper that strings already use. Kotlin mangles the JVM name of functions with value-class parameters, and JNI couldn't resolve it.
  • Unchanged: typedef'd void*, function pointers and pointers to pointers (LongArray?).
  • Escape hatch: address gives the raw pointer, and TsfPointer(0L) passes NULL deliberately.

Breaking for consumers: code passing these pointers as Long must switch to the handle type.

Ownership (freeing, use after free) is out of scope here. It needs a DSL to declare the release function.

Tests

  • GenerateFromNativeIndexTest asserts the value class, the wrapper/external pair, tag naming through a typedef, and a nullable result (sample_create added to sample.h). Full suite: 66 tests, 0 failures.
  • Checked end to end in MikroSoundFont (Android-KMP, AGP 9.3.1) via includeBuild. Delegates moved to TsfPointer compile, and an instrumented test on a physical arm64 device (Android 17) passed 2/2: load, render, tsf_close, and garbage input rejected.

🤖 Generated with Claude Code

lemcoder and others added 2 commits November 3, 2026 00:26
A pointer to a named struct or union was a bare Long: any Long compiled
where a tsf* was expected, and a NULL result was 0L that the next call
dereferenced. Each such record now gets
@JvmInline value class <Tag>Pointer(val address: Long), named by the
record's tag (or an anonymous record's typedef), so node_t* and
struct node* are one NodePointer.

Parameters take the handle non-null; results are nullable, NULL being
null. The external keeps the Long behind the <name>$kni wrapper strings
already use: Kotlin mangles the JVM name of a function with a value-class
parameter, which JNI could not resolve.

Typedef'd void*, function pointers and pointers to pointers are unchanged.
Bindings that passed these pointers as Long must use the handle type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds a C counter, passes its handle back, and gets NULL as null: a
wrapper whose $kni external did not resolve would fail the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lemcoder
lemcoder merged commit 69ac93e into feature/konan-dsl Nov 3, 2026
1 check passed
@lemcoder lemcoder mentioned this pull request Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant