diff --git a/pkgs/ffi/CHANGELOG.md b/pkgs/ffi/CHANGELOG.md index 8d404e9b4..b292e448e 100644 --- a/pkgs/ffi/CHANGELOG.md +++ b/pkgs/ffi/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/pkgs/ffi/lib/ffi.dart b/pkgs/ffi/lib/ffi.dart index 661a27b56..774ee9e86 100644 --- a/pkgs/ffi/lib/ffi.dart +++ b/pkgs/ffi/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/pkgs/ffi/lib/src/arena.dart b/pkgs/ffi/lib/src/arena.dart new file mode 100644 index 000000000..1fdd1d2a7 --- /dev/null +++ b/pkgs/ffi/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> _managedMemoryPointers = []; + + /// Callbacks for releasing native resources under management by this [Arena]. + final List _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 allocate(int byteCount, {int? alignment}) { + _ensureInUse(); + final p = _wrappedAllocator.allocate(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 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 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 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 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? 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/pkgs/ffi/pubspec.yaml b/pkgs/ffi/pubspec.yaml index d2daa1b38..1050caf1c 100644 --- a/pkgs/ffi/pubspec.yaml +++ b/pkgs/ffi/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/pkgs/ffi/test/arena_test.dart b/pkgs/ffi/test/arena_test.dart new file mode 100644 index 000000000..3af0bb5f5 --- /dev/null +++ b/pkgs/ffi/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 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 freed = []; + void freeInt(int i) { + freed.add(i); + } + + Future 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 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(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(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 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 freed = []; + void freeInt(int i) { + freed.add(i); + } + + Future 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 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(), (Pointer discard) { + arena(); + }); + + 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 allocate(int byteCount, {int? alignment}) { + allocationCount++; + return wrappedAllocator.allocate(byteCount, alignment: alignment); + } + + @override + void free(Pointer pointer) { + freeCount++; + return wrappedAllocator.free(pointer); + } +}