-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Undocumented Module.then causes infinite loop #5820
Comments
This is anyway an undocumented feature, we should be able to rename this to |
I have production code using This is an odd situation, because if I understand correctly,
This would remove the entire point of making If it was changed to make It would probably be worth putting all the loading errors through the same system so that you could call And it's not completely undocumented, it is mentioned here: http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html#modular-output It should really be mentioned elsewhere in the docs too. |
It seems I had misunderstood the concept of thenable. Any thenable object will receive two function arguments as the Promise constructor callback does, which means that it won't be resolved until one of the arguments are called. PS: So I modified the original post. |
This is the minimal repro of the Promise thenable behavior: var o = { then(resolve, reject) { console.log("called then()"); resolve(o) } };
Promise.resolve(o);
// logger logs indefinitely |
Sorry, I'm not really sure what you're trying to do. But a proper thenable should call Here's the simplest example I could find from noq:
Where |
This is not what I'm trying to do, it is what the current postamble.js do. |
Ah, true. I think emscripten is making a valid thenable (aside from ignoring the onRejected arg), but it's not a proper promise. I could be wrong. It probably should be a full proper promise though. |
A valid thenable should not resolve by passing itself, as the greedy algorithm will cause infinite loop. (Forget about the return value story from my previous deleted comment, it was just wrong. Sorry 😭) |
(Ignore my another deleted comment. Sorry) Converting module to a real Promise still may not solve the problem, as the Promise resolver still uses greedy algorithm. new Promise((resolve, reject) => {
resolve(Promise.resolve(3))
}).then(console.log); // logs 3, not Promise (As it will still have to resolve by passing itself to keep compatibility) |
So I would:
|
Or should the MODULARIZE mode Module be effectively a factory? And you only get access to the true Module when your then function is called? |
That would be neat 👍 but is there any use case that requires module access before initialized? Probably not? |
Um, possibly if you have no external files (either no memory or inline memory) then it might be available immediately? It would probably help to outline the use cases for MODULARIZE, so then we can see whether they would be better served by being separated out a little.
Can you think of any others? |
My previous use case for MODULARIZE was to run a CLI command multiple times in a single worker. (as calling |
Was bitten by this exact behaviour -- thanks for the detailed flesh-out of the issue. If someone stumbles upon this, inspiration as a hack-around could look something like this:
|
On the referenced issue on binaryen, a proposed solution would be to make Module.ready a thenable instead of Module itself. (This might of course break some old code). |
I think we can do it as a breaking change, if it's necessary to get a fully working API, and if we add a clear error if people use the old API - could we set |
I don't think there is a way to fix the old Module.then = function(f){
f(Module);
// something like this as the error message?
console.error("Please use Module.ready.then(...) instead.");
delete Module.then;
return Promise.resolve(Module);
} might seem like a solution, but not for long: Module.then(v => console.log(v));
Module.then(v => console.log(v)); // Uncaught TypeError: Instance.then is not a function Alternatively resolving with no value (#5820 (comment)). The new Module['ready'] = new Promise(function(func) {
// We may already be ready to run code at this time. if
// so, just queue a call to the callback.
if (Module['calledRun']) {
func(Module);
} else {
// we are not ready to call then() yet. we must call it
// at the same time we would call onRuntimeInitialized.
var old = Module['onRuntimeInitialized'];
Module['onRuntimeInitialized'] = function() {
if (old) old();
func(Module);
};
}
}); then? |
Stumbled upon this thread when searching for a Promise-based alternative to https://gist.github.com/AnthumChris/c1b5f5526b966011dac39fbb17dacafe |
For anybody stuck on this bug, this is my workaround. Thanks @anthumchris for directions --post-js Module['ready'] = new Promise(function (resolve, reject) {
delete Module['then']
Module['onAbort'] = function (what) {
reject(what)
}
addOnPostRun(function () {
resolve(Module)
})
}) Instantiate new EXPORTED_NAME().ready.then(module => {}) |
Couldn't we have the function at function Module() {
const staticContext = {
ALLOC_DYNAMIC: 3,
ALLOC_NONE: 4,
ALLOC_NORMAL: 0,
// …
};
return new Promise((resolve, reject) => {
// fetch and compile wasm, add exports to `staticContext`
// resolve with modified staticContext if successful, reject otherwise
});
} This would fix ES2017 Module().then(({ export1, export2 }) => {
/* use exports here */
}); from non-asynchronous contexts and const { export1, export2 } = await Module();
/* use exports here */ from asynchronous contexts. |
Merging myself from #6563 into this issue: This is confusing because import myModule from './myModule.js'; // ← This is the file generated by Emscripten
const module = new Promise(resolve => {
myModule({
onRuntimeInitialized() {
resolve(myModule);
}
});
}); I think re-naming |
... a second solution would be to remove the I’m happy to whip up a PR if one of the maintainers can give me a hint which solution is preferred. |
Yes, I mentioned that here as well: #5820 (comment) But the Module would still be a promise that doesn't work as one would expect (just as now). |
I think this might have too many difficulties, and now favour having a ready property. |
This is just throwing a (probably bad) idea into the mix: would it be possible to have the global [1]: Using |
@Taytay That specific problem you're running in to really has nothing to do with this issue. Emscripten expects to be run in a CommonJS environment, and if you use it in another environment that still has a It would be reasonable to add an option to not do any exporting, but that should be a new issue. |
@curiousdannii : I am running in a CommonJS environment, but I still wanted control over the modularization code. I've created a new issue/feature request here, as you suggested: #7835 |
This might be a naive question but why don't you simply delete I just modified the code like so: Module['then'] = function(func) {
delete Module.then;
// ... ... and it works like a charm. It may be hacky but then... couldn't the same be said for other current methods of communicating when the module is ready? (Through the global namespace) |
@s-h-a-d-o-w Because a |
Ah, of course! (I even persist promises in a memoization library I wrote but it's generally something that I very rarely use, so I didn't think of that.) Well... how about documenting here how users can write a wrapper for it? Like: const importWASM = () => {
return new Promise((resolve) => {
require('./webp_wasm.js')().then((module) => {
// Since the module contains a .then(), it needs to be
// wrapped to prevent infinite calls to it.
resolve({module});
});
});
};
const {module} = await importWASM(); |
Just wanted to +1 this and add another case. When using Emscripten with TypeScript, the compiler will not allow the factory function to be awaited on. It gives the error: I'm happy to take on the work to fix this. Instead of adding the Edit: It's also strange that |
Thanks @lourd ! I really would like us to make progress here. I have not been able to do much myself because I am not knowledgeable enough about conventions in the JS ecosystem, and no one else has had the time to work on it it seems. Your suggestion sounds reasonable. I think we can even make it error in a clear way for existing users by, in ASSERTIONS builds, proving an argument to Alternatively, given the severity of this issue, another option is as discussed above to delete I don't have a strong preference between the two (again, I don't know enough about this) so I'd really like to hear from the people that discussed this at length previously, which of those two is best. But I do think we should move forward with one of the two, and soon, as this has been a big footgun for far too long. (Sounds good about the other proposed change, to not return Module from then() - again, with some method to make sure users get a nice error in ASSERTIONS.) |
No, this is a bad option as it's meant to be able to be called more than once. There's some good discussion at #9325 but I'm not sure if that PR should be merged as it's doing too many other things as well.
I like these comments from @kainino0x and think there's a good argument for making the modularised function directly return a promise, because that way there's no accidentally using it before it's ready. For non-modularised builds a Things would be simpler if the non-modularised option was deprecated. (And I think a terminology change would help - we have the confusing fact that "module" refers to what you pass in to it, as well as the object at the end of the process with all of the ASM functions and memory views, while "modularize" is really a factory function.) |
Changing the module-API during lifetime, e.g. being thenable at first, not thenable later, feels wrong. Never seen that kind of behavior in any other API. Removing the then argument may fix the await for module readiness. But if tried to return a module from another async function, Just remove |
I agree changing the API at runtime is icky, and will break on static analysis tools. The most natural API for modern JS I feel is for the module factory function to return a Promise, hiding the actual Module object until it's resolved through the promise. This would be a breaking change, but so would removing the |
Another +1 to not removing Removing I would greatly appreciate if someone else would pick up this task. I'm not working on it anymore; I have another way to achieve what I needed. |
Thanks for the input everyone. It sounds like a good approach would be:
Is that all correct? Should I also make any changes to Any codebase guidance folks have would be appreciated too. Will this change be contained to |
In case of failure, the promise should be rejected with an IIRC what happens now on failure is that an exception gets thrown and then your 'then' callback just never gets called. This is ok for manual debugging but terrible for automatic fallback behavior where you might want to replace the emscripten module with a nice error message or alternate experience. :) |
Edit: @Brion Ah sorry, totally misunderstood the point you were making! ( Hi again by the way ;) ) I'll just keep this here as a possible example for loading modules as discussed earlier: // Promise with success/error handlers
loadModule().then(onModuleLoaded, onModuleFail);
// Promise with fallback and fail handling
loadModule()
.catch(error => { /* try returning fallback module or re-throw */ });
.then(module => { /* use module */ })
.catch(error => { /* show error */ });
// Async style
try {
const module = await loadModule();
/* use module */
} catch (error) {
/* show error */
} |
Whatever the fix, it would be great if someone can contribute minimal test cases for the use cases/loading patterns that are being used that showcase this failure. Reading through the long conversation thread, I don't think I was able to find a test case. (sorry if I missed one) If there was a PR that showcases "I would like to do this, but it fails - if you fix this loading code to work, all would be good", I think that would help towards what is needed. (also, do people have different loading patterns that exhibit this problem from different angles?) |
@pwuertz I mean that not getting a rejection is terrible. When you get one, you can handle it and that's great! :) @juj the failure cases I want to catch are mostly things like "oops we were asked to load module with SIMD instructions but no SIMD support in browser" or "the .wasm file is missing or inaccessible, so can't fully instantiate". The former is dependent on actual browser behavior so might not be suitable for test cases; the latter could be simulated in tests by deliberately corrupting or deleting the wasm file, perhaps. Similar to @pwuertz's samples above, a basic instantiation in classic (non-async) code would look something like: MyCoolModule({
locateFile: blah,
options: foo,
}).then(function(module) {
module._do_something();
}).catch(function(err) {
// This will catch any errors during instantiation or
// at runtime during the 'then' handler.
// We could also put this before the 'then' if we wanted
// to treat instantiation errors separately from runtime errors.
displayError("Error loading module: " + err);
}); You could be fancier with a fallback to loading another module, but the important thing is just getting the 'catch' callback on failure. |
Thanks for the feedback everyone! Sounds like there is general consensus on the solution @lourd and @Brion describe here? It sounds good to me. Perhaps a good next step is a PR from @lourd , including tests (which would also address @juj's point above)? Regarding code, yes, I'd expect it to be just |
Just submitted a PR to start work on this. Would love any review or collaboration on it from folks subscribed here. |
Running into some hairy issues with the changes needed to do this as related to pthreads and the |
This removes the Module.then function and replaces it with a proper Promise being returned from the initialization function. That is, when in MODULARIZE mode, calling `Module()` does not return an instance, but instead returns a Promise you can wait on for when it is ready. Fixes #5820
I'm investigating a browser crash. I came across this issue while browsing issues related to it. Can anyone here confirm that the below pattern is okay or not?
|
Yeah, that looks fine |
Or alternatively:
|
https://github.com/kripken/emscripten/blob/93479ecbd390aec4f8a3765fe04bcb365d0b31b2/src/postamble.js#L118-L141
postamble.js adds
Module.then
when-s MODULARIZE=1
but using it causes indefinite loop onPromise.resolve()
and ES2017await
statement.By the spec the resolving strategy is greedy: if an object has
then
method then it will be called with two function arguments including resolver and rejector. If the resolver is called with an object with athen
method it will be called again with two function arguments, and then again, again.Module.then
resolves withModule
which still hasthen
, so theawait
statement loops indefinitely.PS: Fixed some wrong concepts.
The text was updated successfully, but these errors were encountered: