Для быстрого ознакомления:
Что такое Native Assets - объяснение для новичков
История и эволюция
Архитектура системы
Build Hooks - сердце системы
Практические примеры
Продвинутые концепции
Лучшие практики
Troubleshooting и отладка
Экосистема и дальнейшее развитие
Реальные кейсы использования
Производительность и оптимизация
Что такое Native Assets - объяснение для новичков
Простыми словами
Представьте, что у вас есть Dart-программа, и вы хотите использовать готовую библиотеку, написанную на C, C++, Rust или другом языке. Раньше это было сложно - нужно было вручную компилировать библиотеку, следить за тем, чтобы она попала в нужное место, и писать много дополнительного кода.
Native Assets - это система, которая автоматизирует весь этот процесс. Она позволяет вашему Dart-пакету "включать в себя" нативный код и автоматически его компилировать и подключать.
Аналогия из жизни
Это как заказ еды с доставкой:
Раньше: Вы сами покупали продукты, готовили, мыли посуду.
С Native Assets: Вы просто говорите "хочу пиццу", а система сама заказывает, доставляет и даже убирает за собой.
Техническое определение
Native Assets — это официальная система в Dart, которая позволяет пакетам содержать не только Dart-код, но и исходный код на других языках (C, C++, Rust и др.). Система автоматически управляет сборкой этого нативного кода в динамические библиотеки (.so, .dll, .dylib), их упаковкой в приложение и обеспечивает прозрачный доступ к ним из Dart-кода во время выполнения через FFI (Foreign Function Interface).
История и эволюция
До Native Assets (темные времена)
Разработчик хочет использовать C-библиотеку:
Вручную компилирует библиотеку для каждой платформы.
Копирует
.so/.dll/.dylibфайлы в правильные места.Пишет FFI-биндинги.
Молится, чтобы все работало на разных устройствах.
Повторяет все это при каждом обновлении.
С Native Assets (светлое будущее)
Разработчик хочет использовать C-библиотеку:
Добавляет зависимость в
pubspec.yaml.Описывает процесс сборки в
hook/build.dart(один раз).Система автоматически все компилирует и подключает.
Работает везде "из коробки".
Timeline развития
Dart 3.2: Появилась первая версия Native Assets за экспериментальным флагом
--enable-experiment=native-assets.Dart 3.4 (Май 2024): Стабилизация Native Assets. Функция стала доступна по умолчанию, флаг больше не требуется.
Flutter 3.22 (Май 2024): Интеграция Native Assets во Flutter. Система стала официально поддерживаться для сборки приложений на всех платформах.
Архитектура системы
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Dart Package │───▶│ Build Hooks │───▶│ Native Assets │ │ │ │ │ │ │ │ - pubspec.yaml │ │ - hook/build.dart│ │ - .so/.dll/.dylib│ │ - lib/*.dart │ │ │ │ - metadata.json │ │ - src/*.c │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘
Package Structure
my_native_package/ ├── pubspec.yaml # Зависимости и конфигурация пакета ├── lib/ │ └── my_package.dart # Dart API для взаимодействия с нативным кодом ├── src/ │ ├── my_lib.c # Нативный исходный код │ └── my_lib.h # Заголовочные файлы └── hook/ └── build.dart # Скрипт-инструкция по сборке нативного кодаBuild System Flow
dart pub getилиflutter buildобнаруживает наличиеhook/build.dart.Запускает этот скрипт (build hook) в изолированной среде для каждой целевой платформы.
Hook компилирует нативный код, используя системные компиляторы (GCC, Clang, MSVC) или специализированные инструменты (Cargo, CMake).
Hook генерирует метаданные о созданных ассетах (библиотеках).
Система сборки упаковывает эти ассеты вместе с приложением.
Dart-код может использовать FFI для вызова нативных функций, при этом загрузка библиотеки происходит автоматически.
Build Hooks - сердце системы
Что такое Build Hook
Build Hook — это специальный Dart-скрипт (hook/build.dart), который говорит системе сборки:
Какой нативный код нужно скомпилировать.
Как именно его компилировать (какие флаги, зависимости).
Где разместить результат.
Простой пример hook/build.dart (для демонстрации)
Важно: Этот пример использует прямой вызов gcc и не является кроссплатформенным. Он показан только для понимания принципа. В реальных проектах всегда используйте обертки, такие как native_toolchain_c.
// hook/build.dart import 'package:native_assets_cli/native_assets_cli.dart'; import 'dart:io'; void main(List<String> args) async { await build(args, (config, output) async { // Определяем, что мы хотим скомпилировать final packageName = config.packageName; final sourceFile = config.packageRoot.resolve('src/my_lib.c'); final outDir = config.outputDirectory; // Имя библиотеки зависит от платформы String libName; if (config.targetOS == OS.windows) { libName = 'my_lib.dll'; } else if (config.targetOS == OS.macOS) { libName = 'libmy_lib.dylib'; } else { libName = 'libmy_lib.so'; } final outFile = outDir.resolve(libName); // Компилируем C код в динамическую библиотеку final result = await Process.run( 'gcc', [ '-shared', '-fPIC', '-o', outFile.toFilePath(), sourceFile.toFilePath(), ], ); if (result.exitCode != 0) { throw Exception('Compilation failed: ${result.stderr}'); } // Сообщаем системе о созданной библиотеке output.addAsset(NativeCodeAsset( package: packageName, name: 'src/my_lib.c', // Имя ассета должно соответствовать пути к исходнику file: outFile, linkMode: LinkMode.dynamic, os: config.targetOS, architecture: config.targetArchitecture, )); }); }
Продвинутый и рекомендуемый пример с native_toolchain_c
Этот пакет предоставляет удобный CBuilder для кроссплатформенной компиляции.
// hook/build.dart import 'package:native_assets_cli/native_assets_cli.dart'; import 'package:native_toolchain_c/native_toolchain_c.dart'; void main(List<String> args) async { await build(args, (config, output) async { final cbuilder = CBuilder.library( name: 'my_complex_lib', assetName: 'path/to/my_lib.dart', // Путь к Dart файлу, который будет использовать библиотеку sources: [ 'src/core.c', 'src/utils.c', ], includes: [ 'src/include/', ], defines: { 'VERSION': '1.0.0', 'DEBUG': config.buildMode == BuildMode.debug ? '1' : '0', }, ); await cbuilder.run( buildConfig: config, buildOutput: output, // Для отладки можно добавить логгер // logger: Logger()..level = Level.all, ); }); }
Практические примеры
Пример 1: Простая математическая библиотека
Структура проекта
math_native/ ├── pubspec.yaml ├── lib/ │ └── math_native.dart ├── src/ │ ├── math_ops.c │ └── math_ops.h └── hook/ └── build.dart
C-код (src/math_ops.c) (C нет в редакторе хабра в выпадающем списке языков, поставил С++)
// src/math_ops.c #include "math_ops.h" // Простой итеративный Фибоначчи int fibonacci(int n) { if (n <= 1) return n; int a = 0, b = 1, c; for (int i = 2; i <= n; i++) { c = a + b; a = b; b = c; } return b; }
Заголовочный файл (src/math_ops.h)
// src/math_ops.h #ifndef MATH_OPS_H #define MATH_OPS_H // Dart FFI требует явного указания видимости для Windows #if defined(_WIN32) #define API __declspec(dllexport) #else #define API #endif API int fibonacci(int n); #endif
Build Hook (hook/build.dart)
// hook/build.dart // Примечание: для этого кода нужно добавить пакет native_toolchain_rust import 'package.native_assets_cli/native_assets_cli.dart'; import 'package.native_toolchain_rust/native_toolchain_rust.dart'; void main(List<String> args) async { await build(args, (config, output) async { // Создаем сборщик для Rust-библиотеки final rustBuilder = RustBuilder.library( name: 'string_processor', // Имя вашего пакета из файла Cargo.toml // Указываем Dart-файл, который будет использовать эту библиотеку assetName: 'package:имя_вашего_пакета/имя_dart_файла.dart', ); // Запускаем сборку await rustBuilder.run( buildConfig: config, buildOutput: output, ); }); }
Dart API (lib/math_native.dart)
// lib/math_native.dart import 'dart:ffi'; // Аннотация @Native указывает FFI на имя нативной функции. // Система сборки Native Assets автоматически найдет и загрузит // нужную библиотеку, скомпилированную для текущей платформы. @Native<Int32 Function(Int32)>('fibonacci') external int _fibonacci(int n); /// Вычисление числа Фибоначчи с использованием нативной реализации. int fibonacci(int n) => _fibonacci(n);
pubspec.yaml
name: math_native description: Fast native math operations. version: 1.0.0 publish_to: 'none' # Для локального примера environment: sdk: '>=3.4.0 <4.0.0' dependencies: ffi: ^2.1.0 # Пакеты для сборки теперь не нужно указывать, # Dart SDK находит и использует их автоматически. # dev_dependencies: # native_assets_cli: ... # native_toolchain_c: ...
Пример 2: Интеграция с Rust
Rust-код (src/lib.rs)
// src/lib.rs use std::ffi::{CStr, CString}; use std::os::raw::c_char; #[no_mangle] pub extern "C" fn process_string(input: *const c_char) -> *mut c_char { let c_str = unsafe { CStr::from_ptr(input) }; let rust_string = c_str.to_string_lossy(); // Обработка строки в Rust (реверс и верхний регистр) let processed = rust_string.chars().rev().collect::<String>().to_uppercase(); let c_string = CString::new(processed).unwrap(); c_string.into_raw() } #[no_mangle] pub extern "C" fn free_string(ptr: *mut c_char) { if !ptr.is_null() { unsafe { // Восстанавливаем CString из указателя и освобождаем память let _ = CString::from_raw(ptr); }; } }
Cargo.toml
Ini, TOML (TOML нет в редакторе хабра в выпадающем списке языков)
[package] name = "string_processor" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"]
Build Hook для Rust (hook/build.dart)
// hook/build.dart import 'dart:io'; import 'package:native_assets_cli/native_assets_cli.dart'; void main(List<String> args) async { await build(args, (config, output) async { // Вызов Cargo для сборки Rust проекта final result = await Process.run( 'cargo', ['build', '--release'], workingDirectory: config.packageRoot.toFilePath(), ); if (result.exitCode != 0) { throw Exception('Rust build failed: ${result.stderr}'); } // Находим путь к скомпилированной библиотеке // ... логика поиска .dll/.so/.dylib в target/release/ ... // Для простоты примера, опустим эту часть // Добавляем ассет в вывод (в реальном коде нужно найти файл) // output.addAsset(...) }); } // Примечание: для Rust существуют более удобные пакеты-обертки, // такие как `native_toolchain_rust`, которые автоматизируют этот процесс.
Продвинутые концепции
1. Условная компиляция
Build hook может анализировать конфигурацию сборки (config) и включать разные исходники или флаги.
// hook/build.dart void main(List<String> args) async { await build(args, (config, output) async { final sources = <String>['src/core.c']; final defines = <String, String>{}; // Платформо-зависимый код switch (config.targetOS) { case OS.windows: sources.add('src/platform/windows.c'); defines['PLATFORM_WINDOWS'] = '1'; break; case OS.linux: sources.add('src/platform/linux.c'); defines['PLATFORM_LINUX'] = '1'; break; case OS.macOS: sources.add('src/platform/macos.c'); defines['PLATFORM_MACOS'] = '1'; break; default: // Обработка неподдерживаемых платформ } final cbuilder = CBuilder.library( name: 'cross_platform_lib', assetName: 'package:my_package/my_package.dart', sources: sources, defines: defines, ); await cbuilder.run(buildConfig: config, buildOutput: output); }); }
2. Управление зависимостями
В build hook можно скачивать и компилировать сторонние библиотеки.
// hook/build.dart // (Концептуальный пример) void main(List<String> args) async { await build(args, (config, output) async { final depsDir = config.outputDirectory.resolve('deps'); // Скачиваем и распаковываем зависимость (например, libjpeg) if (!await Directory.fromUri(depsDir).exists()) { await downloadAndExtract( url: 'https://example.com/libjpeg.tar.gz', destination: depsDir, ); // Запускаем ./configure && make для сборки зависимости await buildDependency(depsDir); } final cbuilder = CBuilder.library( name: 'my_image_lib', assetName: 'package:my_package/my_package.dart', sources: ['src/image_utils.c'], includes: [ 'src/', depsDir.resolve('include').path, ], // Линкуемся со статически собранной зависимостью libraries: [ depsDir.resolve('lib/libjpeg.a').path, ], ); await cbuilder.run(buildConfig: config, buildOutput: output); }); }
Вспомогательные функции downloadAndExtract и buildDependency должны быть реализованы отдельно.
Лучшие практики
1. Структура проекта
Хорошо организованный проект облегчает поддержку.
my_native_package/ ├── pubspec.yaml ├── README.md ├── lib/ │ ├── my_package.dart # Публичный Dart API │ └── src/ │ ├── bindings.dart # FFI-биндинги (@Native) │ └── exceptions.dart # Кастомные исключения ├── src/ # Нативный код │ ├── core/ │ │ ├── api.h # Публичный C API │ │ └── api.c │ └── platform/ # Платформо-специфичный код └── hook/ └── build.dart # Build hook
2. Обработка ошибок в Build Hooks
Build hook должен быть надежным и предоставлять понятные сообщения об ошибках.
// hook/build.dart void main(List<String> args) async { await build(args, (config, output) async { try { // Проверяем наличие необходимых инструментов (например, CMake) await _ensureToolExists('cmake'); final cbuilder = CBuilder.library(...); await cbuilder.run( buildConfig: config, buildOutput: output, logger: Logger.root, // Включаем логирование ); } on ToolNotFoundException catch (e) { print('Ошибка сборки: ${e.message}'); print('Рекомендация: Установите ${e.toolName} и добавьте в PATH.'); rethrow; } catch (e, stackTrace) { print('Непредвиденная ошибка во время сборки: $e'); print('Стек вызовов: $stackTrace'); rethrow; } }); } // Вспомогательная функция _ensureToolExists и исключение ToolNotFoundException
3. Кроссплатформенная совместимость в Dart
Правильный подход: С Native Assets вам не нужно писать код для загрузки библиотек под разные платформы в Dart. Эта логика полностью находится в hook/build.dart. Dart-код остается чистым и платформо-независимым.
Устаревший подход (НЕ ИСПОЛЬЗОВАТЬ С NATIVE ASSETS):
// НЕПРАВИЛЬНО: Ручная загрузка библиотеки if (Platform.isWindows) { DynamicLibrary.open('my_lib.dll'); } // ... и т.д.
Правильный Dart-код:
// lib/src/bindings.dart import 'dart:ffi'; // Этот код будет работать на Windows, macOS, Linux, Android и iOS // без каких-либо изменений. @Native<Int32 Function(Int32)>('my_function') external int _myFunction(int value); class MyLib { static int myFunction(int value) { try { return _myFunction(value); } catch (e) { // Можно обернуть ошибку FFI в кастомное исключение throw NativeException('Failed to call my_function: $e'); } } } class NativeException implements Exception { final String message; NativeException(this.message); }
4. Тестирование Native Assets
Тесты должны проверять как успешное выполнение, так и обработку ошибок.
// test/native_test.dart import 'package:test/test.dart'; import 'package:math_native/math_native.dart'; // Импортируем наш пакет void main() { group('Native Fibonacci Tests', () { test('should return correct values for base cases', () { expect(fibonacci(0), 0); expect(fibonacci(1), 1); }); test('should calculate fibonacci correctly for a small number', () { expect(fibonacci(10), 55); }); // Можно добавить тесты на производительность test('should execute within reasonable time', () { final stopwatch = Stopwatch()..start(); fibonacci(40); // Достаточно большая нагрузка stopwatch.stop(); expect(stopwatch.elapsedMilliseconds, lessThan(100)); }); }); }
Troubleshooting и отладка
Частые проблемы и решения
Сборка падает с непонятной ошибкой
Решение: Запустите сборку с максимальной детализацией логов:
flutter build <platform> -v. Ищите ошибки от компилятора (Clang, GCC, MSVC) или от вашего build hook.
UnsatisfiedLinkErrorилиLookup failedПричина: Dart FFI не может найти нативную функцию.
Решение:
Проверьте, что имя функции в аннотации
@Native<...>('my_function') точно совпадает с именем в нативном коде.Для C++ убедитесь, что функция обернута в
extern "C".Для Windows убедитесь, что функция экспортируется с помощью
__declspec(dllexport).Проверьте
assetNameвCBuilder.library()вhook/build.dart. Он должен указывать на Dart-файл, где используется@Native.
Build Hook не выполняется
Проверьте:
Файл точно называется
build.dartи находится в папкеhookв корне пакета.Выполните
dart pub getилиflutter cleanиflutter pub get, чтобы система сборки заново обнаружила hook (горячая перезагрузка здесь не работает).
Экосистема и дальнейшее развитие
Текущие возможности (не будущее)
Декларативные биндинги: Аннотация
@Native<...>является основным способом связывания с нативным кодом.Интеграция с Flutter: Native Assets полностью поддерживаются во Flutter для сборки на Android, iOS, Windows, macOS и Linux.
Интеграция с IDE: IDE (VS Code, Android Studio) предоставляют базовую поддержку, включая подсветку синтаксиса и возможность отладки Dart-кода. Прямой переход к нативному коду пока ограничен.
Сторонние инструменты: Сообщество активно развивает пакеты-обертки для популярных систем сборки (
native_toolchain_c,native_toolchain_cmake,native_toolchain_rust), которые значительно упрощают написание build-хуков.
Планируемые улучшения
Генерация биндингов: Инструменты, такие как
package:ffigen, адаптируются для работы с Native Assets, что позволит автоматически генерировать Dart-код из C/C++ заголовочных файлов.Улучшенная отладка: Работа над возможностью бесшовной отладки с переходом между Dart и нативным кодом.
Поддержка WebAssembly (Wasm): Интеграция с Wasm позволит использовать тот же нативный код (скомпилированный в Wasm) в веб-приложениях.
Реальные кейсы использования
Кейс 1: Высокопроизводительная обработка изображений
Проблема: Приложение для обработки фотографий работало медленно из-за операций над пикселями в Dart-коде.
Решение с Native Assets: Вынести ресурсоемкие алгоритмы (фильтры, размытие) в C/C++ и вызывать их через FFI.
Dart-код (c корректным FFI):
// lib/image_processor.dart import 'dart:ffi'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; @Native<Void Function(Pointer<Uint8>, Int32, Int32, Double)>('apply_sepia_filter') external void _applySepiaFilter(Pointer<Uint8> imageData, int width, int height, double intensity); void applySepiaFilter(Uint8List imageData, int width, int height, {double intensity = 1.0}) { // Выделяем память в нативной куче final pointer = calloc<Uint8>(imageData.length); // Копируем данные из Dart-массива в нативную память pointer.asTypedList(imageData.length).setAll(0, imageData); try { // Вызываем быструю нативную функцию _applySepiaFilter(pointer, width, height, intensity); // Копируем результат обратно в Dart-массив imageData.setAll(0, pointer.asTypedList(imageData.length)); } finally { // Обязательно освобождаем память calloc.free(pointer); } }
Нативный код для фильтра apply_sepia_filter пишется на C.
Результат: Обработка изображений становится в теории в 10-15 раз быстрее, но у меня ускорение в пределах от 3 до 8 раз, если кто-то в курсе что я делаю не так - пожалуйста прокоментируйте.
Кейс 2: Интеграция с существующей C++ библиотекой
Проблема: Есть готовая, протестированная C++ библиотека для работы с 3D-геометрией, которую нужно использовать в Dart-приложении.
Решение: Создать тонкую C-обертку (wrapper) для C++ кода и подключить ее через Native Assets.
Обертка C API (src/geometry_wrapper.cpp)
#include "geometry_lib.hpp" // Существующая C++ библиотека // extern "C" отключает C++ name mangling, делая функции видимыми для C FFI extern "C" { API double calculate_distance(Point3D* p1, Point3D* p2) { // Кастуем указатели к C++ типам и вызываем C++ код auto* point1 = reinterpret_cast<geometry::Point3D*>(p1); auto* point2 = reinterpret_cast<geometry::Point3D*>(p2); return geometry::distance(*point1, *point2); } }
Build hook для этого будет компилировать C++ файлы с помощью g++ или clang++.
Производительность и оптимизация
Benchmarking Native Assets
Используйте package:benchmark_harness для сравнения производительности Dart и нативной реализации.
// benchmark/performance_test.dart import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:my_native_package/my_native_package.dart'; // Наш пакет // Benchmark для нативной реализации class NativeBenchmark extends BenchmarkBase { NativeBenchmark() : super('MatrixMultiplication.Native'); @override void run() => nativeMatrixMultiply(); // вызов нативной функции } // Benchmark для Dart-реализации class DartBenchmark extends BenchmarkBase { DartBenchmark() : super('MatrixMultiplication.Dart'); @override void run() => dartMatrixMultiply(); // вызов Dart-функции } void main() { NativeBenchmark().report(); DartBenchmark().report(); }
Оптимизация Build Hooks
Для ускорения сборки и повышения производительности нативного кода:
Используйте флаги оптимизации: В
hook/build.dartдля релизных сборок добавляйте флаги-O3(максимальная оптимизация) и-flto(Link Time Optimization).Кэширование: Реализуйте в build hook логику кэширования. Если исходные файлы не изменились, используйте уже скомпилированные артефакты вместо повторной сборки.
Платформо-специфичные флаги: Используйте флаги, оптимизированные для конкретных архитектур, например
-march=native.Проверьте
assetNameвCBuilder.library()в файлеhook/build.dart. Он должен точно указывать на тот Dart-файл, где используется аннотация@Native. Если вы используете@Nativeв файлеlib/src/bindings.dart, то иassetNameдолжен быть'package:имя_пакета/src/bindings.dart'.
// hook/build.dart // Функция для получения флагов компилятора C List<String> _getOptimizedCFlags(BuildMode buildMode) { if (buildMode == BuildMode.release) { return ['-O3', '-DNDEBUG']; // Оптимизация и отключение assert'ов } return ['-g', '-DDEBUG']; // Флаги для отладки } // Функция для получения флагов компоновщика List<String> _getOptimizedLdFlags(BuildMode buildMode) { if (buildMode == BuildMode.release) { return ['-flto']; // Оптимизация на этапе компоновки } return []; } void main(List<String> args) async { await build(args, (config, output) async { final cbuilder = CBuilder.library( name: 'my_optimized_lib', assetName: 'package:my_package/my_package.dart', sources: ['src/my_lib.c'], // Правильная передача флагов cFlags: _getOptimizedCFlags(config.buildMode), ldFlags: _getOptimizedLdFlags(config.buildMode), ); await cbuilder.run(buildConfig: config, buildOutput: output); }); }
Заключение
Dart Native Assets представляют собой революционное решение для интеграции нативного кода. Эта система решает множество проблем, с которыми сталкивались разработчики при работе с FFI, и является стабильным, мощным инструментом.
Ключевые преимущества
Автоматизация: Полностью автоматизированный процесс компиляции и подключения нативных библиотек.
Кроссплатформенность: Единый build-скрипт для сборки под все целевые платформы (мобильные, десктопные).
Производительность: Прямые вызовы нативны�� функций без накладных расходов, присущих Method Channels.
Простота: Значительно упрощенный процесс разработки и поддержки пакетов с нативным кодом.
На 2025 год система Native Assets является зрелой и рекомендованной для всех задач, требующих высокой производительности или интеграции с существующими C/C++/Rust библиотеками в экосистеме Dart и Flutter.
