Skip to content

Commit

Permalink
[samples/ffi] Native resource lifetime management
Browse files Browse the repository at this point in the history
Samples for managing native memory without finalizers.

Design: go/dart-ffi-resource-lifetime

Related issue: #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>
  • Loading branch information
dcharkes authored and commit-bot@chromium.org committed Dec 12, 2019
1 parent 1f791e0 commit 4bd3166
Show file tree
Hide file tree
Showing 8 changed files with 603 additions and 1 deletion.
33 changes: 33 additions & 0 deletions runtime/bin/ffi_test/ffi_test_dynamic_library.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
194 changes: 194 additions & 0 deletions samples/ffi/resource_management/pool.dart
Original file line number Diff line number Diff line change
@@ -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;
89 changes: 89 additions & 0 deletions samples/ffi/resource_management/pool_isolate_shutdown_sample.dart
Original file line number Diff line number Diff line change
@@ -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 {}
Loading

0 comments on commit 4bd3166

Please sign in to comment.