Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dcharkes opened this issue Aug 8, 2024 · 11 comments
Open

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

dcharkes opened this issue Aug 8, 2024 · 11 comments
Labels
area-sdk Use area-sdk for general purpose SDK issues (packaging, distribution, …). area-web-js Issues related to JavaScript support for Dart Web, including DDC, dart2js, and JS interop web-libraries Issues impacting dart:html, etc., libraries web-platform

Comments

@dcharkes
Copy link
Contributor

dcharkes commented Aug 8, 2024

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 into package: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

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.

@dcharkes dcharkes added web-dart2js area-dart2wasm Issues for the dart2wasm compiler. area-web-js Issues related to JavaScript support for Dart Web, including DDC, dart2js, and JS interop area-sdk Use area-sdk for general purpose SDK issues (packaging, distribution, …). and removed web-dart2js labels Aug 8, 2024
@jakemac53
Copy link
Contributor

  • 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
Copy link
Member

lrhn commented Aug 8, 2024

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

  • "files" (sequences of bytes)
  • structure (hierarchical, traditionally, providing a way to identify files.)
  • metadate. File kinds, bit flags, timestamps.
  • possibly mutable (but not if mounted read-only).

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
Copy link
Contributor Author

dcharkes commented Aug 8, 2024

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
Copy link
Member

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

  • high-level abstractions that would abstract over platform differences, packages like package:file / package:http / ... that work everywhere
  • low-level access to platform-specific features, package:posix, package:winapi, ...
    => can be implemented via C FFI (missing features can be implemented - shared memory multithreading will be one)
  • make the high-level package use conditional imports to use the low-level ones
  • start slimming down & eventually removing dart:io

@jakemac53
Copy link
Contributor

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.

@fishythefish fishythefish added web-platform web-libraries Issues impacting dart:html, etc., libraries labels Aug 8, 2024
@sigmundch
Copy link
Member

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

@kevmoo
Copy link
Member

kevmoo commented Aug 8, 2024

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
Copy link

+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
Copy link
Contributor

ykmnkmi commented Aug 9, 2024

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

@brianquinlan
Copy link
Contributor

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
Copy link

iapicca commented Oct 31, 2024

this seems to me at least tangentially related to

@mkustermann mkustermann removed the area-dart2wasm Issues for the dart2wasm compiler. label Jan 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-sdk Use area-sdk for general purpose SDK issues (packaging, distribution, …). area-web-js Issues related to JavaScript support for Dart Web, including DDC, dart2js, and JS interop web-libraries Issues impacting dart:html, etc., libraries web-platform
Projects
None yet
Development

No branches or pull requests