Add Arena Allocator (#103)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d404e9..b292e44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 1.1.0
+
+Adds the `arena` allocator.
+
 ## 1.0.0
 
 Bumping the version of this package to `1.0.0`.
diff --git a/lib/ffi.dart b/lib/ffi.dart
index 661a27b..774ee9e 100644
--- a/lib/ffi.dart
+++ b/lib/ffi.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+export 'src/allocation.dart' show calloc, malloc;
+export 'src/arena.dart';
 export 'src/utf8.dart';
 export 'src/utf16.dart';
-export 'src/allocation.dart' show calloc, malloc;
diff --git a/lib/src/arena.dart b/lib/src/arena.dart
new file mode 100644
index 0000000..1fdd1d2
--- /dev/null
+++ b/lib/src/arena.dart
@@ -0,0 +1,183 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+//
+// Explicit arena used for managing resources.
+
+import 'dart:async';
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart';
+
+/// An [Allocator] which frees all allocations at the same time.
+///
+/// The arena allows you to allocate heap memory, but ignores calls to [free].
+/// Instead you call [releaseAll] to release all the allocations at the same
+/// time.
+///
+/// Also allows other resources to be associated with the arena, through the
+/// [using] method, to have a release function called for them when the arena
+/// is released.
+///
+/// An [Allocator] can be provided to do the actual allocation and freeing.
+/// Defaults to using [calloc].
+class Arena implements Allocator {
+  /// The [Allocator] used for allocation and freeing.
+  final Allocator _wrappedAllocator;
+
+  /// Native memory under management by this [Arena].
+  final List<Pointer<NativeType>> _managedMemoryPointers = [];
+
+  /// Callbacks for releasing native resources under management by this [Arena].
+  final List<void Function()> _managedResourceReleaseCallbacks = [];
+
+  bool _inUse = true;
+
+  /// Creates a arena of allocations.
+  ///
+  /// The [allocator] is used to do the actual allocation and freeing of
+  /// memory. It defaults to using [calloc].
+  Arena([Allocator allocator = calloc]) : _wrappedAllocator = allocator;
+
+  /// Allocates memory and includes it in the arena.
+  ///
+  /// Uses the allocator provided to the [Arena] constructor to do the
+  /// allocation.
+  ///
+  /// Throws an [ArgumentError] if the number of bytes or alignment cannot be
+  /// satisfied.
+  @override
+  Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
+    _ensureInUse();
+    final p = _wrappedAllocator.allocate<T>(byteCount, alignment: alignment);
+    _managedMemoryPointers.add(p);
+    return p;
+  }
+
+  /// Registers [resource] in this arena.
+  ///
+  /// Executes [releaseCallback] on [releaseAll].
+  ///
+  /// Returns [resource] again, to allow for easily inserting
+  /// `arena.using(resource, ...)` where the resource is allocated.
+  T using<T>(T resource, void Function(T) releaseCallback) {
+    _ensureInUse();
+    releaseCallback = Zone.current.bindUnaryCallback(releaseCallback);
+    _managedResourceReleaseCallbacks.add(() => releaseCallback(resource));
+    return resource;
+  }
+
+  /// Registers [releaseResourceCallback] to be executed on [releaseAll].
+  void onReleaseAll(void Function() releaseResourceCallback) {
+    _managedResourceReleaseCallbacks.add(releaseResourceCallback);
+  }
+
+  /// Releases all resources that this [Arena] manages.
+  ///
+  /// If [reuse] is `true`, the arena can be used again after resources
+  /// have been released. If not, the default, then the [allocate]
+  /// and [using] methods must not be called after a call to `releaseAll`.
+  ///
+  /// If any of the callbacks throw, [releaseAll] is interrupted, and should
+  /// be started again.
+  void releaseAll({bool reuse = false}) {
+    if (!reuse) {
+      _inUse = false;
+    }
+    // The code below is deliberately wirtten to allow allocations to happen
+    // during `releaseAll(reuse:true)`. The arena will still be guaranteed
+    // empty when the `releaseAll` call returns.
+    while (_managedResourceReleaseCallbacks.isNotEmpty) {
+      _managedResourceReleaseCallbacks.removeLast()();
+    }
+    for (final p in _managedMemoryPointers) {
+      _wrappedAllocator.free(p);
+    }
+    _managedMemoryPointers.clear();
+  }
+
+  /// Does nothing, invoke [releaseAll] instead.
+  @override
+  void free(Pointer<NativeType> pointer) {}
+
+  void _ensureInUse() {
+    if (!_inUse) {
+      throw StateError(
+          'Arena no longer in use, `releaseAll(reuse: false)` was called.');
+    }
+  }
+}
+
+/// Runs [computation] with a new [Arena], and releases all allocations at the
+/// end.
+///
+/// If the return value of [computation] is a [Future], all allocations are
+/// released when the future completes.
+///
+/// If the isolate is shut down, through `Isolate.kill()`, resources are _not_
+/// cleaned up.
+R using<R>(R Function(Arena) computation,
+    [Allocator wrappedAllocator = calloc]) {
+  final arena = Arena(wrappedAllocator);
+  bool isAsync = false;
+  try {
+    final result = computation(arena);
+    if (result is Future) {
+      isAsync = true;
+      return (result.whenComplete(arena.releaseAll) as R);
+    }
+    return result;
+  } finally {
+    if (!isAsync) {
+      arena.releaseAll();
+    }
+  }
+}
+
+/// Creates a zoned [Arena] to manage native resources.
+///
+/// The arena is availabe through [zoneArena].
+///
+/// If the isolate is shut down, through `Isolate.kill()`, resources are _not_
+/// cleaned up.
+R withZoneArena<R>(R Function() computation,
+    [Allocator wrappedAllocator = calloc]) {
+  final arena = Arena(wrappedAllocator);
+  var arenaHolder = [arena];
+  bool isAsync = false;
+  try {
+    return runZoned(() {
+      final result = computation();
+      if (result is Future) {
+        isAsync = true;
+        result.whenComplete(arena.releaseAll);
+      }
+      return result;
+    }, zoneValues: {#_arena: arenaHolder});
+  } finally {
+    if (!isAsync) {
+      arena.releaseAll();
+      arenaHolder.clear();
+    }
+  }
+}
+
+/// A zone-specific [Arena].
+///
+/// Asynchronous computations can share a [Arena]. Use [withZoneArena] to create
+/// a new zone with a fresh [Arena], and that arena will then be released
+/// automatically when the function passed to [withZoneArena] completes.
+/// All code inside that zone can use `zoneArena` to access the arena.
+///
+/// The current arena must not be accessed by code which is not running inside
+/// a zone created by [withZoneArena].
+Arena get zoneArena {
+  final List<Arena>? arenaHolder = Zone.current[#_arena];
+  if (arenaHolder == null) {
+    throw StateError('Not inside a zone created by `useArena`');
+  }
+  if (arenaHolder.isNotEmpty) {
+    return arenaHolder.single;
+  }
+  throw StateError('Arena has already been cleared with releaseAll.');
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index d2daa1b..1050caf 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: ffi
-version: 1.0.0
+version: 1.1.0
 homepage: https://github.com/dart-lang/ffi
 description: Utilities for working with Foreign Function Interface (FFI) code.
 
diff --git a/test/arena_test.dart b/test/arena_test.dart
new file mode 100644
index 0000000..3af0bb5
--- /dev/null
+++ b/test/arena_test.dart
@@ -0,0 +1,204 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:ffi';
+
+import 'package:ffi/ffi.dart';
+import 'package:test/test.dart';
+
+void main() async {
+  test('sync', () async {
+    List<int> freed = [];
+    void freeInt(int i) {
+      freed.add(i);
+    }
+
+    using((Arena arena) {
+      arena.using(1234, freeInt);
+      expect(freed.isEmpty, true);
+    });
+    expect(freed, [1234]);
+  });
+
+  test('async', () async {
+    /// Calling [using] waits with releasing its resources until after
+    /// [Future]s complete.
+    List<int> freed = [];
+    void freeInt(int i) {
+      freed.add(i);
+    }
+
+    Future<int> myFutureInt = using((Arena arena) {
+      return Future.microtask(() {
+        arena.using(1234, freeInt);
+        return 1;
+      });
+    });
+
+    expect(freed.isEmpty, true);
+    await myFutureInt;
+    expect(freed, [1234]);
+  });
+
+  test('throw', () {
+    /// [using] waits with releasing its resources until after [Future]s
+    /// complete.
+    List<int> freed = [];
+    void freeInt(int i) {
+      freed.add(i);
+    }
+
+    // Resources are freed also when abnormal control flow occurs.
+    var didThrow = false;
+    try {
+      using((Arena arena) {
+        arena.using(1234, freeInt);
+        expect(freed.isEmpty, true);
+        throw Exception('Exception 1');
+      });
+    } on Exception {
+      expect(freed.single, 1234);
+      didThrow = true;
+    }
+    expect(didThrow, true);
+  });
+
+  test(
+    'allocate',
+    () {
+      final countingAllocator = CountingAllocator();
+      // To ensure resources are freed, wrap them in a [using] call.
+      using((Arena arena) {
+        final p = arena<Int64>(2);
+        p[1] = p[0];
+      }, countingAllocator);
+      expect(countingAllocator.freeCount, 1);
+    },
+  );
+
+  test('allocate throw', () {
+    // Resources are freed also when abnormal control flow occurs.
+    bool didThrow = false;
+    final countingAllocator = CountingAllocator();
+    try {
+      using((Arena arena) {
+        final p = arena<Int64>(2);
+        p[0] = 25;
+        throw Exception('Exception 2');
+      }, countingAllocator);
+    } on Exception {
+      expect(countingAllocator.freeCount, 1);
+      didThrow = true;
+    }
+    expect(didThrow, true);
+  });
+
+  test('toNativeUtf8', () {
+    final countingAllocator = CountingAllocator();
+    using((Arena arena) {
+      final p = 'Hello world!'.toNativeUtf8(allocator: arena);
+      expect(p.toDartString(), 'Hello world!');
+    }, countingAllocator);
+    expect(countingAllocator.freeCount, 1);
+  });
+
+  test('zone', () async {
+    List<int> freed = [];
+    void freeInt(int i) {
+      freed.add(i);
+    }
+
+    withZoneArena(() {
+      zoneArena.using(1234, freeInt);
+      expect(freed.isEmpty, true);
+    });
+    expect(freed.length, 1);
+    expect(freed.single, 1234);
+  });
+
+  test('zone async', () async {
+    /// [using] waits with releasing its resources until after [Future]s
+    /// complete.
+    List<int> freed = [];
+    void freeInt(int i) {
+      freed.add(i);
+    }
+
+    Future<int> myFutureInt = withZoneArena(() {
+      return Future.microtask(() {
+        zoneArena.using(1234, freeInt);
+        return 1;
+      });
+    });
+
+    expect(freed.isEmpty, true);
+    await myFutureInt;
+    expect(freed.length, 1);
+    expect(freed.single, 1234);
+  });
+
+  test('zone throw', () {
+    /// [using] waits with releasing its resources until after [Future]s
+    /// complete.
+    List<int> freed = [];
+    void freeInt(int i) {
+      freed.add(i);
+    }
+
+    // Resources are freed also when abnormal control flow occurs.
+    bool didThrow = false;
+    try {
+      withZoneArena(() {
+        zoneArena.using(1234, freeInt);
+        expect(freed.isEmpty, true);
+        throw Exception('Exception 3');
+      });
+    } on Exception {
+      expect(freed.single, 1234);
+      didThrow = true;
+    }
+    expect(didThrow, true);
+    expect(freed.single, 1234);
+  });
+
+  test('allocate during releaseAll', () {
+    final countingAllocator = CountingAllocator();
+    final arena = Arena(countingAllocator);
+
+    arena.using(arena<Uint8>(), (Pointer discard) {
+      arena<Uint8>();
+    });
+
+    expect(countingAllocator.allocationCount, 1);
+    expect(countingAllocator.freeCount, 0);
+
+    arena.releaseAll(reuse: true);
+
+    expect(countingAllocator.allocationCount, 2);
+    expect(countingAllocator.freeCount, 2);
+  });
+}
+
+/// Keeps track of the number of allocates and frees for testing purposes.
+class CountingAllocator implements Allocator {
+  final Allocator wrappedAllocator;
+
+  int allocationCount = 0;
+  int freeCount = 0;
+
+  CountingAllocator([this.wrappedAllocator = calloc]);
+
+  @override
+  Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
+    allocationCount++;
+    return wrappedAllocator.allocate(byteCount, alignment: alignment);
+  }
+
+  @override
+  void free(Pointer<NativeType> pointer) {
+    freeCount++;
+    return wrappedAllocator.free(pointer);
+  }
+}