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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<!-- markdownlint-disable-file MD025 -->

# 5.2.0

- **BinaryWriter**:
- **New Feature**: Added `skip(int count)` — advances the write position by [count] bytes without writing data, for reserving space (Reserve & Backpatch pattern).
- **New Feature**: Added `shiftBytes(int start, int end, int target)` — shifts a block of written bytes within the buffer, enabling in-place compaction when reserved header space exceeds actual needs.

# 5.1.0

- **BinaryWriterPool**:
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

```yaml
dependencies:
pro_binary: ^5.1.0
pro_binary: ^5.2.0
```

## Quick Start
Expand Down Expand Up @@ -170,7 +170,7 @@ Explore the [example](example/) directory for complete, runnable projects:

| Component | Description |
| --------- | ----------- |
| **BinaryWriter** | Fast encoder for fixed-width, VarInt/ZigZag, and one-pass strings. Features automatic expansion and pooling. |
| **BinaryWriter** | Fast encoder for fixed-width, VarInt/ZigZag, and one-pass strings. Features automatic expansion, pooling, and in-place buffer manipulation (`skip`, `shiftBytes`). |
| **BinaryReader** | Zero-copy decoder with advanced navigation (`seek`, `rewind`, `peek`). Optimized for performance. |
| **StreamBinaryReader** | Handles async data chunks seamlessly with a transactional `bookmark`/`rollback` model for partial data. |
| **BinaryStreamTransformer** | The easiest way to parse a `Stream<List<int>>` into a stream of typed messages or objects. |
Expand All @@ -184,16 +184,16 @@ Run benchmarks to see it in action:

```bash
# Serialization (Writer)
dart run performance/serialization_bench.dart
dart run benchmark_harness:bench --flavor aot --target performance/serialization_bench.dart

# Deserialization (Reader)
dart run performance/deserialization_bench.dart
dart run benchmark_harness:bench --flavor aot --target performance/deserialization_bench.dart

# String encoding (One-pass vs Two-pass vs Standard)
dart run performance/strings_bench.dart
dart run benchmark_harness:bench --flavor aot --target performance/strings_bench.dart

# Object Pooling (GC impact mitigation)
dart run performance/pool_bench.dart
dart run benchmark_harness:bench --flavor aot --target performance/pool_bench.dart
```

## Testing
Expand Down
18 changes: 13 additions & 5 deletions lib/src/binary_reader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ extension type BinaryReader._(_ReaderState _rs) {
var byte = list[offset++];
if ((byte & 0x80) == 0) {
_rs.offset = offset;

return byte;
}

Expand All @@ -121,6 +122,7 @@ extension type BinaryReader._(_ReaderState _rs) {

if ((byte & 0x80) == 0) {
_rs.offset = offset;

return result;
}

Expand All @@ -134,6 +136,7 @@ extension type BinaryReader._(_ReaderState _rs) {

if ((byte & 0x80) == 0) {
_rs.offset = offset;

return result;
}

Expand All @@ -149,6 +152,7 @@ extension type BinaryReader._(_ReaderState _rs) {

if ((byte & 0x80) == 0) {
_rs.offset = offset;

return result;
}

Expand Down Expand Up @@ -417,11 +421,12 @@ extension type BinaryReader._(_ReaderState _rs) {

// Create a view of the underlying buffer without copying
final bOffset = _rs.baseOffset;
final bytes = _rs.data.buffer.asUint8List(bOffset + _rs.offset, length);
final offset = _rs.offset;
final view = _rs.data.buffer.asUint8List(bOffset + offset, length);

_rs.offset += length;

return bytes;
return view;
}

/// Reads all remaining bytes from the current position to the end of the
Expand Down Expand Up @@ -500,7 +505,9 @@ extension type BinaryReader._(_ReaderState _rs) {
_checkBounds(length, 'String');

final bOffset = _rs.baseOffset;
final view = _rs.data.buffer.asUint8List(bOffset + _rs.offset, length);
final offset = _rs.offset;
final view = _rs.data.buffer.asUint8List(bOffset + offset, length);

_rs.offset += length;

return utf8.decode(view, allowMalformed: allowMalformed);
Expand Down Expand Up @@ -640,12 +647,13 @@ extension type BinaryReader._(_ReaderState _rs) {
}

final peekOffset = offset ?? _rs.offset;

_checkBounds(length, 'Peek Bytes', peekOffset);

final bOffset = _rs.baseOffset;
final bytes = _rs.data.buffer.asUint8List(bOffset + peekOffset, length);
final view = _rs.data.buffer.asUint8List(bOffset + peekOffset, length);

return bytes;
return view;
}

/// Returns the byte at the current read position without advancing the
Expand Down
64 changes: 63 additions & 1 deletion lib/src/binary_writer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ extension type BinaryWriter._(_WriterState _ws) {
@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
// Disable lint to allow positional boolean parameter for simplicity
// ignore: avoid_positional_boolean_parameters
void writeBool(bool value) {
writeUint8(value ? 1 : 0);
}
Expand Down Expand Up @@ -843,6 +842,63 @@ extension type BinaryWriter._(_WriterState _ws) {
_ws.offset = position;
}

/// Advances the write position by [count] bytes without writing data.
///
/// The skipped bytes may contain garbage. This is primarily used to reserve
/// space for a header that will be written later
/// (Reserve & Backpatch pattern).
///
/// Throws [RangeError] if [count] is negative.
@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
void skip(int count) {
if (count < 0) {
throw RangeError.value(count, 'count', 'must be non-negative');
}

if (count == 0) {
return;
}

_ws
..ensureSize(count)
..offset += count;
}

/// Shifts a block of written bytes within the buffer.
///
/// Used for the "Reserve & Backpatch" pattern when the reserved header space
/// was larger than actually needed. This allows shifting the payload left to
/// overwrite the unused reserved space, avoiding a new array allocation.
///
/// [start] - The starting index of the block to shift.
/// [end] - The ending index (exclusive) of the block to shift.
/// [target] - The index where the block should be moved to
/// (must be <= start).
///
/// Throws [RangeError] if parameters define an invalid range or would cause
/// data corruption.
@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
void shiftBytes(int start, int end, int target) {
assert(start >= 0, 'start must be non-negative');
assert(end >= start, 'end must be >= start');
assert(end <= _ws.offset, 'end exceeds current bytesWritten');
assert(target >= 0, 'target must be non-negative');
assert(target <= start, 'target must be <= start (can only shift left)');

final length = end - start;
if (length == 0) {
return;
}

_ws.list.setRange(target, target + length, _ws.list, start);

if (end == _ws.offset) {
_ws.offset = target + length;
}
}

/// Returns the byte at the specified [index] without changing the current
/// write position.
///
Expand Down Expand Up @@ -918,6 +974,7 @@ extension type BinaryWriter._(_WriterState _ws) {
list[offset] = 0xEF;
list[offset + 1] = 0xBF;
list[offset + 2] = 0xBD;

return offset + 3;
}

Expand Down Expand Up @@ -991,6 +1048,7 @@ final class _WriterState {
@pragma('dart2js:tryInline')
void ensureSize(int size) {
assert(!_isInPool, 'Cannot ensure size on a pooled writer');

if (offset + size > capacity) {
_expand(size);
}
Expand All @@ -1000,6 +1058,7 @@ final class _WriterState {
@pragma('dart2js:tryInline')
void ensureOneByte() {
assert(!_isInPool, 'Cannot ensure size on a pooled writer');

if (offset + 1 > capacity) {
_expand(1);
}
Expand All @@ -1009,6 +1068,7 @@ final class _WriterState {
@pragma('dart2js:tryInline')
void ensureTwoBytes() {
assert(!_isInPool, 'Cannot ensure size on a pooled writer');

if (offset + 2 > capacity) {
_expand(2);
}
Expand All @@ -1018,6 +1078,7 @@ final class _WriterState {
@pragma('dart2js:tryInline')
void ensureFourBytes() {
assert(!_isInPool, 'Cannot ensure size on a pooled writer');

if (offset + 4 > capacity) {
_expand(4);
}
Expand All @@ -1027,6 +1088,7 @@ final class _WriterState {
@pragma('dart2js:tryInline')
void ensureEightBytes() {
assert(!_isInPool, 'Cannot ensure size on a pooled writer');

if (offset + 8 > capacity) {
_expand(8);
}
Expand Down
Loading
Loading