[vm,dartdev] Support dynamic linking between libraries
The same bundling that is used for `dart build` is now also used for
`dart test` and `dart run`, except that the output directory is
`.dart_tool/native_assets`. This way all native code assets are placed
next to each other in the `lib` directory, and loaded from there
instead of loading them in place from where the build/link hooks
placed them. By standardizing on this layout the different modes of
running dart code that support native assets can use the same
mechanisms to support dynamic linking between libraries.
On macOS, install names of dylibs are rewritten to support dynamic
linking, similar to the changes in
https://github.com/flutter/flutter/pull/153054.
On Windows, loading of DLLs is altered so that the directory of the DLL
that is being loaded is considered when loading dependent DLLs.
Tests are added to verify that dynamic linking works as expected.
TEST=pkg/dartdev/test/native_assets/{build,run,test}_test.dart
R=mosum@google.com
Related: https://github.com/dart-lang/native/issues/190
Fixes: https://github.com/dart-lang/sdk/issues/56459
Change-Id: Ie4a41e5b7382ab1cea39e93d29d085bf9986828b
Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-arm64-try,pkg-linux-release-try,pkg-mac-release-arm64-try,pkg-mac-release-try,pkg-win-release-arm64-try,pkg-win-release-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381580
Reviewed-by: Moritz Sümmermann <mosum@google.com>
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Daco Harkes <dacoharkes@google.com>
diff --git a/pkg/dartdev/lib/src/commands/build.dart b/pkg/dartdev/lib/src/commands/build.dart
index 7e9f8a0..e882846 100644
--- a/pkg/dartdev/lib/src/commands/build.dart
+++ b/pkg/dartdev/lib/src/commands/build.dart
@@ -8,6 +8,7 @@
import 'package:dart2native/generate.dart';
import 'package:dartdev/src/commands/compile.dart';
import 'package:dartdev/src/experiments.dart';
+import 'package:dartdev/src/native_assets_bundling.dart';
import 'package:dartdev/src/sdk.dart';
import 'package:dartdev/src/utils.dart';
import 'package:front_end/src/api_prototype/compiler_options.dart'
@@ -21,9 +22,6 @@
import '../core.dart';
import '../native_assets.dart';
-const _libOutputDirectory = 'lib';
-const _dataOutputDirectory = 'assets';
-
class BuildCommand extends DartdevCommand {
static const String cmdName = 'build';
static const String outputOptionName = 'output';
@@ -240,60 +238,34 @@
return 255;
}
- final tempUri = tempDir.uri;
- Uri? assetsDartUri;
final allAssets = linkResult.encodedAssets;
- final dataAssets = allAssets
- .where((e) => e.type == DataAsset.type)
- .map(DataAsset.fromEncoded)
- .toList();
- final codeAssets = allAssets
+
+ final staticAssets = allAssets
.where((e) => e.type == CodeAsset.type)
.map(CodeAsset.fromEncoded)
- .toList();
-
- final staticAssets =
- codeAssets.where((e) => e.linkMode == StaticLinking());
+ .where((e) => e.linkMode == StaticLinking());
if (staticAssets.isNotEmpty) {
stderr.write(
"""'dart build' does not yet support CodeAssets with static linking.
Use linkMode as dynamic library instead.""");
return 255;
}
- if (allAssets.isNotEmpty) {
- final kernelAssets = <KernelAsset>[];
- final filesToCopy = <(String id, Uri, KernelAssetRelativePath)>[];
- for (final asset in codeAssets) {
- final kernelAsset = asset.targetLocation(target);
- kernelAssets.add(kernelAsset);
- final targetPath = kernelAsset.path;
- if (targetPath is KernelAssetRelativePath) {
- filesToCopy.add((asset.id, asset.file!, targetPath));
- }
- }
- for (final asset in dataAssets) {
- final kernelAsset = asset.targetLocation(target);
- kernelAssets.add(kernelAsset);
- final targetPath = kernelAsset.path;
- if (targetPath is KernelAssetRelativePath) {
- filesToCopy.add((asset.id, asset.file, targetPath));
- }
- }
- assetsDartUri = await _writeAssetsYaml(
- kernelAssets,
- assetsDartUri,
- tempUri,
+ Uri? nativeAssetsYamlUri;
+ if (allAssets.isNotEmpty) {
+ final kernelAssets = await bundleNativeAssets(
+ allAssets,
+ target,
+ outputUri,
+ relocatable: true,
+ verbose: true,
);
- if (allAssets.isNotEmpty) {
- stdout.writeln(
- 'Copying ${filesToCopy.length} build assets: ${filesToCopy.map((e) => e.$1)}');
- _copyAssets(filesToCopy, outputUri);
- }
+ nativeAssetsYamlUri =
+ await writeNativeAssetsYaml(kernelAssets, tempDir.uri);
}
await snapshotGenerator.generate(
- nativeAssets: assetsDartUri?.toFilePath(),
+ nativeAssets: nativeAssetsYamlUri?.toFilePath(),
);
// End linking here.
@@ -302,29 +274,6 @@
}
return 0;
}
-
- void _copyAssets(
- List<(String id, Uri, KernelAssetRelativePath)> assetTargetLocations,
- Uri output,
- ) {
- for (final (_, file, targetPath) in assetTargetLocations) {
- file.copyTo(targetPath, output);
- }
- }
-
- Future<Uri> _writeAssetsYaml(
- List<KernelAsset> assetTargetLocations,
- Uri? nativeAssetsDartUri,
- Uri tempUri,
- ) async {
- stdout.writeln('Writing native_assets.yaml.');
- nativeAssetsDartUri = tempUri.resolve('native_assets.yaml');
- final assetsContent =
- KernelAssets(assetTargetLocations).toNativeAssetsFile();
- await Directory.fromUri(nativeAssetsDartUri.resolve('.')).create();
- await File(nativeAssetsDartUri.toFilePath()).writeAsString(assetsContent);
- return nativeAssetsDartUri;
- }
}
extension on String {
@@ -333,57 +282,6 @@
String removeDotDart() => replaceFirst(RegExp(r'\.dart$'), '');
}
-extension on Uri {
- void copyTo(KernelAssetRelativePath target, Uri outputUri) {
- if (this != target.uri) {
- final targetUri = outputUri.resolveUri(target.uri);
- File.fromUri(targetUri).createSync(
- recursive: true,
- exclusive: true,
- );
- File.fromUri(this).copySync(targetUri.toFilePath());
- }
- }
-}
-
-extension on CodeAsset {
- KernelAsset targetLocation(Target target) {
- final KernelAssetPath kernelAssetPath;
- switch (linkMode) {
- case DynamicLoadingSystem dynamicLoading:
- kernelAssetPath = KernelAssetSystemPath(dynamicLoading.uri);
- case LookupInExecutable _:
- kernelAssetPath = KernelAssetInExecutable();
- case LookupInProcess _:
- kernelAssetPath = KernelAssetInProcess();
- case DynamicLoadingBundled _:
- kernelAssetPath = KernelAssetRelativePath(
- Uri(path: path.join(_libOutputDirectory, file!.pathSegments.last)),
- );
- default:
- throw Exception(
- 'Unsupported CodeAsset linkMode ${linkMode.runtimeType} in asset $this',
- );
- }
- return KernelAsset(
- id: id,
- target: target,
- path: kernelAssetPath,
- );
- }
-}
-
-extension on DataAsset {
- KernelAsset targetLocation(Target target) {
- return KernelAsset(
- id: id,
- target: target,
- path: KernelAssetRelativePath(
- Uri(path: path.join(_dataOutputDirectory, file.pathSegments.last))),
- );
- }
-}
-
// TODO(https://github.com/dart-lang/package_config/issues/126): Expose this
// logic in package:package_config.
Future<Uri?> packageConfigUri(Uri uri) async {
diff --git a/pkg/dartdev/lib/src/native_assets.dart b/pkg/dartdev/lib/src/native_assets.dart
index dd1b835..24af926 100644
--- a/pkg/dartdev/lib/src/native_assets.dart
+++ b/pkg/dartdev/lib/src/native_assets.dart
@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:io';
+import 'package:dartdev/src/native_assets_bundling.dart';
import 'package:dartdev/src/sdk.dart';
import 'package:dartdev/src/utils.dart';
import 'package:logging/logging.dart';
@@ -100,62 +101,24 @@
runPackageName: runPackageName,
);
if (assets == null) return null;
- final codeAssets = assets
- .where((e) => e.type == CodeAsset.type)
- .map(CodeAsset.fromEncoded)
- .toList();
- final dataAssets = assets
- .where((e) => e.type == DataAsset.type)
- .map(DataAsset.fromEncoded)
- .toList();
- final kernelAssets = KernelAssets([
- ...[
- for (final asset in codeAssets) _targetLocation(asset),
- ],
- ...[
- for (final asset in dataAssets) _dataTargetLocation(asset),
- ]
- ]);
- final workingDirectory = Directory.current.uri;
- final assetsUri = workingDirectory.resolve('.dart_tool/native_assets.yaml');
- final nativeAssetsYaml = '''# Native assets mapping for host OS in JIT mode.
-# Generated by dartdev and package:native_assets_builder.
-${kernelAssets.toNativeAssetsFile()}''';
- final assetFile = File(assetsUri.toFilePath());
- await assetFile.writeAsString(nativeAssetsYaml);
- return assetsUri;
-}
+ final dartToolUri = Directory.current.uri.resolve('.dart_tool/');
+ final outputUri = dartToolUri.resolve('native_assets/');
+ await Directory.fromUri(outputUri).create(recursive: true);
-KernelAsset _targetLocation(CodeAsset asset) {
- final linkMode = asset.linkMode;
- final KernelAssetPath kernelAssetPath;
- switch (linkMode) {
- case DynamicLoadingSystem _:
- kernelAssetPath = KernelAssetSystemPath(linkMode.uri);
- case LookupInExecutable _:
- kernelAssetPath = KernelAssetInExecutable();
- case LookupInProcess _:
- kernelAssetPath = KernelAssetInProcess();
- case DynamicLoadingBundled _:
- kernelAssetPath = KernelAssetAbsolutePath(asset.file!);
- default:
- throw Exception(
- 'Unsupported CodeAsset linkMode ${linkMode.runtimeType} in asset $asset',
- );
- }
- return KernelAsset(
- id: asset.id,
- target: Target.fromArchitectureAndOS(asset.architecture!, asset.os),
- path: kernelAssetPath,
+ final kernelAssets = await bundleNativeAssets(
+ assets,
+ Target.current,
+ outputUri,
+ relocatable: false,
);
-}
-KernelAsset _dataTargetLocation(DataAsset asset) {
- return KernelAsset(
- id: asset.id,
- target: Target.current,
- path: KernelAssetAbsolutePath(asset.file),
+ return await writeNativeAssetsYaml(
+ kernelAssets,
+ dartToolUri,
+ header: '''# Native assets mapping for host OS in JIT mode.
+# Generated by dartdev and package:native_assets_builder.
+''',
);
}
diff --git a/pkg/dartdev/lib/src/native_assets_bundling.dart b/pkg/dartdev/lib/src/native_assets_bundling.dart
new file mode 100644
index 0000000..c6f499a
--- /dev/null
+++ b/pkg/dartdev/lib/src/native_assets_bundling.dart
@@ -0,0 +1,169 @@
+// 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.
+
+import 'dart:io';
+
+import 'package:dartdev/src/native_assets_macos.dart';
+import 'package:native_assets_builder/native_assets_builder.dart';
+import 'package:native_assets_cli/code_assets_builder.dart';
+import 'package:native_assets_cli/data_assets_builder.dart';
+
+final libOutputDirectoryUri = Uri.file('lib/');
+final dataOutputDirectoryUri = Uri.file('assets/');
+
+Future<KernelAssets> bundleNativeAssets(
+ Iterable<EncodedAsset> assets,
+ Target target,
+ Uri outputUri, {
+ required bool relocatable,
+ bool verbose = false,
+}) async {
+ final targetMapping = _targetMapping(assets, target, outputUri, relocatable);
+ await _copyAssets(targetMapping, target, outputUri, relocatable, verbose);
+ return KernelAssets(targetMapping.map((asset) => asset.target).toList());
+}
+
+Future<Uri> writeNativeAssetsYaml(
+ KernelAssets assets,
+ Uri outputUri, {
+ String? header,
+}) async {
+ final nativeAssetsYamlUri = outputUri.resolve('native_assets.yaml');
+ final nativeAssetsYamlFile = File(nativeAssetsYamlUri.toFilePath());
+ await nativeAssetsYamlFile.create(recursive: true);
+
+ var contents = assets.toNativeAssetsFile();
+ if (header != null) {
+ contents = '$header\n$contents';
+ }
+
+ await nativeAssetsYamlFile.writeAsString(contents);
+ return nativeAssetsYamlUri;
+}
+
+Future<void> _copyAssets(
+ List<({Object asset, KernelAsset target})> targetMapping,
+ Target target,
+ Uri outputUri,
+ bool relocatable,
+ bool verbose,
+) async {
+ final filesToCopy = <({String id, Uri src, Uri dest})>[];
+ final codeAssetUris = <Uri>[];
+
+ for (final (:asset, :target) in targetMapping) {
+ final targetPath = target.path;
+ if (targetPath
+ case KernelAssetRelativePath(:final uri) ||
+ KernelAssetAbsolutePath(:final uri)) {
+ final targetUri = outputUri.resolveUri(uri);
+
+ switch (asset) {
+ case CodeAsset(:final file!):
+ filesToCopy.add((
+ id: asset.id,
+ src: file,
+ dest: targetUri,
+ ));
+ codeAssetUris.add(targetUri);
+ case DataAsset(:final file):
+ filesToCopy.add((
+ id: asset.id,
+ src: file,
+ dest: targetUri,
+ ));
+ default:
+ throw UnimplementedError();
+ }
+ }
+ }
+
+ if (filesToCopy.isNotEmpty) {
+ if (verbose) {
+ stdout.writeln(
+ 'Copying ${filesToCopy.length} build assets:\n'
+ '${filesToCopy.map((e) => e.id).join('\n')}',
+ );
+ }
+
+ // TODO(https://dartbug.com/59668): Cache copying and rewriting of install names
+ await Future.wait(filesToCopy.map((file) => file.src.copyTo(file.dest)));
+
+ if (target.os == OS.macOS) {
+ await rewriteInstallNames(codeAssetUris, relocatable: relocatable);
+ }
+ }
+}
+
+List<({Object asset, KernelAsset target})> _targetMapping(
+ Iterable<EncodedAsset> assets,
+ Target target,
+ Uri outputUri,
+ bool relocatable,
+) {
+ final codeAssets = assets
+ .where((asset) => asset.type == CodeAsset.type)
+ .map(CodeAsset.fromEncoded);
+ final dataAssets = assets
+ .where((asset) => asset.type == DataAsset.type)
+ .map(DataAsset.fromEncoded);
+
+ return [
+ for (final asset in codeAssets)
+ (
+ asset: asset,
+ target: asset.targetLocation(target, outputUri, relocatable)
+ ),
+ for (final asset in dataAssets)
+ (
+ asset: asset,
+ target: asset.targetLocation(target, outputUri, relocatable)
+ ),
+ ];
+}
+
+extension on CodeAsset {
+ KernelAsset targetLocation(Target target, Uri outputUri, bool relocatable) {
+ final kernelAssetPath = switch (linkMode) {
+ DynamicLoadingSystem(:final uri) => KernelAssetSystemPath(uri),
+ LookupInExecutable() => KernelAssetInExecutable(),
+ LookupInProcess() => KernelAssetInProcess(),
+ DynamicLoadingBundled() => () {
+ final relativeUri =
+ libOutputDirectoryUri.resolve(file!.pathSegments.last);
+ return relocatable
+ ? KernelAssetRelativePath(relativeUri)
+ : KernelAssetAbsolutePath(outputUri.resolveUri(relativeUri));
+ }(),
+ _ => throw UnsupportedError(
+ 'Unsupported NativeCodeAsset linkMode ${linkMode.runtimeType} in asset $this',
+ ),
+ };
+ return KernelAsset(
+ id: id,
+ target: target,
+ path: kernelAssetPath,
+ );
+ }
+}
+
+extension on DataAsset {
+ KernelAsset targetLocation(Target target, Uri outputUri, bool relocatable) {
+ final relativeUri = dataOutputDirectoryUri.resolve(file.pathSegments.last);
+ return KernelAsset(
+ id: id,
+ target: target,
+ path: relocatable
+ ? KernelAssetRelativePath(relativeUri)
+ : KernelAssetAbsolutePath(outputUri.resolveUri(relativeUri)),
+ );
+ }
+}
+
+extension on Uri {
+ Future<void> copyTo(Uri targetUri) async {
+ await File.fromUri(targetUri).create(recursive: true);
+ await File.fromUri(this).copy(targetUri.toFilePath());
+ }
+}
diff --git a/pkg/dartdev/lib/src/native_assets_macos.dart b/pkg/dartdev/lib/src/native_assets_macos.dart
new file mode 100644
index 0000000..d471695
--- /dev/null
+++ b/pkg/dartdev/lib/src/native_assets_macos.dart
@@ -0,0 +1,160 @@
+// 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.
+
+import 'dart:io';
+
+import 'package:dartdev/src/native_assets_bundling.dart';
+import 'package:native_assets_cli/code_assets.dart';
+
+final _rpathUri = Uri.file('@rpath/');
+
+Future<void> rewriteInstallNames(
+ List<Uri> dylibs, {
+ required bool relocatable,
+}) async {
+ final oldToNewInstallNames = <String, String>{};
+ final dylibInfos = <(Uri, String)>[];
+
+ await Future.wait(dylibs.map((dylib) async {
+ final newInstallName = relocatable
+ ? _rpathUri
+ .resolveUri(libOutputDirectoryUri)
+ .resolve(dylib.pathSegments.last)
+ .toFilePath()
+ : dylib.toFilePath();
+ final oldInstallName = await _getInstallName(dylib);
+ oldToNewInstallNames[oldInstallName] = newInstallName;
+ dylibInfos.add((dylib, newInstallName));
+ }));
+
+ await Future.wait(dylibInfos.map((info) async {
+ final (dylib, newInstallName) = info;
+ await _setInstallNames(dylib, newInstallName, oldToNewInstallNames);
+ await _codeSignDylib(dylib);
+ }));
+}
+
+Future<String> _getInstallName(Uri dylib) async {
+ final otoolResult = await Process.run(
+ 'otool',
+ [
+ '-D',
+ dylib.toFilePath(),
+ ],
+ );
+ if (otoolResult.exitCode != 0) {
+ throw Exception(
+ 'Failed to get install name for dylib $dylib: ${otoolResult.stderr}',
+ );
+ }
+ final architectureSections =
+ parseOtoolArchitectureSections(otoolResult.stdout);
+ if (architectureSections.length != 1) {
+ throw Exception(
+ 'Expected a single architecture section in otool output: $otoolResult',
+ );
+ }
+ return architectureSections.values.first.single;
+}
+
+Future<void> _setInstallNames(
+ Uri dylib,
+ String newInstallName,
+ Map<String, String> oldToNewInstallNames,
+) async {
+ final installNameToolResult = await Process.run(
+ 'install_name_tool',
+ [
+ '-id',
+ newInstallName,
+ for (final entry in oldToNewInstallNames.entries) ...[
+ '-change',
+ entry.key,
+ entry.value,
+ ],
+ dylib.toFilePath(),
+ ],
+ );
+ if (installNameToolResult.exitCode != 0) {
+ throw Exception(
+ 'Failed to set install names for dylib $dylib:\n'
+ 'id -> $newInstallName\n'
+ 'dependencies -> $oldToNewInstallNames\n'
+ '${installNameToolResult.stderr}',
+ );
+ }
+}
+
+Future<void> _codeSignDylib(Uri dylib) async {
+ final codesignResult = await Process.run(
+ 'codesign',
+ [
+ '--force',
+ '--sign',
+ '-',
+ dylib.toFilePath(),
+ ],
+ );
+ if (codesignResult.exitCode != 0) {
+ throw Exception(
+ 'Failed to codesign dylib $dylib: ${codesignResult.stderr}',
+ );
+ }
+}
+
+Map<Architecture?, List<String>> parseOtoolArchitectureSections(String output) {
+ // The output of `otool -D`, for example, looks like below. For each
+ // architecture, there is a separate section.
+ //
+ // /build/native_assets/ios/buz.framework/buz (architecture x86_64):
+ // @rpath/libbuz.dylib
+ // /build/native_assets/ios/buz.framework/buz (architecture arm64):
+ // @rpath/libbuz.dylib
+ //
+ // Some versions of `otool` don't print the architecture name if the
+ // binary only has one architecture:
+ //
+ // /build/native_assets/ios/buz.framework/buz:
+ // @rpath/libbuz.dylib
+
+ const Map<String, Architecture> outputArchitectures = <String, Architecture>{
+ 'arm': Architecture.arm,
+ 'arm64': Architecture.arm64,
+ 'x86_64': Architecture.x64,
+ };
+ final RegExp architectureHeaderPattern =
+ RegExp(r'^[^(]+( \(architecture (.+)\))?:$');
+ final Iterator<String> lines = output.trim().split('\n').iterator;
+ Architecture? currentArchitecture;
+ final Map<Architecture?, List<String>> architectureSections =
+ <Architecture?, List<String>>{};
+
+ while (lines.moveNext()) {
+ final String line = lines.current;
+ final Match? architectureHeader =
+ architectureHeaderPattern.firstMatch(line);
+ if (architectureHeader != null) {
+ if (architectureSections.containsKey(null)) {
+ throw Exception(
+ 'Expected a single architecture section in otool output: $output',
+ );
+ }
+ final String? architectureString = architectureHeader.group(2);
+ if (architectureString != null) {
+ currentArchitecture = outputArchitectures[architectureString];
+ if (currentArchitecture == null) {
+ throw Exception(
+ 'Unknown architecture in otool output: $architectureString',
+ );
+ }
+ }
+ architectureSections[currentArchitecture] = <String>[];
+ continue;
+ } else {
+ architectureSections[currentArchitecture]!.add(line.trim());
+ }
+ }
+
+ return architectureSections;
+}
diff --git a/pkg/dartdev/test/native_assets/build_test.dart b/pkg/dartdev/test/native_assets/build_test.dart
index 20f1136..a813eaa 100644
--- a/pkg/dartdev/test/native_assets/build_test.dart
+++ b/pkg/dartdev/test/native_assets/build_test.dart
@@ -283,6 +283,46 @@
});
});
}
+
+ test(
+ 'dart build with native dynamic linking',
+ timeout: longTimeout,
+ () async {
+ await nativeAssetsTest('native_dynamic_linking', (packageUri) async {
+ await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ 'build',
+ 'bin/native_dynamic_linking.dart',
+ ],
+ workingDirectory: packageUri,
+ logger: logger,
+ );
+
+ final outputDirectory =
+ Directory.fromUri(packageUri.resolve('bin/native_dynamic_linking'));
+ expect(outputDirectory.existsSync(), true);
+
+ File dylibFile(String name) {
+ final libDirectoryUri = (outputDirectory.uri.resolve('lib/'));
+ final dylibBasename =
+ OS.current.libraryFileName(name, DynamicLoadingBundled());
+ return File.fromUri(libDirectoryUri.resolve(dylibBasename));
+ }
+
+ expect(dylibFile('add').existsSync(), true);
+ expect(dylibFile('math').existsSync(), true);
+ expect(dylibFile('debug').existsSync(), true);
+
+ final proccessResult = await runProcess(
+ executable: outputDirectory.uri.resolve('native_dynamic_linking.exe'),
+ logger: logger,
+ throwOnUnexpectedExitCode: true,
+ );
+ expect(proccessResult.stdout, contains('42'));
+ });
+ },
+ );
}
Future<void> _withTempDir(Future<void> Function(Uri tempUri) fun) async {
diff --git a/pkg/dartdev/test/native_assets/helpers.dart b/pkg/dartdev/test/native_assets/helpers.dart
index 40ab442..c4e3414 100644
--- a/pkg/dartdev/test/native_assets/helpers.dart
+++ b/pkg/dartdev/test/native_assets/helpers.dart
@@ -145,11 +145,26 @@
}
/// Logger that outputs the full trace when a test fails.
-final logger = Logger('')
- ..level = Level.ALL
- ..onRecord.listen((record) {
- printOnFailure('${record.level.name}: ${record.time}: ${record.message}');
- });
+Logger get logger => _logger ??= () {
+ // A new logger is lazily created for each test so that the messages
+ // captured by printOnFailure are scoped to the correct test.
+ addTearDown(() => _logger = null);
+ return _createTestLogger();
+ }();
+
+Logger? _logger;
+
+Logger createCapturingLogger(List<String> capturedMessages) =>
+ _createTestLogger(capturedMessages: capturedMessages);
+
+Logger _createTestLogger({List<String>? capturedMessages}) =>
+ Logger.detached('')
+ ..level = Level.ALL
+ ..onRecord.listen((record) {
+ printOnFailure(
+ '${record.level.name}: ${record.time}: ${record.message}');
+ capturedMessages?.add(record.message);
+ });
final dartExecutable = Uri.file(Platform.resolvedExecutable);
@@ -168,6 +183,7 @@
'drop_dylib_link',
'native_add_duplicate',
'native_add',
+ 'native_dynamic_linking',
'treeshaking_native_libs',
],
Platform.script.resolve(
diff --git a/pkg/dartdev/test/native_assets/run_test.dart b/pkg/dartdev/test/native_assets/run_test.dart
index 09cf606d..a7f92d2 100644
--- a/pkg/dartdev/test/native_assets/run_test.dart
+++ b/pkg/dartdev/test/native_assets/run_test.dart
@@ -153,4 +153,19 @@
);
});
});
+
+ test('dart run with native dynamic linking', timeout: longTimeout, () async {
+ await nativeAssetsTest('native_dynamic_linking', (packageUri) async {
+ final result = await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ 'run',
+ 'bin/native_dynamic_linking.dart',
+ ],
+ workingDirectory: packageUri,
+ logger: logger,
+ );
+ expect(result.stdout, contains('42'));
+ });
+ });
}
diff --git a/pkg/dartdev/test/native_assets/test_test.dart b/pkg/dartdev/test/native_assets/test_test.dart
index b29bafc..0a3cdf2 100644
--- a/pkg/dartdev/test/native_assets/test_test.dart
+++ b/pkg/dartdev/test/native_assets/test_test.dart
@@ -91,4 +91,26 @@
},
);
});
+
+ test('with native dynamic linking', timeout: longTimeout, () async {
+ await nativeAssetsTest('native_dynamic_linking', (packageUri) async {
+ final result = await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ 'test',
+ ],
+ workingDirectory: packageUri,
+ logger: logger,
+ );
+ expect(
+ result.stdout,
+ stringContainsInOrder(
+ [
+ 'invoke native function',
+ 'All tests passed!',
+ ],
+ ),
+ );
+ });
+ });
}
diff --git a/runtime/bin/native_assets_api_impl.cc b/runtime/bin/native_assets_api_impl.cc
index e783d56..3834c68 100644
--- a/runtime/bin/native_assets_api_impl.cc
+++ b/runtime/bin/native_assets_api_impl.cc
@@ -39,6 +39,14 @@
cstr[i] = cstr[i] == '\\' ? '/' : cstr[i];
}
}
+
+// Replaces forward slashes with back slashes in place.
+static void ReplaceForwardSlashes(char* cstr) {
+ const intptr_t length = strlen(cstr);
+ for (int i = 0; i < length; i++) {
+ cstr[i] = cstr[i] == '/' ? '\\' : cstr[i];
+ }
+}
#endif
const char* file_schema = "file://";
@@ -79,9 +87,11 @@
// If an error occurs populates |error| (if provided) with an error message
// (caller must free this message when it is no longer needed).
static void* LoadDynamicLibrary(const char* library_file,
+ bool search_dll_load_dir = false,
char** error = nullptr) {
char* utils_error = nullptr;
- void* handle = Utils::LoadDynamicLibrary(library_file, &utils_error);
+ void* handle = Utils::LoadDynamicLibrary(library_file, search_dll_load_dir,
+ &utils_error);
if (utils_error != nullptr) {
if (error != nullptr) {
SET_ERROR_MSG(error, "Failed to load dynamic library '%s': %s",
@@ -121,7 +131,8 @@
void* NativeAssets::DlopenAbsolute(const char* path, char** error) {
// If we'd want to be strict, it should not take into account include paths.
- void* handle = LoadDynamicLibrary(path, error);
+ void* handle =
+ LoadDynamicLibrary(path, /* search_dll_load_dir= */ true, error);
WrapError(path, error);
return handle;
}
@@ -142,8 +153,12 @@
SET_ERROR_MSG(error, "Failed to resolve '%s' relative to '%s'.", path_copy,
platform_script_cstr.get());
} else {
- const char* target_path = target_uri.get() + file_schema_length;
- handle = LoadDynamicLibrary(target_path, error);
+ char* target_path = target_uri.get() + file_schema_length;
+#if defined(DART_TARGET_OS_WINDOWS)
+ ReplaceForwardSlashes(target_path);
+#endif
+ handle =
+ LoadDynamicLibrary(target_path, /* search_dll_load_dir= */ true, error);
}
free(path_copy);
WrapErrorRelative(path, script_uri, error);
@@ -152,7 +167,8 @@
void* NativeAssets::DlopenSystem(const char* path, char** error) {
// Should take into account LD_PATH etc.
- void* handle = LoadDynamicLibrary(path, error);
+ void* handle =
+ LoadDynamicLibrary(path, /* search_dll_load_dir= */ false, error);
WrapError(path, error);
return handle;
}
@@ -167,7 +183,7 @@
}
void* NativeAssets::DlopenExecutable(char** error) {
- return LoadDynamicLibrary(nullptr, error);
+ return LoadDynamicLibrary(nullptr, /* search_dll_load_dir= */ false, error);
}
#if defined(DART_HOST_OS_WINDOWS)
diff --git a/runtime/lib/ffi_dynamic_library.cc b/runtime/lib/ffi_dynamic_library.cc
index 5912f3f..ec42580 100644
--- a/runtime/lib/ffi_dynamic_library.cc
+++ b/runtime/lib/ffi_dynamic_library.cc
@@ -74,7 +74,8 @@
static void* LoadDynamicLibrary(const char* library_file,
char** error = nullptr) {
char* utils_error = nullptr;
- void* handle = Utils::LoadDynamicLibrary(library_file, &utils_error);
+ void* handle = Utils::LoadDynamicLibrary(
+ library_file, /* search_dll_load_dir= */ false, &utils_error);
if (utils_error != nullptr) {
if (error != nullptr) {
*error = OS::SCreate(
diff --git a/runtime/platform/utils.cc b/runtime/platform/utils.cc
index 9eff992..67bbe13 100644
--- a/runtime/platform/utils.cc
+++ b/runtime/platform/utils.cc
@@ -286,7 +286,9 @@
#endif
}
-void* Utils::LoadDynamicLibrary(const char* library_path, char** error) {
+void* Utils::LoadDynamicLibrary(const char* library_path,
+ bool search_dll_load_dir,
+ char** error) {
void* handle = nullptr;
#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_MACOS) || \
@@ -307,7 +309,12 @@
MultiByteToWideChar(CP_UTF8, /*dwFlags=*/0, library_path,
/*cbMultiByte=*/-1, name.get(), name_len);
RELEASE_ASSERT(written_len == name_len);
- handle = LoadLibraryW(name.get());
+ if (search_dll_load_dir) {
+ handle =
+ LoadLibraryExW(name.get(), NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
+ } else {
+ handle = LoadLibraryW(name.get());
+ }
}
}
#endif
diff --git a/runtime/platform/utils.h b/runtime/platform/utils.h
index 9c52ba5..a6cd3db 100644
--- a/runtime/platform/utils.h
+++ b/runtime/platform/utils.h
@@ -663,6 +663,7 @@
// |error| (if provided) with an error message (caller must free this message
// when it is no longer needed).
static void* LoadDynamicLibrary(const char* library_path,
+ bool search_dll_load_dir = false,
char** error = nullptr);
// Resolve the given |symbol| within the library referenced by the