Skip to content

Commit ad13362

Browse files
authored
Add partial support for -fwasm-exceptions in Asyncify (#5343) (#5475)
This supports Try in general, but not unwinding from a Catch. In asserts mode we error on an unwind nested in a Catch.
1 parent a9829f9 commit ad13362

File tree

3 files changed

+1253
-0
lines changed

3 files changed

+1253
-0
lines changed

src/passes/Asyncify.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@
9696
// Overall, this should allow good performance with small overhead that is
9797
// mostly noticed at rewind time.
9898
//
99+
// Exceptions handling (-fwasm-exceptions) is partially supported, everything
100+
// except for handling unwinding from within a catch block. If assertions mode
101+
// is enabled then this pass will check for that problem, and if so, throw an
102+
// unreachable exception. (If "ignore unwind from catch" mode is enabled then
103+
// Asyncify will silently skip any unwind call from within catch blocks, see
104+
// below.)
105+
//
99106
// After this pass is run a new i32 global "__asyncify_state" is added, which
100107
// has the following values:
101108
//
@@ -239,6 +246,12 @@
239246
// an unwind/rewind in an invalid place (this can be helpful for manual
240247
// tweaking of the only-list / remove-list, see later).
241248
//
249+
// --pass-arg=asyncify-ignore-unwind-from-catch
250+
//
251+
// When an unwind operation is triggered from inside a wasm-exceptions
252+
// catch block, which is not supported, silently ignore it rather than
253+
// fail during rewinding later. (This is unsafe in general.)
254+
//
242255
// --pass-arg=asyncify-verbose
243256
//
244257
// Logs out instrumentation decisions to the console. This can help figure
@@ -1138,6 +1151,18 @@ struct AsyncifyFlow : public Pass {
11381151
// here as well.
11391152
results.push_back(makeCallSupport(curr));
11401153
continue;
1154+
} else if (auto* try_ = curr->dynCast<Try>()) {
1155+
if (item.phase == Work::Scan) {
1156+
work.push_back(Work{curr, Work::Finish});
1157+
work.push_back(Work{try_->body, Work::Scan});
1158+
// catchBodies are ignored because we assume that pause/resume will
1159+
// not happen inside them
1160+
continue;
1161+
}
1162+
try_->body = results.back();
1163+
results.pop_back();
1164+
results.push_back(try_);
1165+
continue;
11411166
}
11421167
// We must handle all control flow above, and all things that can change
11431168
// the state, so there should be nothing that can reach here - add it
@@ -1316,6 +1341,93 @@ struct AsyncifyAssertInNonInstrumented : public Pass {
13161341
Module* module;
13171342
};
13181343

1344+
struct AsyncifyUnwindWalker
1345+
: WalkerPass<ExpressionStackWalker<AsyncifyUnwindWalker>> {
1346+
Function* function;
1347+
Module* module;
1348+
1349+
// Adds a check for Call that is inside a Catch block (we do not handle
1350+
// unwinding there).
1351+
template<typename T> void replaceCallWithCheck(T* call) {
1352+
auto builder = std::make_unique<Builder>(*module);
1353+
auto check = builder->makeIf(
1354+
builder->makeBinary(NeInt32,
1355+
builder->makeGlobalGet(ASYNCIFY_STATE, Type::i32),
1356+
builder->makeConst(int32_t(State::Normal))),
1357+
builder->makeUnreachable());
1358+
if (call->type.isConcrete()) {
1359+
auto temp = builder->addVar(function, call->type);
1360+
replaceCurrent(builder->makeBlock(
1361+
{
1362+
builder->makeLocalSet(temp, call),
1363+
check,
1364+
builder->makeLocalGet(temp, call->type),
1365+
},
1366+
call->type));
1367+
} else {
1368+
replaceCurrent(builder->makeBlock(
1369+
{
1370+
call,
1371+
check,
1372+
},
1373+
call->type));
1374+
}
1375+
}
1376+
1377+
template<typename T> void visitCallLike(T* curr) {
1378+
assert(!expressionStack.empty());
1379+
// A return_call (curr->isReturn) can be ignored here: It returns first,
1380+
// leaving the Catch, before calling.
1381+
if (curr->isReturn) {
1382+
return;
1383+
}
1384+
// Go up the stack and see if we are in a Catch.
1385+
Index i = expressionStack.size() - 1;
1386+
while (i > 0) {
1387+
auto* expr = expressionStack[i];
1388+
if (Try* aTry = expr->template dynCast<Try>()) {
1389+
// check if curr is inside body of aTry (which is safe),
1390+
// otherwise do replace a call
1391+
assert(i + 1 < expressionStack.size());
1392+
if (expressionStack[i + 1] != aTry->body) {
1393+
replaceCallWithCheck(curr);
1394+
}
1395+
break;
1396+
}
1397+
i--;
1398+
}
1399+
}
1400+
1401+
void visitCall(Call* curr) { visitCallLike(curr); }
1402+
1403+
void visitCallRef(CallRef* curr) { visitCallLike(curr); }
1404+
1405+
void visitCallIndirect(CallIndirect* curr) { visitCallLike(curr); }
1406+
};
1407+
1408+
struct AsyncifyAssertUnwindCorrectness : Pass {
1409+
bool isFunctionParallel() override { return true; }
1410+
1411+
ModuleAnalyzer* analyzer;
1412+
Module* module;
1413+
1414+
AsyncifyAssertUnwindCorrectness(ModuleAnalyzer* analyzer, Module* module) {
1415+
this->analyzer = analyzer;
1416+
this->module = module;
1417+
}
1418+
1419+
std::unique_ptr<Pass> create() override {
1420+
return std::make_unique<AsyncifyAssertUnwindCorrectness>(analyzer, module);
1421+
}
1422+
1423+
void runOnFunction(Module* module_, Function* function) override {
1424+
AsyncifyUnwindWalker walker;
1425+
walker.function = function;
1426+
walker.module = module_;
1427+
walker.walk(function->body);
1428+
}
1429+
};
1430+
13191431
// Instrument local saving/restoring.
13201432
struct AsyncifyLocals : public WalkerPass<PostWalker<AsyncifyLocals>> {
13211433
bool isFunctionParallel() override { return true; }
@@ -1761,6 +1873,8 @@ struct Asyncify : public Pass {
17611873
PassRunner runner(module);
17621874
runner.add(std::make_unique<AsyncifyAssertInNonInstrumented>(
17631875
&analyzer, pointerType, asyncifyMemory));
1876+
runner.add(
1877+
std::make_unique<AsyncifyAssertUnwindCorrectness>(&analyzer, module));
17641878
runner.setIsNested(true);
17651879
runner.setValidateGlobally(false);
17661880
runner.run();

0 commit comments

Comments
 (0)