diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d75521667cad..01ffc72c55ebc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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" @@ -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" diff --git a/src/library_wasmfs_opfs.js b/src/library_wasmfs_opfs.js new file mode 100644 index 0000000000000..b03fc723057c8 --- /dev/null +++ b/src/library_wasmfs_opfs.js @@ -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(); + 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); + } +}); diff --git a/src/modules.js b/src/modules.js index c31f096d1e8d7..9632f3bf357fe 100644 --- a/src/modules.js +++ b/src/modules.js @@ -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) diff --git a/src/struct_info_cxx.json b/src/struct_info_cxx.json index 2b73cbca0ee62..c3770bc129e24 100644 --- a/src/struct_info_cxx.json +++ b/src/struct_info_cxx.json @@ -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" + ] + } } ] diff --git a/src/struct_info_internal.json b/src/struct_info_internal.json index 9061907eea27f..9d46fa7c19463 100644 --- a/src/struct_info_internal.json +++ b/src/struct_info_internal.json @@ -48,15 +48,6 @@ ] } }, - { - "file": "async_callback.h", - "structs": { - "CallbackState": [ - "result", - "offset" - ] - } - }, { "file": "proxying_notification_state.h", "defines": [ diff --git a/system/include/emscripten/wasmfs.h b/system/include/emscripten/wasmfs.h index 264fcdf22abab..b3401d5027b66 100644 --- a/system/include/emscripten/wasmfs.h +++ b/system/include/emscripten/wasmfs.h @@ -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 diff --git a/system/lib/wasmfs/backends/opfs_backend.cpp b/system/lib/wasmfs/backends/opfs_backend.cpp new file mode 100644 index 0000000000000..f4946af18c9cc --- /dev/null +++ b/system/lib/wasmfs/backends/opfs_backend.cpp @@ -0,0 +1,283 @@ +// Copyright 2022 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +#include "backend.h" +#include "file.h" +#include "support.h" +#include "thread_utils.h" +#include "wasmfs.h" +#include + +using namespace wasmfs; + +extern "C" { + +// Ensure that the root OPFS directory is initialized with ID 0. +void _wasmfs_opfs_init_root_directory(em_proxying_ctx* ctx); + +// Look up the child under `parent` with `name`. Write 1 to `child_type` if it's +// a regular file or 2 if it's a directory. Write the child's file or directory +// ID to `child_id`, or -1 if the child does not exist, or -2 if the child +// exists but cannot be opened. +void _wasmfs_opfs_get_child(em_proxying_ctx* ctx, + int parent, + const char* name, + int* child_type, + int* child_id); + +// Create a file under `parent` with `name` and store its ID in `child_id`. +void _wasmfs_opfs_insert_file(em_proxying_ctx* ctx, + int parent, + const char* name, + int* child_id); + +// Create a directory under `parent` with `name` and store its ID in `child_id`. +void _wasmfs_opfs_insert_directory(em_proxying_ctx* ctx, + int parent, + const char* name, + int* child_id); + +void _wasmfs_opfs_move(em_proxying_ctx* ctx, + int file_id, + int new_dir_id, + const char* name); + +void _wasmfs_opfs_remove_child(em_proxying_ctx* ctx, + int dir_id, + const char* name); + +void _wasmfs_opfs_get_entries(em_proxying_ctx* ctx, + int dirID, + std::vector* entries); + +void _wasmfs_opfs_open(em_proxying_ctx* ctx, int file_id, int* access_id); + +void _wasmfs_opfs_close(em_proxying_ctx* ctx, int access_id); + +void _wasmfs_opfs_free_file(int file_id); + +void _wasmfs_opfs_free_directory(int dir_id); + +// Synchronous read. Return the number of bytes read. +int _wasmfs_opfs_read(int access_id, uint8_t* buf, uint32_t len, uint32_t pos); + +// Synchronous write. Return the number of bytes written. +int _wasmfs_opfs_write(int access_id, + const uint8_t* buf, + uint32_t len, + uint32_t pos); + +void _wasmfs_opfs_get_size(em_proxying_ctx* ctx, int access_id, uint32_t* size); + +void _wasmfs_opfs_set_size(em_proxying_ctx* ctx, int access_id, uint32_t size); + +void _wasmfs_opfs_flush(em_proxying_ctx* ctx, int access_id); + +} // extern "C" + +namespace { + +using ProxyWorker = emscripten::ProxyWorker; + +class OPFSFile : public DataFile { +public: + ProxyWorker& proxy; + + // The IDs of the corresponding file handle and, if the file is open, the + // corresponding access handle. + int fileID; + int accessID = -1; + + // The number of times this file has been opened. We only close its + // AccessHandle when this falls to zero. + size_t openCount = 0; + + OPFSFile(mode_t mode, backend_t backend, int fileID, ProxyWorker& proxy) + : DataFile(mode, backend), fileID(fileID), proxy(proxy) {} + + ~OPFSFile() override { + assert(openCount == 0); + assert(accessID == -1); + proxy([&]() { _wasmfs_opfs_free_file(fileID); }); + } + +private: + size_t getSize() override { + uint32_t size; + proxy([&](auto ctx) { _wasmfs_opfs_get_size(ctx.ctx, accessID, &size); }); + return size_t(size); + } + + void setSize(size_t size) override { + proxy([&](auto ctx) { _wasmfs_opfs_set_size(ctx.ctx, accessID, size); }); + } + + void open(oflags_t flags) override { + if (openCount == 0) { + proxy([&](auto ctx) { _wasmfs_opfs_open(ctx.ctx, fileID, &accessID); }); + ++openCount; + } + // TODO: proper error handling. + assert(accessID >= 0); + } + + void close() override { + assert(openCount >= 1); + if (--openCount == 0) { + proxy([&](auto ctx) { _wasmfs_opfs_close(ctx.ctx, accessID); }); + accessID = -1; + } + } + + __wasi_errno_t read(uint8_t* buf, size_t len, off_t offset) override { + uint32_t nread; + proxy([&]() { nread = _wasmfs_opfs_read(accessID, buf, len, offset); }); + // TODO: Add a way to report the actual bytes read. We currently assume the + // available bytes can't change under us. + return __WASI_ERRNO_SUCCESS; + } + + __wasi_errno_t write(const uint8_t* buf, size_t len, off_t offset) override { + uint32_t nwritten; + proxy([&]() { nwritten = _wasmfs_opfs_write(accessID, buf, len, offset); }); + // TODO: Add a way to report the actual bytes written. We currently assume + // the write cannot be short. + return __WASI_ERRNO_SUCCESS; + } + + void flush() override { + proxy([&](auto ctx) { _wasmfs_opfs_flush(ctx.ctx, accessID); }); + } +}; + +class OPFSDirectory : public Directory { +public: + ProxyWorker& proxy; + + // The ID of this directory in the JS library. + int dirID = 0; + + OPFSDirectory(mode_t mode, backend_t backend, int dirID, ProxyWorker& proxy) + : Directory(mode, backend), dirID(dirID), proxy(proxy) {} + + ~OPFSDirectory() override { + // Never free the root directory ID. + if (dirID != 0) { + proxy([&]() { _wasmfs_opfs_free_directory(dirID); }); + } + } + +private: + std::shared_ptr getChild(const std::string& name) override { + int childType = 0, childID = 0; + proxy([&](auto ctx) { + _wasmfs_opfs_get_child( + ctx.ctx, dirID, name.c_str(), &childType, &childID); + }); + if (childID < 0) { + // TODO: More fine-grained error reporting. + return NULL; + } + if (childType == 1) { + return std::make_shared(0777, getBackend(), childID, proxy); + } else if (childType == 2) { + return std::make_shared( + 0777, getBackend(), childID, proxy); + } else { + WASMFS_UNREACHABLE("Unexpected child type"); + } + } + + std::shared_ptr insertDataFile(const std::string& name, + mode_t mode) override { + int childID = 0; + proxy([&](auto ctx) { + _wasmfs_opfs_insert_file(ctx.ctx, dirID, name.c_str(), &childID); + }); + // TODO: Handle errors gracefully. + assert(childID >= 0); + return std::make_shared(mode, getBackend(), childID, proxy); + } + + std::shared_ptr insertDirectory(const std::string& name, + mode_t mode) override { + int childID = 0; + proxy([&](auto ctx) { + _wasmfs_opfs_insert_directory(ctx.ctx, dirID, name.c_str(), &childID); + }); + // TODO: Handle errors gracefully. + assert(childID >= 0); + return std::make_shared(mode, getBackend(), childID, proxy); + } + + std::shared_ptr insertSymlink(const std::string& name, + const std::string& target) override { + // Symlinks not supported. + return nullptr; + } + + bool insertMove(const std::string& name, + std::shared_ptr file) override { + auto old_file = std::static_pointer_cast(file); + proxy([&](auto ctx) { + _wasmfs_opfs_move(ctx.ctx, old_file->fileID, dirID, name.c_str()); + }); + // TODO: Handle errors. + return true; + } + + bool removeChild(const std::string& name) override { + proxy([&](auto ctx) { + _wasmfs_opfs_remove_child(ctx.ctx, dirID, name.c_str()); + }); + return true; + } + + size_t getNumEntries() override { return getEntries().size(); } + + std::vector getEntries() override { + std::vector entries; + proxy( + [&](auto ctx) { _wasmfs_opfs_get_entries(ctx.ctx, dirID, &entries); }); + return entries; + } +}; + +class OPFSBackend : public Backend { +public: + ProxyWorker proxy; + + std::shared_ptr createFile(mode_t mode) override { + // No way to support a raw file without a parent directory. + // TODO: update the core system to document this as a possible result of + // `createFile` and to handle it gracefully. + return nullptr; + } + + std::shared_ptr createDirectory(mode_t mode) override { + proxy([](auto ctx) { _wasmfs_opfs_init_root_directory(ctx.ctx); }); + return std::make_shared(mode, this, 0, proxy); + } + + std::shared_ptr createSymlink(std::string target) override { + // Symlinks not supported. + return nullptr; + } +}; + +} // anonymous namespace + +extern "C" { + +backend_t wasmfs_create_opfs_backend() { + return wasmFS.addBackend(std::make_unique()); +} + +void EMSCRIPTEN_KEEPALIVE _wasmfs_opfs_record_entry( + std::vector* entries, const char* name, int type) { + entries->push_back({name, File::FileKind(type), 0}); +} + +} // extern "C" diff --git a/system/lib/wasmfs/file.h b/system/lib/wasmfs/file.h index 8b0b8198494ca..953414d350f98 100644 --- a/system/lib/wasmfs/file.h +++ b/system/lib/wasmfs/file.h @@ -44,7 +44,12 @@ using oflags_t = uint32_t; // to implement the mapping from `File` objects to their underlying files. class File : public std::enable_shared_from_this { public: - enum FileKind { UnknownKind, DataFileKind, DirectoryKind, SymlinkKind }; + enum FileKind { + UnknownKind = 0, + DataFileKind = 1, + DirectoryKind = 2, + SymlinkKind = 3 + }; const FileKind kind; diff --git a/tests/reference_struct_info.json b/tests/reference_struct_info.json index acd86d9438ebb..64a28381a4dc2 100644 --- a/tests/reference_struct_info.json +++ b/tests/reference_struct_info.json @@ -274,6 +274,10 @@ "F_SETLKW64": 7, "F_SETOWN": 8, "F_UNLCK": 2, + "File::DataFileKind": 1, + "File::DirectoryKind": 2, + "File::SymlinkKind": 3, + "File::UnknownKind": 0, "INADDR_LOOPBACK": 2130706433, "INT_MAX": 2147483647, "IPPROTO_TCP": 6, diff --git a/tests/test_browser.py b/tests/test_browser.py index 10364f575e6e3..af0365e0bd8c6 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -5294,6 +5294,14 @@ def test_wasmfs_fetch_backend(self, args): self.btest_exit(test_file('wasmfs/wasmfs_fetch.c'), args=['-sWASMFS', '-sUSE_PTHREADS'] + args) + @requires_threads + @no_firefox('no OPFS support yet') + def test_wasmfs_opfs(self): + test = test_file('wasmfs/wasmfs_opfs.c') + args = ['-sWASMFS', '-pthread', '-sPROXY_TO_PTHREAD'] + self.btest_exit(test, args=args + ['-DWASMFS_SETUP']) + self.btest_exit(test, args=args + ['-DWASMFS_RESUME']) + @no_firefox('no 4GB support yet') def test_zzz_zzz_emmalloc_memgrowth(self, *args): self.btest(test_file('browser/emmalloc_memgrowth.cpp'), expected='0', args=['-sMALLOC=emmalloc', '-sALLOW_MEMORY_GROWTH=1', '-sABORTING_MALLOC=0', '-sASSERTIONS=2', '-sMINIMAL_RUNTIME=1', '-sMAXIMUM_MEMORY=4GB']) diff --git a/tests/wasmfs/wasmfs_opfs.c b/tests/wasmfs/wasmfs_opfs.c new file mode 100644 index 0000000000000..b6089be3ade4a --- /dev/null +++ b/tests/wasmfs/wasmfs_opfs.c @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// Define WASMFS_SETUP then WASMFS_RESUME to run the test as two separate +// programs to test persistence. Alternatively, define neither and run the full +// test as a single program. + +void cleanup(void); + +int main(int argc, char* argv[]) { + int err, fd; + const char* msg = "Hello, OPFS!"; + + emscripten_console_log("starting main"); + + backend_t opfs = wasmfs_create_opfs_backend(); + emscripten_console_log("created OPFS backend"); + + err = wasmfs_create_directory("/opfs", 0777, opfs); + assert(err == 0); + emscripten_console_log("mounted OPFS root directory"); + +#ifdef WASMFS_RESUME + + fd = open("/opfs/working/foo.txt", O_RDWR); + assert(fd > 0); + emscripten_console_log("opened existing OPFS file"); + +#else // !WASMFS_RESUME + + // Remove old files if they exist. + cleanup(); + + err = mkdir("/opfs/working", 0777); + assert(err == 0); + emscripten_console_log("created OPFS directory"); + + fd = open("/opfs/working/foo.txt", O_RDWR | O_CREAT | O_EXCL, 0777); + assert(fd > 0); + emscripten_console_log("created OPFS file"); + + int nwritten = write(fd, msg, strlen(msg)); + assert(nwritten == strlen(msg)); + emscripten_console_logf("wrote message: %s (%d)", msg, nwritten); + + int off = lseek(fd, 0, SEEK_SET); + assert(off == 0); + emscripten_console_log("seeked"); + +#endif // !WASMFS_RESUME + + char buf[100] = {}; + int nread = read(fd, buf, 100); + assert(nread == strlen(msg)); + assert(strcmp(buf, msg) == 0); + emscripten_console_logf("read message: %s (%d)", buf, nread); + + fdatasync(fd); + emscripten_console_log("flushed"); + + struct stat stat; + err = fstat(fd, &stat); + assert(err == 0); + assert(stat.st_size == strlen(msg)); + emscripten_console_log("statted"); + +#ifndef WASMFS_SETUP + + err = ftruncate(fd, 100); + assert(err == 0); + err = fstat(fd, &stat); + assert(err == 0); + assert(stat.st_size == 100); + emscripten_console_log("truncated to 100"); + + err = ftruncate(fd, 0); + assert(err == 0); + err = fstat(fd, &stat); + assert(err == 0); + assert(stat.st_size == 0); + emscripten_console_log("truncated to 0"); + + struct dirent** entries; + int nentries = scandir("/opfs/working", &entries, NULL, alphasort); + assert(nentries == 3); + assert(strcmp(entries[0]->d_name, ".") == 0); + assert(strcmp(entries[1]->d_name, "..") == 0); + assert(strcmp(entries[2]->d_name, "foo.txt") == 0); + assert(entries[2]->d_type == DT_REG); + for (int i = 0; i < nentries; i++) { + free(entries[i]); + } + free(entries); + emscripten_console_log("read /opfs/working entries"); + + nentries = scandir("/opfs", &entries, NULL, alphasort); + assert(nentries == 3); + assert(strcmp(entries[2]->d_name, "working") == 0); + assert(entries[2]->d_type == DT_DIR); + for (int i = 0; i < nentries; i++) { + free(entries[i]); + } + free(entries); + emscripten_console_log("read /opfs entries"); + + err = close(fd); + assert(err == 0); + emscripten_console_log("closed file"); + + err = rename("/opfs/working/foo.txt", "/opfs/foo.txt"); + assert(err == 0); + err = access("/opfs/working/foo.txt", F_OK); + assert(err == -1); + err = access("/opfs/foo.txt", F_OK); + assert(err == 0); + emscripten_console_log("moved file"); + + err = unlink("/opfs/foo.txt"); + assert(err == 0); + err = access("/opfs/foo.txt", F_OK); + assert(err == -1); + emscripten_console_log("removed OPFS file"); + + err = rmdir("/opfs/working"); + assert(err == 0); + err = access("/opfs/working", F_OK); + assert(err == -1); + emscripten_console_log("removed OPFS directory"); + + emscripten_console_log("done"); + +#endif // !WASMFS_SETUP +} + +void cleanup(void) { + unlink("/opfs/working/foo.txt"); + rmdir("/opfs/working"); + unlink("/opfs/foo.txt"); +} diff --git a/tools/deps_info.py b/tools/deps_info.py index c931b0ecf6b9d..5877d3de47eda 100644 --- a/tools/deps_info.py +++ b/tools/deps_info.py @@ -218,4 +218,8 @@ def get_deps_info(): _deps_info['emscripten_set_offscreencanvas_size_on_target_thread_js'] = ['malloc'] if settings.USE_PTHREADS: _deps_info['emscripten_set_canvas_element_size_calling_thread'] = ['emscripten_dispatch_to_thread_'] + + if settings.WASMFS: + _deps_info['wasmfs_create_opfs_backend'] = ['emscripten_proxy_finish'] + return _deps_info diff --git a/tools/gen_struct_info.py b/tools/gen_struct_info.py index 5ec984554cda1..bcd8107c2d625 100755 --- a/tools/gen_struct_info.py +++ b/tools/gen_struct_info.py @@ -434,12 +434,13 @@ def main(args): '-I' + utils.path_from_root('system/lib/libc/musl/src/internal'), '-I' + utils.path_from_root('system/lib/libc/musl/src/include'), '-I' + utils.path_from_root('system/lib/pthread/'), - '-I' + utils.path_from_root('system/lib/wasmfs/'), ] cxxflags = [ '-I' + utils.path_from_root('system/lib/libcxxabi/src'), '-D__USING_EMSCRIPTEN_EXCEPTIONS__', + '-I' + utils.path_from_root('system/lib/wasmfs/'), + '-std=c++17', ] # Look for structs in all passed headers. diff --git a/tools/system_libs.py b/tools/system_libs.py index 74df1fe79e62d..0eadc8ad41430 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -1493,6 +1493,7 @@ def get_files(self): 'js_file_backend.cpp', 'memory_backend.cpp', 'node_backend.cpp', + 'opfs_backend.cpp', 'proxied_file_backend.cpp']) return backends + files_in_path( path='system/lib/wasmfs',