wasm: Don't suppress exceptions during main()

If there's e.g. an infinite loop during main() that
would previously result in a blank page, but not error
message. The expected case is that we would get a RangeError
exception, but that exception never reaches the catch
handlers in qtloader.js.

Work around this by setting noInitialRun, followed by
calling main manually. We then need to handle the case
where the app.exec() workaround throws, which should
not trigger an error.

Pick-to: 6.7
Change-Id: Ia8431279308770981316cd168e4316341bfb2531
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
This commit is contained in:
Morten Sørvig 2023-10-25 12:42:02 +02:00 committed by Morten Johan Sørvig
parent ac4619a36a
commit db93cd4f61
5 changed files with 59 additions and 4 deletions

View File

@ -9,9 +9,9 @@ exists($$QMAKE_QT_CONFIG) {
## qmake puts a space if done otherwise
!isEmpty(QT_WASM_EXTRA_EXPORTED_METHODS): {
EXPORTED_METHODS = UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS,$$QT_WASM_EXTRA_EXPORTED_METHODS
EXPORTED_METHODS = UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS,callMain,$$QT_WASM_EXTRA_EXPORTED_METHODS
} else {
EXPORTED_METHODS = UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS
EXPORTED_METHODS = UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS,callMain
}
EMCC_LFLAGS += -s EXPORTED_RUNTIME_METHODS=$$EXPORTED_METHODS

View File

@ -97,7 +97,7 @@ endfunction()
function(_qt_internal_add_wasm_extra_exported_methods target)
get_target_property(wasm_extra_exported_methods "${target}" QT_WASM_EXTRA_EXPORTED_METHODS)
set(wasm_default_exported_methods "UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS")
set(wasm_default_exported_methods "UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS,callMain")
if(NOT wasm_extra_exported_methods)
set(wasm_extra_exported_methods ${QT_WASM_EXTRA_EXPORTED_METHODS})

View File

@ -105,6 +105,12 @@ async function qtLoad(config)
config.qtFontDpi = config.qt.fontDpi;
delete config.qt.fontDpi;
// Make Emscripten not call main(); this gives us more control over
// the startup sequence.
const originalNoInitialRun = config.noInitialRun;
const originalArguments = config.arguments;
config.noInitialRun = true;
// Used for rejecting a failed load's promise where emscripten itself does not allow it,
// like in instantiateWasm below. This allows us to throw in case of a load error instead of
// hanging on a promise to entry function, which emscripten unfortunately does.
@ -219,7 +225,18 @@ async function qtLoad(config)
try {
instance = await Promise.race(
[circuitBreaker, config.qt.entryFunction(config)]);
// Call main after creating the instance. We've opted into manually
// calling main() by setting noInitialRun in the config. Thie Works around
// issue where Emscripten suppresses all exceptions thrown during main.
if (!originalNoInitialRun)
instance.callMain(originalArguments);
} catch (e) {
// If this is the exception thrown by app.exec() then that is a normal
// case and we suppress it.
if (e == "unwind") // not much to go on
return;
if (!onExitCalled) {
onExitCalled = true;
config.qt.onExit?.({

View File

@ -69,6 +69,11 @@ void crash()
std::abort();
}
void stackOverflow()
{
stackOverflow(); // should eventually termniate with exception
}
void exitApp()
{
emscripten_force_exit(ExitValueFromExitApp);
@ -143,8 +148,15 @@ int main(int argc, char **argv)
if (crashImmediately)
crash();
const bool stackOverflowImmediately =
std::find(arguments.begin(), arguments.end(), QStringLiteral("--stack-owerflow-immediately"))
!= arguments.end();
if (stackOverflowImmediately)
stackOverflow();
const bool noGui = std::find(arguments.begin(), arguments.end(), QStringLiteral("--no-gui"))
!= arguments.end();
if (!noGui) {
AppWindow window;
window.show();

View File

@ -345,7 +345,7 @@ export class QtLoaderIntegrationTests
caughtException = e;
}
assert.isUndefined(caughtException);
assert.isTrue(caughtException !== undefined);
assert.equal(1, onExitMock.calls.length);
const exitStatus = onExitMock.calls[0][0];
assert.isTrue(exitStatus.crashed);
@ -353,6 +353,32 @@ export class QtLoaderIntegrationTests
assert.isNotUndefined(exitStatus.text);
}
async stackOwerflowImmediately()
{
const onExitMock = new Mock();
let caughtException;
try {
await qtLoad({
arguments: ['--no-gui', '--stack-owerflow-immediately'],
qt: {
onExit: onExitMock,
entryFunction: tst_qtloader_integration_entry,
}
});
} catch (e) {
caughtException = e;
}
assert.isTrue(caughtException !== undefined);
assert.equal(1, onExitMock.calls.length);
const exitStatus = onExitMock.calls[0][0];
assert.isTrue(exitStatus.crashed);
assert.isUndefined(exitStatus.code);
// text should be "RangeError: Maximum call stack
// size exceeded", or similar.
assert.isNotUndefined(exitStatus.text);
}
async userAbortCallbackCalled()
{
const onAbortMock = new Mock();