Swappable WASIDrive Implementations #327
Replies: 2 comments 2 replies
-
|
Okay these are great thoughts. Thanks heaps for taking the time to write it out. An important consideration will be the difference between running I'd also like to (where possible) maintain conceptual compatibility with the way Node's
You're right, when running Some ideas for how I've been imagining this working follow. First Idea - Simplified Abstraction through CallbacksWhen I was considering this I was imagining some kind of object based interface with a few special handler functions that create a simplified abstraction over a filesystem: WASI.start(/* ... */, {
customFS: {
exists: (path: string): boolean => {},
read: (path: string): UInt8Array => {},
write: (path: string, content: UInt8Array) => {},
// Note: We will also need to consider how this works for directories
},
/* ... */
});The concept would be to abstract away all the details of reading particular ranges of bytes, writing particular ranges etc. There would be an implementation on the Second Idea - Copy the OPFS APIInstead of inventing our own API, we could take a We could provide our own abstraction like Internally we would still need to manage a synchronisation process to/from the SummarySome considerations that will need to be made:
I'd love to hear your thoughts! |
Beta Was this translation helpful? Give feedback.
-
|
Hey @taybenlor, thanks for the response. It seems I did not fully appreciate the difficulty of passing data to the web worker, however this may also be an opportunity to integrate async-backed file systems through the use of Synchronous FilesystemsIf we are running a export interface SyncDrive {
fs: WASIFS;
open(fdDir: FileDescriptor, path: WASIPath, oflags: number, fdflags: number): DriveResult<FileDescriptor>;
close(fd: FileDescriptor): Result;
read(fd: FileDescriptor, bytes: number): DriveResult<Uint8Array>;
write(fd: FileDescriptor, data: Uint8Array): Result;
/* Other synchronous system call definitions */
}
export class WASIDrive implements SyncDrive {
constructor(public fs: WASIFS) { /* ... */ }
/* Existing WASIDrive implementations of system calls */
}and, as mentioned, we can extend the export class WASIContext {
fs: WASIFS | SyncDrive;
/* ... */
}As you stated, this is a bit much for most consumers, but we could simplify the API a bit by providing, e.g. a export class CallbacksDrive implements SyncDrive { /* ... */ }
WASI.start(/* ... */, {
fs: new CallbacksDrive({
exists: (path: string): boolean => {},
read: (path: string): UInt8Array => {},
write: (path: string, content: UInt8Array) => {},
/* ... */
})
});Asynchronous FilesystemsAs you noted, we can't just pass the export type AsyncDrive = {
[K in keyof SyncDrive]: SyncDrive[K] extends (...args: infer A) => infer R
? (...args: A) => R | Promise<R>
: SyncDrive[K];
};
type WASIWorkerHostContext = Partial<Omit<WASIContextOptions, "stdin" | "fs">> & {
fs: WASIFS | AsyncDrive;
};
export class WASIWorkerHost {
constructor(binaryURL: string, context: WASIWorkerHostContext) { /* ... */ }
};Notice that every export class OPFSDrive implements AsyncDrive {
readonly fs: WASIFS = {}; // Unused
root: FileSystemDirectoryHandle; // Opened in constructor
async open(fdDir: FileDescriptor, path: WASIPath, oflags: number, fdflags: number): Promise<DriveResult<FileDescriptor>> {
const file = await this.root.getFileHandle(path);
// Rest of implementation...
}
}ImplementationWithout going into too many specifics, here's what would happen during a
To address your considerations, I think:
Let me know what you think! I am still familiarizing myself with web workers, so if I said something that sounds impossible, please let me know! I am a bit concerned with serialization/message passing overhead with this approach and the performance of blocking with |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi there!
I am interested in exploring the discussion at the end of #244 more. Right now, there is not a good way to interface with file reads/writes on the JS end: we can use the hack in #244 to create fake
Uint8Arrayobjects and pass them as the content of aWASIFile, but it would be better to have a more generic interface built intorunnoitself.I propose adding a swappable
WASIDriveimplementation toWASIContext, e.g.:and then implementing drive creation in
WASIin terms ofcontext.drive. This would allow creating customWASIDriveimplementations that might, e.g. support observing file reads:However, this interface leaves something to be desired as it seems like there's some undesirable overlap between this new
driveoption and the existingfsinWASIContext. For this, I propose two options.Option A: Breaking Changes
In the extreme case, an implementation could entirely obviate the use of a
WASIFS(a customWASIDrivemight choose not to use a backingWASIFSat all), so one option is to have an interface like:and then for backwards compatibility, have the
WASIContextlook like:If a
WASIFSis passed, aWASIDrivegets created from instead.However, this probably goes a step too far, since the
WASIExecutionResultis built around aWASIFS--how do we get the correspondingfsfrom theFileDrive? We could changeWASIExecutionResultto return aFileDriveas its result instead, but this would likely lead to way too many breaking changes as it fundamentally changes the@runno/wasiinterface.Option B: Preferred Version
A more conservative option with no or fewer breaking changes is to keep the
WASIDrivetethered around aWASIFS, at least in terms of interface, perhaps like so:and then define:
Again, such that for the
fswe can either pass aWASIFS, from which aWASIDrivegets created, or our ownFileDrive. If a customFileDriveimplementation does not need to use aWASIFSfor whatever reason, they could just pass{}to theFileDrivesuper constructor. This also does not lead to any breaking changes toWASIExecutionResult,since it just returnsdrive.fsfor the resultfs.Use Case: Interactive Canvas
My primary use case for this is to enable interaction with a
WASIWorkerHostbeyondstdin/stdout. In my particular case, I am looking into creating an interactive canvas library driven through the filesystem. An executingrunnoprogram could write to a special file on the filesystem to draw something to the screen, and my customFileDriveimplementation could observe that write and render to the canvas instead. For example:However, this system would also enable receiving data as well inside
runno--if we wanted arunnoprogram to read something from the browser, e.g. get the current mouse position, we could do something similar where we override thereadsystem call and the browser writes the current coordinates.Overview
This is effectively a way of adding imports/exports to the WebAssembly module, but keeps the spirit of the
runnointerface. It also lines up well with the idea of a Unix synthetic filesystem where special entries (e.g./dev/null) do not actually correspond to physical files (in this case theWASIFS) but special behaviour to be implemented by the OS (e.g. us).One issue #244 presents is how this interface would handle a backing filesystem whose operations are fundamentally asynchronous, e.g. the fact that file handle creation in OPFS is asynchronous. I am not sure how to handle this aside from making the
FileDriveoperations themselves asynchronous, but I don't know if that would play nicely with the wasm exports. This might just be out of scope for this change, and it could just be easier for now to mandate thatFileDriveimplementations are synchronous.Would love to hear some thoughts on fine-tuning this interface/thinking through any gaps/breaking changes I may have missed. I am happy to submit a PR once there are some thoughts on what a good interface for this looks like.
Beta Was this translation helpful? Give feedback.
All reactions