-
Notifications
You must be signed in to change notification settings - Fork 3.5k
[WasmFS] Initial OPFS/AccessHandles backend #16813
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
Changes from all commits
906af59
371a579
df035c4
837b249
7a2a66d
d677978
4cc379b
44bd93d
df5a15e
9a5306c
5dce23c
2eb90f1
d90b3f9
2153e73
3a2c8ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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]; | ||
| } | ||
| }, | ||
|
|
||
| $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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 It could be good to keep using
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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 Some other issues I noticed include:
in opfs_backend.cpp:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great feedback, thanks @goldwaving.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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); | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
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.