Skip to content

Commit 57a52ce

Browse files
authored
Implement writeAsBytes for Windows (#210)
1 parent 770e271 commit 57a52ce

File tree

7 files changed

+204
-43
lines changed

7 files changed

+204
-43
lines changed

.github/workflows/io_file.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ jobs:
7272
with:
7373
sdk: ${{ matrix.sdk }}
7474
- run: dart pub get
75-
- name: 🪑 Run benchmarks
75+
- name: 🪑 Run Read Benchmarks
7676
run: dart run benchmarks/read_as_bytes.dart
77+
- name: 🪑 Run Write Benchmarks
78+
run: dart run benchmarks/write_as_bytes.dart
7779

7880
ios-vm-test:
7981
runs-on: macos-latest
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 'package:benchmark_harness/benchmark_harness.dart';
9+
import 'package:io_file/io_file.dart';
10+
11+
class WriteAsBytesBenchmark extends BenchmarkBase {
12+
late Directory dir;
13+
late String path;
14+
final Uint8List data;
15+
16+
WriteAsBytesBenchmark(super.name, int size) : data = Uint8List(size);
17+
18+
@override
19+
void setup() {
20+
dir = Directory.systemTemp.createTempSync('bench');
21+
path = '${dir.path}/file';
22+
}
23+
24+
@override
25+
void teardown() {
26+
dir.deleteSync(recursive: true);
27+
}
28+
}
29+
30+
class IOFilesWriteAsBytesBenchmark extends WriteAsBytesBenchmark {
31+
IOFilesWriteAsBytesBenchmark(int size)
32+
: super('IOFilesWriteAsBytesBenchmark($size)', size);
33+
34+
static void main(int size) {
35+
IOFilesWriteAsBytesBenchmark(size).report();
36+
}
37+
38+
@override
39+
void run() {
40+
fileSystem.writeAsBytes(path, data, WriteMode.truncateExisting);
41+
}
42+
}
43+
44+
class DartIOWriteAsBytesBenchmark extends WriteAsBytesBenchmark {
45+
DartIOWriteAsBytesBenchmark(int size)
46+
: super('DartIOWriteAsBytesBenchmark($size)', size);
47+
48+
static void main(int size) {
49+
DartIOWriteAsBytesBenchmark(size).report();
50+
}
51+
52+
@override
53+
void run() {
54+
File(path).writeAsBytesSync(data);
55+
}
56+
}
57+
58+
void main() {
59+
for (var size in [0, 1024, 64 * 1024, 1024 * 1024, 64 * 1024 * 1024]) {
60+
DartIOWriteAsBytesBenchmark.main(size);
61+
IOFilesWriteAsBytesBenchmark.main(size);
62+
}
63+
}

pkgs/io_file/lib/src/file_system.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ base class FileSystem {
5656
}
5757

5858
/// Write the given bytes to a file.
59+
///
60+
/// If `path` is a broken symlink and `mode` is [WriteMode.failExisting]:
61+
///
62+
/// - On Windows, the target of the symlink is created, using `data` as its
63+
/// contents.
64+
/// - On POSIX, [writeAsBytes] throws `PathExistsException`.
5965
void writeAsBytes(
6066
String path,
6167
Uint8List data, [

pkgs/io_file/lib/src/internal_constants.dart

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,33 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

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.
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.
1919
const int maxReadSize = 16 * 1024 * 1024; // 16MB.
2020

21-
// If the size of a file is unknown, read in blocks of this size.
21+
/// If the size of a file is unknown, read in blocks of this size.
2222
const int blockSize = 64 * 1024;
23+
24+
/// The maximum number of bytes to read in a single call to `read`.
25+
///
26+
/// `write` on FreeBSD returns `EINVAL` if nbytes is greater than
27+
/// `INT_MAX`.
28+
/// See https://man.freebsd.org/cgi/man.cgi?write(2)
29+
///
30+
/// A smaller value has the advantage of making vm-service clients
31+
/// (e.g. debugger) more responsive.
32+
///
33+
/// A bigger value reduces the number of system calls.
34+
const int maxWriteSize = 16 * 1024 * 1024; // 16MB.

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import 'package:stdlibc/stdlibc.dart' as stdlibc;
1313
import 'file_system.dart';
1414
import 'internal_constants.dart';
1515

16-
const _maxInt32 = 2147483647;
17-
1816
/// The default `mode` to use with `open` calls that may create a file.
1917
const _defaultMode = 438; // => 0666 => rw-rw-rw-
2018

@@ -179,15 +177,10 @@ base class PosixFileSystem extends FileSystem {
179177
buffer.asTypedList(data.length).setAll(0, data);
180178
var remaining = data.length;
181179

182-
// `write` on FreeBSD returns `EINVAL` if nbytes is greater than
183-
// `INT_MAX`.
184-
// See https://man.freebsd.org/cgi/man.cgi?write(2)
185-
final maxWriteSize =
186-
(io.Platform.isIOS || io.Platform.isMacOS) ? _maxInt32 : remaining;
187-
188180
while (remaining > 0) {
189181
final w = _tempFailureRetry(
190-
() => write(fd, buffer, min(remaining, maxWriteSize)),
182+
() =>
183+
write(fd, buffer, min(remaining, min(maxWriteSize, remaining))),
191184
);
192185
if (w == -1) {
193186
final errno = stdlibc.errno;

pkgs/io_file/lib/src/vm_windows_file_system.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,68 @@ base class WindowsFileSystem extends FileSystem {
195195
rethrow;
196196
}
197197
}
198+
199+
@override
200+
void writeAsBytes(
201+
String path,
202+
Uint8List data, [
203+
WriteMode mode = WriteMode.failExisting,
204+
]) => using((arena) {
205+
// Calling `GetLastError` for the first time causes the `GetLastError`
206+
// symbol to be loaded, which resets `GetLastError`. So make a harmless
207+
// call before the value is needed.
208+
win32.GetLastError();
209+
210+
var createFlags = 0;
211+
createFlags |= switch (mode) {
212+
WriteMode.appendExisting => win32.OPEN_ALWAYS,
213+
WriteMode.failExisting => win32.CREATE_NEW,
214+
WriteMode.truncateExisting => win32.CREATE_ALWAYS,
215+
_ => throw ArgumentError.value(mode, 'invalid write mode'),
216+
};
217+
218+
final f = win32.CreateFile(
219+
path.toNativeUtf16(),
220+
mode == WriteMode.appendExisting
221+
? win32.FILE_APPEND_DATA
222+
: win32.FILE_GENERIC_WRITE,
223+
0,
224+
nullptr,
225+
createFlags,
226+
win32.FILE_ATTRIBUTE_NORMAL,
227+
0,
228+
);
229+
if (f == win32.INVALID_HANDLE_VALUE) {
230+
final errorCode = win32.GetLastError();
231+
throw _getError(errorCode, 'open failed', path);
232+
}
233+
234+
try {
235+
// TODO(brianquinlan): Remove this copy when typed data pointers are
236+
// available for non-leaf calls.
237+
var buffer = arena<Uint8>(data.length);
238+
buffer.asTypedList(data.length).setAll(0, data);
239+
final bytesWritten = arena<win32.DWORD>();
240+
var remaining = data.length;
241+
242+
while (remaining > 0) {
243+
if (win32.WriteFile(
244+
f,
245+
buffer.cast(),
246+
min(maxWriteSize, remaining),
247+
bytesWritten,
248+
nullptr,
249+
) ==
250+
win32.FALSE) {
251+
final errorCode = win32.GetLastError();
252+
throw _getError(errorCode, 'write failed', path);
253+
}
254+
255+
remaining -= bytesWritten.value;
256+
buffer += bytesWritten.value;
257+
}
258+
} finally {
259+
win32.CloseHandle(f);
260+
}
261+
});
198262
}

pkgs/io_file/test/write_as_bytes_test.dart

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
@TestOn('posix')
5+
@TestOn('vm')
66
library;
77

88
import 'dart:io';
@@ -11,6 +11,7 @@ import 'dart:typed_data';
1111
import 'package:io_file/io_file.dart';
1212
import 'package:stdlibc/stdlibc.dart' as stdlibc;
1313
import 'package:test/test.dart';
14+
import 'package:win32/win32.dart' as win32;
1415

1516
import 'test_utils.dart';
1617

@@ -34,7 +35,11 @@ void main() {
3435
throwsA(
3536
isA<FileSystemException>()
3637
.having((e) => e.message, 'message', 'open failed')
37-
.having((e) => e.osError?.errorCode, 'errorCode', stdlibc.EISDIR)
38+
.having(
39+
(e) => e.osError?.errorCode,
40+
'errorCode',
41+
Platform.isWindows ? win32.ERROR_ACCESS_DENIED : stdlibc.EISDIR,
42+
)
3843
.having((e) => e.path, 'path', tmp),
3944
),
4045
);
@@ -61,23 +66,39 @@ void main() {
6166
Link(symlinkPath).createSync(filePath);
6267
File(filePath).deleteSync();
6368

64-
expect(
65-
() => fileSystem.writeAsBytes(
69+
if (Platform.isWindows) {
70+
// Windows considers a broken symlink to not be an existing file.
71+
fileSystem.writeAsBytes(
6672
symlinkPath,
6773
Uint8List.fromList(data),
6874
WriteMode.failExisting,
69-
),
70-
throwsA(
71-
isA<PathExistsException>()
72-
.having((e) => e.message, 'message', 'open failed')
73-
.having(
74-
(e) => e.osError?.errorCode,
75-
'errorCode',
76-
stdlibc.EEXIST,
77-
)
78-
.having((e) => e.path, 'path', symlinkPath),
79-
),
80-
);
75+
);
76+
77+
// Should write at the symlink target, which should also mean that the
78+
// symlink is no longer broken.
79+
expect(fileSystem.readAsBytes(symlinkPath), data);
80+
expect(fileSystem.readAsBytes(filePath), data);
81+
} else {
82+
expect(
83+
() => fileSystem.writeAsBytes(
84+
symlinkPath,
85+
Uint8List.fromList(data),
86+
WriteMode.failExisting,
87+
),
88+
throwsA(
89+
isA<PathExistsException>()
90+
.having((e) => e.message, 'message', 'open failed')
91+
.having(
92+
(e) => e.osError?.errorCode,
93+
'errorCode',
94+
Platform.isWindows
95+
? win32.ERROR_FILE_EXISTS
96+
: stdlibc.EEXIST,
97+
)
98+
.having((e) => e.path, 'path', symlinkPath),
99+
),
100+
);
101+
}
81102
});
82103
test('truncateExisting', () {
83104
final filePath = '$tmp/file1';
@@ -165,7 +186,7 @@ void main() {
165186
.having(
166187
(e) => e.osError?.errorCode,
167188
'errorCode',
168-
stdlibc.EEXIST,
189+
Platform.isWindows ? win32.ERROR_FILE_EXISTS : stdlibc.EEXIST,
169190
)
170191
.having((e) => e.path, 'path', path),
171192
),
@@ -205,7 +226,7 @@ void main() {
205226
}
206227

207228
test('Write very large file', () {
208-
// FreeBSD cannot write more than INT_MAX at once.
229+
// FreeBSD/Windows cannot write more than INT_MAX at once.
209230
final data = randomUint8List(1 << 31 + 1);
210231
final path = '$tmp/file';
211232

0 commit comments

Comments
 (0)