Skip to content

[native_assets_cli] Add DartCApi #1937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/native.yaml
Original file line number Diff line number Diff line change
@@ -111,6 +111,9 @@ jobs:
- 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' }}

56 changes: 56 additions & 0 deletions pkgs/native_assets_builder/test/build_runner/dart_c_api_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
4 changes: 4 additions & 0 deletions pkgs/native_assets_builder/test/build_runner/helpers.dart
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ Future<BuildResult?> buildCodeAssets(
Uri packageUri, {
String? runPackageName,
List<String>? capturedLogs,
DartCApi? dartCApi,
}) =>
build(
packageUri,
@@ -63,6 +64,7 @@ Future<BuildResult?> buildCodeAssets(
buildValidator: validateCodeAssetBuildOutput,
applicationAssetValidator: validateCodeAssetInApplication,
runPackageName: runPackageName,
dartCApi: dartCApi,
);

Future<BuildResult?> build(
@@ -84,6 +86,7 @@ Future<BuildResult?> build(
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 @@ Future<BuildResult?> build(
android: targetOS == OS.android
? AndroidCodeConfig(targetNdkApi: targetAndroidNdkApi!)
: null,
dartCApi: dartCApi,
);
}
return inputBuilder;
8 changes: 8 additions & 0 deletions pkgs/native_assets_builder/test_data/manifest.yaml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions pkgs/native_assets_builder/test_data/use_dart_api/README.md
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions pkgs/native_assets_builder/test_data/use_dart_api/ffigen.yaml
Original file line number Diff line number Diff line change
@@ -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:
41 changes: 41 additions & 0 deletions pkgs/native_assets_builder/test_data/use_dart_api/hook/build.dart
Original file line number Diff line number Diff line change
@@ -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}');
}),
);
});
}
Original file line number Diff line number Diff line change
@@ -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,
);
Original file line number Diff line number Diff line change
@@ -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';
22 changes: 22 additions & 0 deletions pkgs/native_assets_builder/test_data/use_dart_api/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
});
}
1 change: 1 addition & 0 deletions pkgs/native_assets_cli/lib/code_assets.dart
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ export 'src/code_assets/config.dart'
CodeAssetLinkOutputBuilder,
CodeAssetLinkOutputBuilderAdd,
CodeConfig,
DartCApi,
IOSCodeConfig,
MacOSCodeConfig;
export 'src/code_assets/ios_sdk.dart' show IOSSdk;
3 changes: 3 additions & 0 deletions pkgs/native_assets_cli/lib/src/code_assets/code_asset.dart
Original file line number Diff line number Diff line change
@@ -184,6 +184,9 @@ final class CodeAsset {
}..sortOnKey());

static const String type = 'native_code';

@override
String toString() => 'CodeAsset(${encode().encoding})';
}

extension OSLibraryNaming on OS {
59 changes: 59 additions & 0 deletions pkgs/native_assets_cli/lib/src/code_assets/config.dart
Original file line number Diff line number Diff line change
@@ -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 @@ class CodeConfig {
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 @@ class CodeConfig {
AndroidCodeConfig? androidConfig,
IOSCodeConfig? iOSConfig,
MacOSCodeConfig? macOSConfig,
this.dartCApi,
}) : _targetArchitecture = targetArchitecture,
cCompiler = cCompilerConfig,
_iOSConfig = iOSConfig,
@@ -98,6 +103,11 @@ class CodeConfig {
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 @@ class CodeConfig {
iOSConfig: iOSConfig,
androidConfig: androidConfig,
macOSConfig: macOSConfig,
dartCApi: dartCApi,
);
}

@@ -282,6 +293,7 @@ extension CodeAssetBuildInputBuilder on HookConfigBuilder {
AndroidCodeConfig? android,
IOSCodeConfig? iOS,
MacOSCodeConfig? macOS,
DartCApi? dartCApi,
}) {
if (targetArchitecture != null) {
json[_targetArchitectureKey] = targetArchitecture.toString();
@@ -298,6 +310,9 @@ extension CodeAssetBuildInputBuilder on HookConfigBuilder {
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 @@ extension CodeAssetLinkOutput on LinkOutputAssets {
.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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation might not be sufficient in the long wrong - how would I check that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One should check the major version is identical and the minor version is at least what you need.

/// 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 _androidKey = 'android';
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);
15 changes: 15 additions & 0 deletions pkgs/native_assets_cli/test/code_assets/config_test.dart
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import 'dart:io';

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 @@ void main() async {
late Uri fakeAr;
late List<EncodedAsset> assets;
late Uri fakeVcVars;
late Uri dartCApiIncludeDirectory;

setUp(() async {
final tempUri = Directory.systemTemp.uri;
@@ -31,6 +33,7 @@ void main() async {
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 @@ void main() async {
},
},
},
'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 @@ void main() async {
),
),
),
dartCApi: DartCApi(
includeDirectory: dartCApiIncludeDirectory,
version: Version(2, 5, 0),
),
);
final input = BuildInput(inputBuilder.json);
expect(input.json, inputJson());
@@ -265,6 +276,10 @@ void main() async {
),
),
),
dartCApi: DartCApi(
includeDirectory: dartCApiIncludeDirectory,
version: Version(2, 5, 0),
),
);
final input = LinkInput(inputBuilder.json);
expect(input.json, inputJson(hookType: 'link'));