Skip to content

DitchOoM/buffer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

511 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Buffer

See the project website for documentation and APIs.

Kotlin Multiplatform byte buffers — same code on JVM, Android, iOS, macOS, Linux, JS, and WASM with zero-copy performance.

Buffer gives you one ReadBuffer/WriteBuffer API that delegates to platform-native types — ByteBuffer on JVM, NSData on Apple, malloc on Linux, Uint8Array on JS — so data never copies between your code and the OS.

Maven Central OpenSSF Scorecard

Why Buffer?

Why not just use ByteArray?

Concern ByteArray Buffer
Platform I/O Copy into ByteBuffer/NSData/Uint8Array every time Zero-copy — delegates to the native type directly
Memory reuse Allocate per request, GC cleans up BufferPool — acquire, use, release, same buffer reused
Fragmented data Manual accumulator + boundary tracking StreamProcessor — peek/read across chunk boundaries
Compression Platform-specific zlib wrappers compress()/decompress() on any ReadBuffer
Streaming transforms Build your own pipeline mapBuffer(), asStringFlow(), lines() — compose with Flow
Cross-platform Only type that's truly portable Same API on JVM, Android, iOS, macOS, Linux, JS, WASM

Installation

plugins {
    id("com.google.devtools.ksp") version "<ksp-version>" // Only needed for codec code generation
}

dependencies {
    // Core buffer library
    implementation("com.ditchoom:buffer:<latest-version>")

    // Optional: Protocol codecs (annotation-driven code generation)
    implementation("com.ditchoom:buffer-codec:<latest-version>")
    ksp("com.ditchoom:buffer-codec-processor:<latest-version>")
    // Optional: codec wire-compat gate — apply the plugin in the plugins {} block:
    //   id("com.ditchoom.buffer.codec-schema") version "<latest-version>"

    // Optional: Compression (gzip, deflate)
    implementation("com.ditchoom:buffer-compression:<latest-version>")

    // Optional: Flow extensions (lines, mapBuffer, asStringFlow)
    implementation("com.ditchoom:buffer-flow:<latest-version>")
}

Find the latest version on Maven Central.

Modules

Module Description
buffer Core ReadBuffer/WriteBuffer interfaces, BufferPool, StreamProcessor
buffer-codec Codec<T> interface (operates directly on ReadBuffer/WriteBuffer), annotations for code generation
buffer-codec-processor KSP processor — generates Codec implementations from @ProtocolMessage annotations
buffer-codec-schema Wire-format schema descriptor model, parser, and drift classifier (shared by the processor and the Gradle plugin)
buffer-codec-gradle-plugin Gradle plugin (com.ditchoom.buffer.codec-schema) — baselines the wire shape and fails the build on breaking drift
buffer-compression compress()/decompress() (gzip, deflate) on ReadBuffer
buffer-flow Kotlin Flow extensions: mapBuffer(), asStringFlow(), lines()
buffer-crypto Hashing (SHA family, HMAC, HKDF, CRC-32), AEAD, signatures, key agreement, secureRandom, SecureBuffer, persistent KeyStore
buffer-kotlinx-io Zero-/single-copy bridges to kotlinx-io RawSource/RawSink/Buffer
buffer-ktor Ktor IO/HTTP interop: channel bridges, BufferCodecConverter, WebSocket masking
buffer-okio Okio interop: Source/Sink/BufferedSource bridges
buffer-sqldelight SQLDelight ColumnAdapter (copy-at-boundary) for storing buffers as BLOBs

Quick Example

// Works identically on JVM, Android, iOS, macOS, Linux, JS, WASM
val buffer = BufferFactory.Default.allocate(1024)
buffer.writeInt(42)
buffer.writeString("Hello!")
buffer.resetForRead()

val number = buffer.readInt()     // 42
val text = buffer.readString(6)   // "Hello!"

Tip: Always allocate through BufferFactory (Default / managed() / shared() / deterministic()). Factories compose with pooling, deterministic cleanup, and custom allocation strategies. For structured binary data, use Protocol Codecs instead of manual read/write sequences.

Protocol Codecs — Stop Hand-Writing Parsers

For structured data, always use buffer-codec instead of manual readInt()/writeInt() sequences. Hand-written encode/decode is error-prone (field order mismatches, type mismatches) and doesn't guarantee round-trip correctness. Define a data class, annotate it, and the codec writes itself:

@ProtocolMessage
data class SensorReading(
    val sensorId: UShort,       // 2 bytes
    val temperature: Int,       // 4 bytes
    @LengthPrefixed val label: String,  // 2-byte length prefix + UTF-8
)

The KSP processor generates a full Codec<SensorReading> — encode, decode, and wireSize — at compile time:

val buffer = BufferFactory.Default.allocate(64)
SensorReadingCodec.encode(buffer, reading, EncodeContext.Empty)
buffer.resetForRead()

val decoded = SensorReadingCodec.decode(buffer, DecodeContext.Empty)

Annotations cover common binary protocol patterns:

  • @LengthPrefixed(prefix) — length-prefixed strings (1, 2, or 4 byte prefix)
  • @RemainingBytes — consume all remaining bytes
  • @LengthFrom("field") — string length from a preceding numeric field
  • @WireBytes(n) — custom wire width for numeric fields (e.g., 3-byte integers)
  • @WireOrder(order) — per-field byte order override (big/little endian)
  • @When("expr") — conditional nullable fields
  • @UseCodec(codec) — delegate to an existing Codec object
  • @Payload — generic payload type parameter
  • @PacketType(value, wire) on sealed interface variants — auto-dispatched decode; wire for spec-compliant encode values
  • @DispatchOn(Discriminator::class) + @DispatchValue — custom multi-byte or bit-packed discriminator dispatch
  • @FramedBy(codec, after) — framework-owned length framing computed from and bounding a sealed message's body
  • @ForwardCompatible(unknown) + @UnknownVariant — skip and preserve unrecognized sealed variants byte-identically (forward-compatible relay/persistence)
  • Value classes wrapping primitives — zero-overhead typed wrappers (e.g., value class PacketId(val raw: UShort))

Generated codecs also provide peekFrameSize(stream) for zero-boilerplate stream framing and CodecContext for typed runtime configuration through codec chains.

Generated codecs implement the same Codec<T> interface used for manual codecs, so all patterns (streaming, round-trip testing, composition) work unchanged.

Note: Generated code appears after compilation (./gradlew build). IDE features like autocomplete and navigation for generated codecs require an initial build.

See the full protocol codecs guide for details.

Guard against wire-breaking changes (experimental)

Because codecs encode positionally, edits like reordering an enum, inserting a field, or changing a wire width silently break peers already on the wire — and round-trip tests can't catch them (the same new code encodes and decodes). The com.ditchoom.buffer.codec-schema Gradle plugin baselines your protocol's wire shape into a committed, diffable file and fails the build when a change is wire-breaking:

// settings.gradle.kts — the plugin is on Maven Central, not the Gradle Plugin Portal
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}
// build.gradle.kts
plugins {
    id("com.ditchoom.buffer.codec-schema") version "<latest-version>"
}

codecSchema {
    failOnBreaking.set(true) // fail on breaking drift (default: warn only)
}

The first check writes src/codecSchema/codec-schema.txt — commit it. After an intentional wire change, run ./gradlew updateCodecSchema and commit the diff. See the plugin README for the full drift-classification table and workflow.

Buffer Pooling

Allocate once, reuse for every request — no GC pressure:

withPool(defaultBufferSize = 8192) { pool ->
    repeat(10_000) {
        pool.withBuffer(1024) { buffer ->
            buffer.writeInt(requestId)
            buffer.writeString(payload)
            buffer.resetForRead()
            sendToNetwork(buffer)
        } // buffer returned to pool, not GC'd
    }
} // pool cleared

Without pooling: 10,000 requests = 10,000 allocations. With pooling: 10,000 requests = ~64 allocations (maxPoolSize).

Stream Processing

Parse protocols that span chunk boundaries — no manual accumulator code:

val processor = StreamProcessor.create(pool)

// Chunks arrive from the network
processor.append(chunk1)  // partial message
processor.append(chunk2)  // rest of message + start of next

// Parse length-prefixed messages across boundaries
while (processor.available() >= 4) {
    val length = processor.peekInt()
    if (processor.available() < 4 + length) break  // wait for more data
    processor.skip(4)
    // readBufferScoped auto-releases the buffer back to the pool
    val message = processor.readBufferScoped(length) {
        MyMessage(readInt(), readString(remaining()))
    }
    handleMessage(message)
}

readBufferScoped vs readBuffer: Prefer readBufferScoped — it passes the buffer to your block as a receiver and automatically releases it back to the pool. readBuffer returns an unmanaged buffer that is not returned to the pool, which can cause pool exhaustion under load.

Compression

Compress and decompress any ReadBuffer — works on all platforms:

val data = "Hello, World!".toReadBuffer()
val compressed = compress(data, CompressionAlgorithm.Gzip).getOrThrow()
val decompressed = decompress(compressed, CompressionAlgorithm.Gzip).getOrThrow()

Flow Extensions

Compose streaming transforms with Kotlin Flow:

// Transform a flow of raw buffers into processed lines
bufferFlow
    .mapBuffer { decompress(it, Gzip).getOrThrow() }
    .asStringFlow()
    .lines()
    .collect { line -> process(line) }

Deterministic Memory

For explicit memory management without relying on GC:

BufferFactory.deterministic().allocate(8192).use { buffer ->
    buffer.writeInt(42)
    buffer.writeString("Hello")
    buffer.resetForRead()
    // Use buffer...
} // Memory freed immediately

Platform Implementations

Platform Native Type Notes
JVM java.nio.ByteBuffer Direct buffers for zero-copy NIO
Android ByteBuffer + SharedMemory IPC via Parcelable
iOS/macOS NSMutableData Foundation integration
JavaScript Uint8Array SharedArrayBuffer support
WASM LinearBuffer / ByteArrayBuffer Zero-copy JS interop
Linux NativeBuffer (malloc/free) Zero-copy io_uring I/O

Part of the DitchOoM Stack

Buffer is the foundation for the Socket library — cross-platform TCP + TLS that streams these same ReadBuffer/WriteBuffer types.

┌─────────────────────────────┐
│  Your Protocol              │
├─────────────────────────────┤
│  buffer-codec               │  ← com.ditchoom:buffer-codec (+ KSP processor)
├─────────────────────────────┤
│  socket (TCP + TLS)         │  ← com.ditchoom:socket
├─────────────────────────────┤
│  buffer-compression         │  ← com.ditchoom:buffer-compression
├─────────────────────────────┤
│  buffer-flow                │  ← com.ditchoom:buffer-flow
├─────────────────────────────┤
│  buffer                     │  ← com.ditchoom:buffer
└─────────────────────────────┘

License

Copyright 2022 DitchOoM

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

Kotlin Multiplatform bytebuffer/byte[] wrapper

Resources

License

Stars

83 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors