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

Looking for a way to report unhandled c++ exceptions to 3rd party crash reporting system #22613

Open
kurrak opened this issue Sep 24, 2024 · 10 comments

Comments

@kurrak
Copy link

kurrak commented Sep 24, 2024

This is a question, not a bug.

We're trying to find a generic way to report stack traces for all exceptions unhandled in c++ (meaning: also for c++ exceptions that are handled in JS). We're using -fwasm-exceptions and -sEXCEPTION_STACK_TRACES.

We found out that Emscripten's ___throw_exception_with_stack_trace JS function is called whenever c++ exception is thrown. What we'd like to do is plug into this function, so we can get a stack trace and send it to 3rd party crash reporting system.

We've been trying different approaches, but had little to no success. We're looking for any guidance, or the reason for why it is bad/wrong idea in the first place.

Version of emscripten/emsdk:

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.61 (67fa4c16496b157a7fc3377afd69ee0445e8a6e3)
clang version 19.0.0git (https:/github.com/llvm/llvm-project 7cfffe74eeb68fbb3fb9706ac7071f8caeeb6520)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /Users/kurak/Developer/emsdk/upstream/bin
@sbc100
Copy link
Collaborator

sbc100 commented Sep 24, 2024

IIUC the way this is normally done on the web is to install a global exception handler on your page and report all unhandled exceptions that way. Exceptions thrown from native code (when using EXCEPTION_STACK_TRACES) should have stack traces attacked just like Error's thrown by your JS code.

I think the window.onerror handler is the way to install a global error handler (at least on the web).

@aheejin might know more here.

@kurrak
Copy link
Author

kurrak commented Sep 24, 2024

@sbc100 thanks for reply and thoughts. Yes, we're working on web solution.

The problem with window.onerror handler is that it is not called for errors that are handled in JS. We're trying to collect stack traces for all c++ exceptions not handled in c++. This is important, because we cannot guarantee that JS clients of our c++ API are never catching errors somewhere in call stack that in the end reaches out to c++.

To be honest, plugging into ___throw_exception_with_stack_trace is not great solution either, as it is also called when c++ exception is handled in c++ (we wouldn't like to report such events). I'd love to know if there is any way to distinguish these two scenarios, too.

Side note: window.onerror is also not called for errors that are thrown from c++ code called from within JS promise (in this case browser logs error starting with Uncaught (in promise) message), which translates to JS promise rejection. This can be easly handled by additional window.onUnhandledRejection, though.

@sbc100
Copy link
Collaborator

sbc100 commented Sep 24, 2024

So in addition to onerror + onUnhandledRejection you would like to know when one of your users catches and exception generated by your code? This sounds like a somewhat unusual request, but there are probably ways you can me it work.

One way to do this would be to ask you users to call your exception reporting function whenever they catch and ignore exceptions from your code.

Another way would be something like ABORT_ON_WASM_EXCEPTIONS which wraps ever wasm export in a try catch. See

emscripten/src/preamble.js

Lines 525 to 602 in 543993e

#if ABORT_ON_WASM_EXCEPTIONS
// `abortWrapperDepth` counts the recursion level of the wrapper function so
// that we only handle exceptions at the top level letting the exception
// mechanics work uninterrupted at the inner level. Additionally,
// `abortWrapperDepth` is also manually incremented in callMain so that we know
// to ignore exceptions from there since they're handled by callMain directly.
var abortWrapperDepth = 0;
function makeAbortWrapper(original) {
return (...args) => {
// Don't allow this function to be called if we're aborted!
if (ABORT) {
throw 'program has already aborted!';
}
abortWrapperDepth++;
try {
return original(...args);
} catch (e) {
if (
ABORT // rethrow exception if abort() was called in the original function call above
|| abortWrapperDepth > 1 // rethrow exceptions not caught at the top level if exception catching is enabled; rethrow from exceptions from within callMain
#if SUPPORT_LONGJMP == 'emscripten' // Rethrow longjmp if enabled
#if EXCEPTION_STACK_TRACES
|| e instanceof EmscriptenSjLj // EXCEPTION_STACK_TRACES=1 will throw an instance of EmscriptenSjLj
#else
|| e === Infinity // EXCEPTION_STACK_TRACES=0 will throw Infinity
#endif // EXCEPTION_STACK_TRACES
#endif
|| e === 'unwind'
) {
throw e;
}
abort('unhandled exception: ' + [e, e.stack]);
}
finally {
abortWrapperDepth--;
}
}
}
// Instrument all the exported functions to:
// - abort if an unhandled exception occurs
// - throw an exception if someone tries to call them after the program has aborted
// See settings.ABORT_ON_WASM_EXCEPTIONS for more info.
function instrumentWasmExportsWithAbort(exports) {
// Override the exported functions with the wrappers and copy over any other symbols
var instExports = {};
for (var name in exports) {
var original = exports[name];
if (typeof original == 'function') {
instExports[name] = makeAbortWrapper(original);
} else {
instExports[name] = original;
}
}
return instExports;
}
function instrumentWasmTableWithAbort() {
// Override the wasmTable get function to return the wrappers
var realGet = wasmTable.get;
var wrapperCache = {};
wasmTable.get = (i) => {
var func = realGet.call(wasmTable, i);
var cached = wrapperCache[i];
if (!cached || cached.func !== func) {
cached = wrapperCache[i] = {
func,
wrapper: makeAbortWrapper(func)
}
}
return cached.wrapper;
};
}
#endif
. This approach does have runtime cost as well as code size cost though.

@aheejin
Copy link
Member

aheejin commented Sep 24, 2024

I'm not sure if I understood your request correctly. When -sEXCEPTION_STACK_TRACES (or -sASSERIONS or -O0, both of which set -sEXCEPTION_STACK_TRACES) is set, all native exceptions thrown in C++ call out to JS ___throw_exception_with_stack_trace to get the stack traces embedded in the object, as you've observed. At this point we don't know where or whether exception is going to be handled. I'm not sure what you mean by "plugging into" ___throw_exception_with_stack_trace.

And whenever you have that exception object in JS, you can access its WebAssebly.stack property to get the stack trace string. It can be any of the handlers you mentioned.

I guess I don't understand what the problem is correctly. Can you elaborate more?

@kurrak
Copy link
Author

kurrak commented Sep 25, 2024

@aheejin, @sbc100 thanks for replies 🙌

I guess I don't understand what the problem is correctly. Can you elaborate more?

Sure! Here's the thing:

  • I maintain c++ library which is used in browsers via wasm compiled with Emscripten
  • The library has APIs (hounders) for JS clients to use; emscripten run loop is also used internally, so library's c++ code is called stating from both contexts: client JS code and internal run loop
  • The library uses c++ exceptions internally for error handling
    • The intent is to catch and handle c++ exceptions in c++ and to treat not handled exceptions similarly to crash (they should never happen)
  • If this makes any difference, I'm compiling with -fwasm-exceptions
  • I don't control JS clients and my goal is for clients to
    • have no idea that APIs exported from my library is implemented in wasm
    • have no obligations for handling my library crashes / unhandled errors by sending them to my crash reporting system

One way to do this would be to ask you users to call your exception reporting function whenever they catch and ignore exceptions from your code.

This is specifically something I try to avoid.

Another way would be something like ABORT_ON_WASM_EXCEPTIONS which wraps ever wasm export in a try catch. See

ABORT_ON_WASM_EXCEPTIONS sounds like a behaviour I want: to treat c++ exception not handled in c++ similarly to c++ crash. What I was missing before was the customisation point that I can plug my code to send proper stack trace to crash reporting system (window.onerror and window.onunhandledrejection events are not an option, because they can be easily not called whenever client code catches exception anywhere along throwing call stack). Now I see there is Module.onAbort callback that seems exactly like what I need.

The challenge now is to get correct stack trace as onAbort is called differenty depending on scenario:

  • when c++ calls abort() function - onAbort's what parameter is string with "cause" (which can be empty), but "current" call stack contains all necessary frames
  • when c++ exception is not handled – onAbort's what parameter contains string with call stack I'm interested in, but "current" call stack is just JS frames

Do you see a good/correct way to distinguish these two scenarios in onAbort callback so I can decide how to get correct call stack? Do you know there are other ways onAbort can be called that I should handle differently?

As a side note: do you think onAbort should receive WebAssembly.Exception for scenario with not handled c++ exception?

@sbc100
Copy link
Collaborator

sbc100 commented Sep 25, 2024

So just to be clear, if I'm a user of your library, and your library crashes as part of my application, you want the crash report to go to your server by default? Wouldn't that be something that I, as the user of the library, would want to opt into explictly?

@sbc100
Copy link
Collaborator

sbc100 commented Sep 25, 2024

So just to be clear, if I'm a user of your library, and your library crashes as part of my application, you want the crash report to go to your server by default? Wouldn't that be something that I, as the user of the library, would want to opt into explictly?

Or is this actually a normal/common thing to do in the world of JS libraries? (I'm not super familiar with the latest JS library trends).

@kurrak
Copy link
Author

kurrak commented Sep 25, 2024

So just to be clear, if I'm a user of your library, and your library crashes as part of my application, you want the crash report to go to your server by default? Wouldn't that be something that I, as the user of the library, would want to opt into explictly?
Or is this actually a normal/common thing to do in the world of JS libraries? (I'm not super familiar with the latest JS library trends).

To be specific: this is internal library used by multiple client apps within our organisation. That being said this behaviour is opt-in and available behind a setting passed during library setup. I want to keep it this way: one time configuration and no more responsibility on client app side for this to work.

@kurrak
Copy link
Author

kurrak commented Sep 26, 2024

@sbc100 our current idea for getting the access to correct call stack is to use --post-js script to override Emscripten-generated abort function. We'd wrap it in additional try/catch, get the call stack in catch block and rethrow it. We think this should work ok nevertheless of aborting scenario (crash vs. unhandled exception). Do you think it is a good approach?

@oatgnauh
Copy link

there is a try .. catch in side handleMessage of file worker.mjs, you may want to process with the exception throw

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

No branches or pull requests

4 participants