Skip to content

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

Open
@dcharkes

Description

@dcharkes

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-sdkUse area-sdk for general purpose SDK issues (packaging, distribution, …).area-web-jsIssues related to JavaScript support for Dart Web, including DDC, dart2js, and JS interop.web-librariesIssues impacting dart:html, etc., librariesweb-platform

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions