Skip to content

Commit 556cef8

Browse files
authored
Merge pull request #2 from kdroidFilter/feat/flow-dataclass-support
Add Flow<DataClass> support to FFM bridge generator
2 parents a0f83b7 + 1815979 commit 556cef8

11 files changed

Lines changed: 478 additions & 9 deletions

File tree

.github/workflows/check.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
sudo apt-get install -y libnotify-dev libglib2.0-dev libdbusmenu-glib-dev
4444
4545
- name: Plugin tests
46-
run: ./gradlew --project-dir plugin-build :plugin:check
46+
run: ./gradlew --project-dir plugin-build :nucleusnativeaccess:check
4747

4848
- name: Examples tests
4949
run: ./gradlew :examples:calculator:check :examples:systeminfo:check :examples:benchmark:check

README.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,9 @@ No JNI. No annotations. No boilerplate. Just write Kotlin/Native and use it from
121121

122122
## What's supported
123123

124-
### Types — test coverage (739 end-to-end FFM tests)
124+
### Types — test coverage (770+ end-to-end FFM tests)
125125

126-
Every test compiles Kotlin/Native → `libcalculator.so` (200+ exported symbols) → loads via FFM `MethodHandle` → verifies on JVM. Zero mocks — all 739 tests cross the real native boundary. Includes 25 load tests (500K+ FFM calls), concurrent stress tests (10 threads), 53 suspend function tests with cancellation, and 23 Flow tests.
126+
Every test compiles Kotlin/Native → `libcalculator.so` (200+ exported symbols) → loads via FFM `MethodHandle` → verifies on JVM. Zero mocks — all 770+ tests cross the real native boundary. Includes 25 load tests (500K+ FFM calls), concurrent stress tests (10 threads), 53 suspend function tests with cancellation, and 50+ Flow tests (including `Flow<DataClass>`).
127127

128128
| Feature | As param | As return | As property | CB param | CB return | Notes |
129129
|---------|----------|-----------|-------------|----------|-----------|-------|
@@ -150,7 +150,7 @@ Every test compiles Kotlin/Native → `libcalculator.so` (200+ exported symbols)
150150
| `Map<K, V>` | ✅ 12t | ✅ 12t | &mdash; | ✅ 2t | ✅ 2t | String→Int, Int→String, Int→Int, String→String + merge/empty |
151151
| `Map<K, V>?` | &mdash; | ✅ 4t | &mdash; | &mdash; | &mdash; | -1 count = null sentinel |
152152
| `(T) -> R` (lambda) | ✅ 15t | &mdash; | &mdash; | &mdash; | &mdash; | persistent `Arena.ofShared()` |
153-
| `Flow<T>` | &mdash; |23t | &mdash; | &mdash; | &mdash; | `channelFlow` + 3 callbacks (onNext, onError, onComplete) |
153+
| `Flow<T>` | &mdash; |50t+ | &mdash; | &mdash; | &mdash; | `channelFlow` + 3 callbacks (onNext, onError, onComplete), incl. `Flow<DataClass>` |
154154

155155
### Declarations
156156

@@ -168,7 +168,7 @@ Every test compiles Kotlin/Native → `libcalculator.so` (200+ exported symbols)
168168
| Data classes (nativeMain) || auto-generates JVM data class + field marshalling |
169169
| Data classes (commonMain) || reuses existing JVM type, no proxy generated |
170170
| Suspend functions || `suspendCancellableCoroutine` + bidirectional cancellation (53 tests) |
171-
| Flow&lt;T&gt; return || `channelFlow` + onNext/onError/onComplete callbacks (23 tests) |
171+
| Flow&lt;T&gt; return || `channelFlow` + onNext/onError/onComplete callbacks (50+ tests, incl. DataClass) |
172172
| Exception propagation || `try/catch` wrapping, `KotlinNativeException` on JVM |
173173
| Object lifecycle || `Cleaner` for GC + `close()` for explicit release |
174174

@@ -213,7 +213,23 @@ calc.infiniteFlow().take(3).toList() // [0, 1, 2] — auto-cancelled
213213

214214
**Cancellation**: collecting only N elements (via `take`, `first`) automatically cancels the native Flow collection. Manual `Job.cancel()` also propagates.
215215

216-
**Supported element types**: `Int`, `Long`, `Double`, `Float`, `Boolean`, `Byte`, `Short`, `String`, `enum class`, `Object`.
216+
**Supported element types**: `Int`, `Long`, `Double`, `Float`, `Boolean`, `Byte`, `Short`, `String`, `enum class`, `Object`, `data class` (including nested data classes).
217+
218+
**Data class in Flow**: data classes are serialized element-by-element via `StableRef` + per-type reader bridges. Nested data classes (e.g. `Flow<Rect>` where `Rect` contains two `Point`) are fully supported.
219+
220+
```kotlin
221+
// Kotlin/Native
222+
data class MemoryInfo(val totalMB: Long, val availableMB: Long)
223+
224+
fun memoryFlow(intervalMs: Long = 1000L): Flow<MemoryInfo> = flow {
225+
while (true) { emit(MemoryInfo(getTotalMemoryMB(), getAvailableMemoryMB())); delay(intervalMs) }
226+
}
227+
228+
// JVM — transparent Flow<DataClass> collection
229+
desktop.memoryFlow(2000L).collect { info ->
230+
println("${info.availableMB} MB / ${info.totalMB} MB")
231+
}
232+
```
217233

218234
### Callbacks & lambdas
219235

@@ -427,7 +443,7 @@ The plugin auto-generates GraalVM reachability metadata under `META-INF/native-i
427443

428444
- `reflect-config.json` &mdash; all generated proxy classes
429445
- `resource-config.json` &mdash; bundled native library resources
430-
- `reachability-metadata.json` &mdash; FFM foreign downcall descriptors, reflection, and resources
446+
- `reachability-metadata.json` &mdash; FFM foreign downcall + upcall descriptors, reflection, and resources
431447

432448
For GraalVM native-image builds, the native `.so`/`.dylib` must be placed next to the executable (the plugin bundles it in the JAR for JVM, but native-image can't extract at runtime).
433449

@@ -503,7 +519,7 @@ The repository includes two complete examples in [`examples/`](examples/):
503519

504520
| Example | Description |
505521
|---------|-------------|
506-
| [`calculator/`](examples/calculator/) | Stateful Calculator class with 739 end-to-end tests: all types, callbacks, collections, suspend, Flow, nested classes, concurrency |
522+
| [`calculator/`](examples/calculator/) | Stateful Calculator class with 770+ end-to-end tests: all types, callbacks, collections, suspend, Flow (incl. DataClass), nested classes, concurrency |
507523
| [`systeminfo/`](examples/systeminfo/) | Linux system info (`/proc`, POSIX, `gethostname`) + native notifications via `libnotify` cinterop, with Compose Desktop UI |
508524
| [`benchmark/`](examples/benchmark/) | Performance benchmarks: native vs JVM (fibonacci, pi, sort, string, allocation, concurrent) |
509525

@@ -512,7 +528,7 @@ Run them:
512528
```bash
513529
./gradlew :examples:calculator:run
514530
./gradlew :examples:systeminfo:run
515-
./gradlew :examples:calculator:jvmTest # 739 end-to-end FFM tests
531+
./gradlew :examples:calculator:jvmTest # 770+ end-to-end FFM tests
516532
./gradlew :examples:benchmark:jvmTest # Performance benchmarks (native vs JVM)
517533
```
518534

examples/calculator/src/jvmTest/kotlin/com/example/calculator/CalculatorTest.kt

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6187,4 +6187,289 @@ class CalculatorTest {
61876187
assertEquals(5, received)
61886188
}
61896189
}
6190+
6191+
// ═══════════════════════════════════════════════════════════════════════════
6192+
// Flow<DataClass> tests
6193+
// ═══════════════════════════════════════════════════════════════════════════
6194+
6195+
// ── Flow<Point> ─────────────────────────────────────────────────────────
6196+
6197+
@Test fun `flow dc - pointFlow basic`() = runBlocking {
6198+
Calculator(0).use { calc ->
6199+
val points = calc.pointFlow(3).toList()
6200+
assertEquals(3, points.size)
6201+
assertEquals(Point(0, 0), points[0])
6202+
assertEquals(Point(1, 2), points[1])
6203+
assertEquals(Point(2, 4), points[2])
6204+
}
6205+
}
6206+
6207+
@Test fun `flow dc - pointFlow single`() = runBlocking {
6208+
Calculator(0).use { calc ->
6209+
val points = calc.pointFlow(1).toList()
6210+
assertEquals(listOf(Point(0, 0)), points)
6211+
}
6212+
}
6213+
6214+
@Test fun `flow dc - pointFlow empty`() = runBlocking {
6215+
Calculator(0).use { calc ->
6216+
assertEquals(emptyList<Point>(), calc.pointFlow(0).toList())
6217+
}
6218+
}
6219+
6220+
@Test fun `flow dc - emptyPointFlow`() = runBlocking {
6221+
Calculator(0).use { calc ->
6222+
assertEquals(emptyList<Point>(), calc.emptyPointFlow().toList())
6223+
}
6224+
}
6225+
6226+
@Test fun `flow dc - singlePointFlow`() = runBlocking {
6227+
Calculator(5).use { calc ->
6228+
val points = calc.singlePointFlow().toList()
6229+
assertEquals(listOf(Point(5, 10)), points)
6230+
}
6231+
}
6232+
6233+
@Test fun `flow dc - singlePointFlow zero`() = runBlocking {
6234+
Calculator(0).use { calc ->
6235+
assertEquals(listOf(Point(0, 0)), calc.singlePointFlow().toList())
6236+
}
6237+
}
6238+
6239+
@Test fun `flow dc - pointFlow large`() = runBlocking {
6240+
Calculator(0).use { calc ->
6241+
val points = calc.pointFlow(100).toList()
6242+
assertEquals(100, points.size)
6243+
assertEquals(Point(99, 198), points.last())
6244+
}
6245+
}
6246+
6247+
@Test fun `flow dc - pointFlow take first`() = runBlocking {
6248+
Calculator(0).use { calc ->
6249+
val first = calc.pointFlow(100).first()
6250+
assertEquals(Point(0, 0), first)
6251+
}
6252+
}
6253+
6254+
@Test fun `flow dc - pointFlow take 3`() = runBlocking {
6255+
Calculator(0).use { calc ->
6256+
val items = calc.pointFlow(100).take(3).toList()
6257+
assertEquals(3, items.size)
6258+
assertEquals(Point(2, 4), items[2])
6259+
}
6260+
}
6261+
6262+
// ── Flow<NamedValue> (String field) ─────────────────────────────────────
6263+
6264+
@Test fun `flow dc - namedValueFlow basic`() = runBlocking {
6265+
Calculator(10).use { calc ->
6266+
calc.label = "hello"
6267+
val items = calc.namedValueFlow().toList()
6268+
assertEquals(2, items.size)
6269+
assertEquals(NamedValue("hello", 10), items[0])
6270+
assertEquals(NamedValue("second", 20), items[1])
6271+
}
6272+
}
6273+
6274+
@Test fun `flow dc - namedValueFlow default label`() = runBlocking {
6275+
Calculator(7).use { calc ->
6276+
val items = calc.namedValueFlow().toList()
6277+
assertEquals("default", items[0].name)
6278+
assertEquals(7, items[0].value)
6279+
}
6280+
}
6281+
6282+
@Test fun `flow dc - namedValueFlow unicode`() = runBlocking {
6283+
Calculator(1).use { calc ->
6284+
calc.label = "Kotlin"
6285+
val items = calc.namedValueFlow().toList()
6286+
assertEquals("Kotlin", items[0].name)
6287+
}
6288+
}
6289+
6290+
// ── Flow<TaggedPoint> (nested DC + enum) ────────────────────────────────
6291+
6292+
@Test fun `flow dc - taggedPointFlow`() = runBlocking {
6293+
Calculator(5).use { calc ->
6294+
val items = calc.taggedPointFlow().toList()
6295+
assertEquals(Operation.entries.size, items.size)
6296+
items.forEachIndexed { i, tp ->
6297+
assertEquals(Point(5, 10), tp.point)
6298+
assertEquals(Operation.entries[i], tp.tag)
6299+
}
6300+
}
6301+
}
6302+
6303+
@Test fun `flow dc - taggedPointFlow zero accumulator`() = runBlocking {
6304+
Calculator(0).use { calc ->
6305+
val items = calc.taggedPointFlow().toList()
6306+
assertEquals(Operation.entries.size, items.size)
6307+
items.forEach { assertEquals(Point(0, 0), it.point) }
6308+
}
6309+
}
6310+
6311+
@Test fun `flow dc - taggedPointFlow negative`() = runBlocking {
6312+
Calculator(-3).use { calc ->
6313+
val items = calc.taggedPointFlow().toList()
6314+
assertEquals(Point(-3, -6), items[0].point)
6315+
}
6316+
}
6317+
6318+
// ── Flow<CalcResult> (common DC with String) ────────────────────────────
6319+
6320+
@Test fun `flow dc - calcResultFlow basic`() = runBlocking {
6321+
Calculator(10).use { calc ->
6322+
val items = calc.calcResultFlow(3).toList()
6323+
assertEquals(3, items.size)
6324+
assertEquals(CalcResult(10, "step_0"), items[0])
6325+
assertEquals(CalcResult(11, "step_1"), items[1])
6326+
assertEquals(CalcResult(12, "step_2"), items[2])
6327+
}
6328+
}
6329+
6330+
@Test fun `flow dc - calcResultFlow empty`() = runBlocking {
6331+
Calculator(0).use { calc ->
6332+
assertEquals(emptyList<CalcResult>(), calc.calcResultFlow(0).toList())
6333+
}
6334+
}
6335+
6336+
@Test fun `flow dc - calcResultFlow single`() = runBlocking {
6337+
Calculator(42).use { calc ->
6338+
val items = calc.calcResultFlow(1).toList()
6339+
assertEquals(listOf(CalcResult(42, "step_0")), items)
6340+
}
6341+
}
6342+
6343+
@Test fun `flow dc - calcResultFlow take`() = runBlocking {
6344+
Calculator(0).use { calc ->
6345+
val first = calc.calcResultFlow(50).first()
6346+
assertEquals(CalcResult(0, "step_0"), first)
6347+
}
6348+
}
6349+
6350+
// ── Flow<Rect> (deeply nested DC) ───────────────────────────────────────
6351+
6352+
@Test fun `flow dc - rectFlow basic`() = runBlocking {
6353+
Calculator(10).use { calc ->
6354+
val items = calc.rectFlow().toList()
6355+
assertEquals(2, items.size)
6356+
assertEquals(Rect(Point(0, 0), Point(10, 10)), items[0])
6357+
assertEquals(Rect(Point(1, 1), Point(11, 11)), items[1])
6358+
}
6359+
}
6360+
6361+
@Test fun `flow dc - rectFlow zero`() = runBlocking {
6362+
Calculator(0).use { calc ->
6363+
val items = calc.rectFlow().toList()
6364+
assertEquals(Rect(Point(0, 0), Point(0, 0)), items[0])
6365+
}
6366+
}
6367+
6368+
@Test fun `flow dc - rectFlow negative`() = runBlocking {
6369+
Calculator(-5).use { calc ->
6370+
val items = calc.rectFlow().toList()
6371+
assertEquals(Rect(Point(0, 0), Point(-5, -5)), items[0])
6372+
assertEquals(Rect(Point(1, 1), Point(-4, -4)), items[1])
6373+
}
6374+
}
6375+
6376+
// ── Flow<DC> error handling ─────────────────────────────────────────────
6377+
6378+
@Test fun `flow dc - failingPointFlow throws after first`() = runBlocking {
6379+
Calculator(0).use { calc ->
6380+
val items = mutableListOf<Point>()
6381+
assertFailsWith<KotlinNativeException> {
6382+
calc.failingPointFlow().collect { items.add(it) }
6383+
}
6384+
assertEquals(listOf(Point(1, 2)), items)
6385+
}
6386+
}
6387+
6388+
// ── Flow<DC> concurrency ────────────────────────────────────────────────
6389+
6390+
@Test fun `flow dc - 5 concurrent pointFlow`() = runBlocking {
6391+
Calculator(1).use { calc ->
6392+
val results = (1..5).map { async(Dispatchers.Default) {
6393+
calc.pointFlow(5).toList()
6394+
} }.awaitAll()
6395+
assertEquals(5, results.size)
6396+
results.forEach { points ->
6397+
assertEquals(5, points.size)
6398+
assertEquals(Point(0, 0), points[0])
6399+
}
6400+
}
6401+
}
6402+
6403+
@Test fun `flow dc - concurrent on separate instances`() = runBlocking {
6404+
val results = (1..10).map { i ->
6405+
async(Dispatchers.Default) {
6406+
Calculator(i).use { calc -> calc.singlePointFlow().toList() }
6407+
}
6408+
}.awaitAll()
6409+
results.forEachIndexed { i, points ->
6410+
assertEquals(1, points.size)
6411+
assertEquals(Point(i + 1, (i + 1) * 2), points[0])
6412+
}
6413+
}
6414+
6415+
// ── Flow<DC> sequential stress ──────────────────────────────────────────
6416+
6417+
@Test fun `flow dc - 50 sequential pointFlows`() = runBlocking {
6418+
Calculator(0).use { calc ->
6419+
repeat(50) {
6420+
val points = calc.pointFlow(3).toList()
6421+
assertEquals(3, points.size)
6422+
}
6423+
}
6424+
}
6425+
6426+
@Test fun `flow dc - pointFlow then sync methods`() = runBlocking {
6427+
Calculator(5).use { calc ->
6428+
val points = calc.pointFlow(3).toList()
6429+
assertEquals(3, points.size)
6430+
assertEquals(5, calc.current)
6431+
calc.add(10)
6432+
assertEquals(15, calc.current)
6433+
}
6434+
}
6435+
6436+
@Test fun `flow dc - pointFlow then suspend`() = runBlocking {
6437+
Calculator(0).use { calc ->
6438+
calc.pointFlow(2).toList()
6439+
val r = calc.delayedAdd(3, 4)
6440+
assertEquals(7, r)
6441+
}
6442+
}
6443+
6444+
@Test fun `flow dc - pointFlow then other flow types`() = runBlocking {
6445+
Calculator(5).use { calc ->
6446+
val points = calc.pointFlow(2).toList()
6447+
assertEquals(2, points.size)
6448+
val ints = calc.countUp(3).toList()
6449+
assertEquals(listOf(1, 2, 3), ints)
6450+
val results = calc.calcResultFlow(2).toList()
6451+
assertEquals(2, results.size)
6452+
}
6453+
}
6454+
6455+
@Test fun `flow dc - mix all dc flow types`() = runBlocking {
6456+
Calculator(3).use { calc ->
6457+
calc.label = "test"
6458+
val points = calc.pointFlow(2).toList()
6459+
val named = calc.namedValueFlow().toList()
6460+
val tagged = calc.taggedPointFlow().toList()
6461+
val results = calc.calcResultFlow(2).toList()
6462+
val rects = calc.rectFlow().toList()
6463+
6464+
assertEquals(2, points.size)
6465+
assertEquals(2, named.size)
6466+
assertEquals(Operation.entries.size, tagged.size)
6467+
assertEquals(2, results.size)
6468+
assertEquals(2, rects.size)
6469+
6470+
assertEquals("test", named[0].name)
6471+
assertEquals(Point(3, 6), tagged[0].point)
6472+
assertEquals(CalcResult(3, "step_0"), results[0])
6473+
}
6474+
}
61906475
}

0 commit comments

Comments
 (0)