DDC implementation of experimental dynamic modules.
In particular,
* Provides a DDC implementation for dynamic modules, where the download
and instantiation of the module is delegated to the embedder of the
program.
* Exposes an embedding API to allow embedders to define the loading logic.
* Adds a flag to compile code as a dynamic module. This includes
generating an entrypoint trampoline and checks to validate that a
dynamic module doesn't stump over previously defined libraries.
* Adds test coverage for DDC under `pkg/dynamic_modules/test/`.
Test suite can be run by executing:
```
DART_CONFIGURATION=ReleaseX64 out/ReleaseX64/dart-sdk/bin/dart \
pkg/dynamic_modules/test/runner/main.dart -t ddc
```
Once we provide integration of test configuration results to that test
runner, we will add it as part of the test matrix.
Change-Id: I626b5fefe9a27546cc6d1630d17e812544a711c6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/379748
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Commit-Queue: Sigmund Cherem <sigmund@google.com>
diff --git a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js
index 6e9083c..7ecde9a 100644
--- a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js
+++ b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js
@@ -360,6 +360,14 @@
// Map from module name to corresponding app to proxy library map.
let _proxyLibs = new Map();
+ /**
+ * Returns an instantiated module given its module name.
+ *
+ * Note: this method is not meant to be used outside DDC generated code,
+ * however it is currently being used in many places becase DDC lacks an
+ * Embedding API. This API will be removed in the future once the Embedding
+ * API is established.
+ */
function import_(name, appName) {
// For backward compatibility.
if (!appName && _lastStartedSubapp) {
@@ -609,8 +617,25 @@
let _firstStartedAppName;
let _lastStartedSubapp;
- /// Starts a subapp that is identified with `uuid`, `moduleName`, and
- /// `libraryName` inside a parent app that is identified by `appName`.
+ /**
+ * Runs a Dart program's main method.
+ *
+ * Intended to be invoked by the bootstrapping code where the application
+ * is being embedded.
+ *
+ * Because multiple programs can be part of a single application on a page
+ * at once, we identify the entrypoint using multiple keys: `appName`
+ * denotes the parent application, this program within that application
+ * (aka. the subapp) is identified by an `uuid`. Finally the entrypoint
+ * method is located from the `moduleName` and `libraryName`.
+ *
+ * Often, when a page contains a single app, the uuid is a fixed trivial
+ * value like '00000000-0000-0000-0000-000000000000'.
+ *
+ * This is one of the current public Embedding APIs exposed by DDC.
+ * Note: this API will be replaced by `dartDevEmbedder.runMain` in the
+ * future.
+ */
function start(appName, uuid, moduleName, libraryName, isReload) {
console.info(
`DDC: Subapp Module [${appName}:${moduleName}:${uuid}] is starting`);
@@ -676,6 +701,75 @@
library.main([]);
}
dart_library.start = start;
+
+
+ /**
+ * Configure the DDC runtime.
+ *
+ * Must be called before invoking [start].
+ *
+ * The configuration parameter is an object that may provide any of the
+ * following keys:
+ *
+ * - weakNullSafetyErrors: throw errors when types violate sound null
+ * safety (deprecated).
+ *
+ * - nonNullAsserts: insert non-null assertions no non-nullable method
+ * parameters (deprecated, was used to aid in null safety
+ * migrations).
+ *
+ * - nativeNonNullAsserts: inject non-null assertions checks to validate
+ * that browser APIs are sound. This is helpful because browser APIs
+ * are generated from IDLs and cannot be proven to be correct
+ * statically.
+ *
+ * - jsInteropNonNullAsserts: inject non-null assertiosn to check
+ * nullability of `package:js` JS-interop APIs. The new
+ * `dart:js_interop` always includes non-null assertions.
+ *
+ * - dynamicModuleLoader: provide the implementation of dynamic module
+ * loading. Dynamic modules is an experimental feature.
+ * The DDC runtime delegates to the embedder how code for dynamic
+ * modules is downloaded. This entry in the configuration must be a
+ * function with the signature:
+ * `function(String, onLoad)`
+ * It accepts a `String` containing a Uri that denotes the module to
+ * be loaded. When the load completes, the loader must invoke
+ * `onLoad` and provide the module name of the dynamic module to it.
+ *
+ * Note: eventually we may want to make the loader return a promise.
+ * We avoided that for now because it interfereres with our testing
+ * in d8.
+ */
+ dart_library.configure = function configure(appName, configuration) {
+ let runtimeLibrary = dart_library.import("dart_sdk", appName).dart;
+ if (!!configuration.weakNullSafetyErrors) {
+ runtimeLibrary.weakNullSafetyErrors(configuration.weakNullSafetyErrors);
+ }
+ if (!!configuration.nonNullAsserts) {
+ runtimeLibrary.nonNullAsserts(configuration.nonNullAsserts);
+ }
+ if (!!configuration.nativeNonNullAsserts) {
+ runtimeLibrary.nativeNonNullAsserts(configuration.nativeNonNullAsserts);
+ }
+ if (!!configuration.jsInteropNonNullAsserts) {
+ runtimeLibrary.jsInteropNonNullAsserts(
+ configuration.jsInteropNonNullAsserts);
+ }
+ if (!!configuration.dynamicModuleLoader) {
+ let loader = configuration.dynamicModuleLoader;
+ runtimeLibrary.setDynamicModuleLoader(loader, (moduleName) => {
+ // As mentioned in the docs above, loader will not invoke the
+ // entrypoint, but just return the moduleName to find where to call
+ // it. By using the module name, we don't need to expose the
+ // `import` as part of the embedding API.
+ // In the future module system, this will change to a library name,
+ // rather than a module name.
+ return import_(moduleName, appName).__dynamic_module_entrypoint__();
+ });
+ }
+ }
+
})(dart_library);
}
diff --git a/pkg/dev_compiler/lib/src/compiler/shared_command.dart b/pkg/dev_compiler/lib/src/compiler/shared_command.dart
index f4b21a2..5c2a00b 100644
--- a/pkg/dev_compiler/lib/src/compiler/shared_command.dart
+++ b/pkg/dev_compiler/lib/src/compiler/shared_command.dart
@@ -103,6 +103,9 @@
/// isolated namespace.
final bool emitLibraryBundle;
+ /// Whether the compiler is generating a dynamic module.
+ final bool dynamicModule;
+
/// When `true` stars "*" will appear to represent legacy types when printing
/// runtime types in the compiled application.
final bool printLegacyStars = false;
@@ -133,6 +136,7 @@
this.experiments = const {},
this.soundNullSafety = true,
this.canaryFeatures = false,
+ this.dynamicModule = false,
this.precompiledMacros = const [],
this.macroSerializationMode})
: emitLibraryBundle = canaryFeatures &&
@@ -160,6 +164,7 @@
args['enable-experiment'] as List<String>),
soundNullSafety: args['sound-null-safety'] as bool,
canaryFeatures: args['canary'] as bool,
+ dynamicModule: args['dynamic-module'] as bool,
precompiledMacros: args['precompiled-macro'] as List<String>,
macroSerializationMode:
args['macro-serialization-mode'] as String?);
@@ -231,7 +236,11 @@
..addOption('macro-serialization-mode',
help: 'The serialization mode for communicating with macros.',
allowed: ['bytedata', 'json'],
- defaultsTo: 'bytedata');
+ defaultsTo: 'bytedata')
+ ..addFlag('dynamic-module',
+ help: 'Compile to generate a dynamic module',
+ negatable: false,
+ defaultsTo: false);
}
/// Adds only the arguments used to compile the SDK from a full dill file.
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 5a23f33..fb68e5f 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -388,6 +388,9 @@
/// classes.
final _afterClassDefItems = <js_ast.ModuleItem>[];
+ /// The entrypoint method of a dynamic module, if any.
+ Procedure? _dynamicEntrypoint;
+
final Class _jsArrayClass;
final Class _privateSymbolClass;
final Class _linkedHashMapImplClass;
@@ -3229,6 +3232,25 @@
_currentUri = savedUri;
_staticTypeContext.leaveMember(p);
+
+ if (_options.dynamicModule &&
+ p.annotations.any((a) => _isEntrypointPragma(a, _coreTypes))) {
+ if (_dynamicEntrypoint == null) {
+ if (p.function.requiredParameterCount > 0) {
+ // TODO(sigmund): this error should be caught by a kernel checker that
+ // runs prior to DDC.
+ throw StateError('Entrypoint ${p.name.text} must accept being called '
+ 'with 0 arguments.');
+ } else {
+ _dynamicEntrypoint = p;
+ }
+ } else {
+ // TODO(sigmund): this error should be caught by a kernel checker that
+ // runs prior to DDC.
+ throw StateError('A module should define a single entrypoint.');
+ }
+ }
+
return js_ast.Statement.from(body);
}
@@ -8213,6 +8235,20 @@
// full library uri if we wanted to save space.
var libraryName = js.escapedString(_jsLibraryDebuggerName(library));
properties.add(js_ast.Property(libraryName, value));
+
+ // Dynamic modules shouldn't define a library that was previously defined.
+ // We leverage that we track which libraries have been defined via
+ // `trackedLibraries` to query whether a library already exists.
+ if (_options.dynamicModule) {
+ _moduleItems.add(js.statement('''if (# != null) {
+ throw Error(
+ "Dynamic module provides second definition for " + #);
+ }''', [
+ _runtimeCall('getLibrary(#)', [libraryName]),
+ libraryName
+ ]));
+ }
+
var partNames = _jsPartDebuggerNames(library);
if (partNames.isNotEmpty) {
parts.add(js_ast.Property(libraryName, js.stringArray(partNames)));
@@ -8222,7 +8258,10 @@
var partMap = js_ast.ObjectInitializer(parts, multiline: true);
// Track the module name for each library in the module.
- // This data is consumed by the debugger and by the stack trace mapper.
+ // This data is mainly consumed by the debugger and by the stack trace
+ // mapper. It is also used for the experimental dynamic modules feature
+ // to validate that a dynamic module doesn't reintroduce an existing
+ // library.
//
// See also the implementation of this API in the SDK.
_moduleItems.add(_runtimeStatement(
@@ -8284,6 +8323,14 @@
// Emit all top-level JS symbol containers.
items.addAll(_symbolContainer.emit());
+ if (_dynamicEntrypoint != null) {
+ // Expose the entrypoint of the dynamic module under a reserved name.
+ // TODO(sigmund): this could use a reserved symbol from dartx.
+ var name = _emitTopLevelName(_dynamicEntrypoint!);
+ _moduleItems.add(js_ast.ExportDeclaration(
+ js('var __dynamic_module_entrypoint__ = #', [name])));
+ }
+
// Add the module's code (produced by visiting compilation units, above)
_copyAndFlattenBlocks(items, _moduleItems);
_moduleItems.clear();
@@ -8400,3 +8447,18 @@
_SwitchLabelState(this.label, this.variable);
}
+
+/// Whether [expression] is a constant of the form
+/// `const pragma('dyn-module:entrypoint')`.
+///
+/// Used to denote the entrypoint method of a dynamic module.
+// TODO(sigmund): move to package:kernel.
+bool _isEntrypointPragma(Expression expression, CoreTypes coreTypes) {
+ if (expression is! ConstantExpression) return false;
+ final value = expression.constant;
+ if (value is! InstanceConstant) return false;
+ if (value.classReference != coreTypes.pragmaClass.reference) return false;
+ final name = value.fieldValues[coreTypes.pragmaName.fieldReference];
+ if (name is! StringConstant) return false;
+ return name.value == 'dyn-module:entrypoint';
+}
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
index 323b6b1..165084c 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler_new.dart
@@ -503,6 +503,9 @@
/// classes.
final _afterClassDefItems = <js_ast.ModuleItem>[];
+ /// The entrypoint method of a dynamic module, if any.
+ Procedure? _dynamicEntrypoint;
+
final Class _jsArrayClass;
final Class _privateSymbolClass;
final Class _linkedHashMapImplClass;
@@ -3360,6 +3363,25 @@
_currentUri = savedUri;
_staticTypeContext.leaveMember(p);
+
+ if (_options.dynamicModule &&
+ p.annotations.any((a) => _isEntrypointPragma(a, _coreTypes))) {
+ if (_dynamicEntrypoint == null) {
+ if (p.function.requiredParameterCount > 0) {
+ // TODO(sigmund): this error should be caught by a kernel checker that
+ // runs prior to DDC.
+ throw StateError('Entrypoint ${p.name.text} must accept being called '
+ 'with 0 arguments.');
+ } else {
+ _dynamicEntrypoint = p;
+ }
+ } else {
+ // TODO(sigmund): this error should be caught by a kernel checker that
+ // runs prior to DDC.
+ throw StateError('A module should define a single entrypoint.');
+ }
+ }
+
return js_ast.Statement.from(body);
}
@@ -8235,6 +8257,21 @@
// full library uri if we wanted to save space.
var libraryName = js.escapedString(_jsLibraryDebuggerName(library));
properties.add(js_ast.Property(libraryName, value));
+
+ // Dynamic modules shouldn't define a library that was previously defined.
+ // We leverage that we track which libraries have been defined via
+ // `trackedLibraries` to query whether a library already exists.
+ // TODO(sigmund): enable when `trackLibraries()` is added again.
+ //if (_options.dynamicModule) {
+ // _moduleItems.add(js.statement('''if (# != null) {
+ // throw Error(
+ // "Dynamic module provides second definition for " + #);
+ // }''', [
+ // _runtimeCall('getLibrary(#)', [libraryName]),
+ // libraryName
+ // ]));
+ //}
+
var partNames = _jsPartDebuggerNames(library);
if (partNames.isNotEmpty) {
parts.add(js_ast.Property(libraryName, js.stringArray(partNames)));
@@ -8246,7 +8283,10 @@
// var partMap = js_ast.ObjectInitializer(parts, multiline: true);
// Track the module name for each library in the module.
- // This data is consumed by the debugger and by the stack trace mapper.
+ // This data is mainly consumed by the debugger and by the stack trace
+ // mapper. It is also used for the experimental dynamic modules feature
+ // to validate that a dynamic module doesn't reintroduce an existing
+ // library.
//
// See also the implementation of this API in the SDK.
// _moduleItems.add(_runtimeStatement(
@@ -8299,6 +8339,14 @@
// Emit all top-level JS symbol containers.
items.addAll(_symbolContainer.emit());
+ if (_dynamicEntrypoint != null) {
+ // Expose the entrypoint of the dynamic module under a reserved name.
+ // TODO(sigmund): this could use a reserved symbol from dartx.
+ var name = _emitTopLevelName(_dynamicEntrypoint!);
+ _moduleItems.add(js_ast.ExportDeclaration(
+ js('var __dynamic_module_entrypoint__ = #', [name])));
+ }
+
// Add the module's code (produced by visiting compilation units, above)
_copyAndFlattenBlocks(items, _moduleItems);
_moduleItems.clear();
@@ -8415,3 +8463,17 @@
_SwitchLabelState(this.label, this.variable);
}
+
+/// Whether [expression] is a constant of the form
+/// `const pragma('dyn-module:entrypoint')`.
+///
+/// Used to denote the entrypoint method of a dynamic module.
+bool _isEntrypointPragma(Expression expression, CoreTypes coreTypes) {
+ if (expression is! ConstantExpression) return false;
+ final value = expression.constant;
+ if (value is! InstanceConstant) return false;
+ if (value.classReference != coreTypes.pragmaClass.reference) return false;
+ final name = value.fieldValues[coreTypes.pragmaName.fieldReference];
+ if (name is! StringConstant) return false;
+ return name.value == 'dyn-module:entrypoint';
+}
diff --git a/pkg/dynamic_modules/test/common/testing.dart b/pkg/dynamic_modules/test/common/testing.dart
index 264fd95..0cb7933 100644
--- a/pkg/dynamic_modules/test/common/testing.dart
+++ b/pkg/dynamic_modules/test/common/testing.dart
@@ -6,11 +6,19 @@
/// provide information to the test harness.
library;
+import 'package:dynamic_modules/dynamic_modules.dart';
+
/// Load a module and invoke it's entrypoint.
///
/// The module is identified by the name of its entrypoint file within the
/// `modules/` subfolder.
-Future<Object?> load(String moduleName) => throw "unimplemented";
+Future<Object?> load(String moduleName) {
+ if (const bool.fromEnvironment('dart.library.html')) {
+ // DDC implementation
+ return loadModuleFromUri(Uri(scheme: '', path: moduleName));
+ }
+ throw "load is not implemented for the VM or dart2wasm";
+}
/// Notify the test harness that the test has run to completion.
void done() {
diff --git a/pkg/dynamic_modules/test/runner/ddc.dart b/pkg/dynamic_modules/test/runner/ddc.dart
new file mode 100644
index 0000000..98e520a
--- /dev/null
+++ b/pkg/dynamic_modules/test/runner/ddc.dart
@@ -0,0 +1,194 @@
+// Copyright (c) 2024, 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.
+
+/// Implementation of the [TargetExecutor] for DDC.
+library;
+
+import 'dart:io';
+import '../common/testing.dart' as helper;
+
+import 'model.dart';
+import 'util.dart';
+import 'target.dart';
+
+/// Logic to build and execute dynamic modules in DDC.
+///
+/// In particular:
+/// * The initial app is built as a regular DDC target, except that
+/// a dynamic interface is used to validate that the public API matches
+/// real declarations.
+/// * For dynamic modules, DDC validates that the module only accesses what's
+/// allowed by the dynamic interface.
+/// * For dynamic modules, DDC also produces a slighly different output
+/// to implement library isolation.
+/// * Tests are executed in d8 using a custom bootstrapping logic. Eventually
+/// this logic needs to be centralized inside the compiler.
+class DdcExecutor implements TargetExecutor {
+ static const rootScheme = 'dev-dart-app';
+ late final bool _shouldCleanup;
+ late final Directory _tmp;
+ final Logger _logger;
+
+ DdcExecutor(this._logger) {
+ /// Allow using an environment variable to run tests on a fixed directory.
+ /// This prevents the directory from getting deleted too.
+ var path = Platform.environment['TMP_DIR'] ?? '';
+ if (path.isEmpty) {
+ _tmp = Directory.systemTemp.createTempSync('_dynamic_module-');
+ _shouldCleanup = true;
+ } else {
+ _tmp = Directory(path);
+ if (!_tmp.existsSync()) _tmp.createSync();
+ _shouldCleanup = false;
+ }
+ }
+
+ @override
+ Future<void> suiteComplete() async {
+ if (!_shouldCleanup) return;
+ try {
+ _tmp.delete(recursive: true);
+ } on FileSystemException {
+ // Windows bots sometimes fail to delete folders, and can make tests
+ // flaky. It is OK in those cases to leak some files in the tmp folder,
+ // these will eventually be cleared when a new bot instance is created.
+ _logger.warning('Error trying to delete $_tmp');
+ }
+ }
+
+ // TODO(sigmund): add support to run also in the ddc-canary mode.
+ Future _compile(
+ String testName, String source, Uri sourceDir, bool isMain) async {
+ var testDir = _tmp.uri.resolve(testName).toFilePath();
+ var args = [
+ '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+ ddcSnapshot.toFilePath(),
+ '--modules=ddc',
+ '--no-summarize',
+ '--no-source-map',
+ '--multi-root',
+ sourceDir.resolve('../../').toFilePath(),
+ '--multi-root-scheme',
+ rootScheme,
+ '$rootScheme:/data/$testName/$source',
+ '--dart-sdk-summary',
+ ddcSdkOutline.toFilePath(),
+ // Note: this needs to change if we ever intend to support packages within
+ // the dynamic loading tests themselves
+ '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+ if (!isMain) ...[
+ // TODO(sigmund): consider specifying the module name directly
+ '--dynamic-module',
+ '--summary=main.dart.dill=main.dart',
+ ],
+ '-o',
+ '$source.js',
+ ];
+ await runProcess(Platform.resolvedExecutable, args, testDir, _logger,
+ 'compile $testName/$source');
+ }
+
+ Future _buildKernelOutline(
+ String testName, String source, Uri sourceDir) async {
+ assert(source == 'main.dart');
+ var testDir = _tmp.uri.resolve(testName).toFilePath();
+ var args = [
+ '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+ kernelWOrkerSnapshot.toFilePath(),
+ '--summary-only',
+ '--target',
+ 'ddc',
+ '--multi-root',
+ sourceDir.resolve('../../').toFilePath(),
+ '--multi-root-scheme',
+ rootScheme,
+ '--packages-file=${repoRoot.toFilePath()}/.dart_tool/package_config.json',
+ '--dart-sdk-summary',
+ ddcSdkOutline.toFilePath(),
+ '--source',
+ '$rootScheme:/data/$testName/$source',
+ '--output',
+ '$source.dill',
+ ];
+
+ await runProcess(Platform.resolvedExecutable, args, testDir, _logger,
+ 'sumarize $testName/$source');
+ }
+
+ @override
+ Future compileApplication(DynamicModuleTest test) async {
+ _ensureDirectory(test.name);
+ _logger.info('Compile ${test.name} app');
+ await _buildKernelOutline(test.name, test.main, test.folder);
+ await _compile(test.name, test.main, test.folder, true);
+ }
+
+ @override
+ Future compileDynamicModule(DynamicModuleTest test, String name) async {
+ _logger.info('Compile module ${test.name}.$name');
+ _ensureDirectory(test.name);
+ await _compile(test.name, test.dynamicModules[name]!, test.folder, false);
+ }
+
+ @override
+ Future executeApplication(DynamicModuleTest test) async {
+ _logger.info('Execute ${test.name}');
+ _ensureDirectory(test.name);
+
+ // We generate a self contained script that loads necessary preambles,
+ // ddc module loader, the necessary modules (the SDK and the main module),
+ // and finally launches the app.
+ var testDir = _tmp.uri.resolve('${test.name}/');
+ var bootstrapUri = testDir.resolve('bootstrap.js');
+ // TODO(sigmund): remove hardwired entrypoint name
+ File.fromUri(bootstrapUri).writeAsStringSync('''
+ load('${ddcPreamblesJs.toFilePath()}'); // preambles/d8.js
+ load('${ddcSealNativeObjectJs.toFilePath()}'); // seal_native_object.js
+ load('${ddcModuleLoaderJs.toFilePath()}'); // ddc_module_loader.js
+ load('${ddcSdkJs.toFilePath()}'); // dart_sdk.js
+ load('main.dart.js'); // compiled test module
+
+ self.dartMainRunner(function () {
+ dart_library.configure(
+ "_dynamic_module_test", // app name
+ {
+ dynamicModuleLoader: (uri, onLoad) => {
+ let name = uri; // our test framework simply
+ // provides the module name as
+ // the uri.
+ load(`modules/\${name}.js`);
+ onLoad(name);
+ },
+ });
+ dart_library.start(
+ "_dynamic_module_test", // app name
+ '00000000-0000-0000-0000-000000000000', // uuid
+ "main.dart", // module
+ "data__${test.name}__main", // library containing main
+ false
+ );
+ });
+ ''');
+ var result = await runProcess(
+ d8Uri.toFilePath(),
+ [bootstrapUri.toFilePath()],
+ testDir.toFilePath(),
+ _logger,
+ 'd8 ${test.name}/bootstrap.js');
+ var stdout = result.stdout as String;
+ if (!stdout.contains(helper.successToken)) {
+ _logger.error('Error: test didn\'t complete as expected.\n'
+ 'Make sure the test finishes and calls `helper.done()`.\n'
+ 'Test output:\n$stdout');
+ throw Exception('missing helper.done');
+ }
+ }
+
+ void _ensureDirectory(String name) {
+ var dir = Directory.fromUri(_tmp.uri.resolve(name));
+ if (!dir.existsSync()) {
+ dir.createSync();
+ }
+ }
+}
diff --git a/pkg/dynamic_modules/test/runner/main.dart b/pkg/dynamic_modules/test/runner/main.dart
index 4fc9e15..065f402 100644
--- a/pkg/dynamic_modules/test/runner/main.dart
+++ b/pkg/dynamic_modules/test/runner/main.dart
@@ -7,6 +7,7 @@
import 'package:args/args.dart';
+import 'ddc.dart';
import 'load.dart';
import 'model.dart';
import 'target.dart';
@@ -47,7 +48,7 @@
late TargetExecutor executor;
try {
executor = switch (target) {
- Target.ddc => UnimplementedExecutor(logger),
+ Target.ddc => DdcExecutor(logger),
Target.aot => UnimplementedExecutor(logger),
Target.dart2wasm => UnimplementedExecutor(logger),
};
diff --git a/pkg/dynamic_modules/test/runner/util.dart b/pkg/dynamic_modules/test/runner/util.dart
index 2b1a6d5..21c47af 100644
--- a/pkg/dynamic_modules/test/runner/util.dart
+++ b/pkg/dynamic_modules/test/runner/util.dart
@@ -7,6 +7,7 @@
import 'dart:convert';
import 'dart:io';
+import 'dart:ffi' show Abi;
/// Locate the root of the SDK repository.
///
@@ -23,6 +24,39 @@
return script.resolve("../" * (segments.length - index - 1));
})();
+String _outFolder = Platform.isMacOS ? 'xcodebuild' : 'out';
+String configuration =
+ Platform.environment['DART_CONFIGURATION'] ?? 'ReleaseX64';
+String buildFolder = '$_outFolder/$configuration/';
+String arch = Abi.current().toString().split('_')[1];
+String _d8Path = (() {
+ if (Platform.isWindows) {
+ return 'third_party/d8/windows/$arch/d8.exe';
+ } else if (Platform.isLinux) {
+ return 'third_party/d8/linux/$arch/d8';
+ } else if (Platform.isMacOS) {
+ return 'third_party/d8/macos/$arch/d8';
+ } else {
+ throw UnsupportedError('Unsupported platform for running d8: '
+ '${Platform.operatingSystem}');
+ }
+})();
+
+Uri d8Uri = repoRoot.resolve(_d8Path);
+Uri _dartBin = Uri.file(Platform.resolvedExecutable);
+Uri ddcSnapshot = _dartBin.resolve('snapshots/dartdevc.dart.snapshot');
+Uri kernelWOrkerSnapshot =
+ _dartBin.resolve('snapshots/kernel_worker.dart.snapshot');
+Uri buildRootUri = repoRoot.resolve(buildFolder);
+Uri ddcSdkOutline = buildRootUri.resolve('ddc_outline.dill');
+Uri ddcSdkJs = buildRootUri.resolve('gen/utils/ddc/stable/sdk/ddc/dart_sdk.js');
+Uri ddcPreamblesJs = repoRoot
+ .resolve('sdk/lib/_internal/js_dev_runtime/private/preambles/d8.js');
+Uri ddcSealNativeObjectJs = repoRoot.resolve(
+ 'sdk/lib/_internal/js_runtime/lib/preambles/seal_native_object.js');
+Uri ddcModuleLoaderJs =
+ repoRoot.resolve('pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js');
+
// Encodes test results in the format expected by Dart's CI infrastructure.
class TestResultOutcome {
// This encoder must generate each output element on its own line.
diff --git a/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart
index 1aacab0..0941d9f 100644
--- a/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart
+++ b/sdk/lib/_internal/js_dev_runtime/patch/internal_patch.dart
@@ -4,6 +4,7 @@
import 'dart:core' hide Symbol;
import 'dart:core' as core show Symbol;
+import 'dart:async' show Completer;
import 'dart:_js_primitives' show printString;
import 'dart:_internal' show patch;
import 'dart:_interceptors' show JSArray;
@@ -67,5 +68,32 @@
T unsafeCast<T>(dynamic v) => v;
@patch
-Future<Object?> loadDynamicModule({Uri? uri, Uint8List? bytes}) =>
- throw 'Unsupported operation';
+Future<Object?> loadDynamicModule({Uri? uri, Uint8List? bytes}) {
+ if (bytes != null) {
+ throw ArgumentError('DDC implementation of dynamic modules doesn\'t'
+ ' accept bytes as input');
+ }
+ if (uri == null) {
+ throw ArgumentError('DDC implementation of dynamic modules expects a'
+ 'non-null Uri input.');
+ }
+ if (dart.dynamicModuleLoader == null) {
+ throw StateError('Dynamic module loader has not be configured.');
+ }
+ var completer = Completer<Object?>();
+ void _callback(String moduleName) {
+ try {
+ var result = JS('!', '#(#)', dart.dynamicEntrypointHelper, moduleName);
+ completer.complete(result);
+ } catch (e, st) {
+ completer.completeError(e, st);
+ }
+ }
+
+ try {
+ JS('!', '#(#, #)', dart.dynamicModuleLoader, uri.toString(), _callback);
+ } catch (e, st) {
+ completer.completeError(e, st);
+ }
+ return completer.future;
+}
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
index 8b029c1..50e9ec6 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
@@ -1037,6 +1037,17 @@
}
}
+/// Provides the experimental functionality for dynamic modules.
+Object? dynamicModuleLoader;
+Object? dynamicEntrypointHelper;
+void setDynamicModuleLoader(Object loaderFunction, Object entrypointHelper) {
+ if (dynamicModuleLoader != null) {
+ throw StateError('Dynamic module loader already configured.');
+ }
+ dynamicModuleLoader = loaderFunction;
+ dynamicEntrypointHelper = entrypointHelper;
+}
+
/// Defines lazy statics.
///
/// TODO: Remove useOldSemantics when non-null-safe late static field behavior is
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
index db56066..9d67d12 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
@@ -203,9 +203,12 @@
/// * To convert JS stack traces to Dart: the
/// stack trace mapper companion program will request source maps via
/// [getSourceMap].
+/// * To validate a library is only defined by one dynamic module: the compiler
+/// generates calls to [getLibrary] when compiling the experimental dynamic
+/// modules feature.
///
/// Note: calls to [getLibrary], [getLibraryMetadata], [getSourceMap], among
-/// others don't originate from code in the SDK repo. For example, the
+/// others don't always originate from code in the SDK repo. For example, the
/// debugger calls are initiated by DWDS, whereas the [getSourceMap] call is
/// done from bootstapping scripts that set up the stack trace mapper.
// TODO(39630): move these public facing APIs to a dedicated public interface.