zerocopy

CI Status License: MIT

A high-performance Flutter/Dart package that entirely eliminates the "Copy Tax" between the Dart VM and the Native (C++) layer.


The "Copy Tax" Problem

Every time you send large data payloads (camera frames, audio buffers, physics simulations, ML tensors) between Dart and Native code via MethodChannel or even standard dart:ffi structs, the runtime serializes and clones the data into a new allocation on the Dart managed heap.

This creates two silent killers in high-performance apps:

Pain Point Root Cause
Latency spikes Serializing megabytes of data takes multiple milliseconds per frame
UI Jank (frame drops) The Dart GC is overwhelmed cleaning up temporary Uint8List clones
Memory bloat Two copies of the same data exist simultaneously during transfer

⚡ The ZeroCopy Solution

zerocopy bypasses all of this entirely by mapping a raw C++ memory address directly into a Dart Uint8List — no serialization, no cloning, zero copy.

┌────────────────────────────────────────────────────────┐
│  Dart Layer                                            │
│  ┌──────────────────────────────────────────────────┐  │
│  │  ZeroCopyBuffer  →  Uint8List  (view)            │  │
│  │                      │                           │  │
│  │               Pointer.asTypedList()              │  │
│  │                      │ ← ZERO COPY              │  │
│  └──────────────────────┼───────────────────────────┘  │
│                         │  dart:ffi @Native            │
├─────────────────────────┼──────────────────────────────┤
│  C++ Layer              │                              │
│  ┌──────────────────────▼───────────────────────────┐  │
│  │  aligned_alloc(64, size)  ← SIMD-aligned          │  │
│  │  std::atomic_flag         ← Atomic Spinlock       │  │
│  └──────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────┘

The four pillars of zerocopy:

  1. 64-byte SIMD-aligned allocationaligned_alloc / posix_memalign / _aligned_malloc ensures the buffer fits perfectly in CPU cache lines, enabling SIMD vectorisation.
  2. Direct pointer bridgingPointer.asTypedList() wraps the raw C++ address as a Uint8List. The Dart VM reads/writes to this view in-place — no copy ever occurs.
  3. Atomic Spinlock — A std::atomic_flag-based spinlock protects the buffer from concurrent native/Dart thread access with zero context switches. Ideal for microsecond-level critical sections.
  4. NativeFinalizer memory safety — The C++ buffer is automatically freed when the Dart object is garbage collected, preventing memory leaks even if you forget to call dispose().

Benchmark: ZeroCopy vs The World (10 MB Payload)

Head-to-head test transferring a 10 MB byte array 100 times in Flutter profile mode:

Method Total Latency (100 runs) Jank Frames (>16 ms) GC Heap Impact
MethodChannel ~4,200 ms 100 / 100 Severe — constant GC pauses
Dart Isolate ~1,800 ms 85 / 100 High
ZeroCopy < 10 ms 0 / 100 None — flat heap

ZeroCopy delivers orders-of-magnitude better throughput while keeping the Dart GC completely idle. Run the bundled example app in profile mode to reproduce these numbers on your own device.


Platform Support

Platform Status Native Toolchain
Android ✅ Supported Android NDK (clang)
iOS ✅ Supported Apple Clang (Xcode)
macOS ✅ Supported Apple Clang (Xcode)
Windows ✅ Supported MSVC / MinGW
Linux ✅ Supported GCC / Clang

All platforms are compiled automatically via the Dart 3 Native Assets (build.dart) pipeline — no manual CMake, CocoaPods, or Gradle configuration required.


Installation

Add zerocopy to your pubspec.yaml:

dependencies:
  zerocopy: ^0.1.0

Then run:

dart pub get
# or for Flutter projects:
flutter pub get

Usage

Basic Read/Write

import 'package:zerocopy/zerocopy.dart';

void main() {
  // 1. Allocate a 1 MB native buffer.
  //    Memory lives in C++ — completely outside the Dart GC heap.
  final buffer = ZeroCopyBuffer(sizeInBytes: 1024 * 1024); // 1 MB

  // 2. Write a single byte. Goes directly to C++ memory — no copy.
  buffer.set(0, 255);
  buffer.set(1, 128);

  // 3. Read a single byte. Also zero-copy.
  print(buffer.get(0)); // 255

  // 4. Dispose. Frees the C++ memory immediately.
  //    Optional — NativeFinalizer will auto-free on GC if you forget.
  buffer.dispose();
}

Bulk Operations via view

For bulk reads and writes, use the view getter which exposes the buffer as a Uint8List — the highest-performance path:

final buffer = ZeroCopyBuffer(sizeInBytes: 4 * 1024 * 1024); // 4 MB

// Bulk write (zero-copy — writes go directly to C++ memory)
buffer.view.setAll(0, myLargeByteArray);

// Bulk read
final snapshot = buffer.view.sublist(0, 1024);

buffer.dispose();

Thread-Safe Access with the Atomic Spinlock

When a native thread and the Dart isolate both need to access the buffer, use lock() / unlock() to coordinate:

final buffer = ZeroCopyBuffer(sizeInBytes: 1024);

// Acquire the C++ atomic spinlock (non-blocking, zero context switch)
buffer.lock();

try {
  buffer.set(0, 42);
  buffer.view.setAll(1, [10, 20, 30]);
} finally {
  // Always release the lock — prefer try/finally to avoid deadlocks
  buffer.unlock();
}

buffer.dispose();

Important: lock() is a spinlock — it actively burns CPU cycles until the lock is free. Use it only for very short critical sections (microseconds). For long-running operations, use Dart Isolate message passing instead.

Real-World: Passing a Camera Frame to Native

Future<void> processFrame(Uint8List rawFrameBytes) async {
  final buffer = ZeroCopyBuffer(sizeInBytes: rawFrameBytes.length);

  // Write the frame into the shared C++ buffer (zero-copy)
  buffer.view.setAll(0, rawFrameBytes);

  // Signal your C++ image-processing pipeline (via a separate FFI call)
  // nativeLib.process_frame(buffer.view.address, buffer.sizeInBytes);

  buffer.dispose();
}

API Reference

ZeroCopyBuffer

The core class. Allocates and manages a SIMD-aligned native memory buffer.

Constructor

ZeroCopyBuffer({required int sizeInBytes})
Parameter Type Description
sizeInBytes int Size of the buffer in bytes. Must be > 0. Throws ArgumentError if invalid, OutOfMemoryError if allocation fails.

Properties

Property Type Description
sizeInBytes int The size this buffer was allocated with.
view Uint8List A zero-copy Uint8List view directly into C++ memory. Use for bulk operations. Throws StateError if the buffer has been disposed.

Methods

Method Returns Description
set(int index, int value) void Writes an 8-bit value at the given index. Zero-copy.
get(int index) int Reads the 8-bit value at the given index. Zero-copy.
lock() void Acquires the C++ atomic spinlock. Blocks (spins) until available.
unlock() void Releases the C++ atomic spinlock.
dispose() void Frees native memory immediately. Safe to call multiple times. After disposal, all access throws StateError.

Architecture Deep Dive

Native Assets Pipeline (Dart 3)

zerocopy uses the Dart 3 Native Assets build hook (hook/build.dart) to compile the C++ core automatically at build time via native_toolchain_c. This means:

  • No manual CMakeLists.txt to maintain.
  • No manual CocoaPods or Podfile entries for iOS/macOS.
  • No Gradle .so file linking for Android.
  • The correct shared library (.so, .dylib, .dll) is built and linked for your exact target platform and architecture automatically.

Compiler Flags

The C++ core is compiled with aggressive optimisation flags:

Flag Purpose
-O3 Maximum compiler optimisation (loop unrolling, inlining, etc.)
-ffast-math Enables IEEE-unsafe floating-point optimisations for speed
-fPIC Position-Independent Code (required for shared libraries)

Memory Layout

Native Heap (C++)                Dart VM Heap
┌─────────────────────┐         ┌──────────────────────────┐
│  aligned_alloc(64)  │◄────────│  Pointer<Uint8>           │
│  [raw bytes...]     │         │  │                        │
│  64-byte boundary   │         │  └─ .asTypedList()        │
│  SIMD-ready         │         │      → Uint8List (view)   │
└─────────────────────┘         │                          │
         ▲                      │  ZeroCopyBuffer object   │
         │                      │  NativeFinalizer ─────── ┼──► free_buffer_address()
         └──── Direct address ──┘                          │
                                └──────────────────────────┘

The Uint8List returned by view holds a raw pointer to the C++ allocation — not a copy. The NativeFinalizer is attached to the ZeroCopyBuffer Dart object and calls free_buffer_address when it is garbage collected, making this pattern fully memory-safe.

Thread Safety Model

Scenario Recommendation
Single Dart isolate, no native threads Use view directly — no locking needed
Dart isolate + one short-lived native thread Use lock() / unlock()
Dart isolate + sustained native thread workload Use Dart Isolate + message passing for coordination

Running the Example & Benchmarks

The example/ directory contains a full Flutter application that benchmarks ZeroCopy against MethodChannel and Isolate with a 10 MB payload.

cd example
flutter run --profile   # Run in profile mode for accurate benchmark numbers

Contributing

Contributions are warmly welcome! Please read the guidelines below before opening a PR.

  1. Fork the repository and create your branch from main.
  2. Ensure all C++ code compiles on all supported platforms by checking the GitHub Actions CI pipeline (verify.yml).
  3. Write tests for any new Dart-layer behaviour in test/zerocopy_test.dart.
  4. Run the formatter before committing: dart format .
  5. Run the analyser before committing: dart analyze
  6. Open a Pull Request with a clear description of what changed and why.

For major changes, please open an issue first to discuss your proposal.

📋 See CONTRIBUTING.md for the full contributor guide.


Issues & Support

Found a bug or have a feature request?


License

This package is released under the MIT License.

MIT License — Copyright (c) 2026 Muhammad Omar

Built with ❤️ for the Flutter community by Muhammad Omar

Libraries

zerocopy
A high-performance Flutter/Dart package that eliminates the "Copy Tax" between the Dart VM and the Native (C++) layer.