-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[samples/ffi] Native resource lifetime management
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
Showing
8 changed files
with
603 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
89
samples/ffi/resource_management/pool_isolate_shutdown_sample.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
Oops, something went wrong.