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

poll_oneoff - is async io support possible? #14

Open
TristanCacqueray opened this issue Feb 15, 2023 · 22 comments
Open

poll_oneoff - is async io support possible? #14

TristanCacqueray opened this issue Feb 15, 2023 · 22 comments
Labels
enhancement New feature or request

Comments

@TristanCacqueray
Copy link

Hello, thank you for this great library.

When using it for program built with https://gitlab.haskell.org/ghc/ghc-wasm-meta, it is failing early on because of the poll_oneoff "async io not supported" exception. Using return 0 instead makes the program go a bit further, but then it crash again when reading the stdin. Would it be possible to implement this api?

@bjorn3
Copy link
Owner

bjorn3 commented Feb 15, 2023

Should be fine to implement. It can't be actual async io, but that isn't needed anyway as an in memory VFS is used, so every operation finishes pretty much instantly.

@bjorn3 bjorn3 added the enhancement New feature or request label Feb 15, 2023
@bjorn3
Copy link
Owner

bjorn3 commented Feb 15, 2023

Would you mind posting an example wasm binary that hits this error? That would help with testing the implementation.

@TristanCacqueray
Copy link
Author

TristanCacqueray commented Feb 15, 2023

Good to hear! You can get a reproducer from https://tristancacqueray.github.io/tiny-game-haskell-wasm/ , where the shim is patched locally to remove the throw. The expected result is that pressing enter should clear and redraw the terminal. The client source is from https://github.com/TristanCacqueray/tiny-game-haskell-wasm/blob/main/client.js

@hellwolf
Copy link

Yes, please, I am having the same issue.

@bjorn3
Copy link
Owner

bjorn3 commented Feb 24, 2023

I was busy for the past couple of week, but I can probably work on this tomorrow.

@hellwolf
Copy link

Appreciated!

@bjorn3
Copy link
Owner

bjorn3 commented Feb 25, 2023

I just took a closer look and while

diff --git a/src/wasi.js b/src/wasi.js
index 3a1060f..1c341ae 100644
--- a/src/wasi.js
+++ b/src/wasi.js
@@ -426,6 +426,7 @@ export default class WASI {
                 }
             },
             poll_oneoff(in_, out, nsubscriptions) {
+                return 0;
                 throw "async io not supported";
             },
             proc_exit(exit_code/*: number*/) {

fixes the crash, it now gets stuck in an infinite loop waiting for input that will never come as fd_read returns 0. It isn't possible to block for input on the main thread in the browser.

It should be possible to run the wasm code in a web worker and then in this web worker do an Atomics.wait and Atomics.notify on the main thread when input arrives and use a SharedArrayBuffer for communicating the actual input, but I don't think browser_wasi_shim should do all this work at least right now. Maybe in the future browser_wasi_shim could have it as optional feature.

For now if you want to implement this yourself you can overwrite wasi.wasiImport.poll_oneoff before passing wasi.wasiImport to Webassembly.instantiate.

@TristanCacqueray
Copy link
Author

I'm giving the SharedArrayBuffer approach a try with a web worker, and it doesn't seem to ever get to the fd_read call. I guess poll_oneoff needs to do something with its argument, but I can't tell what or how :)

@bjorn3
Copy link
Owner

bjorn3 commented Feb 28, 2023

For me making poll_oneoff always return 0 caused the wasm module to call fd_read in an infinite loop. Can I take a look at what you have?

@TristanCacqueray
Copy link
Author

@bjorn3 sure, I am using this branch: https://github.com/TristanCacqueray/browser_wasi_shim/tree/worker-example

Please note that I've never used web worker and atomics before. When browsing the dist, in the console I see an infinite loop between poll_oneoff and clock_time_get. I expected to see fd_read and Reading! where I would have hooked the waitForInput implementation.

@bjorn3
Copy link
Owner

bjorn3 commented Mar 1, 2023

Looks like poll_oneoff needs to actually return which subscriptions are ready. I managed to hack the following thing together which may help you get further:

diff --git a/examples/worker.html b/examples/worker.html
index 10f2918..27a4d8c 100644
--- a/examples/worker.html
+++ b/examples/worker.html
@@ -4,8 +4,8 @@
     <meta charset="utf-8">
     </head>
   <body>
-    <script type="module">
-      const worker = new Worker("worker.js");
+    <script>
+      const worker = new Worker("./worker.js", {type:"module"});
       worker.onmessage = e => {
           console.log('Main: onMessage', e.data)
       }
diff --git a/examples/worker.js b/examples/worker.js
index 37794e8..68c9f83 100644
--- a/examples/worker.js
+++ b/examples/worker.js
@@ -1,22 +1,26 @@
+// @flow
+
 import { Fd } from "../src/fd.js";
 import { File, Directory } from "../src/fs_core.js";
-import { Fdstat } from "../src/wasi_defs.js";
+import { Fdstat, FILETYPE_UNKNOWN, FDFLAGS_APPEND } from "../src/wasi_defs.js";
 import { PreopenDirectory } from "../src/fs_fd.js";
 import WASI from "../src/wasi.js";
 import { strace } from "../src/strace.js"
 
 class XTermStdio extends Fd {
-    constructor(term) {
+    /*::term: { write: (Uint8Array) => mixed }*/
+
+    constructor(term/*: { write: (Uint8Array) => mixed }*/) {
         super();
         this.term = term;
     }
-    fd_read(x, y) {
-        console.log("Reading!", x, y)
-        return { ret: 0 }
+    fd_read(view8, iovs) {
+        console.log("Reading!", view8, iovs);
+        return { ret: 0, nread: 0 };
     }
     fd_fdstat_get() {
         console.log("FDSTAT")
-        return { ret: 0, fdstat: new Fdstat() };
+        return { ret: 0, fdstat: new Fdstat(FILETYPE_UNKNOWN, FDFLAGS_APPEND) };
     }
     fd_write(view8, iovs) {
         console.log("Writing!")
@@ -38,7 +42,11 @@ onmessage = function(e) {
 
   (async function () {
     const wasm = await WebAssembly.compileStreaming(fetch("tiny-brot.wasm"));
-    const term = {}
+    const term = {
+        write: (buf) => {
+
+        }
+    }
     const fds = [
         new XTermStdio(term),
         new XTermStdio(term),
diff --git a/src/wasi.js b/src/wasi.js
index d4cd18a..5b6ae8f 100644
--- a/src/wasi.js
+++ b/src/wasi.js
@@ -433,8 +433,39 @@ export default class WASI {
                     return wasi.ERRNO_BADF;
                 }
             },
-            poll_oneoff(in_, out, nsubscriptions) {
-                return 0
+            poll_oneoff(in_ptr, out_ptr, nsubscriptions) {
+                // in_: *const subscription
+                // out: *mut event
+                // nsubscription: usize
+                let buffer = new DataView(self.inst.exports.memory.buffer);
+                let in_ = wasi.Subscription.read_bytes_array(
+                    buffer,
+                    in_ptr,
+                    nsubscriptions,
+                );
+                console.log("poll_oneoff", in_, out_ptr, nsubscriptions);
+                let events = [];
+                for (let sub of in_) {
+                    if (sub.u.tag.variant == "fd_read") {
+                        let event = new wasi.Event();
+                        event.userdata = sub.userdata;
+                        event.error = 0;
+                        event.type = new wasi.EventType("fd_read");
+                        event.fd_readwrite = new wasi.EventFdReadWrite(1n, new wasi.EventRwFlags());
+                        events.push(event);
+                    }
+                    if (sub.u.tag.variant == "fd_write") {
+                        let event = new wasi.Event();
+                        event.userdata = sub.userdata;
+                        event.error = 0;
+                        event.type = new wasi.EventType("fd_write");
+                        event.fd_readwrite = new wasi.EventFdReadWrite(1n, new wasi.EventRwFlags());
+                        events.push(event);
+                    }
+                }
+                console.log(events);
+                wasi.Event.write_bytes_array(buffer, out_ptr, events);
+                return events.length;
                 throw "async io not supported";
             },
             proc_exit(exit_code/*: number*/) {
diff --git a/src/wasi_defs.js b/src/wasi_defs.js
index a2f8322..7f34a0e 100644
--- a/src/wasi_defs.js
+++ b/src/wasi_defs.js
@@ -348,3 +348,153 @@ export class Prestat {
         this.inner.write_bytes(view, ptr + 4);
     }
 }
+
+/*::declare type UserData = BigInt */ // u64
+
+export class EventType {
+    /*:: variant: "clock" | "fd_read" | "fd_write"*/
+
+    constructor(variant/*: "clock" | "fd_read" | "fd_write"*/) {
+        this.variant = variant;
+    }
+
+    static from_u8(data/*: number*/)/*: EventType*/ {
+        switch (data) {
+            case EVENTTYPE_CLOCK:
+                return new EventType("clock");
+            case EVENTTYPE_FD_READ:
+                return new EventType("fd_read");
+            case EVENTTYPE_FD_WRITE:
+                return new EventType("fd_write");
+            default:
+                throw "Invalid event type " + String(data);
+        }
+    }
+
+    to_u8()/*: number*/ {
+        switch (this.variant) {
+            case "clock":
+                return EVENTTYPE_CLOCK;
+            case "fd_read":
+                return EVENTTYPE_FD_READ;
+            case "fd_write":
+                return EVENTTYPE_FD_WRITE;
+            default:
+                throw "unreachable";
+        }
+    }
+}
+
+export class EventRwFlags {
+    /*:: hangup: bool*/
+
+    static from_u16(data/*: number*/)/*: EventRwFlags*/ {
+        let self = new EventRwFlags();
+        if ((data & EVENTRWFLAGS_FD_READWRITE_HANGUP) == EVENTRWFLAGS_FD_READWRITE_HANGUP) {
+            self.hangup = true;
+        } else {
+            self.hangup = false;
+        }
+        return self;
+    }
+
+    to_u16()/*: number*/ {
+        let res = 0;
+        if (self.hangup) {
+            res = res | EVENTRWFLAGS_FD_READWRITE_HANGUP;
+        }
+        return res;
+    }
+}
+
+export class EventFdReadWrite {
+    /*:: nbytes: BigInt*/
+    /*:: flags: EventRwFlags*/
+
+    constructor(nbytes/*: BigInt*/, flags/*: EventRwFlags*/) {
+        this.nbytes = nbytes;
+        this.flags = flags;
+    }
+
+    write_bytes(view/*: DataView*/, ptr/*: number*/) {
+        view.setBigUint64(ptr, this.nbytes, true);
+        view.setUint16(ptr + 8, this.flags.to_u16(), true);
+    }
+}
+
+export class Event {
+    /*:: userdata: UserData*/
+    /*:: error: number*/
+    /*:: type: EventType*/
+    /*:: fd_readwrite: EventFdReadWrite | null*/
+
+    write_bytes(view/*: DataView*/, ptr/*: number*/) {
+        view.setBigUint64(ptr, this.userdata, true);
+        view.setUint32(ptr + 8, this.error, true);
+        view.setUint8(ptr + 12, this.type.to_u8());
+        if (this.fd_readwrite) {
+            this.fd_readwrite.write_bytes(view, ptr + 16);
+        }
+    }
+
+    static write_bytes_array(view/*: DataView*/, ptr/*: number*/, events/*: Array<Event>*/) {
+        for (let i = 0; i < events.length; i++) {
+            events[i].write_bytes(view, ptr + 32 * i);
+        }
+    }
+}
+
+export class SubscriptionClock {
+    // FIXME
+}
+
+export class SubscriptionFdReadWrite {
+    /*:: fd: number*/
+
+    static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ {
+        let self = new SubscriptionFdReadWrite();
+        self.fd = view.getUint32(ptr, true);
+        return self;
+    }
+}
+
+export class SubscriptionU {
+    /*:: tag: EventType */
+    /*:: data: SubscriptionClock | SubscriptionFdReadWrite */
+
+    static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionU*/ {
+        let self = new SubscriptionU();
+        self.tag = EventType.from_u8(view.getUint8(ptr));
+        switch (self.tag.variant) {
+            case "clock":
+                break; // FIXME implement
+            case "fd_read":
+            case "fd_write":
+                self.data = SubscriptionFdReadWrite.read_bytes(view, ptr + 4);
+                break;
+            default:
+                throw "unreachable";
+        }
+        return self;
+    }
+}
+
+export class Subscription {
+    /*:: userdata: UserData */
+    /*:: u: SubscriptionU */
+
+    static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: Subscription*/ {
+        let subscription = new Subscription();
+        subscription.userdata = view.getBigUint64(ptr, true);
+        subscription.u = SubscriptionU.read_bytes(view, ptr + 8);
+        return subscription;
+    }
+
+    static read_bytes_array(view/*: DataView*/, ptr/*: number*/, len/*: number*/)/*: Array<Subscription>*/ {
+        let subscriptions = [];
+        for (let i = 0; i < len; i++) {
+            subscriptions.push(Subscription.read_bytes(view, ptr + 12 * i));
+        }
+        return subscriptions;
+    }
+}

I might clean it up at a later time, but there are currently some things going on that have priority.

@TristanCacqueray
Copy link
Author

Thank you, that's super helpful. It seems like returning FILETYPE_CHARACTER_DEVICE in fd_fdstat_get is necessary too.

TristanCacqueray added a commit to TristanCacqueray/browser_wasi_shim that referenced this issue Mar 1, 2023
@TristanCacqueray
Copy link
Author

I updated the worker-example branch with the changes along with a simpler wasm program that just try to read a single char from stdin. I tried different Fdstat values so that the program doesn't close the stdin, but still no fd_read: the program keeps failing with: <stdin>: hGetChar: resource exhausted (Argument list too long).

According to ghc errno.js, this happens because of the E2BIG errno, perhaps @TerrorJack or @hsyl20 would know what could be the cause?

@TerrorJack
Copy link
Contributor

@TristanCacqueray I haven't taken a closer look at the examples yet; what kind of stdin data do you want to feed into the wasm instance, btw? Is it just a plain buffer available before wasm instantiates, or does it come from user input throughout the instance's lifetime?

I think it may be a low effort to enhance poll_oneoff implementations in wasi libs to deal with the former case, but the latter case may need some more heavyweight approach, so I'd like to understand the use case better.

@TristanCacqueray
Copy link
Author

@TerrorJack first I'd like to setup xtermjs to handle interactive wasm instance, so stdin would be fed with keyboard input char or control sequence. Though it would be nice to handle all the use-cases, e.g.: plain buffer, block based input and character input.

@TerrorJack
Copy link
Contributor

@TristanCacqueray So your Haskell logic is a plain loop that polls stdin? That wouldn't work as of now, but a feasible workaround is to restructure your main, instead of a main loop, you expose a callback that takes whatever input chunk that recently became available in xterm, and the js glue invokes that callback. That would also ensure that poll_oneoff wouldn't be needed at runtime.

@TristanCacqueray
Copy link
Author

@TerrorJack well the goal is to enable running the tiny-game-hs in the browser. It seems like some of them already work unmodified with wasmtime, and perhaps they could also work with @bjorn3 wasi shim, e.g. using the worker+atomics solution discussed above.

@bjorn3
Copy link
Owner

bjorn3 commented Mar 1, 2023

Looks like I messed up the abi of poll_oneoff. The event count needs to be written to the memory referenced by a pointer passed as 4th argument and a value of 0 needs to be returned on success. Instead I directly returned the event count, which is interpreted as E2BIG for a certain event count.

@TristanCacqueray
Copy link
Author

@bjorn3 ok, I think I managed to fix the abi, but this results in the poll_oneoff/clock_time_get loop again. I also tried to implement clock event and monotonic clock_time_get, but without luck so far.

@bradrn
Copy link

bradrn commented Jul 5, 2023

I’ve been running into this same issue. An easy workaround (as pointed out on the Haskell Discourse by @brandonchinn178) is to just avoid stdin/stdout altogether and compile the application as a WASI reactor instead. However, I’ve since realised that this issue blocks any kind of asynchronicity on the Haskell side at all (e.g. halting a computation on a timeout). It would be really nice to get this fixed!

@igrep
Copy link

igrep commented Dec 28, 2023

I had the same problem. In my case, I compiled (part of) pandoc as wasm32-wasi, and the entire input is available before running the module. So I implemented poll_oneoff in a simple way like this: https://gist.github.com/igrep/0cf42131477422ebba45107031cd964c

I wish this helps someone with a similar use case!

@TerrorJack
Copy link
Contributor

https://gitlab.haskell.org/ghc/ghc/-/merge_requests/11697 should hopefully mitigate the need for poll_oneoff in the ghc wasm backend when it comes to stdin/stdout/stderr handles

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants