Description
Current situation
Dart's file IO capabilities are fragmented across different platforms and mechanisms. The dart:io
library provides comprehensive file handling for native platforms but all methods throw in web dart2js and dart2wasm. IOOverrides enable overriding a subset of the APIs, but dart:io
contains many more APIs that might not be implementable in the wasm and js backends.
void main(List<String> args) {
IOOverrides.runWithIOOverrides(() {
// ...
}, MemFSIOOverrides());
}
MemFSIOOverrides
import 'dart:io';
import 'dart:js_interop';
import 'dart:convert' as convert;
import 'dart:js_interop_unsafe';
import 'dart:typed_data';
// adapted functions from https://emscripten.org/docs/api_reference/Filesystem-API.html#id2
extension type MemFS(JSObject _) implements JSObject {
external JSArray<JSString> readdir(String path);
external JSUint8Array readFile(String path, [JSObject? opts]);
external void writeFile(String path, String data);
external void unlink(String path);
external void mkdir(String path);
external void rmdir(String path);
external void rename(String oldpath, String newpath);
external String cwd();
external void chdir(String path);
external JSObject analyzePath(String path, bool dontResolveLastLink);
}
@JS('FS')
external MemFS get memfs;
class MemFSDirectory implements Directory {
@override
String path;
MemFSDirectory(this.path);
@override
void createSync({bool recursive = false}) {
memfs.mkdir(path);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class MemFSFile implements File {
@override
String path;
MemFSFile(this.path);
@override
MemFSFile get absolute => MemFSFile(path);
@override
void createSync({bool recursive = false, bool exclusive = false}) {
memfs.writeFile(path, '');
}
@override
void deleteSync({bool recursive = false}) {
memfs.unlink(path);
}
@override
bool existsSync() {
return memfs
.analyzePath(path, false)
.getProperty<JSBoolean>('exists'.toJS)
.toDart;
}
@override
void writeAsStringSync(String contents,
{FileMode mode = FileMode.write,
convert.Encoding encoding = convert.utf8,
bool flush = false}) {
memfs.writeFile(path, contents);
}
@override
Uint8List readAsBytesSync() {
return memfs.readFile(path).toDart;
}
@override
String readAsStringSync({convert.Encoding encoding = convert.utf8}) {
return encoding.decode(readAsBytesSync());
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class MemFSIOOverrides extends IOOverrides {
@override
MemFSDirectory createDirectory(String path) {
return MemFSDirectory(path);
}
@override
MemFSFile createFile(String path) {
return MemFSFile(path);
}
@override
bool fsWatchIsSupported() {
return false;
}
@override
void setCurrentDirectory(String path) {
memfs.chdir(path);
}
@override
MemFSDirectory getCurrentDirectory() {
return MemFSDirectory(memfs.cwd());
}
@override
MemFSDirectory getSystemTempDirectory() {
return MemFSDirectory("/tmp");
}
}
Thanks @TheComputerM! 🚀
If users want to write code that uses file IO that works across multiple backends, this is what they currently can use.
Problems
- Not all
dart:io
APIs might be implementable on all backends. (locks? sockets? stdio? http requests?) - Setting up IOOverrides is not the cleanest of solutions, it requires modifying the main function.
- (We have an exploration going on of trying to unbundle
dart:io
intopackage:io
cc @brianquinlan)
Proposed solution
Introduce a File API in a package, potentially package:file
, that works seamlessly across all Dart platforms. The new API would expose a subset of file operations supported by all targets, enabling developers to write platform-agnostic code for essential file interactions.
We should then be opinionated and tell users to use that package instead of dart:io
directly.
This is how it's done with http, we have package:http
which works on all platforms and it uses conditional imports to use dart:io
on VM and dart:html
on web.
I believe package:file
is currently missing support for using the file system on the web.
Using the memory file system works for producing (temporary) files in Dart, but does not work for:
- mounting files from the context
- invoking file APIs from C code compiled to WASM
Some open questions:
- Is
package:file
the right place to provide the platform abstraction? - What part of mounting files can be done from the embedder or from Dart code? (I believe dart2js already has a list of callbacks somewhere similar to how the DartVM has a list of callbacks for IO?) (A
CallbackFileSytem
would allow wiring up from inside Dart, but it's probably preferable to do this at the embedder level.)
Soliciting input from
package:file
@jonahwilliams @jakemac53- dart2js @sigmundch @kevmoo @natebiggs
- dart2wasm @mkustermann @osa1
dart:io
boss @brianquinlan- Language team? @lrhn
Any feedback is welcome. Should we take a different direction?
Thanks @mkustermann for suggesting this approach. 🙏 And thanks @TheComputerM for bringing this issue up! 🚀
Let's enable our users to write code with file I/O that works everywhere.