Skip to content

Commit 73be9dc

Browse files
authored
Support cross-module calls in the interpreter (#7787)
Previously a runtime instance of a function was just the function name. That is fine for optimizations, and even for executing a single module, but not cross-module calls. To support that, add a FuncData (parallel to ExnData, ContData, etc.). The constructor now requires that (but keep makeFunc as a simple helper for cases when we just want the name). A FuncData contains info about the module instance the function is in, and a std::function to actually call it. We create a proper thunk when we create a function literal, and in call_ref, we use it to do the actual call.
1 parent 25d3540 commit 73be9dc

File tree

9 files changed

+180
-26
lines changed

9 files changed

+180
-26
lines changed

src/ir/possible-contents.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,9 @@ struct InfoCollector
641641
addRoot(curr);
642642
}
643643
void visitRefFunc(RefFunc* curr) {
644-
addRoot(
645-
curr,
646-
PossibleContents::literal(Literal(curr->func, curr->type.getHeapType())));
644+
addRoot(curr,
645+
PossibleContents::literal(
646+
Literal::makeFunc(curr->func, curr->type.getHeapType())));
647647

648648
// The presence of a RefFunc indicates the function may be called
649649
// indirectly, so add the relevant connections for this particular function.
@@ -1800,7 +1800,7 @@ void TNHOracle::infer() {
18001800
// lot of other optimizations become possible anyhow.
18011801
auto target = possibleTargets[0]->name;
18021802
info.inferences[call->target] = PossibleContents::literal(
1803-
Literal(target, wasm.getFunction(target)->type));
1803+
Literal::makeFunc(target, wasm.getFunction(target)->type));
18041804
continue;
18051805
}
18061806

src/ir/properties.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ inline Literal getLiteral(const Expression* curr) {
116116
} else if (auto* n = curr->dynCast<RefNull>()) {
117117
return Literal(n->type);
118118
} else if (auto* r = curr->dynCast<RefFunc>()) {
119-
return Literal(r->func, r->type.getHeapType());
119+
return Literal::makeFunc(r->func, r->type.getHeapType());
120120
} else if (auto* i = curr->dynCast<RefI31>()) {
121121
if (auto* c = i->value->dynCast<Const>()) {
122122
return Literal::makeI31(c->value.geti32(),

src/literal.h

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
namespace wasm {
3232

3333
class Literals;
34+
struct FuncData;
3435
struct GCData;
3536
struct ExnData;
3637
struct ContData;
@@ -45,9 +46,8 @@ class Literal {
4546
int32_t i32;
4647
int64_t i64;
4748
uint8_t v128[16];
48-
// funcref function name. `isNull()` indicates a `null` value.
49-
// TODO: handle cross-module calls using something other than a Name here.
50-
Name func;
49+
// A reference to Function data.
50+
std::shared_ptr<FuncData> funcData;
5151
// A reference to GC data, either a Struct or an Array. For both of those we
5252
// store the referred data as a Literals object (which is natural for an
5353
// Array, and for a Struct, is just the fields in order). The type is used
@@ -90,10 +90,7 @@ class Literal {
9090
explicit Literal(const std::array<Literal, 8>&);
9191
explicit Literal(const std::array<Literal, 4>&);
9292
explicit Literal(const std::array<Literal, 2>&);
93-
explicit Literal(Name func, HeapType type)
94-
: func(func), type(type, NonNullable, Exact) {
95-
assert(type.isSignature());
96-
}
93+
explicit Literal(std::shared_ptr<FuncData> funcData, HeapType type);
9794
explicit Literal(std::shared_ptr<GCData> gcData, HeapType type);
9895
explicit Literal(std::shared_ptr<ExnData> exnData);
9996
explicit Literal(std::shared_ptr<ContData> contData);
@@ -253,9 +250,9 @@ class Literal {
253250
static Literal makeNull(HeapType type) {
254251
return Literal(Type(type.getBottom(), Nullable));
255252
}
256-
static Literal makeFunc(Name func, HeapType type) {
257-
return Literal(func, type);
258-
}
253+
// Simple way to create a function from the name and type, without a full
254+
// FuncData.
255+
static Literal makeFunc(Name func, HeapType type);
259256
static Literal makeI31(int32_t value, Shareability share) {
260257
auto lit = Literal(Type(HeapTypes::i31.getBasic(share), NonNullable));
261258
lit.i32 = value | 0x80000000;
@@ -311,10 +308,8 @@ class Literal {
311308
return bit_cast<double>(i64);
312309
}
313310
std::array<uint8_t, 16> getv128() const;
314-
Name getFunc() const {
315-
assert(type.isFunction() && !func.isNull());
316-
return func;
317-
}
311+
Name getFunc() const;
312+
std::shared_ptr<FuncData> getFuncData() const;
318313
std::shared_ptr<GCData> getGCData() const;
319314
std::shared_ptr<ExnData> getExnData() const;
320315
std::shared_ptr<ContData> getContData() const;

src/tools/wasm-ctor-eval.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ class EvallingModuleRunner : public ModuleRunnerBase<EvallingModuleRunner> {
9292
// internally), but we want to disable table.get for now.
9393
throw FailToEvalException("TODO: table.get");
9494
}
95+
96+
// This needs to be duplicated from ModuleRunner, unfortunately.
97+
Literal makeFuncData(Name name, HeapType type) {
98+
auto allocation =
99+
std::make_shared<FuncData>(name, this, [this, name](Literals arguments) {
100+
return callFunction(name, arguments);
101+
});
102+
#if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer)
103+
__lsan_ignore_object(allocation.get());
104+
#endif
105+
return Literal(allocation, type);
106+
}
95107
};
96108

97109
// Build an artificial `env` module based on a module's imports, so that the

src/wasm-interpreter.h

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,35 @@ class Flow {
130130
}
131131
};
132132

133+
struct FuncData {
134+
// Name of the function in the module.
135+
Name name;
136+
137+
// The interpreter instance we are in. This is only used for equality
138+
// comparisons, as two functions are equal iff they have the same name and are
139+
// in the same instance (in particular, we do *not* compare the |call| field
140+
// below, which is an execution detail).
141+
void* self;
142+
143+
// A way to execute this function. We use this when it is called.
144+
using Call = std::function<Flow(Literals)>;
145+
std::optional<Call> call;
146+
147+
FuncData(Name name,
148+
void* self = nullptr,
149+
std::optional<Call> call = std::nullopt)
150+
: name(name), self(self), call(call) {}
151+
152+
bool operator==(const FuncData& other) const {
153+
return name == other.name && self == other.self;
154+
}
155+
156+
Flow doCall(Literals arguments) {
157+
assert(call);
158+
return (*call)(arguments);
159+
}
160+
};
161+
133162
// Suspend/resume support.
134163
//
135164
// As we operate directly on our structured IR, we do not have a program counter
@@ -277,6 +306,16 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
277306
Execute,
278307
};
279308

309+
Literal makeFuncData(Name name, HeapType type) {
310+
// Identify the interpreter, but do not provide a way to actually call the
311+
// function.
312+
auto allocation = std::make_shared<FuncData>(name, this);
313+
#if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer)
314+
__lsan_ignore_object(allocation.get());
315+
#endif
316+
return Literal(allocation, type);
317+
}
318+
280319
protected:
281320
RelaxedBehavior relaxedBehavior = RelaxedBehavior::NonConstant;
282321

@@ -1801,7 +1840,7 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
18011840
return Literal(int32_t(value.isNull()));
18021841
}
18031842
Flow visitRefFunc(RefFunc* curr) {
1804-
return Literal::makeFunc(curr->func, curr->type.getHeapType());
1843+
return self()->makeFuncData(curr->func, curr->type.getHeapType());
18051844
}
18061845
Flow visitRefEq(RefEq* curr) {
18071846
Flow flow = visit(curr->left);
@@ -3545,7 +3584,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
35453584
if (curr->isReturn) {
35463585
// Return calls are represented by their arguments followed by a reference
35473586
// to the function to be called.
3548-
arguments.push_back(Literal::makeFunc(target, funcType));
3587+
arguments.push_back(self()->makeFuncData(target, funcType));
35493588
return Flow(RETURN_CALL_FLOW, std::move(arguments));
35503589
}
35513590

@@ -3651,7 +3690,7 @@ class ModuleRunnerBase : public ExpressionRunner<SubType> {
36513690
std::cout << self()->indent() << "(calling ref " << targetRef.getFunc()
36523691
<< ")\n";
36533692
#endif
3654-
Flow ret = callFunction(targetRef.getFunc(), arguments);
3693+
Flow ret = targetRef.getFuncData()->doCall(arguments);
36553694
#if WASM_INTERPRETER_DEBUG
36563695
std::cout << self()->indent() << "(returned to " << scope->function->name
36573696
<< ")\n";
@@ -5057,6 +5096,19 @@ class ModuleRunner : public ModuleRunnerBase<ModuleRunner> {
50575096
ExternalInterface* externalInterface,
50585097
std::map<Name, std::shared_ptr<ModuleRunner>> linkedInstances = {})
50595098
: ModuleRunnerBase(wasm, externalInterface, linkedInstances) {}
5099+
5100+
Literal makeFuncData(Name name, HeapType type) {
5101+
// As the super's |makeFuncData|, but here we also provide a way to
5102+
// actually call the function.
5103+
auto allocation =
5104+
std::make_shared<FuncData>(name, this, [this, name](Literals arguments) {
5105+
return callFunction(name, arguments);
5106+
});
5107+
#if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer)
5108+
__lsan_ignore_object(allocation.get());
5109+
#endif
5110+
return Literal(allocation, type);
5111+
}
50605112
};
50615113

50625114
} // namespace wasm

src/wasm/literal.cpp

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ Literal::Literal(const uint8_t init[16]) : type(Type::v128) {
7171
memcpy(&v128, init, 16);
7272
}
7373

74+
Literal::Literal(std::shared_ptr<FuncData> funcData, HeapType type)
75+
: funcData(funcData), type(type, NonNullable, Exact) {
76+
assert(funcData);
77+
assert(type.isSignature());
78+
}
79+
80+
Literal Literal::makeFunc(Name func, HeapType type) {
81+
// Provide only the name of the function, without execution info.
82+
return Literal(std::make_shared<FuncData>(func), type);
83+
}
84+
7485
Literal::Literal(std::shared_ptr<GCData> gcData, HeapType type)
7586
: gcData(gcData), type(type,
7687
gcData ? NonNullable : Nullable,
@@ -140,7 +151,7 @@ Literal::Literal(const Literal& other) : type(other.type) {
140151
return;
141152
}
142153
if (type.isFunction()) {
143-
func = other.func;
154+
new (&funcData) std::shared_ptr<FuncData>(other.funcData);
144155
return;
145156
}
146157
if (type.isContinuation()) {
@@ -187,6 +198,8 @@ Literal::~Literal() {
187198
if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::ext) ||
188199
type.getHeapType().isMaybeShared(HeapType::any)) {
189200
gcData.~shared_ptr();
201+
} else if (isFunction()) {
202+
funcData.~shared_ptr();
190203
} else if (isExn()) {
191204
exnData.~shared_ptr();
192205
} else if (isContinuation()) {
@@ -335,6 +348,16 @@ std::array<uint8_t, 16> Literal::getv128() const {
335348
return ret;
336349
}
337350

351+
Name Literal::getFunc() const {
352+
assert(isFunction());
353+
return getFuncData()->name;
354+
}
355+
356+
std::shared_ptr<FuncData> Literal::getFuncData() const {
357+
assert(isFunction());
358+
return funcData;
359+
}
360+
338361
std::shared_ptr<GCData> Literal::getGCData() const {
339362
assert(isNull() || isData());
340363
return gcData;
@@ -458,8 +481,7 @@ bool Literal::operator==(const Literal& other) const {
458481
return true;
459482
}
460483
if (type.isFunction()) {
461-
assert(func.is() && other.func.is());
462-
return func == other.func;
484+
return *funcData == *other.funcData;
463485
}
464486
if (type.isString()) {
465487
return gcData->values == other.gcData->values;

test/gtest/possible-contents.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class PossibleContentsTest : public testing::Test {
9090
PossibleContents::global("funcGlobal", Type(HeapType::func, NonNullable));
9191

9292
PossibleContents nonNullFunc = PossibleContents::literal(
93-
Literal("func", Signature(Type::none, Type::none)));
93+
Literal::makeFunc("func", Signature(Type::none, Type::none)));
9494

9595
PossibleContents exactI32 = PossibleContents::exactType(Type::i32);
9696
PossibleContents exactAnyref = PossibleContents::exactType(anyref);

test/lit/ctor-eval/call_ref.wast

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
2+
;; RUN: wasm-ctor-eval %s --ctors=test --kept-exports=test --ignore-external-input --quiet -all -g -S -o - | filecheck %s
3+
4+
;; Test we execute call_ref properly. After the call, the global will be set to
5+
;; 1.
6+
(module
7+
;; CHECK: (type $f (func))
8+
(type $f (func))
9+
10+
;; CHECK: (global $g (mut i32) (i32.const 1))
11+
(global $g (export "gg") (mut i32) (i32.const 0))
12+
13+
(elem declare $test2)
14+
15+
(func $test (export "test")
16+
(call_ref $f
17+
(ref.func $called)
18+
)
19+
)
20+
21+
(func $called (type $f)
22+
(global.set $g
23+
(i32.const 1)
24+
)
25+
)
26+
)
27+
28+
;; CHECK: (export "gg" (global $g))
29+
30+
;; CHECK: (export "test" (func $test_2))
31+
32+
;; CHECK: (func $test_2 (type $f)
33+
;; CHECK-NEXT: (nop)
34+
;; CHECK-NEXT: )

test/spec/call_ref_linked.wast

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
;; Test that we can create a function in one module and call it using call_ref
2+
;; from another.
3+
4+
(module
5+
(type $func-i32 (func (result i32)))
6+
7+
(func $inner (result i32)
8+
(i32.const 42)
9+
)
10+
(func $func-i32 (result i32)
11+
;; This call must execute in this module, i.e., call the right $inner.
12+
(call $inner)
13+
)
14+
(func (export "get-func-i32") (result (ref $func-i32))
15+
(return (ref.func $func-i32))
16+
)
17+
)
18+
19+
(register "first")
20+
21+
(module
22+
(type $func-i32 (func (result i32)))
23+
24+
(import "first" "get-func-i32" (func $imported (result (ref $func-i32))))
25+
26+
(func (export "run") (result i32)
27+
(call_ref $func-i32
28+
(call $imported)
29+
)
30+
)
31+
32+
(func $inner (result i32)
33+
;; We should not get here.
34+
(unreachable)
35+
)
36+
)
37+
38+
(assert_return (invoke "run") (i32.const 42))
39+

0 commit comments

Comments
 (0)