Skip to content

Commit d66befd

Browse files
mkustermannCommit Queue
authored andcommitted
[dart2wasm] Add support for @pragma('wasm:weak-export', '<name>')
We explicitly & unconditionally export functions to JS via the `@pragma('wasm:export', '<name>')` annotation. This is mainly useful for external APIs that the JavaScript side can invoke - for example `$invokeMain()`. Though we currently use the same mechanism also in other places where a Dart function `A` (if used) calls to JS which calls back into Dart via calling exported Dart function `A*`. The issue is that this mechanism doesn't work very well with tree shaking: If the function `A` is not used it will be tree shaken. We will then also not emit the JS code, but we still compile the exported function `A*` as it's a root due to `@pragma('wasm:export', '<name')`. We then also have to compile everything reachable from `A*`. This is the case for a few functions in the core libraries but even more pronounced in code that the modular JS interop transformer generates for callbacks: It generates `|_<num>` functions that call out to JS which call back into a dart-exported `_<num>` function. The former may be unused & tree shaken (as well as their JS code) but the ladder are force-exported and therefore treated as entrypoints. This CL solves problem by * Mark function `A*` as weakly exported via `@pragma('wasm:weak-export', '<name>')` => TFA will not consider such functions as entrypoints => TFA will only retain such functions if they are referenced by other functions that aren't tree shaken. => The backend will export such functions as `<name>` if they are referenced by any other code that's compiled. * Making the code that calls function `A` also reference (but not use) `A*`. => This will make TFA retain function `A*` if it retains `A`. => In core libraries we manually reference `A*` in code that uses `A` and mark `A*` as weakly exported => In JS interop transformer we emit similar code for callbacks => We refer `A*` by using `exportWasmFunction()` which is an opaque external function that prevents TFA and backend from optimizing it away - so the function will be generated & exported. Overall this CL ensures we only keep the exported functions if we actually need them (i.e. we call to JS and JS calls those exported functions). This shrinks stripped dart2wasm hello world file in -O4 from 28 KB to 12 KB. It also enables tree shaking of callback using JS interop code. Change-Id: Ie81eac49cbcb574d569ea95a90538e8f417e2a12 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/415220 Reviewed-by: Srujan Gaddam <[email protected]> Commit-Queue: Martin Kustermann <[email protected]>
1 parent 5bad3ac commit d66befd

File tree

10 files changed

+255
-122
lines changed

10 files changed

+255
-122
lines changed

pkg/dart2wasm/lib/code_generator.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,6 +1700,18 @@ abstract class AstCodeGenerator
17001700
}
17011701

17021702
Member? singleTarget = translator.singleTarget(node);
1703+
1704+
// Custom devirtualization because TFA doesn't correctly devirtualize index
1705+
// accesses on constant lists (see https://dartbug.com/60313)
1706+
if (singleTarget == null &&
1707+
target.kind == ProcedureKind.Operator &&
1708+
target.name.text == '[]') {
1709+
final receiver = node.receiver;
1710+
if (receiver is ConstantExpression && receiver.constant is ListConstant) {
1711+
singleTarget = translator.listBaseIndexOperator;
1712+
}
1713+
}
1714+
17031715
if (singleTarget != null) {
17041716
final target = translator.getFunctionEntry(singleTarget.reference,
17051717
uncheckedEntry: useUncheckedEntry);

pkg/dart2wasm/lib/functions.dart

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ class FunctionCollector {
2323
final Map<Reference, w.BaseFunction> _functions = {};
2424
// Wasm function for each function expression and local function.
2525
final Map<Lambda, w.BaseFunction> _lambdas = {};
26-
// Names of exported functions
27-
final Map<Reference, String> _exports = {};
2826
// Selector IDs that are invoked via GDT.
2927
final Set<int> _calledSelectors = {};
3028
final Set<int> _calledUncheckedSelectors = {};
@@ -42,14 +40,13 @@ class FunctionCollector {
4240
void _collectImportsAndExports() {
4341
for (Library library in translator.libraries) {
4442
library.procedures.forEach(_importOrExport);
45-
library.fields.forEach(_importOrExport);
4643
for (Class cls in library.classes) {
4744
cls.procedures.forEach(_importOrExport);
4845
}
4946
}
5047
}
5148

52-
void _importOrExport(Member member) {
49+
void _importOrExport(Procedure member) {
5350
String? importName =
5451
translator.getPragma(member, "wasm:import", member.name.text);
5552
if (importName != null) {
@@ -58,31 +55,34 @@ class FunctionCollector {
5855
assert(!member.isInstanceMember);
5956
String module = importName.substring(0, dot);
6057
String name = importName.substring(dot + 1);
61-
if (member is Procedure) {
62-
w.FunctionType ftype = _makeFunctionType(
63-
translator, member.reference, null,
64-
isImportOrExport: true);
65-
_functions[member.reference] = translator
66-
.moduleForReference(member.reference)
67-
.functions
68-
.import(module, name, ftype, "$importName (import)");
69-
}
58+
final ftype = _makeFunctionType(translator, member.reference, null,
59+
isImportOrExport: true);
60+
_functions[member.reference] = translator
61+
.moduleForReference(member.reference)
62+
.functions
63+
.import(module, name, ftype, "$importName (import)");
7064
}
7165
}
66+
67+
// Ensure any procedures marked as exported are enqueued.
7268
String? exportName =
7369
translator.getPragma(member, "wasm:export", member.name.text);
7470
if (exportName != null) {
75-
if (member is Procedure) {
76-
_makeFunctionType(translator, member.reference, null,
77-
isImportOrExport: true);
78-
}
79-
_exports[member.reference] = exportName;
71+
getFunction(member.reference);
8072
}
8173
}
8274

8375
/// If the member with the reference [target] is exported, get the export
8476
/// name.
85-
String? getExportName(Reference target) => _exports[target];
77+
String? getExportName(Reference target) {
78+
final member = target.asMember;
79+
if (member.reference == target) {
80+
final text = member.name.text;
81+
return translator.getPragma(member, "wasm:export", text) ??
82+
translator.getPragma(member, "wasm:weak-export", text);
83+
}
84+
return null;
85+
}
8686

8787
w.BaseFunction importFunctionToDynamicModule(w.BaseFunction fun) {
8888
assert(translator.isDynamicModule);
@@ -110,31 +110,6 @@ class FunctionCollector {
110110
}
111111
}
112112

113-
// Add exports to the module and add exported functions to the
114-
// compilationQueue.
115-
for (var export in _exports.entries) {
116-
Reference target = export.key;
117-
Member node = target.asMember;
118-
if (node is Procedure) {
119-
assert(!node.isInstanceMember);
120-
assert(!node.isGetter);
121-
w.FunctionType ftype =
122-
_makeFunctionType(translator, target, null, isImportOrExport: true);
123-
final module = translator.moduleForReference(target);
124-
w.FunctionBuilder function = module.functions.define(ftype, "$node");
125-
_functions[target] = function;
126-
module.exports.export(export.value, function);
127-
translator.compilationQueue.add(AstCompilationTask(function,
128-
getMemberCodeGenerator(translator, function, target), target));
129-
} else if (node is Field) {
130-
final module = translator.moduleForReference(target);
131-
w.Table? table = translator.getTable(module, node);
132-
if (table != null) {
133-
module.exports.export(export.value, table);
134-
}
135-
}
136-
}
137-
138113
// Value classes are always implicitly allocated.
139114
recordClassAllocation(
140115
translator.classInfo[translator.boxedBoolClass]!.classId);
@@ -150,9 +125,49 @@ class FunctionCollector {
150125

151126
w.BaseFunction getFunction(Reference target) {
152127
return _functions.putIfAbsent(target, () {
128+
final member = target.asMember;
129+
130+
// If this function is a `@pragma('wasm:import', '<module>:<name>')` we
131+
// import the function and return it.
132+
if (member.reference == target && member.annotations.isNotEmpty) {
133+
final importName =
134+
translator.getPragma(member, 'wasm:import', member.name.text);
135+
if (importName != null) {
136+
assert(!member.isInstanceMember);
137+
int dot = importName.indexOf('.');
138+
if (dot != -1) {
139+
final module = importName.substring(0, dot);
140+
final name = importName.substring(dot + 1);
141+
final ftype = _makeFunctionType(translator, member.reference, null,
142+
isImportOrExport: true);
143+
return _functions[member.reference] = translator
144+
.moduleForReference(member.reference)
145+
.functions
146+
.import(module, name, ftype, "$importName (import)");
147+
}
148+
}
149+
}
150+
151+
// If this function is exported via
152+
// * `@pragma('wasm:export', '<name>')` or
153+
// * `@pragma('wasm:weak-export', '<name>')`
154+
// we export it under the given `<name>`
155+
String? exportName;
156+
if (member.reference == target && member.annotations.isNotEmpty) {
157+
exportName = translator.getPragma(
158+
member, 'wasm:export', member.name.text) ??
159+
translator.getPragma(member, 'wasm:weak-export', member.name.text);
160+
assert(exportName == null || member is Procedure && member.isStatic);
161+
}
162+
163+
final w.FunctionType ftype = exportName != null
164+
? _makeFunctionType(translator, target, null, isImportOrExport: true)
165+
: translator.signatureForDirectCall(target);
166+
153167
final module = translator.moduleForReference(target);
154-
final function = module.functions.define(
155-
translator.signatureForDirectCall(target), getFunctionName(target));
168+
final function = module.functions.define(ftype, getFunctionName(target));
169+
if (exportName != null) module.exports.export(exportName, function);
170+
156171
translator.compilationQueue.add(AstCompilationTask(function,
157172
getMemberCodeGenerator(translator, function, target), target));
158173

pkg/dart2wasm/lib/intrinsics.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ enum StaticIntrinsic {
164164
intBitsToFloat('dart:_internal', null, 'intBitsToFloat'),
165165
doubleToIntBits('dart:_internal', null, 'doubleToIntBits'),
166166
intBitsToDouble('dart:_internal', null, 'intBitsToDouble'),
167+
exportWasmFunction('dart:_internal', null, 'exportWasmFunction'),
167168
getID('dart:_internal', 'ClassID', 'getID'),
168169
loadInt8('dart:ffi', null, '_loadInt8'),
169170
loadUint8('dart:ffi', null, '_loadUint8'),
@@ -1162,6 +1163,28 @@ class Intrinsifier {
11621163
node.arguments.positional.single, w.NumType.i64);
11631164
b.f64_reinterpret_i64();
11641165
return w.NumType.f64;
1166+
case StaticIntrinsic.exportWasmFunction:
1167+
const error =
1168+
'The `dart:_internal:exportWasmFunction` expects its argument '
1169+
'to be a tear-off of a `@pragma(\'wasm:weak-export\', ...)` '
1170+
'annotated function';
1171+
1172+
// Sanity check argument.
1173+
final argument = node.arguments.positional.single;
1174+
if (argument is! ConstantExpression) throw error;
1175+
final constant = argument.constant;
1176+
if (constant is! StaticTearOffConstant) throw error;
1177+
final target = constant.target;
1178+
if (translator.getPragma(target, 'wasm:weak-export', '') == null) {
1179+
throw error;
1180+
}
1181+
1182+
// Ensure we compile the target function & export it.
1183+
translator.functions.getFunction(target.reference);
1184+
1185+
final topType = translator.topInfo.nullableType;
1186+
codeGen.translateExpression(NullLiteral(), topType);
1187+
return topType;
11651188
case StaticIntrinsic.getID:
11661189
ClassInfo info = translator.topInfo;
11671190
codeGen.translateExpression(

pkg/dart2wasm/lib/js/callback_specializer.dart

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,8 @@ class CallbackSpecializer {
6363
/// returned value. [node] is the conversion function that was called to
6464
/// convert the callback.
6565
///
66-
/// Returns a [String] function name representing the name of the wrapping
67-
/// function.
68-
String _createFunctionTrampoline(Procedure node, FunctionType function,
66+
/// Returns the created trampoline [Procedure].
67+
Procedure _createFunctionTrampoline(Procedure node, FunctionType function,
6968
{required bool boxExternRef}) {
7069
// Create arguments for each positional parameter in the function. These
7170
// arguments will be JS objects. The generated wrapper will cast each
@@ -156,7 +155,7 @@ class CallbackSpecializer {
156155
// returned from the supplied callback will be converted with `jsifyRaw` to
157156
// a native JS value before being returned to JS.
158157
final functionTrampolineName = _methodCollector.generateMethodName();
159-
_methodCollector.addInteropProcedure(
158+
return _methodCollector.addInteropProcedure(
160159
functionTrampolineName,
161160
functionTrampolineName,
162161
FunctionNode(functionTrampolineBody,
@@ -169,9 +168,8 @@ class CallbackSpecializer {
169168
returnType: _util.nullableWasmExternRefType)
170169
..fileOffset = node.fileOffset,
171170
node.fileUri,
172-
AnnotationType.export,
171+
AnnotationType.weakExport,
173172
isExternal: false);
174-
return functionTrampolineName;
175173
}
176174

177175
/// Create a [Procedure] that will wrap a Dart callback in a JS wrapper.
@@ -189,12 +187,14 @@ class CallbackSpecializer {
189187
/// function's arguments' length, the cast closure if needed, and the JS
190188
/// function's arguments as arguments.
191189
///
192-
/// Returns the created [Procedure].
193-
Procedure _getJSWrapperFunction(Procedure node, FunctionType type,
190+
/// Returns the created JS wrapper [Procedure] which will call out to JS
191+
/// and the trampoline [Procedure] which will be invoked by the JS code.
192+
(Procedure, Procedure) _getJSWrapperFunction(
193+
Procedure node, FunctionType type,
194194
{required bool boxExternRef,
195195
required bool needsCastClosure,
196196
required bool captureThis}) {
197-
final functionTrampolineName =
197+
final functionTrampoline =
198198
_createFunctionTrampoline(node, type, boxExternRef: boxExternRef);
199199
List<String> jsParameters = [];
200200
var jsParametersLength = type.positionalParameters.length;
@@ -220,7 +220,7 @@ class CallbackSpecializer {
220220
}
221221

222222
// Create Dart procedure stub.
223-
final jsMethodName = functionTrampolineName;
223+
final jsMethodName = functionTrampoline.name.text;
224224
Procedure dartProcedure = _methodCollector.addInteropProcedure(
225225
'|$jsMethodName',
226226
'dart2wasm.$jsMethodName',
@@ -246,10 +246,10 @@ class CallbackSpecializer {
246246
dartProcedure,
247247
jsMethodName,
248248
"$jsMethodParams => finalizeWrapper(f, function($jsWrapperParams) {"
249-
" return dartInstance.exports.$functionTrampolineName($dartArguments) "
249+
" return dartInstance.exports.${functionTrampoline.name.text}($dartArguments) "
250250
"})");
251251

252-
return dartProcedure;
252+
return (dartProcedure, functionTrampoline);
253253
}
254254

255255
/// Lowers an invocation of `allowInterop<type>(foo)` to:
@@ -272,7 +272,7 @@ class CallbackSpecializer {
272272
Expression allowInterop(StaticInvocation staticInvocation) {
273273
final argument = staticInvocation.arguments.positional.single;
274274
final type = argument.getStaticType(_staticTypeContext) as FunctionType;
275-
final jsWrapperFunction = _getJSWrapperFunction(
275+
final (jsWrapperFunction, exportedFunction) = _getJSWrapperFunction(
276276
staticInvocation.target, type,
277277
boxExternRef: false, needsCastClosure: false, captureThis: false);
278278
final v = VariableDeclaration('#var',
@@ -287,12 +287,24 @@ class CallbackSpecializer {
287287
_util.wrapDartFunctionTarget,
288288
Arguments([
289289
VariableGet(v),
290-
StaticInvocation(
291-
jsWrapperFunction,
292-
Arguments([
293-
StaticInvocation(_util.jsObjectFromDartObjectTarget,
294-
Arguments([VariableGet(v)]))
295-
])),
290+
BlockExpression(
291+
Block([
292+
// This ensures TFA will retain the function which the
293+
// JS code will call. The backend in return will export
294+
// the function due to `@pragma('wasm:weak-export', ...)`
295+
ExpressionStatement(StaticInvocation(
296+
_util.exportWasmFunctionTarget,
297+
Arguments([
298+
ConstantExpression(
299+
StaticTearOffConstant(exportedFunction))
300+
])))
301+
]),
302+
StaticInvocation(
303+
jsWrapperFunction,
304+
Arguments([
305+
StaticInvocation(_util.jsObjectFromDartObjectTarget,
306+
Arguments([VariableGet(v)]))
307+
]))),
296308
], types: [
297309
type
298310
])),
@@ -359,19 +371,30 @@ class CallbackSpecializer {
359371
final argument = staticInvocation.arguments.positional.single;
360372
final type = argument.getStaticType(_staticTypeContext) as FunctionType;
361373
final castClosure = _createCastClosure(type);
362-
final jsWrapperFunction = _getJSWrapperFunction(
374+
final (jsWrapperFunction, exportedFunction) = _getJSWrapperFunction(
363375
staticInvocation.target, type,
364376
boxExternRef: true,
365377
needsCastClosure: castClosure != null,
366378
captureThis: captureThis);
367-
return _createJSValue(StaticInvocation(
368-
jsWrapperFunction,
369-
Arguments([
370-
StaticInvocation(
371-
_util.jsObjectFromDartObjectTarget, Arguments([argument])),
372-
if (castClosure != null)
373-
StaticInvocation(
374-
_util.jsObjectFromDartObjectTarget, Arguments([castClosure]))
375-
])));
379+
return _createJSValue(BlockExpression(
380+
Block([
381+
// This ensures TFA will retain the function which the
382+
// JS code will call. The backend in return will export
383+
// the function due to `@pragma('wasm:weak-export', ...)`
384+
ExpressionStatement(StaticInvocation(
385+
_util.exportWasmFunctionTarget,
386+
Arguments([
387+
ConstantExpression(StaticTearOffConstant(exportedFunction))
388+
])))
389+
]),
390+
StaticInvocation(
391+
jsWrapperFunction,
392+
Arguments([
393+
StaticInvocation(
394+
_util.jsObjectFromDartObjectTarget, Arguments([argument])),
395+
if (castClosure != null)
396+
StaticInvocation(_util.jsObjectFromDartObjectTarget,
397+
Arguments([castClosure]))
398+
]))));
376399
}
377400
}

0 commit comments

Comments
 (0)