[native_assets_cli] Add `DartCApi`
diff --git a/.github/workflows/native.yaml b/.github/workflows/native.yaml
index 3744888..6617443 100644
--- a/.github/workflows/native.yaml
+++ b/.github/workflows/native.yaml
@@ -111,6 +111,9 @@
- run: dart pub get -C test_data/no_hook/
if: ${{ matrix.package == 'native_assets_builder' }}
+ - run: dart pub get -C test_data/use_dart_api/
+ if: ${{ matrix.package == 'native_assets_builder' }}
+
- run: dart pub get -C example/build/download_asset/
if: ${{ matrix.package == 'native_assets_cli' }}
diff --git a/pkgs/native_assets_builder/test/build_runner/dart_c_api_test.dart b/pkgs/native_assets_builder/test/build_runner/dart_c_api_test.dart
new file mode 100644
index 0000000..5656f27
--- /dev/null
+++ b/pkgs/native_assets_builder/test/build_runner/dart_c_api_test.dart
@@ -0,0 +1,56 @@
+// 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 'dart:ffi';
+import 'dart:io';
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import '../helpers.dart';
+import 'helpers.dart';
+
+const Timeout longTimeout = Timeout(Duration(minutes: 5));
+
+void main() async {
+ test('use_dart_api build', timeout: longTimeout, () async {
+ await inTempDir((tempUri) async {
+ await copyTestProjects(targetUri: tempUri);
+ final packageUri = tempUri.resolve('use_dart_api/');
+ await runPubGet(
+ workingDirectory: packageUri,
+ logger: logger,
+ );
+
+ // Assume we're run from Dart SDK and try to find the include dir.
+ final includeDirectory = dartExecutable.resolve('../include/');
+ final versionFile = includeDirectory.resolve('dart_version.h');
+ final versionContents =
+ await File(versionFile.toFilePath()).readAsString();
+ final regex = RegExp(
+ r'#define DART_API_DL_MAJOR_VERSION (\d+)\s*#define DART_API_DL_MINOR_VERSION (\d+)');
+ final match = regex.firstMatch(versionContents)!;
+ final major = int.parse(match.group(1)!);
+ final minor = int.parse(match.group(2)!);
+ final dartCApi = DartCApi(
+ includeDirectory: includeDirectory,
+ version: Version(major, minor, 0),
+ );
+
+ // Run the build.
+ final result = (await buildCodeAssets(
+ packageUri,
+ dartCApi: dartCApi,
+ ))!;
+ final codeAsset = CodeAsset.fromEncoded(result.encodedAssets.single);
+
+ // Check that we can load the dylib and run the init.
+ final dylib = DynamicLibrary.open(codeAsset.file!.toFilePath());
+ final initDartApiDl = dylib.lookupFunction<IntPtr Function(Pointer<Void>),
+ int Function(Pointer<Void>)>('InitDartApiDL');
+ final initResult = initDartApiDl(NativeApi.initializeApiDLData);
+ expect(initResult, 0);
+ });
+ });
+}
diff --git a/pkgs/native_assets_builder/test/build_runner/helpers.dart b/pkgs/native_assets_builder/test/build_runner/helpers.dart
index 122772f..d346211 100644
--- a/pkgs/native_assets_builder/test/build_runner/helpers.dart
+++ b/pkgs/native_assets_builder/test/build_runner/helpers.dart
@@ -52,6 +52,7 @@
Uri packageUri, {
String? runPackageName,
List<String>? capturedLogs,
+ DartCApi? dartCApi,
}) =>
build(
packageUri,
@@ -63,6 +64,7 @@
buildValidator: validateCodeAssetBuildOutput,
applicationAssetValidator: validateCodeAssetInApplication,
runPackageName: runPackageName,
+ dartCApi: dartCApi,
);
Future<BuildResult?> build(
@@ -84,6 +86,7 @@
bool linkingEnabled = false,
required List<String> buildAssetTypes,
Map<String, String>? hookEnvironment,
+ DartCApi? dartCApi,
}) async {
final targetOS = target?.os ?? OS.current;
final runPackageName_ =
@@ -122,6 +125,7 @@
android: targetOS == OS.android
? AndroidCodeConfig(targetNdkApi: targetAndroidNdkApi!)
: null,
+ dartCApi: dartCApi,
);
}
return inputBuilder;
diff --git a/pkgs/native_assets_builder/test_data/manifest.yaml b/pkgs/native_assets_builder/test_data/manifest.yaml
index 819718c..7a269b4 100644
--- a/pkgs/native_assets_builder/test_data/manifest.yaml
+++ b/pkgs/native_assets_builder/test_data/manifest.yaml
@@ -164,6 +164,14 @@
- use_all_api/hook/build.dart
- use_all_api/hook/link.dart
- use_all_api/pubspec.yaml
+- use_dart_api/ffigen.yaml
+- use_dart_api/hook/build.dart
+- use_dart_api/lib/src/use_dart_api_bindings_generated.dart
+- use_dart_api/lib/use_dart_api.dart
+- use_dart_api/pubspec.yaml
+- use_dart_api/src/use_dart_api.c
+- use_dart_api/src/use_dart_api.h
+- use_dart_api/test/use_dart_api_test.dart
- wrong_build_output/hook/build.dart
- wrong_build_output/pubspec.yaml
- wrong_build_output_2/hook/build.dart
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/README.md b/pkgs/native_assets_builder/test_data/use_dart_api/README.md
new file mode 100644
index 0000000..bb8514f
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/README.md
@@ -0,0 +1,11 @@
+An example that uses the [C API of the Dart VM].
+
+The example shows how to pass an object from the Dart heap to native code and
+hold on to it via a `PersistentHandle`. For more documentation about handles,
+and the other C API features refer to the documentation in the header files.
+
+## Usage
+
+Run tests with `dart --enable-experiment=native-assets test`.
+
+[C API of the Dart VM]: https://github.com/dart-lang/sdk/tree/main/runtime/include
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/ffigen.yaml b/pkgs/native_assets_builder/test_data/use_dart_api/ffigen.yaml
new file mode 100644
index 0000000..5e0e4e3
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/ffigen.yaml
@@ -0,0 +1,20 @@
+# Run with `flutter pub run ffigen --config ffigen.yaml`.
+name: NativeAddBindings
+description: |
+ Bindings for `src/use_dart_api.h`.
+
+ Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
+output: 'lib/src/use_dart_api_bindings_generated.dart'
+headers:
+ entry-points:
+ - 'src/use_dart_api.h'
+ include-directives:
+ - 'src/use_dart_api.h'
+preamble: |
+ // 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.
+comments:
+ style: any
+ length: full
+ffi-native:
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/hook/build.dart b/pkgs/native_assets_builder/test_data/use_dart_api/hook/build.dart
new file mode 100644
index 0000000..85ce205
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/hook/build.dart
@@ -0,0 +1,41 @@
+// 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:logging/logging.dart';
+import 'package:native_assets_cli/code_assets_builder.dart';
+import 'package:native_assets_cli/native_assets_cli.dart';
+import 'package:native_toolchain_c/native_toolchain_c.dart';
+
+void main(List<String> arguments) async {
+ await build(arguments, (input, output) async {
+ final dartCApi = input.config.code.dartCApi;
+ if (dartCApi == null) {
+ throw UnsupportedError(
+ 'This doesn\'t work with access to the Dart C API!',
+ );
+ }
+
+ final packageName = input.packageName;
+ final cbuilder = CBuilder.library(
+ name: packageName,
+ assetName: 'src/${packageName}_bindings_generated.dart',
+ sources: [
+ 'src/$packageName.c',
+ dartCApi.dartApiDlC.toFilePath(),
+ ],
+ includes: [
+ dartCApi.includeDirectory.toFilePath(),
+ ],
+ );
+ await cbuilder.run(
+ input: input,
+ output: output,
+ logger: Logger('')
+ ..level = Level.ALL
+ ..onRecord.listen((record) {
+ print('${record.level.name}: ${record.time}: ${record.message}');
+ }),
+ );
+ });
+}
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/lib/src/use_dart_api_bindings_generated.dart b/pkgs/native_assets_builder/test_data/use_dart_api/lib/src/use_dart_api_bindings_generated.dart
new file mode 100644
index 0000000..5cb03ae
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/lib/src/use_dart_api_bindings_generated.dart
@@ -0,0 +1,32 @@
+// 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.
+
+// AUTO GENERATED FILE, DO NOT EDIT.
+//
+// Generated by `package:ffigen`.
+// ignore_for_file: type=lint
+import 'dart:ffi' as ffi;
+
+@ffi.Native<ffi.Int32 Function(ffi.Int32, ffi.Int32)>(symbol: 'add')
+external int add(
+ int a,
+ int b,
+);
+
+@ffi.Native<ffi.IntPtr Function(ffi.Pointer<ffi.Void>)>(symbol: 'InitDartApiDL')
+external int InitDartApiDL(
+ ffi.Pointer<ffi.Void> data,
+);
+
+@ffi.Native<ffi.Pointer<ffi.Void> Function(ffi.Handle)>(
+ symbol: 'NewPersistentHandle')
+external ffi.Pointer<ffi.Void> NewPersistentHandle(
+ Object non_persistent_handle,
+);
+
+@ffi.Native<ffi.Handle Function(ffi.Pointer<ffi.Void>)>(
+ symbol: 'HandleFromPersistent')
+external Object HandleFromPersistent(
+ ffi.Pointer<ffi.Void> persistent_handle,
+);
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/lib/use_dart_api.dart b/pkgs/native_assets_builder/test_data/use_dart_api/lib/use_dart_api.dart
new file mode 100644
index 0000000..84062bb
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/lib/use_dart_api.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'src/use_dart_api_bindings_generated.dart';
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/pubspec.yaml b/pkgs/native_assets_builder/test_data/use_dart_api/pubspec.yaml
new file mode 100644
index 0000000..57f0e0a
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/pubspec.yaml
@@ -0,0 +1,22 @@
+name: use_dart_api
+description: Uses some functions from `dart_api_dl.h`.
+version: 0.1.0
+
+publish_to: none
+
+environment:
+ sdk: '>=3.3.0 <4.0.0'
+
+dependencies:
+ logging: ^1.1.1
+ # native_assets_cli: ^0.11.0
+ native_assets_cli:
+ path: ../../../native_assets_cli/
+ # native_toolchain_c: ^0.8.0
+ native_toolchain_c:
+ path: ../../../native_toolchain_c/
+
+dev_dependencies:
+ ffigen: ^10.0.0
+ lints: ^3.0.0
+ test: ^1.23.1
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/src/use_dart_api.c b/pkgs/native_assets_builder/test_data/use_dart_api/src/use_dart_api.c
new file mode 100644
index 0000000..a1accc7
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/src/use_dart_api.c
@@ -0,0 +1,17 @@
+// 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.
+
+#include "use_dart_api.h"
+
+int32_t add(int32_t a, int32_t b) { return a + b; }
+
+intptr_t InitDartApiDL(void *data) { return Dart_InitializeApiDL(data); }
+
+void *NewPersistentHandle(Dart_Handle non_persistent_handle) {
+ return Dart_NewPersistentHandle_DL(non_persistent_handle);
+}
+
+Dart_Handle HandleFromPersistent(void *persistent_handle) {
+ return Dart_HandleFromPersistent_DL(persistent_handle);
+}
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/src/use_dart_api.h b/pkgs/native_assets_builder/test_data/use_dart_api/src/use_dart_api.h
new file mode 100644
index 0000000..b3d15e2
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/src/use_dart_api.h
@@ -0,0 +1,21 @@
+// 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.
+
+#include <stdint.h>
+
+#include "dart_api_dl.h"
+
+#if _WIN32
+#define MYLIB_EXPORT __declspec(dllexport)
+#else
+#define MYLIB_EXPORT
+#endif
+
+MYLIB_EXPORT int32_t add(int32_t a, int32_t b);
+
+MYLIB_EXPORT intptr_t InitDartApiDL(void *data);
+
+MYLIB_EXPORT void *NewPersistentHandle(Dart_Handle non_persistent_handle);
+
+MYLIB_EXPORT Dart_Handle HandleFromPersistent(void *persistent_handle);
diff --git a/pkgs/native_assets_builder/test_data/use_dart_api/test/use_dart_api_test.dart b/pkgs/native_assets_builder/test_data/use_dart_api/test/use_dart_api_test.dart
new file mode 100644
index 0000000..babf018
--- /dev/null
+++ b/pkgs/native_assets_builder/test_data/use_dart_api/test/use_dart_api_test.dart
@@ -0,0 +1,18 @@
+// 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 'dart:ffi';
+
+import 'package:test/test.dart';
+import 'package:use_dart_api/use_dart_api.dart';
+
+void main() {
+ InitDartApiDL(NativeApi.initializeApiDLData);
+
+ test('use dart_api_dl.h', () {
+ const x = 42;
+ final persistentHandle = NewPersistentHandle(x);
+ HandleFromPersistent(persistentHandle);
+ });
+}
diff --git a/pkgs/native_assets_cli/lib/code_assets.dart b/pkgs/native_assets_cli/lib/code_assets.dart
index d1d9c8f..b171f41 100644
--- a/pkgs/native_assets_cli/lib/code_assets.dart
+++ b/pkgs/native_assets_cli/lib/code_assets.dart
@@ -24,6 +24,7 @@
CodeAssetLinkOutputBuilder,
CodeAssetLinkOutputBuilderAdd,
CodeConfig,
+ DartCApi,
IOSCodeConfig,
MacOSCodeConfig;
export 'src/code_assets/ios_sdk.dart' show IOSSdk;
diff --git a/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart b/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart
index 4a65875..d28e4ee 100644
--- a/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart
+++ b/pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart
@@ -184,6 +184,9 @@
}..sortOnKey());
static const String type = 'native_code';
+
+ @override
+ String toString() => 'CodeAsset(${encode().encoding})';
}
extension OSLibraryNaming on OS {
diff --git a/pkgs/native_assets_cli/lib/src/code_assets/config.dart b/pkgs/native_assets_cli/lib/src/code_assets/config.dart
index 832a9e9..30ce604 100644
--- a/pkgs/native_assets_cli/lib/src/code_assets/config.dart
+++ b/pkgs/native_assets_cli/lib/src/code_assets/config.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 'package:pub_semver/pub_semver.dart';
+
import '../config.dart';
import '../json_utils.dart';
import 'architecture.dart';
@@ -52,6 +54,8 @@
final AndroidCodeConfig? _androidConfig;
final MacOSCodeConfig? _macOSConfig;
+ final DartCApi? dartCApi;
+
// Should not be made public, class will be replaced as a view on `json`.
CodeConfig._({
required Architecture? targetArchitecture,
@@ -61,6 +65,7 @@
AndroidCodeConfig? androidConfig,
IOSCodeConfig? iOSConfig,
MacOSCodeConfig? macOSConfig,
+ this.dartCApi,
}) : _targetArchitecture = targetArchitecture,
cCompiler = cCompilerConfig,
_iOSConfig = iOSConfig,
@@ -98,6 +103,11 @@
final macOSConfig =
dryRun || targetOS != OS.macOS ? null : MacOSCodeConfig.fromJson(json);
+ final dartCApi = switch (json.code?.optionalMap(_dartCApiKey)) {
+ final Map<String, Object?> map => DartCApi.fromJson(map),
+ null => null
+ };
+
return CodeConfig._(
targetArchitecture: targetArchitecture,
targetOS: targetOS,
@@ -106,6 +116,7 @@
iOSConfig: iOSConfig,
androidConfig: androidConfig,
macOSConfig: macOSConfig,
+ dartCApi: dartCApi,
);
}
@@ -282,6 +293,7 @@
AndroidCodeConfig? android,
IOSCodeConfig? iOS,
MacOSCodeConfig? macOS,
+ DartCApi? dartCApi,
}) {
if (targetArchitecture != null) {
json[_targetArchitectureKey] = targetArchitecture.toString();
@@ -298,6 +310,9 @@
json[_compilerKey] = cCompiler.toJson(deprecatedTopLevel: true);
json.setNested([_configKey, _codeKey, _compilerKey], cCompiler.toJson());
}
+ if (dartCApi != null) {
+ json.setNested([_configKey, _codeKey, _dartCApiKey], dartCApi.toJson());
+ }
// Note, using ?. instead of !. makes missing data be a semantic error
// rather than a syntactic error to be caught in the validation.
@@ -346,6 +361,46 @@
.toList();
}
+/// Access to the C API for this version of Dart.
+///
+/// Only `dart_api_dl.h` should be used. `dart_api_dl.c` shoud be compiled into
+/// your `CodeAsset`.
+///
+/// Note: If you're precompiling code assets, you must check that [version] is
+/// compatible in your hook.
+class DartCApi {
+ /// The include directory from the Dart SDK.
+ ///
+ /// See the documentation in the following files on how to use the Dart C API.
+ /// - dart_api_dl.c
+ /// - dart_api_dl.h
+ /// - dart_version.h
+ final Uri includeDirectory;
+
+ /// The version of `dart_api_dl.h` in [includeDirectory].
+ ///
+ /// Note that only the major and minor version are used. The patch version is
+ /// always 0.
+ final Version version;
+
+ Uri get dartApiDlC => includeDirectory.resolve('dart_api_dl.c');
+
+ DartCApi({
+ required this.includeDirectory,
+ required this.version,
+ });
+
+ factory DartCApi.fromJson(Map<String, Object?> json) => DartCApi(
+ includeDirectory: json.path(_includeDirectoryKey),
+ version: Version.parse(json.string(_versionKey)),
+ );
+
+ Map<String, Object?> toJson() => {
+ _includeDirectoryKey: includeDirectory.toFilePath(),
+ _versionKey: version.toString(),
+ };
+}
+
const String _compilerKey = 'c_compiler';
const String _linkModePreferenceKey = 'link_mode_preference';
const String _targetNdkApiKey = 'target_ndk_api';
@@ -366,6 +421,10 @@
const _iosKey = 'ios';
const _macosKey = 'macos';
+const _dartCApiKey = 'dart_api';
+const _includeDirectoryKey = 'include_directory';
+const _versionKey = 'version';
+
extension on Map<String, Object?> {
Map<String, Object?>? get code =>
optionalMap(_configKey)?.optionalMap(_codeKey);
diff --git a/pkgs/native_assets_cli/test/code_assets/config_test.dart b/pkgs/native_assets_cli/test/code_assets/config_test.dart
index 26cd8ad..b4b1af1 100644
--- a/pkgs/native_assets_cli/test/code_assets/config_test.dart
+++ b/pkgs/native_assets_cli/test/code_assets/config_test.dart
@@ -6,6 +6,7 @@
import 'package:native_assets_cli/code_assets_builder.dart';
import 'package:native_assets_cli/src/config.dart' show latestVersion;
+import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
void main() async {
@@ -19,6 +20,7 @@
late Uri fakeAr;
late List<EncodedAsset> assets;
late Uri fakeVcVars;
+ late Uri dartCApiIncludeDirectory;
setUp(() async {
final tempUri = Directory.systemTemp.uri;
@@ -31,6 +33,7 @@
fakeLd = tempUri.resolve('fake_ld');
fakeAr = tempUri.resolve('fake_ar');
fakeVcVars = tempUri.resolve('vcvarsall.bat');
+ dartCApiIncludeDirectory = tempUri.resolve('include/');
assets = [
CodeAsset(
@@ -102,6 +105,10 @@
},
},
},
+ 'dart_api': {
+ 'include_directory': dartCApiIncludeDirectory.toFilePath(),
+ 'version': '2.5.0',
+ },
if (targetOS == OS.android) 'android': {'target_ndk_api': 30},
if (targetOS == OS.macOS) 'macos': {'target_version': 13},
if (targetOS == OS.iOS)
@@ -211,6 +218,10 @@
),
),
),
+ dartCApi: DartCApi(
+ includeDirectory: dartCApiIncludeDirectory,
+ version: Version(2, 5, 0),
+ ),
);
final input = BuildInput(inputBuilder.json);
expect(input.json, inputJson());
@@ -265,6 +276,10 @@
),
),
),
+ dartCApi: DartCApi(
+ includeDirectory: dartCApiIncludeDirectory,
+ version: Version(2, 5, 0),
+ ),
);
final input = LinkInput(inputBuilder.json);
expect(input.json, inputJson(hookType: 'link'));