
Bit-packed, zero-allocation serializer for tiny dense wire payloads, offering forward-only reader/writer API, inline typed results, lazy field reads, and in-place frame mutation.
A bit-packed, zero-allocation serialization framework for Kotlin Multiplatform. Built for tiny, dense wire payloads (think BLE characteristics) that still need to be safely decoded on the hot path — no boxing, no exception throwing, no intermediate copies.
import ch.trancee.kompact.runtime.ScalarType
// Write 16 bits: 4 bits battery + 10 bits speed + 1 bit flag + 1 bit reserved
val w = KompactWriter()
w.writeScalar(ScalarType.of(4, signed = false), 5L) // battery = 5
w.writeScalar(ScalarType.of(10, signed = false), 10L) // speed = 10
w.writeBool(true) // malfunction = true
val bytes: ByteArray = w.build() // 2 bytes: 0xA5 0x40
// Read them back as typed results — no exceptions on the success path
val battery: Int = KompactRuntime.readScalar(bytes, 0, ScalarType.of(4, signed = false)).getOrThrow()
val speed: Int = KompactRuntime.readScalar(bytes, 4, ScalarType.of(10, signed = false)).getOrThrow()
val flag: Boolean = KompactRuntime.readBool (bytes, 14 ).getOrThrow()
Kompact's ByteArray is the data structure. The value
class @KompactModel value class VehicleTelemetry(val raw: ByteArray) stores
the wire bytes directly. Field getters call the checked
readScalar / readBool accessors on that same buffer
(the unchecked readBits primitives are available for trusted
in-memory frames — see architecture.md). There is no step that
turns bytes into a separate object, because that step allocates.
This matters because BLE characteristics are tiny (a few bytes) and arrive frequently. The decoder runs on battery-powered devices. Every heap allocation costs power and stalls the radio. Traditional frameworks pay that cost twice: once on decode (allocate a data class, box every field) and once on encode (build an object tree, then walk it).
Kompact avoids both by reading a primitive directly from the buffer with
zero heap activity. The result value classes (IntResult, LongResult,
BooleanResult, FloatResult, DoubleResult, and the rest) are
@JvmInline / value class wrappers over a single packed Long. On
success they cost exactly a Long on the stack — no object header, no
GC pressure.
Reads are also lazy. You pull only the fields you need. A 2-byte
telemetry frame with a 4-bit battery status and a 10-bit speed lets you
read speed without ever decoding batteryStatus. A
deserialize(bytes) → FullObject forces you to parse everything first.
This design also cannot be FlatBuffers-style random access. FlatBuffers stores offset pointers so you can jump to any field. That breaks when a field before it changes size. Kompact's v1 type set includes variable-length strings, blobs, nested composites, and repeats. So offsets would shift on every schema change. Reads are sequential, parse-forward instead — the deliberate but necessary tradeoff: you trade random-access field jumps for zero-allocation, lazy, sequential reads.
Construct a frame from field values with VehicleTelemetry.create(...):
val tel = VehicleTelemetry.create(batteryStatus = 5, speed = 10, isMalfunctioning = true)
// tel.raw is the 2-byte wire buffer: [0xA5, 0x40]Modify a field in-place — the setter writes directly to the backing ByteArray:
tel.speed = 30
// tel.raw is now updated; no new allocationSend the buffer over BLE:
bleCharacteristic.value = tel.rawReceive a frame from BLE and decode it:
val tel = VehicleTelemetry(bleCharacteristic.value)
val speed = tel.speed // 30
val flag = tel.isMalfunctioning // trueSetters work because the ByteArray is a mutable reference shared by the
value class. You read one field, modify one field, and transmit the same
buffer — no intermediate objects, no copy.
Quick reference — all four operations in one snippet:
// 1. One-liner create → raw bytes
val raw: ByteArray = VehicleTelemetry.create(batteryStatus = 5, speed = 10, isMalfunctioning = true).raw
// 2. Construct from raw bytes (e.g. received from BLE)
val tel = VehicleTelemetry(raw)
// 3. Overwrite a field in-place (writes directly to the backing buffer)
tel.speed = 30
// 4. Get the raw bytes again — no copy
bleCharacteristic.value = tel.raw:kompact — the KMP runtime: bit primitives, a forward-only writer, framing
helpers, and seven zero-alloc typed result value classes (ByteResult,
ShortResult, IntResult, LongResult, FloatResult, DoubleResult,
BooleanResult).:kompact-ksp — the KSP annotation processor (@KompactModel /
@KompactField) that generates value-class view bodies from
compile-time-validated field layouts. Apply it with ksp in a consumer
build to generate @KompactModel view classes.jvm (JVM 21), iosArm64, iosSimulatorArm64. Android consumes
the jvm artifact.| If you want to … | Read |
|---|---|
| Try it end-to-end (write a frame, read it back) | docs/getting-started.md |
| Look up an exact API signature, parameter, or error | docs/api-reference.md |
| Understand the design choices (LSB-first, zero-alloc, value classes, framing) | docs/architecture.md |
| Run / understand the CI gates and goldens | docs/ci.md |
| See all of the above at a glance | docs/README.md |
| Define your own message (with code snippets for common cases) | docs/how-to/define-message.md |
| Pack / parse strings, blobs, nested composites, or repeated fields | docs/how-to/long-form-payloads.md |
Handle a KompactDecodeError without throwing on the hot path |
docs/how-to/handle-decode-errors.md |
| Send a frame over BLE / receive one back | docs/how-to/integrate-ble.md |
| Consume Kompact from a separate Kotlin / KMP project | docs/how-to/consume-from-another-project.md |
| All how-to guides (task-oriented recipes) | docs/how-to/README.md |
| Read the original product brief | PROMPT.md |
0.1.0-SNAPSHOT — the runtime, writer, framing, and result value classes are
stable and exercised by the commonTest suite. Publication is wired via
standard maven-publish + signing + Dokka with a custom Central Portal
Publisher API task (centralPortalDeploy, staging to USER_MANAGED). Maven
coordinates ch.trancee.kompact:kompact, license Unlicense (public domain),
but no
release has been cut to Maven Central yet — the Portal namespace, PGP key,
and user token still require user authorization. Build from source or
./gradlew :kompact:publishToMavenLocal and consume the local snapshot.
This is free and unencumbered software released into the public domain.
See LICENSE for the full license text.
A bit-packed, zero-allocation serialization framework for Kotlin Multiplatform. Built for tiny, dense wire payloads (think BLE characteristics) that still need to be safely decoded on the hot path — no boxing, no exception throwing, no intermediate copies.
import ch.trancee.kompact.runtime.ScalarType
// Write 16 bits: 4 bits battery + 10 bits speed + 1 bit flag + 1 bit reserved
val w = KompactWriter()
w.writeScalar(ScalarType.of(4, signed = false), 5L) // battery = 5
w.writeScalar(ScalarType.of(10, signed = false), 10L) // speed = 10
w.writeBool(true) // malfunction = true
val bytes: ByteArray = w.build() // 2 bytes: 0xA5 0x40
// Read them back as typed results — no exceptions on the success path
val battery: Int = KompactRuntime.readScalar(bytes, 0, ScalarType.of(4, signed = false)).getOrThrow()
val speed: Int = KompactRuntime.readScalar(bytes, 4, ScalarType.of(10, signed = false)).getOrThrow()
val flag: Boolean = KompactRuntime.readBool (bytes, 14 ).getOrThrow()
Kompact's ByteArray is the data structure. The value
class @KompactModel value class VehicleTelemetry(val raw: ByteArray) stores
the wire bytes directly. Field getters call the checked
readScalar / readBool accessors on that same buffer
(the unchecked readBits primitives are available for trusted
in-memory frames — see architecture.md). There is no step that
turns bytes into a separate object, because that step allocates.
This matters because BLE characteristics are tiny (a few bytes) and arrive frequently. The decoder runs on battery-powered devices. Every heap allocation costs power and stalls the radio. Traditional frameworks pay that cost twice: once on decode (allocate a data class, box every field) and once on encode (build an object tree, then walk it).
Kompact avoids both by reading a primitive directly from the buffer with
zero heap activity. The result value classes (IntResult, LongResult,
BooleanResult, FloatResult, DoubleResult, and the rest) are
@JvmInline / value class wrappers over a single packed Long. On
success they cost exactly a Long on the stack — no object header, no
GC pressure.
Reads are also lazy. You pull only the fields you need. A 2-byte
telemetry frame with a 4-bit battery status and a 10-bit speed lets you
read speed without ever decoding batteryStatus. A
deserialize(bytes) → FullObject forces you to parse everything first.
This design also cannot be FlatBuffers-style random access. FlatBuffers stores offset pointers so you can jump to any field. That breaks when a field before it changes size. Kompact's v1 type set includes variable-length strings, blobs, nested composites, and repeats. So offsets would shift on every schema change. Reads are sequential, parse-forward instead — the deliberate but necessary tradeoff: you trade random-access field jumps for zero-allocation, lazy, sequential reads.
Construct a frame from field values with VehicleTelemetry.create(...):
val tel = VehicleTelemetry.create(batteryStatus = 5, speed = 10, isMalfunctioning = true)
// tel.raw is the 2-byte wire buffer: [0xA5, 0x40]Modify a field in-place — the setter writes directly to the backing ByteArray:
tel.speed = 30
// tel.raw is now updated; no new allocationSend the buffer over BLE:
bleCharacteristic.value = tel.rawReceive a frame from BLE and decode it:
val tel = VehicleTelemetry(bleCharacteristic.value)
val speed = tel.speed // 30
val flag = tel.isMalfunctioning // trueSetters work because the ByteArray is a mutable reference shared by the
value class. You read one field, modify one field, and transmit the same
buffer — no intermediate objects, no copy.
Quick reference — all four operations in one snippet:
// 1. One-liner create → raw bytes
val raw: ByteArray = VehicleTelemetry.create(batteryStatus = 5, speed = 10, isMalfunctioning = true).raw
// 2. Construct from raw bytes (e.g. received from BLE)
val tel = VehicleTelemetry(raw)
// 3. Overwrite a field in-place (writes directly to the backing buffer)
tel.speed = 30
// 4. Get the raw bytes again — no copy
bleCharacteristic.value = tel.raw:kompact — the KMP runtime: bit primitives, a forward-only writer, framing
helpers, and seven zero-alloc typed result value classes (ByteResult,
ShortResult, IntResult, LongResult, FloatResult, DoubleResult,
BooleanResult).:kompact-ksp — the KSP annotation processor (@KompactModel /
@KompactField) that generates value-class view bodies from
compile-time-validated field layouts. Apply it with ksp in a consumer
build to generate @KompactModel view classes.jvm (JVM 21), iosArm64, iosSimulatorArm64. Android consumes
the jvm artifact.| If you want to … | Read |
|---|---|
| Try it end-to-end (write a frame, read it back) | docs/getting-started.md |
| Look up an exact API signature, parameter, or error | docs/api-reference.md |
| Understand the design choices (LSB-first, zero-alloc, value classes, framing) | docs/architecture.md |
| Run / understand the CI gates and goldens | docs/ci.md |
| See all of the above at a glance | docs/README.md |
| Define your own message (with code snippets for common cases) | docs/how-to/define-message.md |
| Pack / parse strings, blobs, nested composites, or repeated fields | docs/how-to/long-form-payloads.md |
Handle a KompactDecodeError without throwing on the hot path |
docs/how-to/handle-decode-errors.md |
| Send a frame over BLE / receive one back | docs/how-to/integrate-ble.md |
| Consume Kompact from a separate Kotlin / KMP project | docs/how-to/consume-from-another-project.md |
| All how-to guides (task-oriented recipes) | docs/how-to/README.md |
| Read the original product brief | PROMPT.md |
0.1.0-SNAPSHOT — the runtime, writer, framing, and result value classes are
stable and exercised by the commonTest suite. Publication is wired via
standard maven-publish + signing + Dokka with a custom Central Portal
Publisher API task (centralPortalDeploy, staging to USER_MANAGED). Maven
coordinates ch.trancee.kompact:kompact, license Unlicense (public domain),
but no
release has been cut to Maven Central yet — the Portal namespace, PGP key,
and user token still require user authorization. Build from source or
./gradlew :kompact:publishToMavenLocal and consume the local snapshot.
This is free and unencumbered software released into the public domain.
See LICENSE for the full license text.