Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect failure and reset/restart webassembly Module?

I am trying to get some C++ functions to run on the browser using WebSssembly. I am following this tutorial. I would like to know:

  1. How to detect (at JS side) an 'uncaught exception' coming from C++ code?
  2. How to reset/restart WebAssembly Module generated by emcc in a way that avoids memory leaks?

Adding exception catching functionality (DISABLE_EXCEPTION_CATCHING=0) seems to increase the file size too much.

Any help will be greatly appreciated.


The sample C++ code is as follows:

// C++ source code (fib.cc)

#include <stdexcept>
#include <emscripten.h>

extern "C" {

EMSCRIPTEN_KEEPALIVE
int fib(int n) {
  if (n > 12) {
    throw std::out_of_range("input out of range");
  }
  int i, t, a = 0, b = 1;
  for (i = 0; i < n; i++) {
    t = a + b;
    a = b;
    b = t;
  }
  return b;
}

// >>
// other functions with allocations/deallocations

} // end of extern C

It is built with the command:

emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.cc

It is tested with a webpage:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WASM Test Page</title>
</head>
<body>

<script src="a.out.js"></script>
<script>
"use strict";

Module.onRuntimeInitialized = _ => {
  const fib = Module.cwrap('fib', 'number', ['number']);
  console.log(fib(10));
  console.log(fib(14)); // causes exception
};

</script>

</body>
</html>
like image 730
Axeon Thra Avatar asked Apr 03 '20 16:04

Axeon Thra


1 Answers

How to detect (at JS side) an 'uncaught exception' coming from C++ code?

You have to catch the exceptions using try-catch blocks (tutorial).

Example:

try {
    console.log( fib(14) );
}
catch ( e ) {
    console.error( e );
}

But, as of now, it is propagated as a pointer so you will see some number in the console:

5249672

If you want to get a proper error message in JS then you have to write a binding in your C++ code:

#include <emscripten/bind.h>

std::string getExceptionMessage(int eptr)
{
    return reinterpret_cast<std::exception*>(eptr)->what();
}

EMSCRIPTEN_BINDINGS(getExceptionMessageBinding)
{
    emscripten::function("getExceptionMessage", &getExceptionMessage);
};

This would be exposed in the JS code through the Module object. You can use it in JS code like this:

try {
    console.log( fib(14) );
}
catch ( e ) {
    console.error( Module.getExceptionMessage(e) );
}

Output (exception is thrown):

input out of range

Here's the GitHub issue where this has been discussed and suggested.

I've compiled this code with exceptions enabled with C++11 and bindings like this:

~/emsdk/upstream/emscripten$ ./em++ -std=c++11 -Os -fexceptions --bind 
                             -s WASM=1
                             -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
                             -s DISABLE_EXCEPTION_CATCHING=0
                             fib.cc

Here's another similar GitHub issue discussing another approach to this.


Adding exception catching functionality (DISABLE_EXCEPTION_CATCHING=0) seems to increase the file size too much.

If your concern is the increased size of the output files, you can disable exception handling altogether and resort to error checking with invalid values or error codes returning by the function(s) e.g. -1 if the input is invalid.

However, here's an observation:

The file sizes of the previous build were:

110K - a.out.js
187K - a.out.wasm

The exception handling and RTTI for bindings were part of this.

I stripped the code and used inline JS using EM_ASM to throw a JS Error in the following code snippet:

#include <emscripten.h>

extern "C" {

EMSCRIPTEN_KEEPALIVE
int fib(int n) {
    if (n > 12) {
        EM_ASM(
            throw Error("out_of_range");    // JS error with EM_ASM
        );
    }

    int a {0}, b {1};
    for ( int i {0}; i < n; ++i ) {
        const auto t = a + b;
        a = b;
        b = t;
    }
    return b;
}

}

Compiled with exceptions disabled:

$ ./em++ -std=c++11 -Os
  -fno-exceptions 
  -s WASM=1
  -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
  fib1.cc

Here's the HTML file (fib1.html):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WASM Test Page</title>
</head>
<body>

<script src="a.out.js"></script>
<script>
"use strict";

Module.onRuntimeInitialized = _ => {
    const fib = Module.cwrap('fib', 'number', ['number']);

    try {
        console.log(fib(10));
        console.log(fib(14));
    }
    catch ( e ) {
        console.error(e);
     }
};

</script>

</body>
</html>

Console Output (exception caught):

89
fib1.html:21 Error: out_of_range
    at Array.ASM_CONSTS (a.out.js:1)
    at _emscripten_asm_const_i (a.out.js:1)
    at wasm-function[1]:0x6b
    at Module._fib (a.out.js:1)
    at Object.Module.onRuntimeInitialized (fib1.html:18)
    at doRun (a.out.js:1)
    at run (a.out.js:1)
    at runCaller (a.out.js:1)
    at removeRunDependency (a.out.js:1)
    at receiveInstance (a.out.js:1)

And, the file sizes were:

15K - a.out.js
246 - a.out.wasm (bytes)

Throwing a JS Error still works with exceptions disabled and the generated file sizes are far less. You might want to explore this more. Maybe, create some classes inherited from Error with extended functionality. However, the exceptions that are thrown from the standard APIs such as std::vector::at() won't work and cause termination. So, you need to be considerate about those while disabling exceptions.


How to reset/restart WebAssembly Module generated by emcc in a way that avoids memory leaks?

As of now, there is no such API to reset/restart a module. The module itself is automatically garbage collected when it is no longer referenced. You don't have to care about the memory leak in this case. The JS runtime is responsible for this.

But, the C++ object created by the JS code should be destroyed using Module.destroy if it is managing resources (memory, file handles, etc.). The Garbage Collector (GC) won't call the destructor when it collects the object which would result in a memory/resource leak. Calling Module.destory will invoke the destructor and there won't be any memory leaks. Right now, your question doesn't have such an object. So, be mindful when you do and call Module.destory when required.

As for the allocations/deallocations in your C++ code, you yourself are responsible for the deallocation of the resources that you allocate. Here are a few points that might help you in this regard:

  • Avoid Undefined Behavior.

  • Follow the rule of three/five/zero religiously.

  • Use RAII-based C++ standard library facilities for automatic memory management such as std::unique_ptr / std::shared_ptr along with std::make_unique / std::make_shared.

  • Look for STL containers such as std::vector, std::map, etc. for storing and managing collections rather than resorting to writing your own. The standard stuff is well-tested hence fewer worries about bugs.

  • Always refer to the documentation of the APIs that you plan to use. Verify if an API allocates resources that you might have to deallocate a certain way after use.


Here's a thread on loading a WASM module: Loading WebAssembly modules efficiently

like image 78
Azeem Avatar answered Oct 21 '22 12:10

Azeem