diff --git a/.github/workflows/native.yaml b/.github/workflows/native.yaml index 18b822e3d..6437d8713 100644 --- a/.github/workflows/native.yaml +++ b/.github/workflows/native.yaml @@ -110,6 +110,9 @@ jobs: - run: dart pub get -C test_data/treeshaking_native_libs/ if: ${{ matrix.package == 'native_assets_builder' }} + - run: dart pub get -C test_data/native_dynamic_linking/ + if: ${{ matrix.package == 'native_assets_builder' }} + - run: dart pub get -C example/build/native_dynamic_linking/ if: ${{ matrix.package == 'native_assets_cli' }} @@ -141,7 +144,7 @@ jobs: - run: dart --enable-experiment=native-assets test working-directory: pkgs/${{ matrix.package }}/example/build/native_dynamic_linking/ - if: ${{ matrix.package == 'native_assets_cli' && matrix.sdk == 'dev' && !matrix.breaking-chang && matrix.os != 'windows' }} + if: ${{ matrix.package == 'native_assets_cli' && matrix.sdk == 'dev' && !matrix.breaking-change && matrix.os != 'windows' }} - run: dart --enable-experiment=native-assets test working-directory: pkgs/${{ matrix.package }}/example/build/native_add_app/ diff --git a/pkgs/native_assets_builder/test/helpers.dart b/pkgs/native_assets_builder/test/helpers.dart index bd5c50ca1..eba181f04 100644 --- a/pkgs/native_assets_builder/test/helpers.dart +++ b/pkgs/native_assets_builder/test/helpers.dart @@ -8,11 +8,22 @@ import 'dart:io'; import 'package:logging/logging.dart'; import 'package:native_assets_builder/src/utils/run_process.dart' as run_process; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_assets_cli/native_assets_cli_internal.dart' as internal; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; extension UriExtension on Uri { + String get name => pathSegments.where((e) => e != '').last; + Uri get parent => File(toFilePath()).parent.uri; + + FileSystemEntity get fileSystemEntity { + if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) { + return Directory.fromUri(this); + } + return File.fromUri(this); + } } const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; @@ -112,8 +123,63 @@ final pkgNativeAssetsBuilderUri = findPackageRoot('native_assets_builder'); final testDataUri = pkgNativeAssetsBuilderUri.resolve('test_data/'); -extension on Uri { - String get name => pathSegments.where((e) => e != '').last; +String unparseKey(String key) => key.replaceAll('.', '__').toUpperCase(); + +/// Archiver provided by the environment. +/// +/// Provided on Dart CI. +final Uri? ar = Platform + .environment[unparseKey(internal.CCompilerConfigImpl.arConfigKeyFull)] + ?.asFileUri(); + +/// Compiler provided by the environment. +/// +/// Provided on Dart CI. +final Uri? cc = Platform + .environment[unparseKey(internal.CCompilerConfigImpl.ccConfigKeyFull)] + ?.asFileUri(); + +/// Linker provided by the environment. +/// +/// Provided on Dart CI. +final Uri? ld = Platform + .environment[unparseKey(internal.CCompilerConfigImpl.ldConfigKeyFull)] + ?.asFileUri(); + +/// Path to script that sets environment variables for [cc], [ld], and [ar]. +/// +/// Provided on Dart CI. +final Uri? envScript = Platform.environment[ + unparseKey(internal.CCompilerConfigImpl.envScriptConfigKeyFull)] + ?.asFileUri(); + +/// Arguments for [envScript] provided by environment. +/// +/// Provided on Dart CI. +final List? envScriptArgs = Platform.environment[ + unparseKey(internal.CCompilerConfigImpl.envScriptArgsConfigKeyFull)] + ?.split(' '); + +extension on String { + Uri asFileUri() => Uri.file(this); +} + +extension AssetIterable on Iterable { + Future allExist() async { + final allResults = await Future.wait(map((e) => e.exists())); + final missing = allResults.contains(false); + return !missing; + } +} + +extension on Asset { + Future exists() async { + final path_ = file; + return switch (path_) { + null => true, + _ => await path_.fileSystemEntity.exists(), + }; + } } Future copyTestProjects({ diff --git a/pkgs/native_assets_builder/test/test_data/native_dynamic_linking_add.dart b/pkgs/native_assets_builder/test/test_data/native_dynamic_linking_add.dart new file mode 100644 index 000000000..759452fd5 --- /dev/null +++ b/pkgs/native_assets_builder/test/test_data/native_dynamic_linking_add.dart @@ -0,0 +1,15 @@ +// 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:ffi'; + +void main(List arguments) { + final addLibraryPath = arguments[0]; + final a = int.parse(arguments[1]); + final b = int.parse(arguments[2]); + final addLibrary = DynamicLibrary.open(addLibraryPath); + final add = addLibrary.lookupFunction('add'); + print(add(a, b)); +} diff --git a/pkgs/native_assets_builder/test/test_data/native_dynamic_linking_test.dart b/pkgs/native_assets_builder/test/test_data/native_dynamic_linking_test.dart new file mode 100644 index 000000000..f6653289a --- /dev/null +++ b/pkgs/native_assets_builder/test/test_data/native_dynamic_linking_test.dart @@ -0,0 +1,126 @@ +// 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. + +@OnPlatform({ + 'mac-os': Timeout.factor(2), + 'windows': Timeout.factor(10), +}) +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli_internal.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() async { + late Uri tempUri; + const name = 'native_dynamic_linking'; + + setUp(() async { + tempUri = (await Directory.systemTemp.createTemp()).uri; + }); + + tearDown(() async { + await Directory.fromUri(tempUri).delete(recursive: true); + }); + + for (final dryRun in [true, false]) { + final testSuffix = dryRun ? ' dry_run' : ''; + test('native_dynamic_linking build$testSuffix', () async { + final testTempUri = tempUri.resolve('test1/'); + await Directory.fromUri(testTempUri).create(); + final testPackageUri = testDataUri.resolve('$name/'); + final dartUri = Uri.file(Platform.resolvedExecutable); + + final config = BuildConfigImpl( + outputDirectory: tempUri, + packageName: name, + packageRoot: testPackageUri, + targetOS: OSImpl.current, + version: HookConfigImpl.latestVersion, + linkModePreference: LinkModePreferenceImpl.dynamic, + dryRun: dryRun, + linkingEnabled: false, + targetArchitecture: dryRun ? null : ArchitectureImpl.current, + buildMode: dryRun ? null : BuildModeImpl.debug, + cCompiler: dryRun + ? null + : CCompilerConfigImpl( + compiler: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + + final buildConfigUri = testTempUri.resolve('build_config.json'); + File.fromUri(buildConfigUri) + .writeAsStringSync(jsonEncode(config.toJson())); + + final processResult = await Process.run( + dartUri.toFilePath(), + [ + 'hook/build.dart', + '--config=${buildConfigUri.toFilePath()}', + ], + workingDirectory: testPackageUri.toFilePath(), + ); + if (processResult.exitCode != 0) { + print(processResult.stdout); + print(processResult.stderr); + print(processResult.exitCode); + } + expect(processResult.exitCode, 0); + + final buildOutputUri = tempUri.resolve('build_output.json'); + final buildOutput = HookOutputImpl.fromJsonString( + await File.fromUri(buildOutputUri).readAsString()); + final assets = buildOutput.assets; + final dependencies = buildOutput.dependencies; + if (dryRun) { + expect(assets.length, greaterThanOrEqualTo(3)); + expect(dependencies, []); + } else { + expect(assets.length, 3); + expect(await assets.allExist(), true); + expect( + dependencies, + [ + testPackageUri.resolve('src/debug.c'), + testPackageUri.resolve('src/math.c'), + testPackageUri.resolve('src/add.c'), + ], + ); + + final addLibraryPath = assets + .firstWhere((asset) => asset.id.endsWith('add.dart')) + .file! + .toFilePath(); + final addResult = await runProcess( + executable: dartExecutable, + arguments: [ + 'run', + pkgNativeAssetsBuilderUri + .resolve('test/test_data/native_dynamic_linking_add.dart') + .toFilePath(), + addLibraryPath, + '1', + '2', + ], + environment: { + // Add the directory containing the linked dynamic libraries to the + // PATH so that the dynamic linker can find them. + if (Platform.isWindows) + 'PATH': '${tempUri.toFilePath()};${Platform.environment['PATH']}', + }, + throwOnUnexpectedExitCode: true, + logger: logger, + ); + expect(addResult.stdout, 'Adding 1 and 2.\n3\n'); + } + }); + } +} diff --git a/pkgs/native_assets_builder/test_data/manifest.yaml b/pkgs/native_assets_builder/test_data/manifest.yaml index 3f5d46a54..3fdb1d2c8 100644 --- a/pkgs/native_assets_builder/test_data/manifest.yaml +++ b/pkgs/native_assets_builder/test_data/manifest.yaml @@ -113,3 +113,16 @@ - wrong_build_output/pubspec.yaml - wrong_namespace_asset/hook/build.dart - wrong_namespace_asset/pubspec.yaml +- native_dynamic_linking/bin/native_dynamic_linking.dart +- native_dynamic_linking/hook/build.dart +- native_dynamic_linking/lib/add.dart +- native_dynamic_linking/src/add.c +- native_dynamic_linking/src/add.h +- native_dynamic_linking/src/debug.c +- native_dynamic_linking/src/debug.h +- native_dynamic_linking/src/math.c +- native_dynamic_linking/src/math.h +- native_dynamic_linking/test/add_test.dart +- native_dynamic_linking/ffigen.yaml +- native_dynamic_linking/pubspec.yaml +- native_dynamic_linking/README.md diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/README.md b/pkgs/native_assets_builder/test_data/native_dynamic_linking/README.md new file mode 100644 index 000000000..3ef798c54 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/README.md @@ -0,0 +1,7 @@ +An example library that builds 3 native libraries, 2 of which are dynamically +linked to each other. + +## Usage + +Run tests with `dart --enable-experiment=native-assets test`. + diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/bin/native_dynamic_linking.dart b/pkgs/native_assets_builder/test_data/native_dynamic_linking/bin/native_dynamic_linking.dart new file mode 100644 index 000000000..1cd8db61c --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/bin/native_dynamic_linking.dart @@ -0,0 +1,3 @@ +import 'package:native_dynamic_linking/add.dart'; + +void main() => print('${add(24, 18)}'); diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/ffigen.yaml b/pkgs/native_assets_builder/test_data/native_dynamic_linking/ffigen.yaml new file mode 100644 index 000000000..71f7b4f62 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/ffigen.yaml @@ -0,0 +1,20 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: AddBindings +description: | + Bindings for `src/add.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: 'lib/add.dart' +headers: + entry-points: + - 'src/add.h' + include-directives: + - 'src/add.h' +preamble: | + // 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. +comments: + style: any + length: full +ffi-native: diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/hook/build.dart b/pkgs/native_assets_builder/test_data/native_dynamic_linking/hook/build.dart new file mode 100644 index 000000000..9711a8cb8 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/hook/build.dart @@ -0,0 +1,73 @@ +// 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 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +void main(List args) async { + await build(args, (config, output) async { + final logger = Logger('') + ..level = Level.ALL + ..onRecord.listen((record) => print(record.message)); + + final builders = [ + CBuilder.library( + name: 'debug', + assetName: 'debug', + sources: [ + 'src/debug.c', + ], + ), + CBuilder.library( + name: 'math', + assetName: 'math', + sources: [ + 'src/math.c', + ], + // TODO(https://github.com/dart-lang/native/issues/190): Use specific + // API for linking once available. + flags: config.dynamicLinkingFlags('debug'), + ), + CBuilder.library( + name: 'add', + assetName: 'add.dart', + sources: [ + 'src/add.c', + ], + // TODO(https://github.com/dart-lang/native/issues/190): Use specific + // API for linking once available. + flags: config.dynamicLinkingFlags('math'), + ) + ]; + + // Note: This builders need to be run sequentially because they depend on + // each others output. + for (final builder in builders) { + await builder.run( + config: config, + output: output, + logger: logger, + ); + } + }); +} + +extension on BuildConfig { + List dynamicLinkingFlags(String libraryName) => switch (targetOS) { + OS.macOS => [ + '-L${outputDirectory.toFilePath()}', + '-l$libraryName', + ], + OS.linux => [ + r'-Wl,-rpath=$ORIGIN', + '-L${outputDirectory.toFilePath()}', + '-l$libraryName', + ], + OS.windows => [ + outputDirectory.resolve('$libraryName.dll').toFilePath(), + ], + _ => throw UnimplementedError('Unsupported OS: $targetOS'), + }; +} diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/lib/add.dart b/pkgs/native_assets_builder/test_data/native_dynamic_linking/lib/add.dart new file mode 100644 index 000000000..68f7e2c4f --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/lib/add.dart @@ -0,0 +1,15 @@ +// 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. + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +@ffi.Native(symbol: 'add') +external int add( + int a, + int b, +); diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/pubspec.yaml b/pkgs/native_assets_builder/test_data/native_dynamic_linking/pubspec.yaml new file mode 100644 index 000000000..b654820ba --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/pubspec.yaml @@ -0,0 +1,23 @@ +publish_to: none + +name: native_dynamic_linking +description: Dynamically link native libraries to each other. +version: 0.1.0 +repository: https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli/example/build/native_dynamic_linking + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + logging: ^1.1.1 + # native_assets_cli: ^0.7.1 + native_assets_cli: + path: ../../../native_assets_cli/ + # native_toolchain_c: ^0.5.2 + native_toolchain_c: + path: ../../../native_toolchain_c/ + +dev_dependencies: + ffigen: ^8.0.2 + lints: ^3.0.0 + test: ^1.21.0 diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/add.c b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/add.c new file mode 100644 index 000000000..d566a9663 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/add.c @@ -0,0 +1,12 @@ +// 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. + +#include "add.h" +#include "math.h" + +int32_t add(int32_t a, int32_t b) { + // Here we are calling a function from the math library, which will be + // loaded by the dynamic linker. + return math_add(a, b); +} diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/add.h b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/add.h new file mode 100644 index 000000000..275878fe8 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/add.h @@ -0,0 +1,13 @@ +// 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. + +#include + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT int32_t add(int32_t a, int32_t b); diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/debug.c b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/debug.c new file mode 100644 index 000000000..92e42f1d2 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/debug.c @@ -0,0 +1,20 @@ +// 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. + +#include "debug.h" + +#ifdef DEBUG +#include +#include +#endif + +int debug_printf(const char* format, ...) { +#ifdef DEBUG + va_list args; + va_start(args, format); + int ret = vprintf(format, args); + va_end(args); + return ret; +#endif +} diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/debug.h b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/debug.h new file mode 100644 index 000000000..557cba0d1 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/debug.h @@ -0,0 +1,11 @@ +// 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. + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT int debug_printf(const char * format, ...); diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/math.c b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/math.c new file mode 100644 index 000000000..08313a5bb --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/math.c @@ -0,0 +1,11 @@ +// 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. + +#include "debug.h" +#include "math.h" + +int32_t math_add(int32_t a, int32_t b) { + debug_printf("Adding %i and %i.\n", a, b); + return a + b; +} diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/math.h b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/math.h new file mode 100644 index 000000000..7d22a65c8 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/src/math.h @@ -0,0 +1,13 @@ +// 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. + +#include + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT int32_t math_add(int32_t a, int32_t b); diff --git a/pkgs/native_assets_builder/test_data/native_dynamic_linking/test/add_test.dart b/pkgs/native_assets_builder/test_data/native_dynamic_linking/test/add_test.dart new file mode 100644 index 000000000..906e3a8f2 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/native_dynamic_linking/test/add_test.dart @@ -0,0 +1,12 @@ +// 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 'package:native_dynamic_linking/add.dart'; +import 'package:test/test.dart'; + +void main() { + test('invoke native function', () { + expect(add(24, 18), 42); + }); +}