Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 80 additions & 60 deletions src/tools/wasm-ctor-eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,63 +115,79 @@ class EvallingModuleRunner : public ModuleRunnerBase<EvallingModuleRunner> {
}
};

// Build an artificial `env` module based on a module's imports, so that the
// Build artificial modules based on a module's imports, so that the
// interpreter can use correct object instances. It initializes usable global
// imports, and fills the rest with fake values since those are dangerous to
// use. we will fail if dangerous globals are used.
std::unique_ptr<Module> buildEnvModule(Module& wasm) {
auto env = std::make_unique<Module>();
env->name = "env";

// create empty functions with similar signature
ModuleUtils::iterImportedFunctions(wasm, [&](Function* func) {
if (func->module == env->name) {
Builder builder(*env);
auto* copied = ModuleUtils::copyFunction(func, *env);
copied->module = Name();
copied->base = Name();
copied->body = builder.makeUnreachable();
env->addExport(
builder.makeExport(func->base, copied->name, ExternalKind::Function));
}
});

// create tables with similar initial and max values
ModuleUtils::iterImportedTables(wasm, [&](Table* table) {
if (table->module == env->name) {
auto* copied = ModuleUtils::copyTable(table, *env);
copied->module = Name();
copied->base = Name();
env->addExport(Builder(*env).makeExport(
table->base, copied->name, ExternalKind::Table));
}
});

ModuleUtils::iterImportedGlobals(wasm, [&](Global* global) {
if (global->module == env->name) {
auto* copied = ModuleUtils::copyGlobal(global, *env);
copied->module = Name();
copied->base = Name();

Builder builder(*env);
copied->init = builder.makeConst(Literal::makeZero(global->type));
env->addExport(
builder.makeExport(global->base, copied->name, ExternalKind::Global));
}
});

// create an exported memory with the same initial and max size
ModuleUtils::iterImportedMemories(wasm, [&](Memory* memory) {
if (memory->module == env->name) {
auto* copied = ModuleUtils::copyMemory(memory, *env);
copied->module = Name();
copied->base = Name();
env->addExport(Builder(*env).makeExport(
memory->base, copied->name, ExternalKind::Memory));
}
});
// use. Imported globals can't be read anyway; see
// `EvallingModuleRunner::visitGlobalGet`.
// Note: wasi_ modules have stubs generated but won't be called due to the
// special handling in `CtorEvalExternalInterface::getImportedFunction`. We
// still generate the stubs to ensure the link-time validation passes.
std::vector<std::unique_ptr<Module>> buildStubModules(Module& wasm) {
std::map<Name, std::unique_ptr<Module>> modules;

ModuleUtils::iterImports(
wasm,
[&modules](std::variant<Memory*, Table*, Global*, Function*, Tag*> import) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the advantage of this variant-based visiting, replacing iterImported* functions with one iterImports? (I'm not opposed to it, necessarily, I just don't see an obvious reason so I think I may be missing something.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a big difference, I mainly did this because there is some work in common for all imports (the first few lines of this function). If we were to use the iterImported* functions, we'd need to introduce a helper function and call it in the body of each one. Also this way forces us to deal with all types of imports which makes it clear to the reader which ones are handled (e.g. Tags are not handled which was implicit before but is explicit now which I think is a small plus).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we want to handle tags? That seems like it was probably an accidental omission before. (Or do we not have a hook for aborting the execution if an imported tag is used? In that case maybe aborting early in the presence of imported tags is intentional.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question about tags @tlively , but @stevenfontanella otherwise that all sounds good, thanks for the explanation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My impression is that ctor-eval won't work at all on any ctor that references an imported tag: https://github.com/WebAssembly/binaryen/blob/main/src/tools/wasm-ctor-eval.cpp#L327. I guess this is the best we can do unless the tag is somehow referenced in a way that doesn't matter at all? Or as you mentioned, if the same tag is used to throw and catch, then it doesn't matter what the imported value was.

Importable* importable =
std::visit([](auto* i) -> Importable* { return i; }, import);

auto [it, inserted] = modules.try_emplace(importable->module, nullptr);
if (inserted) {
it->second = std::make_unique<Module>();
it->second->name = importable->module;
}
Module* module = it->second.get();

struct Visitor {
Module* module;
void operator()(Memory* memory) {
auto* copied = ModuleUtils::copyMemory(memory, *module);
copied->module = Name();
copied->base = Name();
module->addExport(Builder(*module).makeExport(
memory->base, copied->name, ExternalKind::Memory));
}
void operator()(Table* table) {
// create tables with similar initial and max values
auto* copied = ModuleUtils::copyTable(table, *module);
copied->module = Name();
copied->base = Name();
module->addExport(Builder(*module).makeExport(
table->base, copied->name, ExternalKind::Table));
}
void operator()(Global* global) {
auto* copied = ModuleUtils::copyGlobal(global, *module);
copied->module = Name();
copied->base = Name();

Builder builder(*module);
copied->init = builder.makeConst(Literal::makeZero(global->type));
module->addExport(builder.makeExport(
global->base, copied->name, ExternalKind::Global));
}
void operator()(Function* func) {
Builder builder(*module);
auto* copied = ModuleUtils::copyFunction(func, *module);
copied->module = Name();
copied->base = Name();
copied->body = builder.makeUnreachable();
module->addExport(builder.makeExport(
func->base, copied->name, ExternalKind::Function));
}
void operator()(Tag* tag) {
// no-op
}
};
std::visit(Visitor{module}, import);
});

return env;
std::vector<std::unique_ptr<Module>> modulesVector;
modulesVector.reserve(modules.size());
for (auto& [_, ptr] : modules) {
modulesVector.push_back(std::move(ptr));
}
return modulesVector;
}

// Whether to ignore external input to the program as it runs. If set, we will
Expand Down Expand Up @@ -1356,12 +1372,16 @@ void evalCtors(Module& wasm,

std::map<Name, std::shared_ptr<EvallingModuleRunner>> linkedInstances;

// build and link the env module
auto envModule = buildEnvModule(wasm);
CtorEvalExternalInterface envInterface;
auto envInstance =
std::make_shared<EvallingModuleRunner>(*envModule, &envInterface);
linkedInstances[envModule->name] = envInstance;
// stubModules and interfaces must be kept alive since they are referenced in
// linkedInstances.
std::vector<std::unique_ptr<Module>> stubModules = buildStubModules(wasm);
std::vector<std::unique_ptr<CtorEvalExternalInterface>> interfaces;

for (auto& module : stubModules) {
interfaces.push_back(std::make_unique<CtorEvalExternalInterface>());
linkedInstances[module->name] =
std::make_shared<EvallingModuleRunner>(*module, interfaces.back().get());
}

CtorEvalExternalInterface interface(linkedInstances);
try {
Expand Down
9 changes: 5 additions & 4 deletions test/ctor-eval/global-get-init.wast
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
(module
(import "import" "global" (global $imported i32))
(func $test1 (export "test1")
;; This should be safe to eval in theory, but the imported global stops us,
;; so this function will not be optimized out.
;; TODO: perhaps if we never use that global that is ok?
(func $use-global (export "use-global") (result i32)
(global.get $imported)
)
;; The imported global isn't used in the ctor,
;; so we're free to remove it completely.
(func $test1 (export "test1"))
)
9 changes: 5 additions & 4 deletions test/ctor-eval/global-get-init.wast.out
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
(module
(type $0 (func))
(export "test1" (func $test1))
(func $test1 (type $0)
(nop)
(type $0 (func (result i32)))
(import "import" "global" (global $imported i32))
(export "use-global" (func $use-global))
(func $use-global (type $0) (result i32)
(global.get $imported)
)
)
Loading