Skip to content
Merged
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ commands:
# Currently downloading form our own buckets due to:
# https://github.com/emscripten-core/emscripten/issues/14987
#wget -O ~/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
wget -O ~/chrome.deb https://storage.googleapis.com/webassembly/chrome/google-chrome-stable_current_amd64.deb
wget -O ~/chrome.deb https://storage.googleapis.com/webassembly/chrome/google-chrome-stable_current_amd64_new.deb
dpkg -i ~/chrome.deb
emsdk-env:
description: "emsdk_env.sh"
Expand Down Expand Up @@ -241,7 +241,7 @@ commands:
EMTEST_DETECT_TEMPFILE_LEAKS: "0"
# --no-sandbox becasue we are running as root and chrome requires
# this flag for now: https://crbug.com/638180
CHROME_FLAGS_BASE: "--no-first-run -start-maximized --no-sandbox --use-gl=swiftshader --user-data-dir=/tmp/chrome-emscripten-profile"
CHROME_FLAGS_BASE: "--no-first-run -start-maximized --no-sandbox --use-gl=swiftshader --user-data-dir=/tmp/chrome-emscripten-profile --enable-experimental-web-platform-features"
CHROME_FLAGS_HEADLESS: "--headless --remote-debugging-port=1234"
CHROME_FLAGS_WASM: "--enable-features=WebAssembly --enable-features=SharedArrayBuffer --disable-features=WebAssemblyTrapHandler --js-flags=\"--experimental-wasm-threads --harmony-sharedarraybuffer\""
CHROME_FLAGS_NOCACHE: "--disk-cache-dir=/dev/null --disk-cache-size=1 --media-cache-size=1 --disable-application-cache --incognito"
Expand Down
260 changes: 260 additions & 0 deletions src/library_wasmfs_opfs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
/**
* @license
* Copyright 2022 The Emscripten Authors
* SPDX-License-Identifier: MIT
*/

mergeInto(LibraryManager.library, {
// TODO: Generate these ID pools from a common utility.
$wasmfsOPFSDirectories: {
allocated: [],
free: [],
get: function(i) {
assert(this.allocated[i] !== undefined);
return this.allocated[i];
}
},

$wasmfsOPFSFiles: {
allocated: [],
free: [],
get: function(i) {
assert(this.allocated[i] !== undefined);
return this.allocated[i];
}
},

$wasmfsOPFSAccesses: {
allocated: [],
free: [],
get: function(i) {
assert(this.allocated[i] !== undefined);
return this.allocated[i];
}
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three could all be generated from a single function that creates such instances. A TODO to refactor though might be enough for now. It's possible we have other such stuff that could be refactored with them too.


$wasmfsOPFSAllocate: function(ids, handle) {
let id;
if (ids.free.length > 0) {
id = ids.free.pop();
ids.allocated[id] = handle;
} else {
id = ids.allocated.length;
ids.allocated.push(handle);
}
return id;
},

$wasmfsOPFSFree: function(ids, id) {
delete ids.allocated[id];
ids.free.push(id);
},

_wasmfs_opfs_init_root_directory__deps: ['$wasmfsOPFSDirectories'],
_wasmfs_opfs_init_root_directory: async function(ctx) {
if (wasmfsOPFSDirectories.allocated.length == 0) {
// Directory 0 is reserved as the root
let root = await navigator.storage.getDirectory();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, this is the only line that is specific to the Origin Private part of the File System Access API. This interface could be made more general by allowing the user to provide a directory handle (instead of calling the OPFS version of it). They could choose to give the origin private directory, or use window.showDirectoryPicker to prompt the user for a real folder.

It could be good to keep using navigator.storage.getDirectory() as a default, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is wrong, sorry. I hadn't realized that the sync handle is limited to the OPFS. That's unfortunate.

Any plans on providing a backend for the normal FS API too, so files can be backed by real folders on disk?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have concrete plans to work on that myself, but it would certainly be nice to have. My hope is that we can make it easy to create new WasmFS backends as userspace JS libraries, but it would also be reasonable to generalize this backend once it lands.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, interesting. I'll keep an eye on this space and would be happy to contribute such a library when possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ability to create and mount custom backends in WasmFS is awesome.

I've been experimenting with window.showDirectoryPicker. The OPFS backend does most of the work already, but I found several challenges when replacing the root handle with a native FileSystemDirectoryHandle.

  • The lack of sync handles requires using createWritable instead. The OPFS backend already uses createWritable when pthreads are not available, so we just need to always include the FileSystemAsyncAccessHandle class even when pthreads are available and avoid creating sync handles in the native access case. This requires the most changes (mostly minor).
  • SharedArrayBuffer cannot be used, so the data must be copied to a temporary ArrayBuffer.
  • Passing the native FileSystemDirectoryHandle into the backend is difficult. I had to use IndexedDB to store it, then retrieve it when creating the backend. Is there a better way?
  • If the handle is persisted across sessions, some way of calling requestPermission on the handle is required from the backend (when getEntries is called). This is asynchronous and cannot be done from the proxy thread.

Working around these challenges in a custom backend based on OPSF backend code, I was able to view, create, delete files and directories on the native OS. I think the OPFS backend could be more generalized with a native option so that a whole new backend does not need to be created for native file access. The question is how to specify the FileSystemDirectoryHandle when creating the backend. Using IndexedDB adds some overhead and complexity. In my app, this works fine because I need to persist it anyway.

Some other issues I noticed include:
in wasmfs.h:

  • wasmfs_unmount takes an intptr_t instead of a const char *
  • wasmfs_get_backend_by_path takes a char * instead of a const char *

in opfs_backend.cpp:

  • ~OPFSDirectory compares the dirID to zero to avoid freeing the root ID, however isn't the root ID 1 (zero is undefined)? I think there may have been a couple of other places where ID 0 is assumed to be the root.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great feedback, thanks @goldwaving.

  • Adding a native option to the OPFS backend code to reduce deduplication sounds like a good idea to me.
  • To pass the FileSystemDirectoryHandle to the backend, I hope it would be possible to just store it in a global variable or a fancier global registry so the backend can retrieve it. This might also be an interesting use case for clang's externref support: we could pass the handle directly through C to the backend constructor.
  • The assumption that the root has ID 0 sounds like a bug. I think that used to be true, but we must not have updated all the code when we changed that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A global variable would work for single threaded apps. When using pthreads, however, the FileSystemDirectoryHandle may have to cross two or more thread barriers (from main, to app's proxy, then to OPFS's proxy). That's where it gets tricky. Is there an emscripten way to move the handle across threads or is IndexedDB the only option?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, that does sound tricky. @sbc100, do you know if we provide a way for users to postMessage JS objects from one thread to another?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really.. thats kind of below the abstraction level that our APIs provide.

Many folks have asked for this over the years but we have normally managed to stear them away from needing it at all.

In theory is possible to this this today by looking up wither worker object for a given thread and just using postMessage directly. You would also need to incercept/override the message handler for the thread, but that should also be doable today.

We could add a dedicated API for this, but I'd be tempted to just expose and official pthread->worker lookup function and then show an example of how to use postMessage based on that.

wasmfsOPFSDirectories.allocated.push(root);
}
_emscripten_proxy_finish(ctx);
},

// Return the file ID for the file with `name` under `parent`, creating it if
// it doesn't exist and `create` or otherwise return one of the following
// error codes:
//
// -1: file does not exist.
// -2: file exists but it is actually a directory.
// -3: file exists but an access handle cannot be created for it.
$wasmfsOPFSGetOrCreateFile__deps: ['$wasmfsOPFSAllocate',
'$wasmfsOPFSDirectories',
'$wasmfsOPFSFiles'],
$wasmfsOPFSGetOrCreateFile: async function(parent, name, create) {
let parentHandle = wasmfsOPFSDirectories.get(parent);
let fileHandle;
try {
fileHandle = await parentHandle.getFileHandle(name, {create: create});
} catch (e) {
if (e.name === "NotFoundError") {
return -1;
}
if (e.name === "TypeMismatchError") {
return -2;
}
throw e;
}
return wasmfsOPFSAllocate(wasmfsOPFSFiles, fileHandle);
},

// Return the file ID for the directory with `name` under `parent`, creating
// it if it doesn't exist and `create` or otherwise one of the following error
// codes:
//
// -1: directory does not exist.
// -2: directory exists but is actually a data file.
$wasmfsOPFSGetOrCreateDir__deps: ['$wasmfsOPFSAllocate',
'$wasmfsOPFSDirectories'],
$wasmfsOPFSGetOrCreateDir: async function(parent, name, create) {
let parentHandle = wasmfsOPFSDirectories.get(parent);
let childHandle;
try {
childHandle =
await parentHandle.getDirectoryHandle(name, {create: create});
} catch (e) {
if (e.name === "NotFoundError") {
return -1;
}
if (e.name === "TypeMismatchError") {
return -2;
}
throw e;
}
return wasmfsOPFSAllocate(wasmfsOPFSDirectories, childHandle);
},

_wasmfs_opfs_get_child__deps: ['$wasmfsOPFSGetOrCreateFile',
'$wasmfsOPFSGetOrCreateDir'],
_wasmfs_opfs_get_child:
async function(ctx, parent, namePtr, childTypePtr, childIDPtr) {
let name = UTF8ToString(namePtr);
let childType = 1;
let childID = await wasmfsOPFSGetOrCreateFile(parent, name, false);
if (childID == -2) {
childType = 2;
childID = await wasmfsOPFSGetOrCreateDir(parent, name, false);
}
{{{ makeSetValue('childTypePtr', 0, 'childType', 'i32') }}};
{{{ makeSetValue('childIDPtr', 0, 'childID', 'i32') }}};
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_get_entries__deps: [],
_wasmfs_opfs_get_entries: async function(ctx, dirID, entries) {
let dirHandle = wasmfsOPFSDirectories.get(dirID);
for await (let [name, child] of dirHandle.entries()) {
withStackSave(() => {
let namePtr = allocateUTF8OnStack(name);
let type = child.kind == "file" ?
{{{ cDefine('File::DataFileKind') }}} :
{{{ cDefine('File::DirectoryKind') }}};
__wasmfs_opfs_record_entry(entries, namePtr, type)
});
}
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_insert_file__deps: ['$wasmfsOPFSGetOrCreateFile'],
_wasmfs_opfs_insert_file: async function(ctx, parent, namePtr, childIDPtr) {
let name = UTF8ToString(namePtr);
let childID = await wasmfsOPFSGetOrCreateFile(parent, name, true);
{{{ makeSetValue('childIDPtr', 0, 'childID', 'i32') }}};
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_insert_directory__deps: ['$wasmfsOPFSGetOrCreateDir'],
_wasmfs_opfs_insert_directory: async function(ctx, parent, namePtr, childIDPtr) {
let name = UTF8ToString(namePtr);
let childID = await wasmfsOPFSGetOrCreateDir(parent, name, true);
{{{ makeSetValue('childIDPtr', 0, 'childID', 'i32') }}};
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_move__deps: ['$wasmfsOPFSFiles', '$wasmfsOPFSDirectories'],
_wasmfs_opfs_move: async function(ctx, fileID, newDirID, namePtr) {
let name = UTF8ToString(namePtr);
let fileHandle = wasmfsOPFSFiles.get(fileID);
let newDirHandle = wasmfsOPFSDirectories.get(newDirID);
await fileHandle.move(newDirHandle, name);
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_remove_child__deps: ['$wasmfsOPFSFree', '$wasmfsOPFSDirectories'],
_wasmfs_opfs_remove_child: async function(ctx, dirID, namePtr) {
let name = UTF8ToString(namePtr);
let dirHandle = wasmfsOPFSDirectories.get(dirID);
await dirHandle.removeEntry(name);
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_free_file__deps: ['$wasmfsOPFSFree', '$wasmfsOPFSFiles'],
_wasmfs_opfs_free_file: function(fileID) {
wasmfsOPFSFree(wasmfsOPFSFiles, fileID);
},

_wasmfs_opfs_free_directory__deps: ['$wasmfsOPFSFree',
'$wasmfsOPFSDirectories'],
_wasmfs_opfs_free_directory: function(dirID) {
wasmfsOPFSFree(wasmfsOPFSDirectories, dirID);
},

_wasmfs_opfs_open__deps: ['$wasmfsOPFSAllocate',
'$wasmfsOPFSFiles',
'$wasmfsOPFSAccesses'],
_wasmfs_opfs_open: async function(ctx, fileID, accessIDPtr) {
let fileHandle = wasmfsOPFSFiles.get(fileID);
let accessID;
try {
let accessHandle;
// TODO: Remove this once the Access Handles API has settled.
if (FileSystemFileHandle.prototype.createSyncAccessHandle.length == 0) {
accessHandle = await fileHandle.createSyncAccessHandle();
} else {
accessHandle = await fileHandle.createSyncAccessHandle(
{mode: "in-place"});
}
accessID = wasmfsOPFSAllocate(wasmfsOPFSAccesses, accessHandle);
} catch (e) {
if (e.name === "InvalidStateError") {
accessID = -1;
}
throw e;
}
{{{ makeSetValue('accessIDPtr', 0, 'accessID', 'i32') }}};
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_close__deps: ['$wasmfsOPFSFree', '$wasmfsOPFSAccesses'],
_wasmfs_opfs_close: async function(ctx, accessID) {
let accessHandle = wasmfsOPFSAccesses.get(accessID);
await accessHandle.close();
wasmfsOPFSFree(wasmfsOPFSAccesses, accessID);
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_read__deps: ['$wasmfsOPFSAccesses'],
_wasmfs_opfs_read: function(accessID, bufPtr, len, pos) {
let accessHandle = wasmfsOPFSAccesses.get(accessID);
let data = HEAPU8.subarray(bufPtr, bufPtr + len);
return accessHandle.read(data, {at: pos});
},

_wasmfs_opfs_write__deps: ['$wasmfsOPFSAccesses'],
_wasmfs_opfs_write: function(accessID, bufPtr, len, pos, nwrittenPtr) {
let accessHandle = wasmfsOPFSAccesses.get(accessID);
let data = HEAPU8.subarray(bufPtr, bufPtr + len);
return accessHandle.write(data, {at: pos});
},

_wasmfs_opfs_get_size__deps: ['$wasmfsOPFSAccesses'],
_wasmfs_opfs_get_size: async function(ctx, accessID, sizePtr) {
let accessHandle = wasmfsOPFSAccesses.get(accessID);
let size = await accessHandle.getSize();
{{{ makeSetValue('sizePtr', 0, 'size', 'i32') }}};
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_set_size__deps: ['$wasmfsOPFSAccesses'],
_wasmfs_opfs_set_size: async function(ctx, accessID, size) {
let accessHandle = wasmfsOPFSAccesses.get(accessID);
await accessHandle.truncate(size);
_emscripten_proxy_finish(ctx);
},

_wasmfs_opfs_flush__deps: ['$wasmfsOPFSAccesses'],
_wasmfs_opfs_flush: async function(ctx, accessID) {
let accessHandle = wasmfsOPFSAccesses.get(accessID);
await accessHandle.flush();
_emscripten_proxy_finish(ctx);
}
});
1 change: 1 addition & 0 deletions src/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ global.LibraryManager = {
libraries.push('library_wasmfs_js_file.js');
libraries.push('library_wasmfs_fetch.js');
libraries.push('library_wasmfs_node.js');
libraries.push('library_wasmfs_opfs.js');
}

// Additional JS libraries (without AUTO_JS_LIBRARIES, link to these explicitly via -lxxx.js)
Expand Down
21 changes: 21 additions & 0 deletions src/struct_info_cxx.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,26 @@
"adjustedPtr"
]
}
},
// ===========================================
// WasmFS
// ===========================================
{
"file": "file.h",
"defines": [
"wasmfs::File::UnknownKind",
"wasmfs::File::DataFileKind",
"wasmfs::File::DirectoryKind",
"wasmfs::File::SymlinkKind"
]
},
{
"file": "async_callback.h",
"structs": {
"CallbackState": [
"result",
"offset"
]
}
}
]
9 changes: 0 additions & 9 deletions src/struct_info_internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,6 @@
]
}
},
{
"file": "async_callback.h",
"structs": {
"CallbackState": [
"result",
"offset"
]
}
},
{
"file": "proxying_notification_state.h",
"defines": [
Expand Down
2 changes: 2 additions & 0 deletions system/include/emscripten/wasmfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ backend_t wasmfs_create_fetch_backend(const char* base_url);

backend_t wasmfs_create_node_backend(const char* root);

backend_t wasmfs_create_opfs_backend(void);

#ifdef __cplusplus
}
#endif
Loading