Skip to content
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

[WasmFS] Async proxied JS backend #16229

Merged
merged 65 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
36bb44f
start
kripken Feb 4, 2022
91326ec
work [ci skip]
kripken Feb 4, 2022
91bf512
work [ci skip]
kripken Feb 4, 2022
11f82c7
builds [ci skip]
kripken Feb 4, 2022
fabf251
test passes [ci skip]
kripken Feb 4, 2022
09aa942
fix
kripken Feb 4, 2022
0df35c1
fix
kripken Feb 4, 2022
5a0653a
comment
kripken Feb 4, 2022
17c041f
better
kripken Feb 4, 2022
cb4c42f
format
kripken Feb 4, 2022
5fa4f25
fix
kripken Feb 4, 2022
650efab
comments
kripken Feb 4, 2022
c80a891
comment [ci skip]
kripken Feb 7, 2022
a49a630
Merge remote-tracking branch 'origin/main' into wfjsbs
kripken Feb 8, 2022
6d9e223
Merge remote-tracking branch 'origin/main' into wfjsbs
kripken Feb 8, 2022
e3c9012
rename
kripken Feb 8, 2022
0e5429b
start [ci skip]
kripken Feb 8, 2022
dc20c8d
wip [ci skip]
kripken Feb 8, 2022
6210afa
[ci skip]
kripken Feb 8, 2022
af004a0
work [ci skip]
kripken Feb 8, 2022
19c0c9e
cpp builds [ci skip]
kripken Feb 8, 2022
9a4d052
rename
kripken Feb 8, 2022
8575f0b
Merge branch 'wfjsbs' into wfjsbs2
kripken Feb 8, 2022
4718c65
js 'compiles' [ci skip]
kripken Feb 8, 2022
8b28d20
node? [ci skip]
kripken Feb 8, 2022
3ec5f1f
work [ci skip]
kripken Feb 8, 2022
52bbb0e
work [ci skip]
kripken Feb 8, 2022
4c555bd
work
kripken Feb 8, 2022
9321fe6
format [ci skip]
kripken Feb 8, 2022
55928de
c++ builds again [ci skip]
kripken Feb 8, 2022
56240d7
progress [ci skip]
kripken Feb 8, 2022
3e71d34
progress [ci skip]
kripken Feb 8, 2022
736424f
progress [ci skip]
kripken Feb 8, 2022
e33e58b
progress [ci skip]
kripken Feb 8, 2022
2158d61
progress [ci skip]
kripken Feb 8, 2022
57714f4
progress [ci skip]
kripken Feb 8, 2022
337a777
progress [ci skip]
kripken Feb 8, 2022
85b5967
test passes [ci skip]
kripken Feb 9, 2022
cbe113b
work [ci skip]
kripken Feb 9, 2022
223485a
work [ci skip]
kripken Feb 9, 2022
0cb0560
work [ci skip]
kripken Feb 9, 2022
2819980
work [ci skip]
kripken Feb 9, 2022
5dc1455
work [ci skip]
kripken Feb 9, 2022
db018ca
proper [ci skip]
kripken Feb 9, 2022
b59cc11
proper [ci skip]
kripken Feb 9, 2022
b5b1e33
work [ci skip]
kripken Feb 9, 2022
82edc92
work [ci skip]
kripken Feb 9, 2022
320ad78
work [ci skip]
kripken Feb 9, 2022
0794ed1
work [ci skip]
kripken Feb 9, 2022
ec37827
work [ci skip]
kripken Feb 9, 2022
f2aa924
work [ci skip]
kripken Feb 9, 2022
5436d71
work [ci skip]
kripken Feb 9, 2022
a9f685b
work [ci skip]
kripken Feb 9, 2022
8ad69f2
work [ci skip]
kripken Feb 9, 2022
721acea
work [ci skip]
kripken Feb 9, 2022
329c114
work [ci skip]
kripken Feb 9, 2022
86ec770
format
kripken Feb 9, 2022
945cb46
Merge remote-tracking branch 'origin/main' into wfjsbs2
kripken Feb 9, 2022
771ad77
refactor to avoid creating a FetchBackend class that creates its own …
kripken Feb 9, 2022
621813a
format
kripken Feb 9, 2022
fa649e6
docs
kripken Feb 10, 2022
cacb8c2
update test
kripken Feb 10, 2022
cc77ca9
comment
kripken Feb 10, 2022
2b031d7
indent
kripken Feb 10, 2022
cd0d2ef
use arrow functions
kripken Feb 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -3648,7 +3648,6 @@ LibraryManager.library = {
return x.indexOf('dynCall_') == 0 || unmangledSymbols.includes(x) ? x : '_' + x;
},


$asyncLoad__docs: '/** @param {boolean=} noRunDep */',
$asyncLoad: function(url, onload, onerror, noRunDep) {
var dep = !noRunDep ? getUniqueRunDependency('al ' + url) : '';
Expand Down
167 changes: 110 additions & 57 deletions src/library_wasmfs.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
var WasmfsLibrary = {
$wasmFS$JSMemoryFiles : [],
$wasmFS$JSMemoryFreeList: [],
var WasmFSLibrary = {
$wasmFS$preloadedFiles: [],
$wasmFS$preloadedDirs: [],
$FS__deps: [
'$wasmFS$preloadedFiles',
'$wasmFS$preloadedDirs',
'$wasmFS$JSMemoryFiles',
'$wasmFS$JSMemoryFreeList',
'$asyncLoad',
#if !MINIMAL_RUNTIME
// TODO: when preload-plugins are not used, we do not need this.
Expand Down Expand Up @@ -145,64 +141,121 @@ var WasmfsLibrary = {
var len = lengthBytesUTF8(s) + 1;
stringToUTF8(s, fileNameBuffer, len);
},
_wasmfs_write_js_file: function(index, buffer, length, offset) {
try {
if (!wasmFS$JSMemoryFiles[index]) {
// Initialize typed array on first write operation.
wasmFS$JSMemoryFiles[index] = new Uint8Array(offset + length);
}

if (offset + length > wasmFS$JSMemoryFiles[index].length) {
// Resize the typed array if the length of the write buffer exceeds its capacity.
var oldContents = wasmFS$JSMemoryFiles[index];
var newContents = new Uint8Array(offset + length);
newContents.set(oldContents);
wasmFS$JSMemoryFiles[index] = newContents;
}

wasmFS$JSMemoryFiles[index].set(HEAPU8.subarray(buffer, buffer + length), offset);
return 0;
} catch (err) {
return {{{ cDefine('EIO') }}};
}
},
_wasmfs_read_js_file: function(index, buffer, length, offset) {
try {
HEAPU8.set(wasmFS$JSMemoryFiles[index].subarray(offset, offset + length), buffer);
return 0;
} catch (err) {
return {{{ cDefine('EIO') }}};
}
},
_wasmfs_get_js_file_size: function(index) {
return wasmFS$JSMemoryFiles[index] ? wasmFS$JSMemoryFiles[index].length : 0;
},
_wasmfs_create_js_file: function() {
// Find a free entry in the $wasmFS$JSMemoryFreeList or append a new entry to
// wasmFS$JSMemoryFiles.
if (wasmFS$JSMemoryFreeList.length) {
// Pop off the top of the free list.
var index = wasmFS$JSMemoryFreeList.pop();
return index;
}
wasmFS$JSMemoryFiles.push(null);
return wasmFS$JSMemoryFiles.length - 1;
},
_wasmfs_remove_js_file: function(index) {
wasmFS$JSMemoryFiles[index] = null;
// Add the index to the free list.
wasmFS$JSMemoryFreeList.push(index);
},
_wasmfs_get_preloaded_file_size: function(index) {
return wasmFS$preloadedFiles[index].fileData.length;
},
_wasmfs_copy_preloaded_file_data: function(index, buffer) {
HEAPU8.set(wasmFS$preloadedFiles[index].fileData, buffer);
},
}

mergeInto(LibraryManager.library, WasmfsLibrary);
// Backend support. wasmFS$backends will contain a mapping of backend IDs to
// the JS code that implements them. This is the JS side of the JSImpl* class
// in C++, together with the js_impl calls defined right after it.
$wasmFS$backends: {},

// JSImpl

_wasmfs_jsimpl_alloc_file: function(backend, file) {
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
return wasmFS$backends[backend].allocFile(file);
},

_wasmfs_jsimpl_free_file: function(backend, file) {
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
return wasmFS$backends[backend].freeFile(file);
},

_wasmfs_jsimpl_write: function(backend, file, buffer, length, {{{ defineI64Param('offset') }}}) {
{{{ receiveI64ParamAsDouble('offset') }}}
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
return wasmFS$backends[backend].write(file, buffer, length, offset);
},

_wasmfs_jsimpl_read: function(backend, file, buffer, length, {{{ defineI64Param('offset') }}}) {
{{{ receiveI64ParamAsDouble('offset') }}}
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
return wasmFS$backends[backend].read(file, buffer, length, offset);
},

if (WASMFS) {
DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.push('$FS');
_wasmfs_jsimpl_get_size: function(backend, file) {
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
return wasmFS$backends[backend].getSize(file);
},

// ProxiedAsyncJSImpl. Each function receives a function pointer and a
// parameter. We convert those into a convenient Promise API for the
// implementors of backends: the hooks we call should return Promises, which
// we then connect to the calling C++.

_wasmfs_jsimpl_async_alloc_file: async function(backend, file, fptr, arg) {
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
{{{ runtimeKeepalivePush() }}}
Copy link
Member

Choose a reason for hiding this comment

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

Why is this pushing and popping necessary? Is there a way to make it less manual, like a withRuntimeKeptAlive(...) wrapper function?

Copy link
Member Author

Choose a reason for hiding this comment

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

@sbc100 , did we consider a wrapper function? It's not less code, and only works in some situations, but might be nice.

Another option, for places where we can use await, is to have a macro {{{ makeAwait }}} perhaps that would put the push/pop around it?

await wasmFS$backends[backend].allocFile(file);
{{{ runtimeKeepalivePop() }}}
{{{ makeDynCall('vi', 'fptr') }}}(arg);
Copy link
Member

Choose a reason for hiding this comment

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

Will vi handle wasm64? Is there some form of vp we should use instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. It doesn't look like we have support for that atm, so it's another limitation of wasm64. I'll add a comment here at least to make it easier to fix up later when we do.

},

_wasmfs_jsimpl_async_free_file: async function(backend, file, fptr, arg) {
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
{{{ runtimeKeepalivePush() }}}
await wasmFS$backends[backend].freeFile(file);
{{{ runtimeKeepalivePop() }}}
{{{ makeDynCall('vi', 'fptr') }}}(arg);
},

_wasmfs_jsimpl_async_write: async function(backend, file, buffer, length, {{{ defineI64Param('offset') }}}, fptr, arg) {
{{{ receiveI64ParamAsDouble('offset') }}}
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
{{{ runtimeKeepalivePush() }}}
var size = await wasmFS$backends[backend].write(file, buffer, length, offset);
{{{ runtimeKeepalivePop() }}}
{{{ makeSetValue('arg', C_STRUCTS.CallbackState.result, '0', 'i32') }}};
{{{ makeSetValue('arg', C_STRUCTS.CallbackState.offset, 'size', 'i64') }}};
{{{ makeDynCall('vi', 'fptr') }}}(arg);
},

_wasmfs_jsimpl_async_read: async function(backend, file, buffer, length, {{{ defineI64Param('offset') }}}, fptr, arg) {
{{{ receiveI64ParamAsDouble('offset') }}}
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
{{{ runtimeKeepalivePush() }}}
var size = await wasmFS$backends[backend].read(file, buffer, length, offset);
{{{ runtimeKeepalivePop() }}}
{{{ makeSetValue('arg', C_STRUCTS.CallbackState.result, '0', 'i32') }}};
{{{ makeSetValue('arg', C_STRUCTS.CallbackState.offset, 'size', 'i64') }}};
{{{ makeDynCall('vi', 'fptr') }}}(arg);
},

_wasmfs_jsimpl_async_get_size: async function(backend, file, fptr, arg) {
#if ASSERTIONS
assert(wasmFS$backends[backend]);
#endif
{{{ runtimeKeepalivePush() }}}
var size = await wasmFS$backends[backend].getSize(file);
{{{ runtimeKeepalivePop() }}}
{{{ makeSetValue('arg', C_STRUCTS.CallbackState.result, '0', 'i32') }}};
{{{ makeSetValue('arg', C_STRUCTS.CallbackState.offset, 'size', 'i64') }}};
{{{ makeDynCall('vi', 'fptr') }}}(arg);
},
}

mergeInto(LibraryManager.library, WasmFSLibrary);

DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.push('$FS');
63 changes: 63 additions & 0 deletions src/library_wasmfs_fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
mergeInto(LibraryManager.library, {
// Fetch backend: On first access of the file (either a read or a getSize), it
// will fetch() the data from the network asynchronously. Otherwise, after
// that fetch it behaves just like JSFile (and it reuses the code from there).

_wasmfs_create_fetch_backend_js__deps: [
'$wasmFS$backends',
'$wasmFS$JSMemoryFiles',
'_wasmfs_create_js_file_backend_js',
],
_wasmfs_create_fetch_backend_js: async function(backend) {
// Get a promise that fetches the data and stores it in JS memory (if it has
// not already been fetched).
async function getFile(file) {
if (wasmFS$JSMemoryFiles[file]) {
// The data is already here, so nothing to do before we continue on to
// the actual read below.
return Promise.resolve();
}

// This is the first time we want the file's data.
// TODO: real URL!
var url = 'data.dat';
var response = await fetch(url);
var buffer = await response['arrayBuffer']();
wasmFS$JSMemoryFiles[file] = new Uint8Array(buffer);
}

// Start with the normal JSFile operations. This sets
// wasmFS$backends[backend]
// which we will then augment.
__wasmfs_create_js_file_backend_js(backend);

// Add the async operations on top.
var jsFileOps = wasmFS$backends[backend];
tlively marked this conversation as resolved.
Show resolved Hide resolved
wasmFS$backends[backend] = {
// alloc/free operations are not actually async. Just forward to the
// parent class, but we must return a Promise as the caller expects.
allocFile: async function(file) {
jsFileOps.allocFile(file);
return Promise.resolve();
},
freeFile: async function(file) {
jsFileOps.freeFile(file);
return Promise.resolve();
},

write: async function(file, buffer, length, offset) {
abort("TODO: file writing in fetch backend? read-only for now");
},

// read/getSize fetch the data, then forward to the parent class.
read: async function(file, buffer, length, offset) {
await getFile(file);
return jsFileOps.read(file, buffer, length, offset);
},
getSize: async function(file) {
await getFile(file);
return jsFileOps.getSize(file);
},
};
},
});
53 changes: 53 additions & 0 deletions src/library_wasmfs_js_file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
mergeInto(LibraryManager.library, {
// JSFile backend: Store a file's data in JS. We map File objects in C++ to
// entries here that contain typed arrays.
$wasmFS$JSMemoryFiles: {},

_wasmfs_create_js_file_backend_js__deps: [
'$wasmFS$backends',
'$wasmFS$JSMemoryFiles',
],
_wasmfs_create_js_file_backend_js: function(backend) {
wasmFS$backends[backend] = {
allocFile: function(file) {
// Do nothing: we allocate the typed array lazily, see write()
},
freeFile: function(file) {
// Release the memory, as it now has no references to it any more.
wasmFS$JSMemoryFiles[file] = undefined;
},
write: function(file, buffer, length, offset) {
try {
if (!wasmFS$JSMemoryFiles[file]) {
// Initialize typed array on first write operation.
wasmFS$JSMemoryFiles[file] = new Uint8Array(offset + length);
}

if (offset + length > wasmFS$JSMemoryFiles[file].length) {
// Resize the typed array if the length of the write buffer exceeds its capacity.
var oldContents = wasmFS$JSMemoryFiles[file];
var newContents = new Uint8Array(offset + length);
newContents.set(oldContents);
wasmFS$JSMemoryFiles[file] = newContents;
}

wasmFS$JSMemoryFiles[file].set(HEAPU8.subarray(buffer, buffer + length), offset);
return 0;
} catch (err) {
return {{{ cDefine('EIO') }}};
}
},
read: function(file, buffer, length, offset) {
try {
HEAPU8.set(wasmFS$JSMemoryFiles[file].subarray(offset, offset + length), buffer);
return 0;
} catch (err) {
return {{{ cDefine('EIO') }}};
}
},
getSize: function(file) {
return wasmFS$JSMemoryFiles[file] ? wasmFS$JSMemoryFiles[file].length : 0;
},
};
},
});
2 changes: 2 additions & 0 deletions src/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ global.LibraryManager = {
}
} else if (WASMFS) {
libraries.push('library_wasmfs.js');
libraries.push('library_wasmfs_js_file.js');
libraries.push('library_wasmfs_fetch.js');
}

// Additional JS libraries (without AUTO_JS_LIBRARIES, link to these explicitly via -lxxx.js)
Expand Down
13 changes: 13 additions & 0 deletions src/parseTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,19 @@ function receiveI64ParamAsI32s(name) {
return '';
}

// TODO: use this in library_wasi.js and other places. but we need to add an
// error-handling hook here.
function receiveI64ParamAsDouble(name) {
if (WASM_BIGINT) {
// Just convert the bigint into a double.
return `${name} = Number(${name});`;
}

// Combine the i32 params. Use an unsigned operator on low and shift high by
// 32 bits.
return `${name} = ${name}_high * 0x100000000 + (${name}_low >>> 0);`;
}

function sendI64Argument(low, high) {
if (WASM_BIGINT) {
return 'BigInt(low) | (BigInt(high) << BigInt(32))';
Expand Down
9 changes: 9 additions & 0 deletions src/struct_info_internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,14 @@
"name"
]
}
},
{
"file": "async_callback.h",
tlively marked this conversation as resolved.
Show resolved Hide resolved
"structs": {
"CallbackState": [
"result",
"offset"
]
}
}
]
2 changes: 2 additions & 0 deletions system/include/emscripten/wasmfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ typedef backend_t (*backend_constructor_t)(void*);
backend_t wasmfs_create_proxied_backend(backend_constructor_t create_backend,
void* arg);

backend_t wasmfs_create_fetch_backend(char* base_url);

#ifdef __cplusplus
}
#endif
27 changes: 27 additions & 0 deletions system/lib/wasmfs/async_callback.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.

// This file defines the JS file backend and JS file of the new file system.
// Current Status: Work in Progress.
// See https://github.com/emscripten-core/emscripten/issues/15041.

#pragma once

#include "sys/types.h"
#include "wasi/api.h"

// Callbacks for the async API between C and JS. This is declared in a small
// separate header for convenience of gen_struct_info.

// Callbacks take a pointer to a CallbackState structure, which contains both
// the function to call to resume execution, and storage for any out params.
// Basically this stores the state during an async call.
struct CallbackState {
// The result of the operation, either success or an error code.
__wasi_errno_t result;

// Some syscalls return an offset.
off_t offset;
};
Loading