Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/system/lib/wasmfs/backends/opfs_backend.cpp
6175 views
1
// Copyright 2022 The Emscripten Authors. All rights reserved.
2
// Emscripten is available under two separate licenses, the MIT license and the
3
// University of Illinois/NCSA Open Source License. Both these licenses can be
4
// found in the LICENSE file.
5
6
#include <emscripten/threading.h>
7
#include <stdlib.h>
8
9
#include "backend.h"
10
#include "file.h"
11
#include "opfs_backend.h"
12
#include "support.h"
13
#include "thread_utils.h"
14
#include "wasmfs.h"
15
16
using namespace wasmfs;
17
18
namespace {
19
20
using ProxyWorker = emscripten::ProxyWorker;
21
using ProxyingQueue = emscripten::ProxyingQueue;
22
23
class Worker {
24
public:
25
#ifdef __EMSCRIPTEN_PTHREADS__
26
ProxyWorker proxy;
27
28
template<typename T> void operator()(T func) { proxy(func); }
29
#else
30
// When used with JSPI on the main thread the various wasmfs_opfs_* functions
31
// can be directly executed since they are all async.
32
template<typename T> void operator()(T func) {
33
if constexpr (std::is_invocable_v<T&, ProxyingQueue::ProxyingCtx>) {
34
// TODO: Find a way to remove this, since it's unused.
35
ProxyingQueue::ProxyingCtx p;
36
func(p);
37
} else {
38
func();
39
}
40
}
41
#endif
42
};
43
44
class OpenState {
45
public:
46
enum Kind { None, Access, Blob };
47
48
private:
49
Kind kind = None;
50
int id = -1;
51
size_t openCount = 0;
52
53
public:
54
Kind getKind() { return kind; }
55
56
int open(Worker& proxy, int fileID, oflags_t flags) {
57
if (kind == None) {
58
assert(openCount == 0);
59
switch (flags) {
60
case O_RDWR:
61
case O_WRONLY:
62
// If we need write access, try to open an AccessHandle.
63
proxy(
64
[&](auto ctx) { _wasmfs_opfs_open_access(ctx.ctx, fileID, &id); });
65
// TODO: Fall back to open as a blob instead.
66
if (id < 0) {
67
return id;
68
}
69
kind = Access;
70
break;
71
case O_RDONLY:
72
// We only need read access, so open as a Blob
73
proxy(
74
[&](auto ctx) { _wasmfs_opfs_open_blob(ctx.ctx, fileID, &id); });
75
if (id < 0) {
76
return id;
77
}
78
kind = Blob;
79
break;
80
default:
81
WASMFS_UNREACHABLE("Unexpected open access mode");
82
}
83
} else if (kind == Blob && (flags == O_WRONLY || flags == O_RDWR)) {
84
// Try to upgrade to an AccessHandle.
85
int newID;
86
proxy(
87
[&](auto ctx) { _wasmfs_opfs_open_access(ctx.ctx, fileID, &newID); });
88
if (newID < 0) {
89
return newID;
90
}
91
// We have an AccessHandle, so close the blob.
92
proxy([&]() { _wasmfs_opfs_close_blob(getBlobID()); });
93
id = newID;
94
kind = Access;
95
}
96
++openCount;
97
return 0;
98
}
99
100
int close(Worker& proxy) {
101
// TODO: Downgrade to Blob access once the last writable file descriptor has
102
// been closed.
103
int err = 0;
104
if (--openCount == 0) {
105
switch (kind) {
106
case Access:
107
proxy(
108
[&](auto ctx) { _wasmfs_opfs_close_access(ctx.ctx, id, &err); });
109
break;
110
case Blob:
111
proxy([&]() { _wasmfs_opfs_close_blob(id); });
112
break;
113
case None:
114
WASMFS_UNREACHABLE("Open file should have kind");
115
}
116
kind = None;
117
id = -1;
118
}
119
return err;
120
}
121
122
int getAccessID() {
123
assert(openCount > 0);
124
assert(id >= 0);
125
assert(kind == Access);
126
return id;
127
}
128
129
int getBlobID() {
130
assert(openCount > 0);
131
assert(id >= 0);
132
assert(kind == Blob);
133
return id;
134
}
135
};
136
137
class OPFSFile : public DataFile {
138
public:
139
Worker& proxy;
140
int fileID;
141
OpenState state;
142
143
OPFSFile(mode_t mode, backend_t backend, int fileID, Worker& proxy)
144
: DataFile(mode, backend), proxy(proxy), fileID(fileID) {}
145
146
~OPFSFile() override {
147
assert(state.getKind() == OpenState::None);
148
proxy([&]() { _wasmfs_opfs_free_file(fileID); });
149
}
150
151
private:
152
off_t getSize() override {
153
off_t size;
154
switch (state.getKind()) {
155
case OpenState::None:
156
proxy([&](auto ctx) {
157
_wasmfs_opfs_get_size_file(ctx.ctx, fileID, &size);
158
});
159
break;
160
case OpenState::Access:
161
proxy([&](auto ctx) {
162
_wasmfs_opfs_get_size_access(ctx.ctx, state.getAccessID(), &size);
163
});
164
break;
165
case OpenState::Blob:
166
proxy([&]() { size = _wasmfs_opfs_get_size_blob(state.getBlobID()); });
167
break;
168
default:
169
WASMFS_UNREACHABLE("Unexpected open state");
170
}
171
return size;
172
}
173
174
int setSize(off_t size) override {
175
int err = 0;
176
switch (state.getKind()) {
177
case OpenState::Access:
178
proxy([&](auto ctx) {
179
_wasmfs_opfs_set_size_access(
180
ctx.ctx, state.getAccessID(), size, &err);
181
});
182
break;
183
case OpenState::Blob:
184
// We don't support `truncate` in blob mode because the blob would
185
// become invalidated and refreshing it while ensuring other in-flight
186
// operations on the same file do not observe the invalidated blob would
187
// be extremely complicated.
188
// TODO: Can we assume there are no other in-flight operations on this
189
// file and do something better here?
190
return -EIO;
191
case OpenState::None: {
192
proxy([&](auto ctx) {
193
_wasmfs_opfs_set_size_file(ctx.ctx, fileID, size, &err);
194
});
195
break;
196
}
197
default:
198
WASMFS_UNREACHABLE("Unexpected open state");
199
}
200
return err;
201
}
202
203
int open(oflags_t flags) override { return state.open(proxy, fileID, flags); }
204
205
int close() override { return state.close(proxy); }
206
207
ssize_t read(uint8_t* buf, size_t len, off_t offset) override {
208
// TODO: use an i64 here.
209
int32_t nread;
210
switch (state.getKind()) {
211
case OpenState::Access:
212
proxy([&]() {
213
nread =
214
_wasmfs_opfs_read_access(state.getAccessID(), buf, len, offset);
215
});
216
break;
217
case OpenState::Blob:
218
proxy([&](auto ctx) {
219
_wasmfs_opfs_read_blob(
220
ctx.ctx, state.getBlobID(), buf, len, offset, &nread);
221
});
222
break;
223
case OpenState::None:
224
default:
225
WASMFS_UNREACHABLE("Unexpected open state");
226
}
227
return nread;
228
}
229
230
ssize_t write(const uint8_t* buf, size_t len, off_t offset) override {
231
assert(state.getKind() == OpenState::Access);
232
// TODO: use an i64 here.
233
int32_t nwritten;
234
proxy([&]() {
235
nwritten =
236
_wasmfs_opfs_write_access(state.getAccessID(), buf, len, offset);
237
});
238
return nwritten;
239
}
240
241
int flush() override {
242
int err = 0;
243
switch (state.getKind()) {
244
case OpenState::Access:
245
proxy([&](auto ctx) {
246
_wasmfs_opfs_flush_access(ctx.ctx, state.getAccessID(), &err);
247
});
248
break;
249
case OpenState::Blob:
250
case OpenState::None:
251
default:
252
break;
253
}
254
return err;
255
}
256
};
257
258
class OPFSDirectory : public Directory {
259
public:
260
Worker& proxy;
261
262
// The ID of this directory in the JS library.
263
int dirID = 0;
264
265
OPFSDirectory(mode_t mode, backend_t backend, int dirID, Worker& proxy)
266
: Directory(mode, backend), proxy(proxy), dirID(dirID) {}
267
268
~OPFSDirectory() override {
269
// Never free the root directory ID.
270
if (dirID != 0) {
271
proxy([&]() { _wasmfs_opfs_free_directory(dirID); });
272
}
273
}
274
275
private:
276
std::shared_ptr<File> getChild(const std::string& name) override {
277
int childType = 0, childID = 0;
278
proxy([&](auto ctx) {
279
_wasmfs_opfs_get_child(
280
ctx.ctx, dirID, name.c_str(), &childType, &childID);
281
});
282
if (childID < 0) {
283
// TODO: More fine-grained error reporting.
284
return NULL;
285
}
286
if (childType == 1) {
287
return std::make_shared<OPFSFile>(0777, getBackend(), childID, proxy);
288
} else if (childType == 2) {
289
return std::make_shared<OPFSDirectory>(
290
0777, getBackend(), childID, proxy);
291
} else {
292
WASMFS_UNREACHABLE("Unexpected child type");
293
}
294
}
295
296
std::shared_ptr<DataFile> insertDataFile(const std::string& name,
297
mode_t mode) override {
298
int childID = 0;
299
proxy([&](auto ctx) {
300
_wasmfs_opfs_insert_file(ctx.ctx, dirID, name.c_str(), &childID);
301
});
302
if (childID < 0) {
303
// TODO: Propagate specific errors.
304
return nullptr;
305
}
306
return std::make_shared<OPFSFile>(mode, getBackend(), childID, proxy);
307
}
308
309
std::shared_ptr<Directory> insertDirectory(const std::string& name,
310
mode_t mode) override {
311
int childID = 0;
312
proxy([&](auto ctx) {
313
_wasmfs_opfs_insert_directory(ctx.ctx, dirID, name.c_str(), &childID);
314
});
315
if (childID < 0) {
316
// TODO: Propagate specific errors.
317
return nullptr;
318
}
319
return std::make_shared<OPFSDirectory>(mode, getBackend(), childID, proxy);
320
}
321
322
std::shared_ptr<Symlink> insertSymlink(const std::string& name,
323
const std::string& target) override {
324
// Symlinks not supported.
325
// TODO: Propagate EPERM specifically.
326
return nullptr;
327
}
328
329
int insertMove(const std::string& name, std::shared_ptr<File> file) override {
330
int err = 0;
331
if (file->is<DataFile>()) {
332
auto opfsFile = std::static_pointer_cast<OPFSFile>(file);
333
proxy([&](auto ctx) {
334
_wasmfs_opfs_move_file(
335
ctx.ctx, opfsFile->fileID, dirID, name.c_str(), &err);
336
});
337
} else {
338
// TODO: Support moving directories once OPFS supports that.
339
// EBUSY can be returned when the directory is "in use by the system,"
340
// which can mean whatever we want.
341
err = -EBUSY;
342
}
343
return err;
344
}
345
346
int removeChild(const std::string& name) override {
347
int err = 0;
348
proxy([&](auto ctx) {
349
_wasmfs_opfs_remove_child(ctx.ctx, dirID, name.c_str(), &err);
350
});
351
return err;
352
}
353
354
ssize_t getNumEntries() override {
355
auto entries = getEntries();
356
if (int err = entries.getError()) {
357
return err;
358
}
359
return entries->size();
360
}
361
362
Directory::MaybeEntries getEntries() override {
363
std::vector<Directory::Entry> entries;
364
int err = 0;
365
proxy([&](auto ctx) {
366
_wasmfs_opfs_get_entries(ctx.ctx, dirID, &entries, &err);
367
});
368
if (err) {
369
assert(err < 0);
370
return {err};
371
}
372
return {entries};
373
}
374
};
375
376
class OPFSBackend : public Backend {
377
public:
378
Worker proxy;
379
380
std::shared_ptr<DataFile> createFile(mode_t mode) override {
381
// No way to support a raw file without a parent directory.
382
// TODO: update the core system to document this as a possible result of
383
// `createFile` and to handle it gracefully.
384
return nullptr;
385
}
386
387
std::shared_ptr<Directory> createDirectory(mode_t mode) override {
388
proxy([](auto ctx) { _wasmfs_opfs_init_root_directory(ctx.ctx); });
389
return std::make_shared<OPFSDirectory>(mode, this, 1, proxy);
390
}
391
392
std::shared_ptr<Symlink> createSymlink(std::string target) override {
393
// Symlinks not supported.
394
return nullptr;
395
}
396
};
397
398
} // anonymous namespace
399
400
extern "C" {
401
402
backend_t wasmfs_create_opfs_backend() {
403
// ProxyWorker cannot safely be synchronously spawned from the main browser
404
// thread. See comment in thread_utils.h for more details.
405
assert(
406
!emscripten_is_main_browser_thread() ||
407
emscripten_has_asyncify() == 2 &&
408
"Cannot safely create OPFS backend on main browser thread without JSPI");
409
410
return wasmFS.addBackend(std::make_unique<OPFSBackend>());
411
}
412
413
void EMSCRIPTEN_KEEPALIVE _wasmfs_opfs_record_entry(
414
std::vector<Directory::Entry>* entries, const char* name, int type) {
415
entries->push_back({name, File::FileKind(type), 0});
416
}
417
418
} // extern "C"
419
420