From 44ec0ae07d70bf87708a243e87af85310b48707b Mon Sep 17 00:00:00 2001 From: Martin Kustermann Date: Mon, 23 Jan 2023 09:10:23 +0000 Subject: [PATCH] Extract mmap support from dart2js into package:mmap The purpose of the CL is to enable re-use of the mmap support in dart2js in other tools (e.g. package:heapsnapshot & package:kernel). There's a small refactoring to remove zero-termination logic out of the general mmap support. Change-Id: I7a9889acea43d5ce0ab1eb10dcefbfa74c44bf93 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/279348 Reviewed-by: Jens Johansen Reviewed-by: Nate Biggs Commit-Queue: Martin Kustermann --- pkg/compiler/lib/src/io/mapped_file.dart | 174 ++--------------------- pkg/compiler/pubspec.yaml | 2 +- pkg/mmap/lib/mmap.dart | 84 +++++++++++ pkg/mmap/lib/src/mmap_impl.dart | 135 ++++++++++++++++++ pkg/mmap/pubspec.yaml | 16 +++ pkg/mmap/test/mmap_test.dart | 97 +++++++++++++ pkg/pkg.status | 3 +- 7 files changed, 348 insertions(+), 163 deletions(-) create mode 100644 pkg/mmap/lib/mmap.dart create mode 100644 pkg/mmap/lib/src/mmap_impl.dart create mode 100644 pkg/mmap/pubspec.yaml create mode 100644 pkg/mmap/test/mmap_test.dart diff --git a/pkg/compiler/lib/src/io/mapped_file.dart b/pkg/compiler/lib/src/io/mapped_file.dart index 85df1e026ef..8369871ca60 100644 --- a/pkg/compiler/lib/src/io/mapped_file.dart +++ b/pkg/compiler/lib/src/io/mapped_file.dart @@ -6,172 +6,24 @@ // SDK is rolled. // ignore_for_file: deprecated_member_use -import 'dart:ffi'; -import 'dart:io'; import 'dart:typed_data'; import 'package:compiler/src/source_file_provider.dart'; -import 'package:ffi/ffi.dart'; - -// int open(const char *path, int oflag, ...); -@FfiNative, Int32)>("open") -external int open(Pointer filename, int flags); - -// int close(int fd); -@FfiNative("close") -external int close(int fd); - -// void* mmap(void* addr, size_t length, -// int prot, int flags, -// int fd, off_t offset) -@FfiNative< - Pointer Function( - Pointer, IntPtr, Int32, Int32, Int32, IntPtr)>("mmap") -external Pointer mmap( - Pointer address, int len, int prot, int flags, int fd, int offset); - -// int munmap(void *addr, size_t length) -@FfiNative address, IntPtr len)>("munmap") -external int munmap(Pointer address, int len); - -final processSymbols = DynamicLibrary.process(); -final munmapNative = processSymbols.lookup('munmap'); -final closeNative = processSymbols.lookup('close'); -final freeNative = processSymbols.lookup('free'); - -// int mprotect(void *addr, size_t len, int prot) -@FfiNative, IntPtr, Int32)>("mprotect") -external int mprotect(Pointer addr, int len, int prot); - -// DART_EXPORT Dart_Handle -// Dart_NewExternalTypedDataWithFinalizer(Dart_TypedData_Type type, -// void* data, -// intptr_t length, -// void* peer, -// intptr_t external_allocation_size, -// Dart_HandleFinalizer callback) -typedef Dart_NewExternalTypedDataWithFinalizerNative = Handle Function( - Int32, Pointer, IntPtr, Pointer, IntPtr, Pointer); -typedef Dart_NewExternalTypedDataWithFinalizerDart = Object Function( - int, Pointer, int, Pointer, int, Pointer); -final Dart_NewExternalTypedDataWithFinalizer = processSymbols.lookupFunction< - Dart_NewExternalTypedDataWithFinalizerNative, - Dart_NewExternalTypedDataWithFinalizerDart>( - 'Dart_NewExternalTypedDataWithFinalizer'); - -const int kPageSize = 4096; -const int kProtRead = 1; -const int kProtWrite = 2; -const int kProtExec = 4; -const int kMapPrivate = 2; -const int kMapAnon = 0x20; -const int kMapFailed = -1; - -// We need to attach the finalizer which calls close() and - -final finalizerAddress = () { - final finalizerStub = mmap(nullptr, kPageSize, kProtRead | kProtWrite, - kMapPrivate | kMapAnon, -1, 0); - finalizerStub.cast().asTypedList(kPageSize).setAll(0, [ -// Regenerate by running dart mmap.dart gen -// ASM_START -// #include -// #include -// -// struct PeerData { -// int (*close)(int); -// int (*munmap)(void*, size_t); -// int (*free)(void*); -// void* mapping; -// intptr_t size; -// intptr_t fd; -// }; -// -// extern "C" void finalizer(void* callback_data, void* peer) { -// auto data = static_cast(peer); -// data->munmap(data->mapping, data->size); -// data->close(data->fd); -// data->free(peer); -// } -// - 0x55, 0x48, 0x89, 0xf5, 0x48, 0x8b, 0x76, 0x20, 0x48, 0x8b, 0x7d, 0x18, // - 0xff, 0x55, 0x08, 0x8b, 0x7d, 0x28, 0xff, 0x55, 0x00, 0x48, 0x8b, 0x45, // - 0x10, 0x48, 0x89, 0xef, 0x5d, 0xff, 0xe0, // -// ASM_END - ]); - if (mprotect(finalizerStub, kPageSize, kProtRead | kProtExec) != 0) { - throw 'Failed to write executable code to the memory.'; - } - - return finalizerStub.cast(); -}(); - -class PeerData extends Struct { - external Pointer close; - external Pointer munmap; - external Pointer free; - external Pointer mapping; - @IntPtr() - external int size; - @IntPtr() - external int fd; -} - -Uint8List toExternalDataWithFinalizer( - Pointer memory, int size, int length, int fd) { - final peer = malloc.allocate(sizeOf()); - peer.ref.close = closeNative; - peer.ref.munmap = munmapNative; - peer.ref.free = freeNative; - peer.ref.mapping = memory; - peer.ref.size = size; - peer.ref.fd = fd; - return Dart_NewExternalTypedDataWithFinalizer( - /*Dart_TypedData_kUint8*/ 2, - memory.cast(), - length, - peer.cast(), - size, - finalizerAddress, - ) as Uint8List; -} +import 'package:mmap/mmap.dart'; Uint8List viewOfFile(String filename, bool zeroTerminated) { - final cfilename = filename.toNativeUtf8(); - final int fd = open(cfilename, 0); - malloc.free(cfilename); - if (fd == 0) throw 'failed to open'; - try { - final length = File(filename).lengthSync(); - int lengthRoundedUp = (length + kPageSize - 1) & ~(kPageSize - 1); - final result = - mmap(nullptr, lengthRoundedUp, kProtRead, kMapPrivate, fd, 0); - if (result.address == kMapFailed) throw 'failed to map'; - try { - if (zeroTerminated) { - if (length == lengthRoundedUp) { - // In the rare case we need a zero-terminated list and the file size - // is exactly page-aligned we need to allocate a new list with extra - // room for the terminating 0. - return Uint8List(length + 1) - ..setRange( - 0, - length, - toExternalDataWithFinalizer( - result, lengthRoundedUp, length, fd)); - } - return toExternalDataWithFinalizer( - result, lengthRoundedUp, length + 1, fd); - } - return toExternalDataWithFinalizer(result, lengthRoundedUp, length, fd); - } catch (_) { - munmap(result, lengthRoundedUp); - rethrow; - } - } catch (e) { - close(fd); - rethrow; + final mappedFile = mmapFile(filename); + if (!zeroTerminated) { + return mappedFile.fileBytes; } + if (mappedFile.hasZeroPadding) { + return mappedFile.fileBytesZeroTerminated; + } + // In the rare case we need a zero-terminated list and the file size + // is exactly page-aligned we need to allocate a new list with extra + // room for the terminating 0. + return Uint8List(mappedFile.fileLength + 1) + ..setRange(0, mappedFile.fileLength, mappedFile.fileBytes); } class MemoryMapSourceFileByteReader implements SourceFileByteReader { @@ -179,7 +31,7 @@ class MemoryMapSourceFileByteReader implements SourceFileByteReader { @override Uint8List getBytes(String filename, {bool zeroTerminated = true}) { - if (Platform.isLinux) { + if (supportsMMap) { try { return viewOfFile(filename, zeroTerminated); } catch (e) { diff --git a/pkg/compiler/pubspec.yaml b/pkg/compiler/pubspec.yaml index ed32927dbbf..17a31af8e41 100644 --- a/pkg/compiler/pubspec.yaml +++ b/pkg/compiler/pubspec.yaml @@ -13,12 +13,12 @@ dependencies: collection: any crypto: any dart2js_info: any - ffi: any front_end: any js_ast: any js_runtime: any js_shared: any kernel: any + mmap: any vm_service: any # Use 'any' constraints here; we get our versions from the DEPS file. diff --git a/pkg/mmap/lib/mmap.dart b/pkg/mmap/lib/mmap.dart new file mode 100644 index 00000000000..0af3035cd17 --- /dev/null +++ b/pkg/mmap/lib/mmap.dart @@ -0,0 +1,84 @@ +// Copyright (c) 2022, 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:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +import 'src/mmap_impl.dart'; + +/// Whether memory mapping files is supported. +/// +/// Currently we only support linux-x64. +final bool supportsMMap = Platform.isLinux && sizeOf() == 8; + +class MemoryMappedFile { + /// The memory mapped region of the address space as bytes. + /// + /// The [Uint8List]'s length is a multiple of page size and as such may be + /// larger than the file length. The extra bytes are guaranteed to be zero. + /// + /// Once this list becomes unreachable the underlying mapping will be freed & + /// file descriptor will be closed. + final Uint8List mappedBytes; + + /// The length of the file that was mapped. + final int fileLength; + + MemoryMappedFile(this.mappedBytes, this.fileLength); + + /// Whether the [mappedBytes] contain extra zeros after the file content. + bool get hasZeroPadding => (fileLength % kPageSize) != 0; + + /// The bytes representing the file. + Uint8List get fileBytes => Uint8List.sublistView(mappedBytes, 0, fileLength); + + /// The bytes representing the file plus an extra zero byte. + Uint8List get fileBytesZeroTerminated { + if (!hasZeroPadding) throw 'The mapped file is page aligned.'; + return Uint8List.sublistView(mappedBytes, 0, fileLength + 1); + } +} + +/// Maps the given [filename] into the virtual address space. +/// +/// Notice this only works on platforms where [supportsMMap] returns `true`. +/// Notice that files of length 0 cannot be mapped. +MemoryMappedFile mmapFile(String filename) { + if (!supportsMMap) throw 'MMap not supported'; + + final Pointer cfilename = filename.toNativeUtf8(); + final int fd = open(cfilename, 0); + malloc.free(cfilename); + if (fd == 0) throw 'failed to open'; + try { + final int length = File(filename).lengthSync(); + final int lengthRoundedUp = (length + kPageSize - 1) & ~(kPageSize - 1); + final Pointer result = + mmap(nullptr, lengthRoundedUp, kProtRead, kMapPrivate, fd, 0); + if (result.address == kMapFailed) throw 'failed to map'; + try { + final Uint8List bytes = toExternalDataWithFinalizer( + result, lengthRoundedUp, lengthRoundedUp, fd); + return MemoryMappedFile(bytes, length); + } catch (_) { + munmap(result, lengthRoundedUp); + rethrow; + } + } catch (e) { + close(fd); + rethrow; + } +} + +Uint8List mmapOrReadFileSync(String filename) { + if (supportsMMap) { + try { + return mmapFile(filename).fileBytes; + } catch (_) {} + } + return File(filename).readAsBytesSync(); +} diff --git a/pkg/mmap/lib/src/mmap_impl.dart b/pkg/mmap/lib/src/mmap_impl.dart new file mode 100644 index 00000000000..5df328c8dfd --- /dev/null +++ b/pkg/mmap/lib/src/mmap_impl.dart @@ -0,0 +1,135 @@ +// Copyright (c) 2022, 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. + +// File is compiled with checked in SDK, update [FfiNative]s to [Native] when +// SDK is rolled. +// ignore_for_file: deprecated_member_use + +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +// int open(const char *path, int oflag, ...); +@FfiNative, Int32)>("open") +external int open(Pointer filename, int flags); + +// int close(int fd); +@FfiNative("close") +external int close(int fd); + +// void* mmap(void* addr, size_t length, +// int prot, int flags, +// int fd, off_t offset) +@FfiNative< + Pointer Function( + Pointer, IntPtr, Int32, Int32, Int32, IntPtr)>("mmap") +external Pointer mmap( + Pointer address, int len, int prot, int flags, int fd, int offset); + +// int munmap(void *addr, size_t length) +@FfiNative address, IntPtr len)>("munmap") +external int munmap(Pointer address, int len); + +final DynamicLibrary processSymbols = DynamicLibrary.process(); +final munmapNative = processSymbols.lookup('munmap'); +final closeNative = processSymbols.lookup('close'); +final freeNative = processSymbols.lookup('free'); + +// int mprotect(void *addr, size_t len, int prot) +@FfiNative, IntPtr, Int32)>("mprotect") +external int mprotect(Pointer addr, int len, int prot); + +// DART_EXPORT Dart_Handle +// Dart_NewExternalTypedDataWithFinalizer(Dart_TypedData_Type type, +// void* data, +// intptr_t length, +// void* peer, +// intptr_t external_allocation_size, +// Dart_HandleFinalizer callback) +typedef Dart_NewExternalTypedDataWithFinalizerNative = Handle Function( + Int32, Pointer, IntPtr, Pointer, IntPtr, Pointer); +typedef Dart_NewExternalTypedDataWithFinalizerDart = Object Function( + int, Pointer, int, Pointer, int, Pointer); +final Dart_NewExternalTypedDataWithFinalizer = processSymbols.lookupFunction< + Dart_NewExternalTypedDataWithFinalizerNative, + Dart_NewExternalTypedDataWithFinalizerDart>( + 'Dart_NewExternalTypedDataWithFinalizer'); + +const int kPageSize = 4096; +const int kProtRead = 1; +const int kProtWrite = 2; +const int kProtExec = 4; +const int kMapPrivate = 2; +const int kMapAnon = 0x20; +const int kMapFailed = -1; + +// We need to attach the finalizer which calls close() and + +final finalizerAddress = () { + final Pointer finalizerStub = mmap(nullptr, kPageSize, + kProtRead | kProtWrite, kMapPrivate | kMapAnon, -1, 0); + finalizerStub.cast().asTypedList(kPageSize).setAll(0, [ +// Regenerate by running dart mmap.dart gen +// ASM_START +// #include +// #include +// +// struct PeerData { +// int (*close)(int); +// int (*munmap)(void*, size_t); +// int (*free)(void*); +// void* mapping; +// intptr_t size; +// intptr_t fd; +// }; +// +// extern "C" void finalizer(void* callback_data, void* peer) { +// auto data = static_cast(peer); +// data->munmap(data->mapping, data->size); +// data->close(data->fd); +// data->free(peer); +// } +// + 0x55, 0x48, 0x89, 0xf5, 0x48, 0x8b, 0x76, 0x20, 0x48, 0x8b, 0x7d, 0x18, // + 0xff, 0x55, 0x08, 0x8b, 0x7d, 0x28, 0xff, 0x55, 0x00, 0x48, 0x8b, 0x45, // + 0x10, 0x48, 0x89, 0xef, 0x5d, 0xff, 0xe0, // +// ASM_END + ]); + if (mprotect(finalizerStub, kPageSize, kProtRead | kProtExec) != 0) { + throw 'Failed to write executable code to the memory.'; + } + + return finalizerStub.cast(); +}(); + +class PeerData extends Struct { + external Pointer close; + external Pointer munmap; + external Pointer free; + external Pointer mapping; + @IntPtr() + external int size; + @IntPtr() + external int fd; +} + +Uint8List toExternalDataWithFinalizer( + Pointer memory, int size, int length, int fd) { + final Pointer peer = malloc.allocate(sizeOf()); + peer.ref.close = closeNative; + peer.ref.munmap = munmapNative; + peer.ref.free = freeNative; + peer.ref.mapping = memory; + peer.ref.size = size; + peer.ref.fd = fd; + return Dart_NewExternalTypedDataWithFinalizer( + /*Dart_TypedData_kUint8*/ 2, + memory.cast(), + length, + peer.cast(), + size, + finalizerAddress, + ) as Uint8List; +} diff --git a/pkg/mmap/pubspec.yaml b/pkg/mmap/pubspec.yaml new file mode 100644 index 00000000000..c37def0f81c --- /dev/null +++ b/pkg/mmap/pubspec.yaml @@ -0,0 +1,16 @@ +name: mmap + +# This package is not intended for consumption on pub.dev. DO NOT publish. +publish_to: none + +environment: + sdk: '>=2.14.0 <3.0.0' + +# Use 'any' constraints here; we get our versions from the DEPS file. +dependencies: + ffi: any + +# Use 'any' constraints here; we get our versions from the DEPS file. +dev_dependencies: + expect: any + path: any diff --git a/pkg/mmap/test/mmap_test.dart b/pkg/mmap/test/mmap_test.dart new file mode 100644 index 00000000000..6c7252d5845 --- /dev/null +++ b/pkg/mmap/test/mmap_test.dart @@ -0,0 +1,97 @@ +// Copyright (c) 2023, 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:io'; +import 'dart:typed_data'; + +import 'package:expect/expect.dart'; +import 'package:path/path.dart' as path; + +import 'package:mmap/mmap.dart'; +import 'package:mmap/src/mmap_impl.dart' show kPageSize; + +final sizesToTest = [ + 0, + kPageSize - 1, + kPageSize, + 2 * kPageSize - 1, + 2 * kPageSize +]; + +main() { + final tempDir = Directory.systemTemp.createTempSync('mmap_test'); + try { + testMmapOrReadFile(tempDir); + if (!supportsMMap) { + testUnsupported(); + } else { + testSupported(tempDir); + } + } finally { + tempDir.deleteSync(recursive: true); + } +} + +void testUnsupported() { + Expect.throws(() => mmapFile(Platform.executable)); +} + +void testSupported(Directory tempDir) { + for (final size in sizesToTest) { + final testFile = path.join(tempDir.path, 'file.bin'); + File(testFile).writeAsBytesSync(initBytes(Uint8List(size))); + + final fileLength = File(testFile).lengthSync(); + Expect.equals(size, fileLength); + + if (size == 0) { + Expect.throws(() => mmapFile(testFile)); + continue; + } + + final mapping = mmapFile(testFile); + Expect.equals(size, mapping.fileLength); + Expect.equals(size, mapping.fileBytes.length); + + verifyBytes(mapping.fileBytes); + if (mapping.hasZeroPadding) { + verifyBytes(mapping.fileBytesZeroTerminated, 1); + verifyBytes(mapping.mappedBytes, kPageSize - (size % kPageSize)); + } else { + verifyBytes(mapping.mappedBytes); + Expect.throws(() => mapping.fileBytesZeroTerminated); + } + } +} + +void testMmapOrReadFile(Directory tempDir) { + for (final size in sizesToTest) { + final testFile = path.join(tempDir.path, 'file.bin'); + File(testFile).writeAsBytesSync(initBytes(Uint8List(size))); + + final fileLength = File(testFile).lengthSync(); + Expect.equals(size, fileLength); + + final bytes = mmapOrReadFileSync(testFile); + Expect.equals(size, bytes.length); + + verifyBytes(bytes); + } +} + +Uint8List initBytes(Uint8List bytes) { + for (int i = 0; i < bytes.length; ++i) { + bytes[i] = i % 23; + } + return bytes; +} + +void verifyBytes(Uint8List bytes, [int zeroBytes = 0]) { + for (int i = 0; i < bytes.length - zeroBytes; ++i) { + Expect.equals(i % 23, bytes[i]); + } + for (int i = bytes.length - zeroBytes; i < bytes.length; ++i) { + Expect.equals(0, bytes[i]); + } +} diff --git a/pkg/pkg.status b/pkg/pkg.status index 2908fc88b84..9d014ce684c 100644 --- a/pkg/pkg.status +++ b/pkg/pkg.status @@ -255,5 +255,6 @@ vm/test/modular_kernel_plus_aot_test: SkipByDesign # This test should only run i front_end/test/fasta/*: Skip front_end/tool/_fasta/*: Skip -[ $runtime == chrome || $runtime == ff || $runtime == firefox || $runtime == safari || $jscl ] +[ $browser || $jscl ] compiler/test/*: Skip # dart2js uses #import('dart:io'); and it is not self-hosted (yet). +mmap/*: SkipByDesign # Only meant to run on vm