[samples/ffi] Native resource lifetime management

Samples for managing native memory without finalizers.

Design: go/dart-ffi-resource-lifetime

Related issue: https://github.com/dart-lang/sdk/issues/35770

Change-Id: I2d0ac1acb65a78db9f57aea3dd5f25b4948ef6d6
Cq-Include-Trybots: luci.dart.try:vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64-try,app-kernel-linux-debug-x64-try,vm-kernel-linux-debug-ia32-try,vm-kernel-win-debug-x64-try,vm-kernel-win-debug-ia32-try,vm-kernel-precomp-linux-debug-x64-try,vm-dartkb-linux-release-x64-abi-try,vm-kernel-precomp-android-release-arm64-try,vm-kernel-asan-linux-release-x64-try,vm-kernel-linux-release-simarm-try,vm-kernel-linux-release-simarm64-try,vm-kernel-precomp-android-release-arm_x64-try,vm-kernel-precomp-obfuscate-linux-release-x64-try,vm-kernel-precomp-mac-release-simarm_x64-try,dart-sdk-linux-try,analyzer-analysis-server-linux-try,analyzer-linux-release-try,front-end-linux-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/123662
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Erik Ernst <eernst@google.com>
This commit is contained in:
Daco Harkes 2019-12-12 10:27:22 +00:00 committed by commit-bot@chromium.org
parent 1f791e0668
commit 4bd3166529
8 changed files with 603 additions and 1 deletions

View file

@ -2,6 +2,10 @@
// 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.
#include <stdlib.h>
#include <string.h>
#include <iostream>
#if defined(_WIN32)
#define DART_EXPORT extern "C" __declspec(dllexport)
#else
@ -16,3 +20,32 @@ DART_EXPORT int return42() {
DART_EXPORT double timesFour(double d) {
return d * 4.0;
}
// Wrap memmove so we can easily find it on all platforms.
//
// We use this in our samples to illustrate resource lifetime management.
DART_EXPORT void MemMove(void* destination, void* source, intptr_t num_bytes) {
memmove(destination, source, num_bytes);
}
// Some opaque struct.
typedef struct {
} some_resource;
DART_EXPORT some_resource* AllocateResource() {
void* pointer = malloc(sizeof(int64_t));
// Dummy initialize.
static_cast<int64_t*>(pointer)[0] = 10;
return static_cast<some_resource*>(pointer);
}
DART_EXPORT void UseResource(some_resource* resource) {
// Dummy change.
reinterpret_cast<int64_t*>(resource)[0] += 10;
}
DART_EXPORT void ReleaseResource(some_resource* resource) {
free(resource);
}

View file

@ -0,0 +1,194 @@
// 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 pool used for managing resources.
import "dart:async";
import 'dart:convert';
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart' as packageFfi;
import 'package:ffi/ffi.dart' show Utf8;
/// Manages native resources.
///
/// Primary implementations are [Pool] and [Unmanaged].
abstract class ResourceManager {
/// Allocates memory on the native heap.
///
/// The native memory is under management by this [ResourceManager].
///
/// For POSIX-based systems, this uses malloc. On Windows, it uses HeapAlloc
/// against the default public heap. Allocation of either element size or count
/// of 0 is undefined.
///
/// Throws an ArgumentError on failure to allocate.
Pointer<T> allocate<T extends NativeType>({int count: 1});
}
/// Manages native resources.
class Pool implements ResourceManager {
/// Native memory under management by this [Pool].
final List<Pointer<NativeType>> _managedMemoryPointers = [];
/// Callbacks for releasing native resources under management by this [Pool].
final List<Function()> _managedResourceReleaseCallbacks = [];
/// Allocates memory on the native heap.
///
/// The native memory is under management by this [Pool].
///
/// For POSIX-based systems, this uses malloc. On Windows, it uses HeapAlloc
/// against the default public heap. Allocation of either element size or count
/// of 0 is undefined.
///
/// Throws an ArgumentError on failure to allocate.
Pointer<T> allocate<T extends NativeType>({int count: 1}) {
final p = Unmanaged().allocate<T>(count: count);
_managedMemoryPointers.add(p);
return p;
}
/// Registers [resource] in this pool.
///
/// Executes [releaseCallback] on [releaseAll].
T using<T>(T resource, Function(T) releaseCallback) {
_managedResourceReleaseCallbacks.add(() => releaseCallback(resource));
return resource;
}
/// Registers [releaseResourceCallback] to be executed on [releaseAll].
void onReleaseAll(Function() releaseResourceCallback) {
_managedResourceReleaseCallbacks.add(releaseResourceCallback);
}
/// Releases all resources that this [Pool] manages.
void releaseAll() {
for (final c in _managedResourceReleaseCallbacks) {
c();
}
_managedResourceReleaseCallbacks.clear();
for (final p in _managedMemoryPointers) {
Unmanaged().free(p);
}
_managedMemoryPointers.clear();
}
}
/// Creates a [Pool] to manage native resources.
///
/// If the isolate is shut down, through `Isolate.kill()`, resources are _not_ cleaned up.
R using<R>(R Function(Pool) f) {
final p = Pool();
try {
return f(p);
} finally {
p.releaseAll();
}
}
/// Creates a zoned [Pool] to manage native resources.
///
/// Pool is availabe through [currentPool].
///
/// Please note that all throws are caught and packaged in [RethrownError].
///
/// If the isolate is shut down, through `Isolate.kill()`, resources are _not_ cleaned up.
R usePool<R>(R Function() f) {
final p = Pool();
try {
return runZoned(() => f(),
zoneValues: {#_pool: p},
onError: (error, st) => throw RethrownError(error, st));
} finally {
p.releaseAll();
}
}
/// The [Pool] in the current zone.
Pool get currentPool => Zone.current[#_pool];
class RethrownError {
dynamic original;
StackTrace originalStackTrace;
RethrownError(this.original, this.originalStackTrace);
toString() => """RethrownError(${original})
${originalStackTrace}""";
}
/// Does not manage it's resources.
class Unmanaged implements ResourceManager {
/// Allocates memory on the native heap.
///
/// For POSIX-based systems, this uses malloc. On Windows, it uses HeapAlloc
/// against the default public heap. Allocation of either element size or count
/// of 0 is undefined.
///
/// Throws an ArgumentError on failure to allocate.
Pointer<T> allocate<T extends NativeType>({int count = 1}) =>
packageFfi.allocate(count: count);
/// Releases memory on the native heap.
///
/// For POSIX-based systems, this uses free. On Windows, it uses HeapFree
/// against the default public heap. It may only be used against pointers
/// allocated in a manner equivalent to [allocate].
///
/// Throws an ArgumentError on failure to free.
///
void free(Pointer pointer) => packageFfi.free(pointer);
}
/// Does not manage it's resources.
final Unmanaged unmanaged = Unmanaged();
extension Utf8InPool on String {
/// Convert a [String] to a Utf8-encoded null-terminated C string.
///
/// If 'string' contains NULL bytes, the converted string will be truncated
/// prematurely. Unpaired surrogate code points in [string] will be preserved
/// in the UTF-8 encoded result. See [Utf8Encoder] for details on encoding.
///
/// Returns a malloc-allocated pointer to the result.
///
/// The memory is managed by the [Pool] passed in as [pool].
Pointer<Utf8> toUtf8(ResourceManager pool) {
final units = utf8.encode(this);
final Pointer<Uint8> result = pool.allocate<Uint8>(count: units.length + 1);
final Uint8List nativeString = result.asTypedList(units.length + 1);
nativeString.setAll(0, units);
nativeString[units.length] = 0;
return result.cast();
}
}
extension Utf8Helpers on Pointer<Utf8> {
/// Returns the length of a null-terminated string -- the number of (one-byte)
/// characters before the first null byte.
int strlen() {
final Pointer<Uint8> array = this.cast<Uint8>();
final Uint8List nativeString = array.asTypedList(_maxSize);
return nativeString.indexWhere((char) => char == 0);
}
/// Creates a [String] containing the characters UTF-8 encoded in [this].
///
/// [this] must be a zero-terminated byte sequence of valid UTF-8
/// encodings of Unicode code points. It may also contain UTF-8 encodings of
/// unpaired surrogate code points, which is not otherwise valid UTF-8, but
/// which may be created when encoding a Dart string containing an unpaired
/// surrogate. See [Utf8Decoder] for details on decoding.
///
/// Returns a Dart string containing the decoded code points.
String contents() {
final int length = strlen();
return utf8.decode(Uint8List.view(
this.cast<Uint8>().asTypedList(length).buffer, 0, length));
}
}
const int _kMaxSmi64 = (1 << 62) - 1;
const int _kMaxSmi32 = (1 << 30) - 1;
final int _maxSize = sizeOf<IntPtr>() == 8 ? _kMaxSmi64 : _kMaxSmi32;

View file

@ -0,0 +1,89 @@
// 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.
//
// Sample illustrating resources are not cleaned up when isolate is shutdown.
import 'dart:io';
import "dart:isolate";
import 'dart:ffi';
import 'package:expect/expect.dart';
import 'pool.dart';
import '../dylib_utils.dart';
void main() {
final receiveFromHelper = ReceivePort();
Isolate.spawn(helperIsolateMain, receiveFromHelper.sendPort)
.then((helperIsolate) {
helperIsolate.addOnExitListener(
receiveFromHelper.sendPort,
);
print("Main: Helper started.");
Pointer<SomeResource> resource;
receiveFromHelper.listen((message) {
if (message is int) {
resource = Pointer<SomeResource>.fromAddress(message);
print("Main: Received resource from helper: $resource.");
print("Main: Shutting down helper.");
helperIsolate.kill(priority: Isolate.immediate);
} else {
// Isolate kill message.
Expect.isNull(message);
print("Main: Helper is shut down.");
print(
"Main: Trying to use resource after isolate that was supposed to free it was shut down.");
useResource(resource);
print("Main: Releasing resource manually.");
releaseResource(resource);
print("Main: Shutting down receive port, end of main.");
receiveFromHelper.close();
}
});
});
}
/// If set to `false`, this sample can segfault due to use after free and
/// double free.
const keepHelperIsolateAlive = true;
void helperIsolateMain(SendPort sendToMain) {
using((Pool pool) {
final resource = pool.using(allocateResource(), releaseResource);
pool.onReleaseAll(() {
// Will only run print if [keepHelperIsolateAlive] is false.
print("Helper: Releasing all resources.");
});
print("Helper: Resource allocated.");
useResource(resource);
print("Helper: Sending resource to main: $resource.");
sendToMain.send(resource.address);
print("Helper: Going to sleep.");
if (keepHelperIsolateAlive) {
while (true) {
sleep(Duration(seconds: 1));
print("Helper: sleeping.");
}
}
});
}
final ffiTestDynamicLibrary =
dlopenPlatformSpecific("ffi_test_dynamic_library");
final allocateResource = ffiTestDynamicLibrary.lookupFunction<
Pointer<SomeResource> Function(),
Pointer<SomeResource> Function()>("AllocateResource");
final useResource = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<SomeResource>),
void Function(Pointer<SomeResource>)>("UseResource");
final releaseResource = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<SomeResource>),
void Function(Pointer<SomeResource>)>("ReleaseResource");
/// Represents some opaque resource being managed by a library.
class SomeResource extends Struct {}

View file

@ -0,0 +1,113 @@
// 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.
//
// Sample illustrating resource management with an explicit pool.
import 'dart:ffi';
import 'package:expect/expect.dart';
import 'pool.dart';
import '../dylib_utils.dart';
main() {
final ffiTestDynamicLibrary =
dlopenPlatformSpecific("ffi_test_dynamic_library");
final MemMove = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<Void>, Pointer<Void>, IntPtr),
void Function(Pointer<Void>, Pointer<Void>, int)>("MemMove");
// To ensure resources are freed, wrap them in a [using] call.
using((Pool pool) {
final p = pool.allocate<Int64>(count: 2);
p[0] = 24;
MemMove(p.elementAt(1).cast<Void>(), p.cast<Void>(), sizeOf<Int64>());
print(p[1]);
Expect.equals(24, p[1]);
});
// Resources are freed also when abnormal control flow occurs.
try {
using((Pool pool) {
final p = pool.allocate<Int64>(count: 2);
p[0] = 25;
MemMove(p.elementAt(1).cast<Void>(), p.cast<Void>(), 8);
print(p[1]);
Expect.equals(25, p[1]);
throw Exception("Some random exception");
});
// `free(p)` has been called.
} on Exception catch (e) {
print("Caught exception: $e");
}
// In a pool multiple resources can be allocated, which will all be freed
// at the end of the scope.
using((Pool pool) {
final p = pool.allocate<Int64>(count: 2);
final p2 = pool.allocate<Int64>(count: 2);
p[0] = 1;
p[1] = 2;
MemMove(p2.cast<Void>(), p.cast<Void>(), 2 * sizeOf<Int64>());
Expect.equals(1, p2[0]);
Expect.equals(2, p2[1]);
});
// If the resource allocation happens in a different scope, then one either
// needs to pass the pool to that scope.
f1(Pool pool) {
return pool.allocate<Int64>(count: 2);
}
using((Pool pool) {
final p = f1(pool);
final p2 = f1(pool);
p[0] = 1;
p[1] = 2;
MemMove(p2.cast<Void>(), p.cast<Void>(), 2 * sizeOf<Int64>());
Expect.equals(1, p2[0]);
Expect.equals(2, p2[1]);
});
// Using Strings.
using((Pool pool) {
final p = "Hello world!".toUtf8(pool);
print(p.contents());
});
final allocateResource = ffiTestDynamicLibrary.lookupFunction<
Pointer<SomeResource> Function(),
Pointer<SomeResource> Function()>("AllocateResource");
final useResource = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<SomeResource>),
void Function(Pointer<SomeResource>)>("UseResource");
final releaseResource = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<SomeResource>),
void Function(Pointer<SomeResource>)>("ReleaseResource");
// Using an FFI call to release a resource.
using((Pool pool) {
final r = pool.using(allocateResource(), releaseResource);
useResource(r);
});
// Using an FFI call to release a resource with abnormal control flow.
try {
using((Pool pool) {
final r = pool.using(allocateResource(), releaseResource);
useResource(r);
throw Exception("Some random exception");
});
// Resource has been freed.
} on Exception catch (e) {
print("Caught exception: $e");
}
}
/// Represents some opaque resource being managed by a library.
class SomeResource extends Struct {}

View file

@ -0,0 +1,114 @@
// 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.
//
// Sample illustrating resource management with an implicit pool in the zone.
import 'dart:ffi';
import 'package:expect/expect.dart';
import 'pool.dart';
import '../dylib_utils.dart';
main() {
final ffiTestDynamicLibrary =
dlopenPlatformSpecific("ffi_test_dynamic_library");
final MemMove = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<Void>, Pointer<Void>, IntPtr),
void Function(Pointer<Void>, Pointer<Void>, int)>("MemMove");
// To ensure resources are freed, wrap them in a [using] call.
usePool(() {
final p = currentPool.allocate<Int64>(count: 2);
p[0] = 24;
MemMove(p.elementAt(1).cast<Void>(), p.cast<Void>(), sizeOf<Int64>());
print(p[1]);
Expect.equals(24, p[1]);
});
// Resources are freed also when abnormal control flow occurs.
try {
usePool(() {
final p = currentPool.allocate<Int64>(count: 2);
p[0] = 25;
MemMove(p.elementAt(1).cast<Void>(), p.cast<Void>(), 8);
print(p[1]);
Expect.equals(25, p[1]);
throw Exception("Some random exception");
});
} on RethrownError catch (e) {
// Note that exceptions are wrapped when using zones.
print("Caught exception: ${e.original}");
}
// In a pool multiple resources can be allocated, which will all be freed
// at the end of the scope.
usePool(() {
final p = currentPool.allocate<Int64>(count: 2);
final p2 = currentPool.allocate<Int64>(count: 2);
p[0] = 1;
p[1] = 2;
MemMove(p2.cast<Void>(), p.cast<Void>(), 2 * sizeOf<Int64>());
Expect.equals(1, p2[0]);
Expect.equals(2, p2[1]);
});
// If the resource allocation happens in a different scope, it is in the
// same zone, so it's lifetime is automatically managed by the pool.
f1() {
return currentPool.allocate<Int64>(count: 2);
}
usePool(() {
final p = f1();
final p2 = f1();
p[0] = 1;
p[1] = 2;
MemMove(p2.cast<Void>(), p.cast<Void>(), 2 * sizeOf<Int64>());
Expect.equals(1, p2[0]);
Expect.equals(2, p2[1]);
});
// Using Strings.
usePool(() {
final p = "Hello world!".toUtf8(currentPool);
print(p.contents());
});
final allocateResource = ffiTestDynamicLibrary.lookupFunction<
Pointer<SomeResource> Function(),
Pointer<SomeResource> Function()>("AllocateResource");
final useResource = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<SomeResource>),
void Function(Pointer<SomeResource>)>("UseResource");
final releaseResource = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<SomeResource>),
void Function(Pointer<SomeResource>)>("ReleaseResource");
// Using an FFI call to release a resource.
usePool(() {
final r = currentPool.using(allocateResource(), releaseResource);
useResource(r);
});
// Using an FFI call to release a resource with abnormal control flow.
try {
usePool(() {
final r = currentPool.using(allocateResource(), releaseResource);
useResource(r);
throw Exception("Some random exception");
});
// Resource has been freed.
} on RethrownError catch (e) {
// Note that exceptions are wrapped when using zones.
print("Caught exception: ${e.original}");
}
}
/// Represents some opaque resource being managed by a library.
class SomeResource extends Struct {}

View file

@ -0,0 +1,19 @@
// 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.
//
// This file exercises the sample files so that they are tested.
//
// SharedObjects=ffi_test_dynamic_library ffi_test_functions
import 'pool_isolate_shutdown_sample.dart' as pool_isolate;
import 'pool_sample.dart' as pool;
import 'pool_zoned_sample.dart' as pool_zoned;
import 'unmanaged_sample.dart' as unmanaged;
main() {
pool_isolate.main();
pool.main();
pool_zoned.main();
unmanaged.main();
}

View file

@ -0,0 +1,36 @@
// 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.
//
// Sample illustrating manual resource management, not advised.
import 'dart:ffi';
import 'package:expect/expect.dart';
import 'pool.dart';
import '../dylib_utils.dart';
main() {
final ffiTestDynamicLibrary =
dlopenPlatformSpecific("ffi_test_dynamic_library");
final MemMove = ffiTestDynamicLibrary.lookupFunction<
Void Function(Pointer<Void>, Pointer<Void>, IntPtr),
void Function(Pointer<Void>, Pointer<Void>, int)>("MemMove");
// To ensure resources are freed, call free manually.
//
// For automatic management use a Pool.
final p = unmanaged.allocate<Int64>(count: 2);
p[0] = 24;
MemMove(p.elementAt(1).cast<Void>(), p.cast<Void>(), sizeOf<Int64>());
print(p[1]);
Expect.equals(24, p[1]);
unmanaged.free(p);
// Using Strings.
final p2 = "Hello world!".toUtf8(unmanaged);
print(p2.contents());
unmanaged.free(p2);
}

View file

@ -5,6 +5,9 @@
[ $arch == arm ]
sample_extension/test/*: Skip # Issue 14705
[ $builder_tag == asan ]
ffi/samples_test: SkipByDesign # FFI skips, see ffi.status
[ $builder_tag == optimization_counter_threshold ]
sample_extension/test/sample_extension_app_snapshot_test: SkipByDesign # This test is too slow for testing with low optimization counter threshold.
@ -17,7 +20,8 @@ sample_extension/test/sample_extension_app_snapshot_test: Pass, RuntimeError # I
[ $compiler == none && $runtime == vm && $system == fuchsia ]
*: Skip # Not yet triaged.
[ $arch == simarm || $arch == simarm64 || $builder_tag == asan ]
[ $arch == simarm || $arch == simarm64 ]
ffi/resource_management/resource_management_test: SkipByDesign
ffi/samples_test: SkipByDesign # FFI skips, see ffi.status
[ $arch != x64 || $compiler != dartk || $system != linux || $hot_reload || $hot_reload_rollback ]