dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.3k stars 1.59k forks source link

[native/js/wasm] Platform independent File I/O #56404

Open dcharkes opened 3 months ago

dcharkes commented 3 months ago

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 ```dart 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 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('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! :rocket:

If users want to write code that uses file IO that works across multiple backends, this is what they currently can use.

Problems

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:

Some open questions:

Soliciting input from

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.

jakemac53 commented 3 months ago
  • Is package:file the right place to provide the platform abstraction?

It probably does make sense yes, I don't see why not. It already exists and has a good name, plus much of it is already written. Note that I don't actually work on the package though (I think it has no official owner currently). We would probably want to more officially support it and figure out an owner.

Using this package also makes testing easier/faster because you can use an in memory file system.

lrhn commented 3 months ago

The dart:io library provides comprehensive file handling for native platforms but all methods throw in web dart2js and dart2wasm

It's a hack that you can import dart:io on those platforms at all, it's not intended to be available or supported. (And dart.library.io is not set to true in the compilation environment.)

So a new API is a better idea for supporting other platforms.

The new API would expose a subset of file operations supported by all targets

Maybe just "operations".

Is "file" even the best abstraction? It's not Unix, not everything has to be a file. It's something that can contain ... other things. But so is a database.

A file system is contains

If we are going to abstract over "file system" to platforms that are not POSIX, not operating systems, and has no real file system, it might be worth upping the abstraction level.

And figure out which subset of operations is really needed, without trying to support too much.

(What does "mounting files" mean?)

dcharkes commented 3 months ago

Is "file" even the best abstraction? It's not Unix, not everything has to be a file.

In the context where we use it, native code compiled to WASM with emscripten, it is a Posix-like file API. https://emscripten.org/docs/api_reference/Filesystem-API.html

Trying to use a different abstraction than a file system will not work if the native code is also trying to use the file system. We need to write a file and then pass in a file path to the native code.

It's a hack that you can import dart:io on those platforms at all, it's not intended to be available or supported.

Precisely. That's why should come up with something for users that is intended to be used.

mkustermann commented 3 months ago

it might be worth upping the abstraction level.

I think there's actually a need to go higher in abstraction and lower: Example: With our existing dart:io APIs it's not possible for a programmer know why a file cannot be opened for writing (e.g. the fact that it doesn't have permission - all errors are just FileSystemExceptions).

What would be nice is to have

jakemac53 commented 3 months ago

Note that package:file does implement the existing dart:io types (like File, Directory, etc).

So, it currently has the exact same layer of abstraction as dart:io for the most part. That might make it not a good fit, because it means it requires a (transitive) dart:io dependency to use, and might have abstractions that don't make sense on all platforms.

sigmundch commented 3 months ago

Overall I like this direction and I'd love to eventually remove dart:io from the web backends :)

kevmoo commented 3 months ago

From a browser perspective (JS/Wasm) we're trying to AVOID supporting concrete types in dart: libraries. We're moving everything to https://pub.dev/packages/web and trying to deprecate dart:html (and friends).

Having interfaces that can be implemented is a GREAT idea. I think pkg:http does a good job here.

But I worry about making promises about implementations.

A dart:file implementation for https://developer.mozilla.org/en-US/docs/Web/API/File might be different than an implementation that works for https://github.com/WebAssembly/wasi-filesystem

The important thing is having the API surface that can be shared across use cases but backed by any implementation.

(I originally read this as wanting dart:file. Glad I misread. Picking a package to invest in here sounds GREAT. We'd have to chat about the best ways to expose web bits, but I could imagine a ~straightforward implementation backed by pkg:web, etc)

biggs0125 commented 3 months ago

+1 on the web-specific thoughts so far. We're definitely in favor of moving away from dart:io and it'd be great to provide users an abstraction that they can actually use on the web.

ykmnkmi commented 3 months ago

+1. Less things to patch to run the analyzer on the web. I also like analyzer's FS alternative with Resource and ResourceProvider.

brianquinlan commented 3 months ago

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.

I like the idea of having a package that represents an abstract filesystem but package:file is not the way to do it. package:file implements the dart:io filesystem classes, which:

  1. makes it impossible to change any classes in dart:io (because package:file is widely used and guaranteed to break)
  2. limits package:file to functionality available in dart:io

I have a presentation with a link to a repo containing some ideas that seem lined up with yours.

Some other ideas:

  1. implement the filesystem functionality in pure-Dart using ffi
  2. make the filesystem abstraction base with the default implementation throwing so that the API can be added-to without breaking everyone
  3. make functionality that doesn't require a file descriptor top-level (e.g. filesystem.move(old, new) rather than File(old).move(new)
iapicca commented 1 month ago

this seems to me at least tangentially related to