Skip to content

Commit ed3bf4f

Browse files
authored
Support using JSPI to load the secondary wasm split module. (#5431)
When using JSPI with wasm-split, any calls to secondary module functions will now first check a global to see if the module is loaded. If not loaded it will call a JSPI'ed function that will handle loading module. The setup is split into the JSPI pass and wasm-split tool since the JSPI pass is first run by emscripten and we need to JSPI'ify the load secondary module function. wasm-split then injects all the checks and calls to the load function.
1 parent 992584f commit ed3bf4f

File tree

12 files changed

+334
-203
lines changed

12 files changed

+334
-203
lines changed

src/ir/export-utils.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ namespace wasm::ExportUtils {
2424
std::vector<Function*> getExportedFunctions(Module& wasm);
2525
std::vector<Global*> getExportedGlobals(Module& wasm);
2626

27+
inline bool isExported(const Module& module, const Function& func) {
28+
for (auto& exportFunc : module.exports) {
29+
if (exportFunc->kind == ExternalKind::Function &&
30+
exportFunc->value == func.name) {
31+
return true;
32+
}
33+
}
34+
return false;
35+
};
36+
2737
} // namespace wasm::ExportUtils
2838

2939
#endif // wasm_ir_export_h

src/ir/module-splitting.cpp

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969

7070
#include "ir/module-splitting.h"
7171
#include "ir/element-utils.h"
72+
#include "ir/export-utils.h"
7273
#include "ir/manipulation.h"
7374
#include "ir/module-utils.h"
7475
#include "ir/names.h"
@@ -80,6 +81,8 @@ namespace wasm::ModuleSplitting {
8081

8182
namespace {
8283

84+
static const Name LOAD_SECONDARY_STATUS = "load_secondary_module_status";
85+
8386
template<class F> void forEachElement(Module& module, F f) {
8487
ModuleUtils::iterActiveElementSegments(module, [&](ElementSegment* segment) {
8588
Name base = "";
@@ -273,6 +276,9 @@ struct ModuleSplitter {
273276
// Map placeholder indices to the names of the functions they replace.
274277
std::map<size_t, Name> placeholderMap;
275278

279+
// Internal name of the LOAD_SECONDARY_MODULE function.
280+
Name internalLoadSecondaryModule;
281+
276282
// Initialization helpers
277283
static std::unique_ptr<Module> initSecondary(const Module& primary);
278284
static std::pair<std::set<Name>, std::set<Name>>
@@ -281,8 +287,10 @@ struct ModuleSplitter {
281287

282288
// Other helpers
283289
void exportImportFunction(Name func);
290+
Expression* maybeLoadSecondary(Builder& builder, Expression* callIndirect);
284291

285292
// Main splitting steps
293+
void setupJSPI();
286294
void moveSecondaryFunctions();
287295
void thunkExportedSecondaryFunctions();
288296
void indirectCallsToSecondaryFunctions();
@@ -297,6 +305,9 @@ struct ModuleSplitter {
297305
primaryFuncs(classifiedFuncs.first),
298306
secondaryFuncs(classifiedFuncs.second), tableManager(primary),
299307
exportedPrimaryFuncs(initExportedPrimaryFuncs(primary)) {
308+
if (config.jspi) {
309+
setupJSPI();
310+
}
300311
moveSecondaryFunctions();
301312
thunkExportedSecondaryFunctions();
302313
indirectCallsToSecondaryFunctions();
@@ -306,6 +317,23 @@ struct ModuleSplitter {
306317
}
307318
};
308319

320+
void ModuleSplitter::setupJSPI() {
321+
assert(primary.getExportOrNull(LOAD_SECONDARY_MODULE) &&
322+
"The load secondary module function must exist");
323+
// Remove the exported LOAD_SECONDARY_MODULE function since it's only needed
324+
// internally.
325+
internalLoadSecondaryModule = primary.getExport(LOAD_SECONDARY_MODULE)->value;
326+
primary.removeExport(LOAD_SECONDARY_MODULE);
327+
Builder builder(primary);
328+
// Add a global to track whether the secondary module has been loaded yet.
329+
primary.addGlobal(builder.makeGlobal(LOAD_SECONDARY_STATUS,
330+
Type::i32,
331+
builder.makeConst(int32_t(0)),
332+
Builder::Mutable));
333+
primary.addExport(builder.makeExport(
334+
LOAD_SECONDARY_STATUS, LOAD_SECONDARY_STATUS, ExternalKind::Global));
335+
}
336+
309337
std::unique_ptr<Module> ModuleSplitter::initSecondary(const Module& primary) {
310338
// Create the secondary module and copy trivial properties.
311339
auto secondary = std::make_unique<Module>();
@@ -318,7 +346,12 @@ std::pair<std::set<Name>, std::set<Name>>
318346
ModuleSplitter::classifyFunctions(const Module& primary, const Config& config) {
319347
std::set<Name> primaryFuncs, secondaryFuncs;
320348
for (auto& func : primary.functions) {
321-
if (func->imported() || config.primaryFuncs.count(func->name)) {
349+
// In JSPI mode exported functions cannot be moved to the secondary
350+
// module since that would make them async when they may not have the JSPI
351+
// wrapper. Exported JSPI functions can still benefit from splitting though
352+
// since only the JSPI wrapper stub will remain in the primary module.
353+
if (func->imported() || config.primaryFuncs.count(func->name) ||
354+
(config.jspi && ExportUtils::isExported(primary, *func))) {
322355
primaryFuncs.insert(func->name);
323356
} else {
324357
assert(func->name != primary.start && "The start function must be kept");
@@ -409,6 +442,20 @@ void ModuleSplitter::thunkExportedSecondaryFunctions() {
409442
}
410443
}
411444

445+
Expression* ModuleSplitter::maybeLoadSecondary(Builder& builder,
446+
Expression* callIndirect) {
447+
if (!config.jspi) {
448+
return callIndirect;
449+
}
450+
// Check if the secondary module is loaded and if it isn't, call the
451+
// function to load it.
452+
auto* loadSecondary = builder.makeIf(
453+
builder.makeUnary(EqZInt32,
454+
builder.makeGlobalGet(LOAD_SECONDARY_STATUS, Type::i32)),
455+
builder.makeCall(internalLoadSecondaryModule, {}, Type::none));
456+
return builder.makeSequence(loadSecondary, callIndirect);
457+
}
458+
412459
void ModuleSplitter::indirectCallsToSecondaryFunctions() {
413460
// Update direct calls of secondary functions to be indirect calls of their
414461
// corresponding table indices instead.
@@ -425,12 +472,14 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() {
425472
}
426473
auto* func = parent.secondary.getFunction(curr->target);
427474
auto tableSlot = parent.tableManager.getSlot(curr->target, func->type);
428-
replaceCurrent(
475+
476+
replaceCurrent(parent.maybeLoadSecondary(
477+
builder,
429478
builder.makeCallIndirect(tableSlot.tableName,
430479
tableSlot.makeExpr(parent.primary),
431480
curr->operands,
432481
func->type,
433-
curr->isReturn));
482+
curr->isReturn)));
434483
}
435484
void visitRefFunc(RefFunc* curr) {
436485
assert(false && "TODO: handle ref.func as well");

src/ir/module-splitting.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444

4545
namespace wasm::ModuleSplitting {
4646

47+
static const Name LOAD_SECONDARY_MODULE("__load_secondary_module");
48+
4749
struct Config {
4850
// The set of functions to keep in the primary module. All others are split
4951
// out into the new secondary module. Must include the start function if it
@@ -64,6 +66,9 @@ struct Config {
6466
// false, the original function names will be used (after `newExportPrefix`)
6567
// as the new export names.
6668
bool minimizeNewExportNames = false;
69+
// When JSPI support is enabled the secondary module loading is handled by an
70+
// imported function.
71+
bool jspi = false;
6772
};
6873

6974
struct Results {

src/passes/JSPI.cpp

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "ir/element-utils.h"
1919
#include "ir/import-utils.h"
2020
#include "ir/literal-utils.h"
21+
#include "ir/module-splitting.h"
2122
#include "ir/names.h"
2223
#include "ir/utils.h"
2324
#include "pass.h"
@@ -49,6 +50,12 @@
4950
// Wrap each export in the comma-separated list. Similar to jspi-imports,
5051
// wildcards and separate files are supported.
5152
//
53+
// --pass-arg=jspi-split-module
54+
//
55+
// Enables integration with wasm-split. A JSPI'ed function named
56+
// `__load_secondary_module` will be injected that is used by wasm-split to
57+
// load a secondary module.
58+
//
5259

5360
namespace wasm {
5461

@@ -87,6 +94,22 @@ struct JSPI : public Pass {
8794
options.getArgumentOrDefault("jspi-exports", "")));
8895
String::Split listedExports(stateChangingExports, ",");
8996

97+
bool wasmSplit = options.hasArgument("jspi-split-module");
98+
if (wasmSplit) {
99+
// Make an import for the load secondary module function so a JSPI wrapper
100+
// version will be created.
101+
auto import =
102+
Builder::makeFunction(ModuleSplitting::LOAD_SECONDARY_MODULE,
103+
Signature(Type::none, Type::none),
104+
{});
105+
import->module = ENV;
106+
import->base = ModuleSplitting::LOAD_SECONDARY_MODULE;
107+
module->addFunction(std::move(import));
108+
listedImports.push_back(
109+
ENV.toString() + "." +
110+
ModuleSplitting::LOAD_SECONDARY_MODULE.toString());
111+
}
112+
90113
// Create a global to store the suspender that is passed into exported
91114
// functions and will then need to be passed out to the imported functions.
92115
Name suspender = Names::getValidGlobalName(*module, "suspender");
@@ -128,7 +151,7 @@ struct JSPI : public Pass {
128151
if (im->imported() &&
129152
canChangeState(getFullFunctionName(im->module, im->base),
130153
listedImports)) {
131-
makeWrapperForImport(im, module, suspender);
154+
makeWrapperForImport(im, module, suspender, wasmSplit);
132155
}
133156
}
134157
}
@@ -181,7 +204,10 @@ struct JSPI : public Pass {
181204
return module->addFunction(std::move(wrapperFunc))->name;
182205
}
183206

184-
void makeWrapperForImport(Function* im, Module* module, Name suspender) {
207+
void makeWrapperForImport(Function* im,
208+
Module* module,
209+
Name suspender,
210+
bool wasmSplit) {
185211
Builder builder(*module);
186212
auto wrapperIm = make_unique<Function>();
187213
wrapperIm->name = Names::getValidFunctionName(
@@ -234,6 +260,15 @@ struct JSPI : public Pass {
234260
stub->body = block;
235261
wrapperIm->type = Signature(Type(params), call->type);
236262

263+
if (wasmSplit && im->name == ModuleSplitting::LOAD_SECONDARY_MODULE) {
264+
// In non-debug builds the name of the JSPI wrapper function for loading
265+
// the secondary module will be removed. Create an export of it so that
266+
// wasm-split can find it.
267+
module->addExport(
268+
builder.makeExport(ModuleSplitting::LOAD_SECONDARY_MODULE,
269+
ModuleSplitting::LOAD_SECONDARY_MODULE,
270+
ExternalKind::Function));
271+
}
237272
module->removeFunction(im->name);
238273
module->addFunction(std::move(stub));
239274
module->addFunction(std::move(wrapperIm));

src/tools/wasm-split/split-options.cpp

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,14 @@ WasmSplitOptions::WasmSplitOptions()
205205
[&](Options* o, const std::string& argument) {
206206
placeholderNamespace = argument;
207207
})
208-
.add(
209-
"--asyncify",
210-
"",
211-
"Transform the module to support unwinding the stack from placeholder "
212-
"functions and rewinding it once the secondary module has been loaded.",
213-
WasmSplitOption,
214-
{Mode::Split},
215-
Options::Arguments::Zero,
216-
[&](Options* o, const std::string& argument) { asyncify = true; })
208+
.add("--jspi",
209+
"",
210+
"Transform the module to support asynchronously loading the secondary "
211+
"module before any placeholder functions have been called.",
212+
WasmSplitOption,
213+
{Mode::Split},
214+
Options::Arguments::Zero,
215+
[&](Options* o, const std::string& argument) { jspi = true; })
217216
.add(
218217
"--export-prefix",
219218
"",

src/tools/wasm-split/split-options.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ struct WasmSplitOptions : ToolOptions {
4646
bool emitBinary = true;
4747
bool symbolMap = false;
4848
bool placeholderMap = false;
49-
bool asyncify = false;
49+
bool jspi = false;
5050

5151
// TODO: Remove this. See the comment in wasm-binary.h.
5252
bool emitModuleNames = false;

src/tools/wasm-split/wasm-split.cpp

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ void splitModule(const WasmSplitOptions& options) {
248248
}
249249
}
250250

251+
if (options.jspi) {
252+
// The load secondary module function must be kept in the main module.
253+
keepFuncs.insert(ModuleSplitting::LOAD_SECONDARY_MODULE);
254+
}
255+
251256
if (!options.quiet && keepFuncs.size() == 0) {
252257
std::cerr << "warning: not keeping any functions in the primary module\n";
253258
}
@@ -300,22 +305,13 @@ void splitModule(const WasmSplitOptions& options) {
300305
config.newExportPrefix = options.exportPrefix;
301306
}
302307
config.minimizeNewExportNames = !options.passOptions.debugInfo;
308+
config.jspi = options.jspi;
303309
auto splitResults = ModuleSplitting::splitFunctions(wasm, config);
304310
auto& secondary = splitResults.secondary;
305311

306312
adjustTableSize(wasm, options.initialTableSize);
307313
adjustTableSize(*secondary, options.initialTableSize);
308314

309-
// Run asyncify on the primary module
310-
if (options.asyncify) {
311-
PassOptions passOptions;
312-
passOptions.optimizeLevel = 1;
313-
passOptions.arguments.insert({"asyncify-ignore-imports", ""});
314-
PassRunner runner(&wasm, passOptions);
315-
runner.add("asyncify");
316-
runner.run();
317-
}
318-
319315
if (options.symbolMap) {
320316
writeSymbolMap(wasm, options.primaryOutput + ".symbols");
321317
writeSymbolMap(*secondary, options.secondaryOutput + ".symbols");

test/lit/help/wasm-split.test

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@
6666
;; CHECK-NEXT: import placeholder functions into the
6767
;; CHECK-NEXT: primary module.
6868
;; CHECK-NEXT:
69-
;; CHECK-NEXT: --asyncify [split] Transform the module to support
70-
;; CHECK-NEXT: unwinding the stack from placeholder
71-
;; CHECK-NEXT: functions and rewinding it once the
72-
;; CHECK-NEXT: secondary module has been loaded.
69+
;; CHECK-NEXT: --jspi [split] Transform the module to support
70+
;; CHECK-NEXT: asynchronously loading the secondary
71+
;; CHECK-NEXT: module before any placeholder functions
72+
;; CHECK-NEXT: have been called.
7373
;; CHECK-NEXT:
7474
;; CHECK-NEXT: --export-prefix [split] An identifying prefix to prepend
7575
;; CHECK-NEXT: to new export names created by module
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
2+
;; RUN: wasm-opt %s --jspi --pass-arg=jspi-split-module -all -S -o - | filecheck %s
3+
4+
;; The following should be generated besides the usual JSPI wasm:
5+
;; - function import
6+
;; - JSPI'ed version of the import
7+
;; - export of the above
8+
(module)
9+
;; CHECK: (type $none_=>_none (func))
10+
11+
;; CHECK: (type $externref_=>_none (func (param externref)))
12+
13+
;; CHECK: (import "env" "__load_secondary_module" (func $import$__load_secondary_module (param externref)))
14+
15+
;; CHECK: (global $suspender (mut externref) (ref.null noextern))
16+
17+
;; CHECK: (export "__load_secondary_module" (func $__load_secondary_module))
18+
19+
;; CHECK: (func $__load_secondary_module (type $none_=>_none)
20+
;; CHECK-NEXT: (local $0 externref)
21+
;; CHECK-NEXT: (local.set $0
22+
;; CHECK-NEXT: (global.get $suspender)
23+
;; CHECK-NEXT: )
24+
;; CHECK-NEXT: (call $import$__load_secondary_module
25+
;; CHECK-NEXT: (global.get $suspender)
26+
;; CHECK-NEXT: )
27+
;; CHECK-NEXT: (global.set $suspender
28+
;; CHECK-NEXT: (local.get $0)
29+
;; CHECK-NEXT: )
30+
;; CHECK-NEXT: )

0 commit comments

Comments
 (0)