Skip to content

Commit

Permalink
wasm: Don't suppress exceptions during main()
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Morten Sørvig committed Dec 20, 2023
1 parent ac4619a commit db93cd4
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 4 deletions.
4 changes: 2 additions & 2 deletions mkspecs/features/wasm/wasm.prf
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/corelib/Qt6WasmMacros.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
17 changes: 17 additions & 0 deletions src/plugins/platforms/wasm/qtloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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?.({
Expand Down
12 changes: 12 additions & 0 deletions tests/manual/wasm/qtloader_integration/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ void crash()
std::abort();
}

void stackOverflow()
{
stackOverflow(); // should eventually termniate with exception
}

void exitApp()
{
emscripten_force_exit(ExitValueFromExitApp);
Expand Down Expand Up @@ -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();
Expand Down
28 changes: 27 additions & 1 deletion tests/manual/wasm/qtloader_integration/test_body.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,37 @@ 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);
assert.isUndefined(exitStatus.code);
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);
}

Expand Down

0 comments on commit db93cd4

Please sign in to comment.