-
Notifications
You must be signed in to change notification settings - Fork 18k
syscall/js: allow synchronous callbacks #26045
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
Comments
@myitcv Thanks for creating this issue. Unfortunately I don't have a clean solution for this yet. It is kind of a fundamental conflict between Go's and JS' execution models. The solution that GopherJS uses works, but I'm not really happy about. I'd rather find a cleaner way if possible. This will become even more complicated as soon as threads are available for WebAssembly. Maybe other people have ideas. Let's discuss. /cc @bradfitz @aclements |
The main issue here is that there are JS APIs that expect a callback to synchronously return a value, in contrast to accepting a promise. I haven't yet seen such a case in the DOM API, but some 3rd party JS libraries (e.g. React, mentioned above) seem to do it. |
Not sure what the TL;DR of that video is, but I'd guess it's related to the point about event callbacks?
But I suspect that's largely because the event callbacks in the DOM don't have a return value. Instead, their "contract" is that the event passed as an argument be handled synchronously, and the "contact" of the element for which the event fired is also that the state will be X for the duration of the callback, but afterwards "all bets are off". |
I agree that the Still, we would need some way to express in Go that some code is special in the regard that it runs on the main JS event loop, whereas other Go code is not bound to the JS event loop. So what happens if you block while being bound to the event loop? Do we want to crash, like GopherJS does? If you want to access shared data and you want to use a mutex, can that mutex make it crash if it blocks? Or do we only want to crash if the goroutine on the event loop is blocked AND there is no other goroutine to run? |
I've investigated this some more, with two new insights:
|
One thing I would worry about with an approach that involves continuing to schedule on the Go side until the callback returns is that it could cause deadlocks across the JS -> Go boundary, even without nested callbacks. For example, if the Go callback blocks because it's waiting on a channel message from another callback, but that other callback can't run until the JS event loop runs again, then we're stuck. I'm not sure, but this would seem like a desirable and common pattern. OTOH, any potential solution to this may be fundamentally incompatible with JavaScript's single-threaded event loop, and when programming in mixed JS/wasm perhaps you just have to be conscious of the single-threaded event loop and consider that in your concurrency model. I wonder if this is something that's worth pushing back up to the WebAssembly standards committee. It's possible this could dovetail nicely with the work on threads. I'm not sure what form that's taking, but, for example, if the JavaScript event loop were thought of as just another thread communicating calls and returns with the wasm thread(s), then the Go callback returning could be treated as just a message to resume the blocked JavaScript call. (Implementation-wise, it would be nice if any thread could resume the blocked JavaScript call, but we could build that behavior even if it wasn't directly supported by wasm.) |
That's my gut feel on all of this too. My distant memory of C# and .Net days is that there's a single GUI thread. I think of JS's event-loop thread in much the same way. Else race conditions on things like DOM access become horrendous. |
Yes, this is accurate and I currently think there is no way to avoid it. I hope that higher level libraries will be able to hide this from users.
From looking at the proposal for WebAssembly threads, it looks as if each thread will run in its own JavaScript execution context, similar to Web Workers. This means that only the main thread (on the event loop) is able to access the DOM. WebAssembly code on the main thread will always have to be called from JS code and has to always return the execution to JS so the event loop can continue. In short: The main thread IS the event loop. |
I'd say we should remove the release-blocker tag on this issue. Yes, it is something we should definitely address in the future and it limits what can be done with the first release, but the solution will be involved so it is unlikely to land for 1.11. |
@neelance sounds totally sensible to me. @bradfitz @aclements - you've been looking at this work more than me, any thoughts/objections? |
I've removed the release-blocker label for now. @bradfitz @aclements please shout if that's an issue from your perspective. |
@myitcv We should also change the milestone of this issue. |
Isn't this also true in pure Javascript too? They just get around it with promises or something -- if they do it at all. Frankly making the innards of one callback synchronously dependent on the innards of a different callback seems like a bad idea. What would that even mean? You can't finish pressing this button until you also press that button? No, I think you need to just start a separate goroutine for both of them. (Disclaimer: my frontend experience is limited.) That said, I could imagine that it'd be pretty cool if the runtime could run the handler as far as it could go in the main thread until it blocks, and then run the rest of the handler in a different goroutine. So, like, a blocking operation in a callback would automagically |
Once this is fixed, I think |
How about just |
I'm currently working on this. The WIP can be found at https://github.com/neelance/go/tree/wasm-sync-callbacks. Anyone interested in giving this a try can give feedback to me directly on the Gophers slack. |
With this change, callbacks returned by syscall/js.NewCallback get executed synchronously. This is necessary for the APIs of many JavaScript libraries. Fixes golang#26045 Fixes golang#27441 Change-Id: I591b9e85ab851cef0c746c18eba95fb02ea9e85b
With this change, callbacks returned by syscall/js.NewCallback get executed synchronously. This is necessary for the APIs of many JavaScript libraries. Fixes golang#26045 Fixes golang#27441 Change-Id: I591b9e85ab851cef0c746c18eba95fb02ea9e85b
Change https://golang.org/cl/142004 mentions this issue: |
With this change, callbacks returned by syscall/js.NewCallback get executed synchronously. This is necessary for the APIs of many JavaScript libraries. Synchronous calls to callbacks that have been triggered by calls from Go to JavaScript get executed on the same goroutine. Asynchronous callbacks get executed on an extra goroutine. Fixes golang#26045 Fixes golang#27441 Change-Id: I591b9e85ab851cef0c746c18eba95fb02ea9e85b
Hey everyone. The CL is now polished and passing all tests. I would appreciate if you could test it with your own use cases and post the results here or on the CL. Thanks! PS: The CL's code can also be found here: https://github.com/neelance/go/tree/wasm-sync-callbacks |
Tested branch with https://github.com/johanbrandhorst/grpcweb-wasm-example and https://github.com/johanbrandhorst/wasm-experiments, both which make extensive use of the callbacks in the WASM Roundtripper. No problems encountered. |
See golang/go#26045 (comment) and https://github.com/neelance/go/tree/wasm-sync-callbacks. All examples work (as good as they did before, at least) with native syscall/js. I'd ripped out some code that required sync callbacks, which I haven't put back in yet. Also GopherJS not tested at all.
I've updated my Vue.js wrapper hvue (see automated mentions above) to work with your wasm-sync-callbacks branch. Synchronous callbacks which return values and can access I didn't fiddle around with blocking calls in event handlers and whether or not (or how) this blocks the main JS event loop. Any thoughts on renaming |
@theclapp I agree that the term "callback" is a bit arbitrary. I just tried to change it to |
Please answer these questions before submitting your issue. Thanks!
What version of Go are you using (
go version
)?6fdbed0
Does this issue reproduce with the latest release?
No; this is only present in
tip
.What operating system and processor architecture are you using (
go env
)?What did you do?
Picking up a conversation with @neelance on Slack, which picks up on the following exchange: https://go-review.googlesource.com/c/go/+/114197/8/src/syscall/js/callback.go#57.
Migrating this bit of code from GopherJS to WASM:
requires that I provide a function that (synchronously) returns a boolean value. Given the current async nature of callbacks this is not possible.
Furthermore, I think the use of async callbacks for handling of events is a potential problem/source of bugs. Let's assume I have a calIback for an
onChange
event. And let's assume I've used thesyscall/js.NewEventCallback
function to ensurePreventDefault
. By the time our WASM "callback" has fired, the actual JS event callback will have already returned. Meaning the state of the thing for which we got the event callback might well have changed.Therefore, to my mind this requires that we provide a way to build synchronous callbacks (functions), adding the caveats/warnings etc that @neelance describes in https://go-review.googlesource.com/c/go/+/114197/8/src/syscall/js/callback.go#57 that people should not block in such a function (or use a go routine to carry out blocking calls).
@neelance hopefully this accurately captures our conversation. I've added the "release-blocker" tag... but that can obviously be removed if we conclude that it's not critical, but based on our discussion I think, for now, it is.
The text was updated successfully, but these errors were encountered: