diff --git a/pkg/dart2wasm/lib/constants.dart b/pkg/dart2wasm/lib/constants.dart index 13a1981fca3..f2a0fd8b844 100644 --- a/pkg/dart2wasm/lib/constants.dart +++ b/pkg/dart2wasm/lib/constants.dart @@ -498,8 +498,9 @@ class ConstantCreator extends ConstantVisitor return info; } - static String _dynamicModuleConstantExportName(int id) => '#c$id'; - static String _dynamicModuleInitFunctionExportName(int id) => '#cf$id'; + static String _dynamicModuleConstantExportName(int id) => '#constant$id'; + static String _dynamicModuleInitFunctionExportName(int id) => + '#constantFunction$id'; static int _nextGlobalId = 0; String _constantName(Constant constant) { @@ -577,11 +578,15 @@ class ConstantCreator extends ConstantVisitor translator.typesBuilder.defineFunction(const [], [type]); if (mainModuleExportId != null) { - global = targetModule.globals.import(translator.mainModule.moduleName, - _dynamicModuleConstantExportName(mainModuleExportId), globalType); + global = targetModule.globals.import( + translator.mainModule.moduleName, + translator.dynamicModuleExports! + .getConstantName(mainModuleExportId)!, + globalType); initFunction = targetModule.functions.import( translator.mainModule.moduleName, - _dynamicModuleInitFunctionExportName(mainModuleExportId), + translator.dynamicModuleExports! + .getConstantInitializerName(mainModuleExportId)!, ftype); } else { final name = _constantName(constant); @@ -597,10 +602,16 @@ class ConstantCreator extends ConstantVisitor final exportId = dynamicModuleConstantIdMap[constant] = dynamicModuleConstantIdMap.length; - targetModule.exports.export( - _dynamicModuleConstantExportName(exportId), definedGlobal); - targetModule.exports - .export(_dynamicModuleInitFunctionExportName(exportId), function); + translator.exporter.exportConstant( + targetModule, + _dynamicModuleConstantExportName(exportId), + definedGlobal, + exportId); + translator.exporter.exportConstantInitializer( + targetModule, + _dynamicModuleInitFunctionExportName(exportId), + function, + exportId); } final b2 = function.body; generator(b2); @@ -622,8 +633,11 @@ class ConstantCreator extends ConstantVisitor final globalType = w.GlobalType(type, mutable: false); w.Global global; if (mainModuleExportId != null) { - global = targetModule.globals.import(translator.mainModule.moduleName, - _dynamicModuleConstantExportName(mainModuleExportId), globalType); + global = targetModule.globals.import( + translator.mainModule.moduleName, + translator.dynamicModuleExports! + .getConstantName(mainModuleExportId)!, + globalType); } else { constants.currentlyCreating = true; final definedGlobal = global = @@ -636,8 +650,11 @@ class ConstantCreator extends ConstantVisitor final exportId = dynamicModuleConstantIdMap[constant] = dynamicModuleConstantIdMap.length; - targetModule.exports.export( - _dynamicModuleConstantExportName(exportId), definedGlobal); + translator.exporter.exportConstant( + targetModule, + _dynamicModuleConstantExportName(exportId), + definedGlobal, + exportId); } } diff --git a/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart b/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart index 5703f70a55e..78bd20a23d9 100644 --- a/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart +++ b/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart @@ -27,6 +27,7 @@ import 'class_info.dart'; import 'compiler_options.dart'; import 'dispatch_table.dart'; import 'dynamic_modules.dart'; +import 'exports.dart'; import 'js/method_collector.dart' show JSMethods; import 'serialization.dart'; import 'translator.dart'; @@ -474,6 +475,7 @@ Future<(Component, JSMethods)> generateDynamicSubmoduleComponent( final newComponent = Component() ..addMetadataRepository(DynamicModuleGlobalIdRepository()) ..addMetadataRepository(DynamicModuleConstantRepository()) + ..addMetadataRepository(DynamicModuleExportRepository()) ..addMetadataRepository(ProcedureAttributesMetadataRepository()) ..addMetadataRepository(TableSelectorMetadataRepository()) ..addMetadataRepository(DirectCallMetadataRepository()) diff --git a/pkg/dart2wasm/lib/dynamic_modules.dart b/pkg/dart2wasm/lib/dynamic_modules.dart index fed50ea424c..3ed14d41bfb 100644 --- a/pkg/dart2wasm/lib/dynamic_modules.dart +++ b/pkg/dart2wasm/lib/dynamic_modules.dart @@ -22,6 +22,7 @@ import 'compiler_options.dart'; import 'constants.dart' show maxArrayNewFixedLength; import 'dispatch_table.dart'; import 'dynamic_module_kernel_metadata.dart'; +import 'exports.dart'; import 'intrinsics.dart' show MemberIntrinsic; import 'kernel_nodes.dart'; import 'modules.dart'; @@ -129,6 +130,7 @@ class DynamicMainModuleStrategy extends ModuleStrategy with KernelNodes { component.addMetadataRepository(DynamicModuleConstantRepository()); component.addMetadataRepository(DynamicModuleGlobalIdRepository()); + component.addMetadataRepository(DynamicModuleExportRepository()); } @override diff --git a/pkg/dart2wasm/lib/exports.dart b/pkg/dart2wasm/lib/exports.dart new file mode 100644 index 00000000000..104f4883199 --- /dev/null +++ b/pkg/dart2wasm/lib/exports.dart @@ -0,0 +1,147 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:kernel/ast.dart'; +import 'package:wasm_builder/wasm_builder.dart'; + +import 'util.dart'; + +class DynamicModuleExportRepository + extends MetadataRepository { + static const repositoryTag = 'wasm.dynamic-modules.exports'; + + @override + final String tag = repositoryTag; + + @override + final Map mapping = {}; + + @override + DynamicModuleExports readFromBinary(_, BinarySource source) { + return DynamicModuleExports._readFromBinary(source); + } + + @override + void writeToBinary(DynamicModuleExports exports, Node node, BinarySink sink) { + exports._writeToBinary(sink); + } +} + +class DynamicModuleExports { + final Map _callableNames = {}; + final Map _constantNames = {}; + final Map _constantInitializerNames = {}; + + DynamicModuleExports(); + + factory DynamicModuleExports._readFromBinary(BinarySource source) { + void readMap(Map map) { + final length = source.readUInt30(); + for (int i = 0; i < length; i++) { + final id = source.readUInt30(); + final name = source.readStringReference(); + map[id] = name; + } + } + + final exports = DynamicModuleExports(); + readMap(exports._callableNames); + readMap(exports._constantNames); + readMap(exports._constantInitializerNames); + return exports; + } + + void _writeToBinary(BinarySink sink) { + void writeMap(Map map) { + sink.writeUInt30(map.length); + map.forEach((id, name) { + sink.writeUInt30(id); + sink.writeStringReference(name); + }); + } + + writeMap(_callableNames); + writeMap(_constantNames); + writeMap(_constantInitializerNames); + } + + String? getCallableName(int callableReferenceId) => + _callableNames[callableReferenceId]; + + String? getConstantName(int constantId) => _constantNames[constantId]; + + String? getConstantInitializerName(int constantId) => + _constantInitializerNames[constantId]; +} + +/// A generator for export names (when minification is enabled). +/// +/// For simplicity and readability of generated names, we just use the base64 +/// encoding of an integer counter. +class ExportNamer { + final bool minify; + + ExportNamer(this.minify); + + final Set _reservedNames = {}; + + /// Mark a name as reserved by the `wasm:export` or `wasm:weak-export` + /// annotations so that it will not be generated as a minified export name. + void reserveName(String name) { + if (!minify) return; + final added = _reservedNames.add(name); + assert(added, "Name '$name' is already reserved"); + } + + int _nameCounter = 0; + + String _getExportName(String name) { + if (!minify) return name; + do { + name = intToBase64(_nameCounter++); + } while (_reservedNames.contains(name)); + return name; + } +} + +/// Manages exporting entities in a minification-aware way. +/// +/// The [Exporter] stores associations between entities exported from a dynamic +/// main module and their export names in [dynamicModuleExports] so that dynamic +/// submodules can import these entities with those names. +/// +/// The `export___` methods add relevant associations to [dynamicModuleExports] +/// and add the given exportable to the Wasm exports of the specified module +/// under the computed export name. +class Exporter { + final ExportNamer _namer; + + Exporter(this._namer); + + final DynamicModuleExports dynamicModuleExports = DynamicModuleExports(); + + void exportCallable(ModuleBuilder module, String name, + FunctionBuilder function, int callableReferenceId) { + dynamicModuleExports._callableNames[callableReferenceId] = + _export(module, name, function); + } + + void exportConstant(ModuleBuilder module, String name, GlobalBuilder constant, + int constantId) { + dynamicModuleExports._constantNames[constantId] = + _export(module, name, constant); + } + + void exportConstantInitializer(ModuleBuilder module, String name, + FunctionBuilder initializer, int constantId) { + dynamicModuleExports._constantInitializerNames[constantId] = + _export(module, name, initializer); + } + + String _export(ModuleBuilder module, String name, Exportable exportable) { + final exportName = _namer._getExportName(name); + module.exports.export(exportName, exportable); + return exportName; + } +} diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart index 080c6467054..5638b84e959 100644 --- a/pkg/dart2wasm/lib/functions.dart +++ b/pkg/dart2wasm/lib/functions.dart @@ -67,11 +67,18 @@ class FunctionCollector { } // Ensure any procedures marked as exported are enqueued. - String? exportName = - translator.getPragma(member, "wasm:export", member.name.text); + final text = member.name.text; + String? exportName = translator.getPragma(member, "wasm:export", text); if (exportName != null) { getFunction(member.reference); } + + // Whether a procedure is strongly or weakly exported, we must not use its + // name as the export name of a different function. + exportName ??= translator.getPragma(member, "wasm:weak-export", text); + if (exportName != null) { + translator.exportNamer.reserveName(exportName); + } } /// If the member with the reference [target] is exported, get the export @@ -158,9 +165,11 @@ class FunctionCollector { final callableReferenceId = translator.dynamicModuleInfo?.metadata.callableReferenceIds[target]; if (callableReferenceId != null) { - translator.mainModule.exports.export( + translator.exporter.exportCallable( + translator.mainModule, _generateDynamicSubmoduleCallableName(callableReferenceId), - function); + function, + callableReferenceId); } } @@ -170,7 +179,8 @@ class FunctionCollector { }); } - String _generateDynamicSubmoduleCallableName(int key) => '#dc$key'; + String _generateDynamicSubmoduleCallableName(int key) => + '#dynamicCallable$key'; w.BaseFunction _importFunctionToDynamicSubmodule(Reference target) { assert(translator.isDynamicSubmodule); @@ -186,8 +196,8 @@ class FunctionCollector { } return translator.dynamicSubmodule.functions.import( translator.mainModule.moduleName, - _generateDynamicSubmoduleCallableName( - dynamicSubmoduleCallableReferenceId), + translator.dynamicModuleExports! + .getCallableName(dynamicSubmoduleCallableReferenceId)!, translator.signatureForMainModule(target), getFunctionName(target)); } diff --git a/pkg/dart2wasm/lib/symbols.dart b/pkg/dart2wasm/lib/symbols.dart index b32e949f7f4..c08b60e581e 100644 --- a/pkg/dart2wasm/lib/symbols.dart +++ b/pkg/dart2wasm/lib/symbols.dart @@ -2,10 +2,10 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:convert'; - import 'package:kernel/ast.dart'; +import 'util.dart'; + class Symbols { final bool minify; Symbols(this.minify); @@ -31,7 +31,7 @@ class Symbols { final Map symbolOrdinals = {}; String getMangledSymbolName(SymbolConstant symbol) { if (minify) { - return _intToBase64( + return intToBase64( symbolOrdinals.putIfAbsent(symbol, () => symbolOrdinals.length)); } @@ -50,16 +50,3 @@ class Symbols { return '${symbol.name}@${libraryReference.asLibrary.importUri.hashCode}'; } } - -List _intToLittleEndianBytes(int i) { - List bytes = []; - bytes.add(i & 0xFF); - i >>>= 8; - while (i != 0) { - bytes.add(i & 0xFF); - i >>>= 8; - } - return bytes; -} - -String _intToBase64(int i) => base64.encode(_intToLittleEndianBytes(i)); diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart index 215ab0e4e05..a94a9e4d043 100644 --- a/pkg/dart2wasm/lib/translator.dart +++ b/pkg/dart2wasm/lib/translator.dart @@ -23,6 +23,7 @@ import 'dispatch_table.dart'; import 'dynamic_forwarders.dart'; import 'dynamic_module_kernel_metadata.dart'; import 'dynamic_modules.dart'; +import 'exports.dart'; import 'functions.dart'; import 'globals.dart'; import 'kernel_nodes.dart'; @@ -125,6 +126,9 @@ class Translator with KernelNodes { final Symbols symbols; + final ExportNamer exportNamer; + late final Exporter exporter; + // Kernel input and context. @override final Component component; @@ -177,6 +181,8 @@ class Translator with KernelNodes { late final CompilationQueue compilationQueue; late final FunctionCollector functions; + late final DynamicModuleExports? dynamicModuleExports; + // Information about the program used and updated by the various phases. /// [ClassInfo]s of classes in the compilation unit and the [ClassInfo] for @@ -452,6 +458,7 @@ class Translator with KernelNodes { {bool enableDynamicModules = false, required MainModuleMetadata mainModuleMetadata}) : symbols = Symbols(options.minify), + exportNamer = ExportNamer(options.minify), libraries = component.libraries, hierarchy = ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy { @@ -473,6 +480,13 @@ class Translator with KernelNodes { functions = FunctionCollector(this); types = Types(this); exceptionTag = ExceptionTag(this); + + exporter = Exporter(exportNamer); + + dynamicModuleExports = + (component.metadata[DynamicModuleExportRepository.repositoryTag] + as DynamicModuleExportRepository?) + ?.mapping[component] ??= exporter.dynamicModuleExports; } void _initLoadLibraryImportMap() { diff --git a/pkg/dart2wasm/lib/util.dart b/pkg/dart2wasm/lib/util.dart index ddb7df85327..0526c3a8f55 100644 --- a/pkg/dart2wasm/lib/util.dart +++ b/pkg/dart2wasm/lib/util.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:convert'; + import 'package:kernel/ast.dart'; import 'package:kernel/core_types.dart'; @@ -52,3 +54,16 @@ T addPragma( coreTypes.pragmaName.fieldReference: StringConstant(pragmaName), coreTypes.pragmaOptions.fieldReference: value ?? NullConstant(), }))); + +List _intToLittleEndianBytes(int i) { + List bytes = []; + bytes.add(i & 0xFF); + i >>>= 8; + while (i != 0) { + bytes.add(i & 0xFF); + i >>>= 8; + } + return bytes; +} + +String intToBase64(int i) => base64.encode(_intToLittleEndianBytes(i)); diff --git a/pkg/dynamic_modules/test/data/overrideable_super/main.dart b/pkg/dynamic_modules/test/data/overrideable_super/main.dart index 8e981a8ebcd..c9f6a1c0dfd 100644 --- a/pkg/dynamic_modules/test/data/overrideable_super/main.dart +++ b/pkg/dynamic_modules/test/data/overrideable_super/main.dart @@ -10,7 +10,12 @@ import 'shared/shared.dart' show Child, Other; void main() async { final o1 = Other(); Child(); - Expect.equals('Instance of \'Other\'', o1.toString()); + + // Ensure that the overrides correctly call `super.toString()`, eventually + // ending at `Object.toString`. The class name may be minified, so we don't + // want to make it part of the expectation. + Expect.isTrue(o1.toString().startsWith('Instance of')); + Expect.equals(3, await helper.load('entry1.dart')); helper.done(); } diff --git a/pkg/dynamic_modules/test/runner/dart2wasm.dart b/pkg/dynamic_modules/test/runner/dart2wasm.dart index d08bc59b480..b406e15c048 100644 --- a/pkg/dynamic_modules/test/runner/dart2wasm.dart +++ b/pkg/dynamic_modules/test/runner/dart2wasm.dart @@ -74,6 +74,7 @@ class Dart2wasmExecutor implements TargetExecutor { '--extra-compiler-option=--dynamic-module-main=main.dart.dill', '--extra-compiler-option=--dynamic-module-interface=' '$rootScheme:/data/$testName/dynamic_interface.yaml', + '--extra-compiler-option=--minify', '$rootScheme:/data/$testName/$source', '$source.wasm', ];