Skip to content

Commit cbc7e86

Browse files
authored
Bysyncify: Don't instrument functions that call bysyncify_* directly (#2179)
Those functions are assumed to be part of the runtime. Instrumenting them would mean nothing can work. With this fix, bysyncify is useful with pure wasm, and not just through imports.
1 parent 3f797c8 commit cbc7e86

8 files changed

Lines changed: 678 additions & 695 deletions

src/passes/Bysyncify.cpp

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,14 @@
155155
// around that, you can create imports to bysyncify.start_unwind,
156156
// bysyncify.stop_unwind, bysyncify.start_rewind, and bysyncify.stop_rewind;
157157
// if those exist when this pass runs then it will turn those into direct
158-
// calls to the functions that it creates.
158+
// calls to the functions that it creates. Note that when doing everything
159+
// in wasm like this, Bysyncify must not instrument your "runtime" support
160+
// code, that is, the code that initiates unwinds and rewinds and stops them.
161+
// If it did, the unwind would not stop until you left the wasm module
162+
// entirely, etc. Therefore we do not instrument a function if it has
163+
// a call to the four bysyncify_* methods. Note that you may need to disable
164+
// inlining if that would cause code that does need to be instrumented
165+
// show up in that runtime code.
159166
//
160167
// To use this API, call bysyncify_start_unwind when you want to. The call
161168
// stack will then be unwound, and so execution will resume in the JS or
@@ -237,7 +244,19 @@ class ModuleAnalyzer {
237244
bool canIndirectChangeState;
238245

239246
struct Info {
247+
// If this function can start an unwind/rewind.
240248
bool canChangeState = false;
249+
// If this function is part of the runtime that receives an unwinding
250+
// and starts a rewinding. If so, we do not instrument it, see above.
251+
// This is only relevant when handling things entirely inside wasm,
252+
// as opposed to imports.
253+
bool isBottomMostRuntime = false;
254+
// If this function is part of the runtime that starts an unwinding
255+
// and stops a rewinding. If so, we do not instrument it, see above.
256+
// The difference between the top-most and bottom-most runtime is that
257+
// the top-most part is still marked as changing the state; so things
258+
// that call it are instrumented. This is not done for the bottom.
259+
bool isTopMostRuntime = false;
241260
std::set<Function*> callsTo;
242261
std::set<Function*> calledBy;
243262
};
@@ -257,10 +276,9 @@ class ModuleAnalyzer {
257276
ModuleUtils::ParallelFunctionMap<Info> scanner(
258277
module, [&](Function* func, Info& info) {
259278
if (func->imported()) {
260-
// The bysyncify imports can definitely change the state.
279+
// The relevant bysyncify imports can definitely change the state.
261280
if (func->module == BYSYNCIFY &&
262-
(func->base == START_UNWIND || func->base == STOP_UNWIND ||
263-
func->base == START_REWIND || func->base == STOP_REWIND)) {
281+
(func->base == START_UNWIND || func->base == STOP_REWIND)) {
264282
info.canChangeState = true;
265283
} else {
266284
info.canChangeState =
@@ -275,18 +293,22 @@ class ModuleAnalyzer {
275293
// Redirect the imports to the functions we'll add later.
276294
if (target->base == START_UNWIND) {
277295
curr->target = BYSYNCIFY_START_UNWIND;
296+
info->canChangeState = true;
297+
info->isTopMostRuntime = true;
278298
} else if (target->base == STOP_UNWIND) {
279299
curr->target = BYSYNCIFY_STOP_UNWIND;
300+
info->isBottomMostRuntime = true;
280301
} else if (target->base == START_REWIND) {
281302
curr->target = BYSYNCIFY_START_REWIND;
303+
info->isBottomMostRuntime = true;
282304
} else if (target->base == STOP_REWIND) {
283305
curr->target = BYSYNCIFY_STOP_REWIND;
284-
// TODO: in theory, this does not change the state
306+
info->canChangeState = true;
307+
info->isTopMostRuntime = true;
285308
} else {
286309
Fatal() << "call to unidenfied bysyncify import: "
287310
<< target->base;
288311
}
289-
info->canChangeState = true;
290312
return;
291313
}
292314
info->callsTo.insert(target);
@@ -306,7 +328,15 @@ class ModuleAnalyzer {
306328
walker.module = &module;
307329
walker.canIndirectChangeState = canIndirectChangeState;
308330
walker.walk(func->body);
331+
332+
if (info.isBottomMostRuntime) {
333+
info.canChangeState = false;
334+
// TODO: issue warnings on suspicious things, like a function in
335+
// the bottom-most runtime also doing top-most runtime stuff
336+
// like starting and unwinding.
337+
}
309338
});
339+
310340
map.swap(scanner.map);
311341

312342
// Remove the bysyncify imports, if any.
@@ -338,15 +368,18 @@ class ModuleAnalyzer {
338368
while (!work.empty()) {
339369
auto* func = work.pop();
340370
for (auto* caller : map[func].calledBy) {
341-
if (!map[caller].canChangeState) {
371+
if (!map[caller].canChangeState && !map[caller].isBottomMostRuntime) {
342372
map[caller].canChangeState = true;
343373
work.push(caller);
344374
}
345375
}
346376
}
347377
}
348378

349-
bool canChangeState(Function* func) { return map[func].canChangeState; }
379+
bool needsInstrumentation(Function* func) {
380+
auto& info = map[func];
381+
return info.canChangeState && !info.isTopMostRuntime;
382+
}
350383

351384
bool canChangeState(Expression* curr) {
352385
// Look inside to see if we call any of the things we know can change the
@@ -357,14 +390,17 @@ class ModuleAnalyzer {
357390
// We only implement these at the very end, but we know that they
358391
// definitely change the state.
359392
if (curr->target == BYSYNCIFY_START_UNWIND ||
360-
curr->target == BYSYNCIFY_STOP_UNWIND ||
361-
curr->target == BYSYNCIFY_START_REWIND ||
362393
curr->target == BYSYNCIFY_STOP_REWIND ||
363394
curr->target == BYSYNCIFY_GET_CALL_INDEX ||
364395
curr->target == BYSYNCIFY_CHECK_CALL_INDEX) {
365396
canChangeState = true;
366397
return;
367398
}
399+
if (curr->target == BYSYNCIFY_STOP_UNWIND ||
400+
curr->target == BYSYNCIFY_START_REWIND) {
401+
isBottomMostRuntime = true;
402+
return;
403+
}
368404
// The target may not exist if it is one of our temporary intrinsics.
369405
auto* target = module->getFunctionOrNull(curr->target);
370406
if (target && (*map)[target].canChangeState) {
@@ -381,12 +417,16 @@ class ModuleAnalyzer {
381417
Map* map;
382418
bool canIndirectChangeState;
383419
bool canChangeState = false;
420+
bool isBottomMostRuntime = false;
384421
};
385422
Walker walker;
386423
walker.module = &module;
387424
walker.map = &map;
388425
walker.canIndirectChangeState = canIndirectChangeState;
389426
walker.walk(curr);
427+
if (walker.isBottomMostRuntime) {
428+
walker.canChangeState = false;
429+
}
390430
return walker.canChangeState;
391431
}
392432
};
@@ -451,7 +491,7 @@ struct BysyncifyFlow : public Pass {
451491
module = module_;
452492
// If the function cannot change our state, we have nothing to do -
453493
// we will never unwind or rewind the stack here.
454-
if (!analyzer->canChangeState(func)) {
494+
if (!analyzer->needsInstrumentation(func)) {
455495
return;
456496
}
457497
// Rewrite the function body.
@@ -670,7 +710,7 @@ struct BysyncifyLocals : public WalkerPass<PostWalker<BysyncifyLocals>> {
670710
void doWalkFunction(Function* func) {
671711
// If the function cannot change our state, we have nothing to do -
672712
// we will never unwind or rewind the stack here.
673-
if (!analyzer->canChangeState(func->body)) {
713+
if (!analyzer->needsInstrumentation(func)) {
674714
return;
675715
}
676716
// Note the locals we want to preserve, before we add any more helpers.

0 commit comments

Comments
 (0)