Skip to content

Commit 5645386

Browse files
authored
Implement readAsBytes for Windows (#205)
1 parent 5f89d55 commit 5645386

File tree

9 files changed

+434
-144
lines changed

9 files changed

+434
-144
lines changed

.github/workflows/io_file.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ jobs:
5353
fail-fast: false
5454
matrix:
5555
sdk: [stable]
56-
# TODO(brianquinlan): Run benchmarks on Windows.
57-
os: [ubuntu-latest, macos-latest]
56+
os: [ubuntu-latest, windows-latest, macos-latest]
5857
runs-on: ${{ matrix.os }}
5958
steps:
6059
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// The maximum number of bytes to read in a single call to `read`.
6+
//
7+
// On macOS, it is an error to call
8+
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX`.
9+
//
10+
// The POSIX specification states that the behavior of `read` is
11+
// implementation-defined if `nbyte > SSIZE_MAX`. On Linux, the `read` will
12+
// transfer at most 0x7ffff000 bytes and return the number of bytes actually.
13+
// transfered.
14+
//
15+
// A smaller value has the advantage of making vm-service clients
16+
// (e.g. debugger) more responsive.
17+
//
18+
// A bigger value reduces the number of system calls.
19+
const int maxReadSize = 16 * 1024 * 1024; // 16MB.
20+
21+
// If the size of a file is unknown, read in blocks of this size.
22+
const int blockSize = 64 * 1024;

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,10 @@ import 'dart:math';
88
import 'dart:typed_data';
99

1010
import 'package:ffi/ffi.dart' as ffi;
11-
import 'package:meta/meta.dart';
1211
import 'package:stdlibc/stdlibc.dart' as stdlibc;
1312

1413
import 'file_system.dart';
15-
16-
// The maximum number of bytes to read in a single call to `read`.
17-
//
18-
// On macOS, it is an error to call
19-
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX`.
20-
//
21-
// The POSIX specification states that the behavior of `read` is
22-
// implementation-defined if `nbyte > SSIZE_MAX`. On Linux, the `read` will
23-
// transfer at most 0x7ffff000 bytes and return the number of bytes actually.
24-
// transfered.
25-
//
26-
// A smaller value has the advantage of making vm-service clients
27-
// (e.g. debugger) more responsive.
28-
//
29-
// A bigger value reduces the number of system calls.
30-
@visibleForTesting
31-
const int maxReadSize = 16 * 1024 * 1024; // 16MB.
32-
33-
// If the size of a file is unknown, read in blocks of this size.
34-
@visibleForTesting
35-
const int blockSize = 64 * 1024;
14+
import 'internal_constants.dart';
3615

3716
Exception _getError(int errno, String message, String path) {
3817
//TODO(brianquinlan): In the long-term, do we need to avoid exceptions that
@@ -87,67 +66,72 @@ base class PosixFileSystem extends FileSystem {
8766
}
8867
try {
8968
final stat = stdlibc.fstat(fd);
90-
if (stat != null &&
91-
stat.st_size == 0 &&
92-
stat.st_mode & stdlibc.S_IFREG != 0) {
93-
return Uint8List(0);
94-
}
95-
final length = stat?.st_size ?? 0;
96-
if (length == 0) {
97-
return _readUnknownLength(path, fd);
98-
} else {
99-
return _readKnownLength(path, fd, length);
69+
// The POSIX specification only defines the meaning of `st_size` for
70+
// regular files and symbolic links.
71+
if (stat != null && stat.st_mode & stdlibc.S_IFREG != 0) {
72+
if (stat.st_size == 0) {
73+
return Uint8List(0);
74+
} else {
75+
return _readKnownLength(path, fd, stat.st_size);
76+
}
10077
}
78+
return _readUnknownLength(path, fd);
10179
} finally {
10280
stdlibc.close(fd);
10381
}
10482
}
10583

10684
Uint8List _readUnknownLength(String path, int fd) => ffi.using((arena) {
107-
final buf = ffi.malloc<Uint8>(blockSize);
85+
final buffer = arena<Uint8>(blockSize);
10886
final builder = BytesBuilder(copy: true);
10987

11088
while (true) {
111-
final r = _tempFailureRetry(() => read(fd, buf, blockSize));
89+
final r = _tempFailureRetry(() => read(fd, buffer, blockSize));
11290
switch (r) {
11391
case -1:
11492
final errno = stdlibc.errno;
11593
throw _getError(errno, 'read failed', path);
11694
case 0:
11795
return builder.takeBytes();
11896
default:
119-
final typed = buf.asTypedList(r);
97+
final typed = buffer.asTypedList(r);
12098
builder.add(typed);
12199
}
122100
}
123101
});
124102

125103
Uint8List _readKnownLength(String path, int fd, int length) {
104+
// In the happy path, `buffer` will be returned to the caller as a
105+
// `Uint8List`. If there in an exception, free it and rethrow the exception.
126106
final buffer = ffi.malloc<Uint8>(length);
127-
var bufferOffset = 0;
107+
try {
108+
var bufferOffset = 0;
128109

129-
while (bufferOffset < length) {
130-
final r = _tempFailureRetry(
131-
() => read(
132-
fd,
133-
(buffer + bufferOffset).cast(),
134-
min(length - bufferOffset, maxReadSize),
135-
),
136-
);
137-
switch (r) {
138-
case -1:
139-
final errno = stdlibc.errno;
140-
ffi.calloc.free(buffer);
141-
throw _getError(errno, 'read failed', path);
142-
case 0:
143-
return buffer.asTypedList(
144-
bufferOffset,
145-
finalizer: ffi.calloc.nativeFree,
146-
);
147-
default:
148-
bufferOffset += r;
110+
while (bufferOffset < length) {
111+
final r = _tempFailureRetry(
112+
() => read(
113+
fd,
114+
(buffer + bufferOffset).cast(),
115+
min(length - bufferOffset, maxReadSize),
116+
),
117+
);
118+
switch (r) {
119+
case -1:
120+
final errno = stdlibc.errno;
121+
throw _getError(errno, 'read failed', path);
122+
case 0:
123+
return buffer.asTypedList(
124+
bufferOffset,
125+
finalizer: ffi.calloc.nativeFree,
126+
);
127+
default:
128+
bufferOffset += r;
129+
}
149130
}
131+
return buffer.asTypedList(length, finalizer: ffi.calloc.nativeFree);
132+
} on Exception {
133+
ffi.malloc.free(buffer);
134+
rethrow;
150135
}
151-
return buffer.asTypedList(length, finalizer: ffi.calloc.nativeFree);
152136
}
153137
}

pkgs/io_file/lib/src/vm_windows_file_system.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
import 'dart:ffi';
66
import 'dart:io' as io;
7+
import 'dart:math';
8+
import 'dart:typed_data';
79

810
import 'package:ffi/ffi.dart';
11+
import 'package:ffi/ffi.dart' as ffi;
912
import 'package:win32/win32.dart' as win32;
1013

1114
import 'file_system.dart';
15+
import 'internal_constants.dart';
1216

1317
String _formatMessage(int errorCode) {
1418
final buffer = win32.wsalloc(1024);
@@ -80,4 +84,115 @@ base class WindowsFileSystem extends FileSystem {
8084
throw _getError(errorCode, 'rename failed', oldPath);
8185
}
8286
});
87+
88+
@override
89+
Uint8List readAsBytes(String path) => using((arena) {
90+
// Calling `GetLastError` for the first time causes the `GetLastError`
91+
// symbol to be loaded, which resets `GetLastError`. So make a harmless
92+
// call before the value is needed.
93+
win32.GetLastError();
94+
95+
final f = win32.CreateFile(
96+
path.toNativeUtf16(),
97+
win32.GENERIC_READ | win32.FILE_SHARE_READ,
98+
win32.FILE_SHARE_READ | win32.FILE_SHARE_WRITE,
99+
nullptr,
100+
win32.OPEN_EXISTING,
101+
win32.FILE_ATTRIBUTE_NORMAL,
102+
0,
103+
);
104+
if (f == win32.INVALID_HANDLE_VALUE) {
105+
final errorCode = win32.GetLastError();
106+
throw _getError(errorCode, 'open failed', path);
107+
}
108+
try {
109+
// The result of `GetFileSize` is not defined for non-seeking devices
110+
// such as pipes.
111+
if (win32.GetFileType(f) == win32.FILE_TYPE_DISK) {
112+
final highFileSize = arena<win32.DWORD>();
113+
final lowFileSize = win32.GetFileSize(f, highFileSize);
114+
if (lowFileSize == 0xffffffff) {
115+
// Indicates an error OR that the low order word of the is actually
116+
// that constant.
117+
final errorCode = win32.GetLastError();
118+
if (errorCode != win32.ERROR_SUCCESS) {
119+
return _readUnknownLength(path, f);
120+
}
121+
}
122+
final fileSize = highFileSize.value << 32 | lowFileSize;
123+
if (fileSize == 0) {
124+
return Uint8List(0);
125+
} else {
126+
return _readKnownLength(path, f, fileSize);
127+
}
128+
}
129+
return _readUnknownLength(path, f);
130+
} finally {
131+
win32.CloseHandle(f);
132+
}
133+
});
134+
135+
Uint8List _readUnknownLength(String path, int file) => ffi.using((arena) {
136+
final buffer = arena<Uint8>(blockSize);
137+
final bytesRead = arena<win32.DWORD>();
138+
final builder = BytesBuilder(copy: true);
139+
140+
while (true) {
141+
if (win32.ReadFile(file, buffer, blockSize, bytesRead, nullptr) ==
142+
win32.FALSE) {
143+
final errorCode = win32.GetLastError();
144+
// On Windows, reading from a pipe that is closed by the writer results
145+
// in ERROR_BROKEN_PIPE.
146+
if (errorCode == win32.ERROR_BROKEN_PIPE ||
147+
errorCode == win32.ERROR_SUCCESS) {
148+
return builder.takeBytes();
149+
}
150+
throw _getError(errorCode, 'read failed', path);
151+
}
152+
153+
if (bytesRead.value == 0) {
154+
return builder.takeBytes();
155+
} else {
156+
final typed = buffer.asTypedList(bytesRead.value);
157+
builder.add(typed);
158+
}
159+
}
160+
});
161+
162+
Uint8List _readKnownLength(String path, int file, int length) {
163+
// In the happy path, `buffer` will be returned to the caller as a
164+
// `Uint8List`. If there in an exception, free it and rethrow the exception.
165+
final buffer = ffi.malloc<Uint8>(length);
166+
try {
167+
return ffi.using((arena) {
168+
final bytesRead = arena<win32.DWORD>();
169+
var bufferOffset = 0;
170+
171+
while (bufferOffset < length) {
172+
if (win32.ReadFile(
173+
file,
174+
(buffer + bufferOffset).cast(),
175+
min(length - bufferOffset, maxReadSize),
176+
bytesRead,
177+
nullptr,
178+
) ==
179+
win32.FALSE) {
180+
final errorCode = win32.GetLastError();
181+
throw _getError(errorCode, 'read failed', path);
182+
}
183+
bufferOffset += bytesRead.value;
184+
if (bytesRead.value == 0) {
185+
break;
186+
}
187+
}
188+
return buffer.asTypedList(
189+
bufferOffset,
190+
finalizer: ffi.malloc.nativeFree,
191+
);
192+
});
193+
} on Exception {
194+
ffi.malloc.free(buffer);
195+
rethrow;
196+
}
197+
}
83198
}

pkgs/io_file/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ environment:
99

1010
dependencies:
1111
ffi: ^2.1.4
12-
meta: ^1.16.0
1312
stdlibc:
1413
git:
1514
# Change this to a released version.
@@ -21,3 +20,4 @@ dev_dependencies:
2120
benchmark_harness: ^2.3.1
2221
dart_flutter_team_lints: ^3.4.0
2322
test: ^1.24.0
23+
uuid: ^4.5.1

pkgs/io_file/test/fifo.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
import 'dart:typed_data';
7+
8+
import 'fifo_posix.dart';
9+
import 'fifo_windows.dart';
10+
11+
/// An abstraction for a write-only object with a path.
12+
///
13+
/// Used to test reading from a file that does not report a reliable length.
14+
abstract class Fifo {
15+
static Future<Fifo> create(String suggestedPath) {
16+
if (Platform.isWindows) {
17+
return FifoWindows.create(suggestedPath);
18+
} else {
19+
return FifoPosix.create(suggestedPath);
20+
}
21+
}
22+
23+
/// The file system path of the Fifo used to read it.
24+
String get path;
25+
26+
/// Writes data to the [Fifo]. Does not block.
27+
void write(Uint8List data);
28+
29+
/// Inserts a delay between the next [write] or [close]. Does not block.
30+
void delay(Duration d);
31+
32+
/// Closes the [Fifo]. Does not block.
33+
void close();
34+
}

0 commit comments

Comments
 (0)