Flutter windows dll что это
Google анонсировал поддержку создания приложений для ОС Windows с помощью коссплатформенного фреймворка Flutter.
Изначально Flutter позиционировался как инструмент, с помощью которого можно создавать кроссплатформенные приложения для разных операционных систем, как на мобильных устройствах, так и на десткопах. Тем не менее, до последнего времени с помощью Flutter можно было создавать приложения только для Android и iOS. Теперь же также добавлена возможность создания приложений и под Windows. Кроме того, как отмечают разработчики Flutter, согласно имеющейся у них статистике, более половины разработчиков Flutter используют Windows, поэтому добавление поддержки Windows во Flutter тем более напрашивалось.
Стоит отметить, что поддержка создания приложений под Windows пока находится в альфа-версии, поэтому, если разработчики хотят воспользоваться данной возможностью, они должны переключиться на канал для разработчиков (dev channel) с помощью следующих команд:
$ flutter channel dev $ flutter upgrade $ flutter config — enable-windows-desktop
После этого можно будет создавать приложения под Windows на Flutter c помощью Android Studio или Visual Studio Code или из командной строки.
В итоге после компиляции приложения должен создаваться нативный файл EXE, а также все необходимые сопутствующие библиотеки DLL.
Но естественно, стоит учитывать, что поскольку это альфа-версия, то по всей видимости, данная возможность еще будет дорабатываться и особождаться от потенциальных багов.
Flutter for Desktop — новый подход к разработке приложений

Язык программирования Dart был изначально разработан как перспективная замена JavaScript в веб-приложениях (с поддержкой асинхронности, потоков, классической поддержки ООП и возможностью использования строгой типизации), но, к сожалению, в этом качестве он так и не достиг значительных успехов. Однако в дальнейшем компилятор Dart был доработан для других целевых платформ и наибольшего успеха достиг в сочетании с фреймворком Flutter как инструмент разработки высокопроизводительных мобильных приложений, создаваемых на основе реактивной модели. Но нужно отметить, что наряду с возможностями компиляции в целевые платформы Android и iOS (а также, разумеется, Web), Dart также может использоваться для создания приложений для операционных систем Windows, Linux и MacOS, что в сочетании с возможностями фреймворка Flutter и оптимизированных платформенных реализаций Flutter Engine и Embedder, представило новый путь к созданию нативных приложений с графическим интерфейсом. В этой статье мы рассмотрим возможности и особенности реализации desktop-приложений на Flutter и разберемся с механизмами интеграции внешних библиотек.
Проект на языке Dart представляет из себя один или несколько исходных файлов с расширением .dart (с явным указанием импорта используемых компонентов), файл описания проекта pubspec.yaml (описывает метаданные для компилятора, а также зависимости и используемые ресурсы), а также платформенные компоненты, которые могут быть интегрированы в финальный исполняемый артефакт на этапе сборки проекта. Во время сборки и компиляции исходные тексты проекта и подключенных компонентов, а также платформенные компоненты (при использовании плагинов) объединяются в единый исполняемый образ, который может выполняться как внутри специальной среды выполнения, так и являться полностью автономным исполняемым файлом. Любое приложение, даже если ничего не импортировано явно, интегрирует пакет dart:core, который содержит методы для работы со строками, коллекциями объектов, датой-временем, регулярными выражениями и сетевыми адресами, потоками и асинхронностью, что позволяет использовать базовую функциональность одинаковым образом, независимо от целевой платформы.
Компиляция проекта может происходить в одном из нескольких режимов и выполняется командой dart compile :
- js – компиляция исходных кодов в код на JavaScript для запуска в браузере (например, используется в Flutter for Web или при разработке сайтов с использованием React или AngularDart, либо без использования фреймворка). При компиляции выполняется tree-shaking, в результате которого из кода удаляются все неиспользуемые функции и их зависимости.
- jit-snapshot – создание промежуточного кода для выполнения на конкретной архитектуре (в дальнейшем может быть запущен через команду dart .jit), выполняет тестовый прогон для сохранения состояния памяти и результата just-in-time компиляции для возможности быстрого повторного выполнения.
- aot-snapshot – создание двоичного кода для текущей архитектуры, не включает в себя реализацию среды выполнения. Для запуска снимка можно использовать команду dartaotruntime aot.
- kernel – создание переносимого представления исходного кода (может быть запущено на любой поддерживаемой платформе), может быть в дальнейшем запущено через команду dart .dill
- exe – компиляция в выполняемый файл (включает в себя двоичный код, реализующий логику приложения, а также среду выполнения и связанные библиотеки, необходимые для работы приложения).
Для примера мы создадим простое приложение для вывода информации о зарегистрированных расходах и последовательно будем его дорабатывать и превратим в конечном итоге в полноценное приложение с графическим интерфейсом на Flutter. В качестве целевой платформы мы будем рассматривать Linux, но похожим образом может быть создано приложение и для Windows / MacOS (отличия будут только в способах подключения библиотек и алгоритме сборки финальных распространяемых артефактов). Начнем с простого консольного приложения, которое будет получать информацию о расходах из текстового файла.
import 'dart:io'; import 'dart:convert'; Stream expenses() < return File("expenses.csv").openRead().map(utf8.decode).transform(LineSplitter()).map((l) =>double.tryParse(l)); > void main() < print("Expenses for period:"); expenses().listen((a) < print("*$a"); >); >
Создадим файл с величинами расходов (expenses.csv). Затем выполним компиляцию в исполняемый файл и проверим корректность работы скомпилированного приложения:
dart compile exe expenses.dart ./expenses.exe *100.1 *10.99 *100.5
Заменим извлечение строк из файла на получение информации через сеть, для этого будем использовать возможности пакета http (поддерживается как на мобильных платформах, так и для Web и desktop-приложений).
import 'dart:io'; import 'dart:convert'; import 'package:http/http.dart' as http; Stream expenses() async* < final client = http.Client(); final expenses = await client.get(Uri.parse('https://raw.githubusercontent.com/dzolotov/flutter-linux/main/expenses.csv')); for (final v in LineSplitter().convert(expenses.body).map((l) =>double.tryParse(l))) < yield v; >; > void main() async < print("Expenses for month:"); (await expenses()).listen((a) < print("*$a"); >); >
Мы должны увидеть в консоли список строк со значениями расходов, извлеченных из указанного сетевого расположения.
Следующим этапом добавим поддержку графического интерфейса в нативном приложении. На этапе мы попробуем реализовать поддержку графического интерфейса без использования Flutter, это возможно через связывание разрабатываемого приложения с библиотеками GTK. Dart представляет возможность обращаться к внешним загружаемым библиотекам (so/dll) через поддержку Foreign Function Interface (пакет dart:ffi). FFI представляет набор классов для описания типов данных C и указателей, а также способы определения внешних функций (NativeFunction), управления памятью (Allocator) и предоставляет механизмы для вызова функций Dart из внешней библиотеки на C (NativeApi). Также возможно загружать динамическую библиотеку (.so / .dll) и использовать экспортированные символы через конструкторы класса DynamicLibrary.
Для подключения библиотеки GTK мы будем использовать экспериментальные биндинги из проекта https://github.com/Kleak/gtk
apt-get install llvm-dev libclang1 libclang-cpp-dev clang-dev libclang1-dev dart pub get dart pub run ffigen:setup -I/usr/lib/llvm-13/include -L/usr/lib/llvm-13/lib dart compile exe example/counter.dart
после успешной сборки можно запустить example/counter.exe и получить gtk-вариант приложения со счетчиком.
Если возникает ошибка при запуске, нужно создать символическую ссылку на gtk (sudo ln -s /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 /usr/lib/x86_64-linux-gnu/libgtk-3.so) , либо изменить путь к динамической библиотеки в gtk/lib/src/init.dart .
По аналогии можно было бы создать и наше приложения учета финансов, но в действительности задача не выглядит очень простой. Как минимум нужно будет реализовать подключения к функциям динамической библиотеки, поскольку в существующих биндингах их набор ограничен. Так, например, для создания контейнера необходимо создать определения типов и использовать механизмы для обнаружения экспорта динамической библиотеки, например так:
typedef gtk_container_new_func = Pointer Function(); typedef GtkContainerNew = Pointer Function(); Pointer gtkContainerNew() < final f = gtk.lookupFunction('gtk_container_new'); return f(); >
Для создания графических приложений более удобным способом будет применение фреймворка Flutter, который использует Dart как основную технологию разработки и предоставляет удобные механизмы связывания с графическими библиотеками для нативных платформ (на Linux используется GTK). При этом фреймворк реализует функциональность визуальной компоновки и отслеживания изменений дерева виджетов и позволяет создавать приложения в реактивном стиле с возможностью декларативного связывания конфигурации интерфейса и состояния (которое может быть связано с виджетами, либо храниться отдельно и распространяться с использованием подписки на изменения).
Поскольку на текущий момент поддержка Linux и MacOS находится в стадии эксперимента, ее необходимо явным образом разрешать. Для настройки дополнительных целевых платформ будем использовать команду flutter config —enable-linux-desktop (или flutter config —enable-macos-desktop ). Для корректной сборки также необходимо установить зависимости для компиляции:
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev
Теперь создадим новый проект приложения flutter create –t app expenses
Перейдем в каталог проекта и убедимся, что среди каталогов есть linux (или macos, в зависимости от выбранной целевой платформы). Точкой входа в приложение на Flutter, как и для любого приложения на Dart, является функция main, по умолчанию расположенная в файле lib/main.dart.
Запуск приложения начинается с вызова функции runApp (экспортирован из пакета material/widgets.dart, либо платформенных material.dart / cupertino.dart), которому передается экземпляр корневого виджета. В большинстве случаях для корневого используются виджеты MaterialApp (или CupertionApp для iOS), которые создают необходимый контекст приложения, регистрируют навигацию и тему оформления, а также отвечают за корректную локализацию и иные аспекты взаимодействия с платформой.
Запустим наше приложение: flutter run –d linux. Результатом выполнения будет демонстрационное приложение в стиле Material Design с кнопкой и счетчиком нажатий (запущенное в виде отдельного окна). Как можно увидеть, заголовок окна повторяет название приложения, что не всегда совпадает с ожиданиями. Кроме того нет возможности изменить размеры окна при запуске. Давайте исправим это и добавим в наше приложение плагин window_manager, для этого необходимо в секцию dependencies в pubspec.yaml вписать название плагина и его версию ( window_manager: ^0.2.1 ) и установить необходимые зависимости ( flutter pub get )
Теперь мы можем изменить заголовок и конфигурацию окна до его создания (до запуска runApp). Для этого необходимо убедиться, что необходимый контекст выполнения был инициализирован и окно отображено:
import 'package:window_manager/window_manager.dart'; void main() async < WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); windowManager.waitUntilReadyToShow().then((_) async < // Hide window title bar await windowManager.setTitleBarStyle(TitleBarStyle.normal); await windowManager.setTitle("Expenses Tracker"); await windowManager.setSize(Size(400, 400)); await windowManager.center(); await windowManager.show(); >); runApp(const MyApp()); >
Также можно подписаться на события жизненного цикла окна для отслеживания закрытия, уменьшения и увеличения размера, потери и возвращении фокуса, для этого к состоянию корневого виджета нужно добавить mixin WindowListener.
class _ExpensesState extends State with WindowListener
Реализуем вывод полученных данных из сети в виде списка в окне и добавим кнопку для регистрации нового значения расхода. Для этого заменим тип результата и будем создавать ожидаемое значение (Future), вместо потока, чтобы можно было идентифицировать состояние ожидания (пока идет загрузка).
Future> expenses() async < final client = http.Client(); final expenses = await client.get(Uri.parse('https://raw.githubusercontent.com/dzolotov/flutter-linux/main/expenses.csv')); return LineSplitter().convert(expenses.body).map((l) =>(double.tryParse(l) ?? 0.0)); > class _ExtensesState extends State < @override Widget build(BuildContext context) < return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: FutureBuilder>(future: expenses(), builder: (context, snapshot) < if (snapshot.hasData) < return Column( mainAxisAlignment: MainAxisAlignment.center, children: snapshot.requireData.map( (d) =>Text(d.toString())).toList() ); > else if (snapshot.hasError) < return const Text('Error'); >else < return const CircularProgressIndicator(); >>), ), floatingActionButton: FloatingActionButton( onPressed: () <>, tooltip: 'Add expenses', child: const Icon(Icons.add), ), ); > >
Разработка графических приложений для Desktop ничем принципиально не отличается от создания мобильных приложений на Flutter (нужно только убедиться, что используемые плагины поддерживают конкретную целевую платформу). Важно задать граничные значения для размера окна (через плагины window_manager или window_size ), чтобы сохранять верстку, а также использовать возможности определения размеров контейнеров (например, через LayoutBuilder) для создания адаптивной верстки. Дополнительно можно отключить отображение всплывающих подсказок при наведении, для этого часть дерева может быть обернута в виджет TooltipVisibility со значением visible в false.
Важным аспектом разработки приложений, ориентированных на запуск в Windows/Linux/MacOS является регистрация комбинаций клавиш и их связывание с действиями. Одним из вариантов может быть виджет RawKeyboardListener (определяет события нажатия и отпускания клавиши) или более высокоуровневый FocusableActionDetector, который связывает между собой LogicalKeySet и Intent (в shortcuts), а также Intent и функцию (в actions).
Также для desktop-приложений возможно получать доступ к нативным API операционной системы через ffi (аналогично тому, как ранее мы рассматривали подключение к gtk), для большинства задач существуют готовые плагины (например, win32 для доступа к Win32 API, win32_registry для получения доступа к реестру Windows, win32_gamepad для подключения к геймпаду, posix для доступа к POSIX API на всех операционных системах и др.)
Частый сценарий в desktop-приложениях – необходимость отправить информацию (например, отчет о расходах) на печать или в PDF-документ. Здесь может быть полезной библиотека printing, которая работает на всех платформах и может создавать форматированные PDF-документы. Документация и примеры использования библиотеки могут быть найдены на официальной странице.
Последний вопрос, который мы разберем сегодня – сборка приложения в устанавливаемый артефакт. Алгоритм сборки зависит от выбранной платформы и пошагово описан в официальной документации, мы рассмотрим только сборку приложения в snap для установки на Linux с использованием snapd.
Для сборки snap будет необходимо установить инструментальную поддержку:
snap install snapcraft —classic snap install multipass —classic
Опционально можно установить поддержку сборку с использованием контейнеризации на основе lxd:
snap install lxd sudo lxd init (оставим все ответы по умолчанию)
Создадим файл описания приложения snapcraft.yml
name: expenses version: 0.0.1 summary: Expenses Tracker description: Take control on your expenses! confinement: strict base: core18 grade: stable slots: dbus-expenses: interface: dbus bus: session name: tech.dzolotov.expenses apps: expenses: command: expenses extensions: [flutter-master] # здесь можно поставить экспериментальную ветку plugs: - network slots: - dbus-expenses parts: expenses: source: . plugin: flutter flutter-target: lib/main.dart # файл, содержащий точку входа (функцию main)
Создадим файл с описанием ярлыка в файле /snap/gui/expenses.desktop
[Desktop Entry] Name=Expenses Comment=Take control on your expenses Exec=expenses Icon=$/meta/gui/expenses.png Terminal=false Type=Application Categories=Education;
И также нужно добавить пиктограмму (в том же расположении expenses.png)
Теперь можно выполнить сборку:
snapcraft (для использования виртуальной машины через multipass) или snapcraft —lxd (для использования контейнеризации lxd)
Разработанное приложение может быть загружено (для этого необходимо зарегистрироваться на snapcraft.io, затем войти в учетную запись snapcraft login , зарегистрировать приложение snapcraft register и загрузить snap-файл через snapcraft upload —release=track expenses.snap )
Локально установить приложение можно из созданного snap-файла:
sudo snap install expenses_0.0.1_amd64.snap --dangerous
После чего можно его запустить через /snap/bin/expenses (или через созданный ярлык, зарегистрированный в графической оболочке Linux).
Таким образом мы разработали простой прототип, который может быть доработан с использованием всех доступных возможностей Flutter Framework и библиотек, доступных на pub.dev, что предоставляет качественно новые возможности создания адаптивных пользовательских интерфейсов (с использованием реактивной модели), которое также может использовать существующие библиотеки и компоненты бизнес-логики.
Все исходные тексты приложения размещены в GitHub: ссылка на GitHub.
Как протестировать приложение с информацией из сети? Об этом расскажу уже завтра на бесплатном открытом уроке. В рамках урока мы разберемся как создать тесты для сетевых приложений на Flutter и проверим работу простого клиента для отображения мероприятий из публичного API на всех уровнях (модульные тесты, тесты виджетов, интеграционные тесты). Созданные тесты будут интегрированы в единый сценарий сборки в конвейере CI.
Flutter и настольные приложения

Непрерывный прогресс в области компьютерных технологий Ни для кого не секрет, что миссия разработчиков Flutter заключается в том, чтобы ориентироваться на широкий спектр устройств, включая iOS, Android, Windows, Linux, macOS и веб-сайты из единой кодовой базы, с нативной компиляцией и визуальным представлением на уровне качества игр. В Google Flutter используются проекты от Assistant до Stadia, от Cloud Search до Blogger. За пределами Google, Flutter был принят компаниями от ByteDance до Grab, от Nubank до MGM Resorts, которые извлекают выгоду из производительности и гибкости Flutter. Многих из вас интересует прогресс в настольных операционных системах, включая Windows, macOS и Linux. В опросах и на GitHub, десктоп постоянно является одной из самых популярных новых функций. В ближайшие недели разработчики собираются показать больше своей работы, и начнут с обзора некоторых работ различных групп, которые вносят свой вклад в общий проект. Пока поддержка настольных систем является техническим предварительным обзором, работы впереди предстоит ещё много.
Режим выпуска (Release Mode)
Недавно разработчики проверили профиль и режим выпуска для Windows и Linux, в дополнение к существующей поддержке macOS. Например, если вы используете последние сборки Flutter, теперь вы можете скомпилировать приложение Flutter в исполняемый файл Windows с flutter build windows. Он использует производственный компилятор AOT для создания собственного машинного кода x64, который можно распространять на машину без установленного Flutter.
Десктоп
Независимо от того, создаете ли вы автономные исполняемые файлы или веб-приложения, у вас есть уникальные атрибуты для создания приложений настольных компьютеров. Окна рабочего стола обычно находятся в ландшафтном режиме и имеют изменяемый размер, ввод обычно осуществляется с физической клавиатуры и мыши, а не с экранной клавиатуры и касания, и элементы управления оптимизированы для другой плотности экрана. На уровне инфраструктуры разработчики внесли различные изменения во Flutter для поддержки взаимодействия с настольными компьютерами. • При создании нового проекта в последних сборках вы увидите, что шаблон по умолчанию теперь содержит ссылку на свойство visualDensity, которое позволяет элементам управления адаптировать их плотность в зависимости от платформы на которую они нацелены с более компактным интервалом на настольных платформах. Примером того, как это используется, является TextField, который теперь предлагает компактный, удобный и стандартный интервал в зависимости от заданной плотности. • Разработчики добавили гораздо лучшую поддержку для ввода с помощью мыши и клавиатуры — это включает в себя необработанные коды клавиш в Windows, щелчок правой кнопкой мыши, изменения курсора и поддержку колеса прокрутки. • Теперь вы можете запрашивать конкретную платформу (через класс Platform) и Windows, macOS и Linux предоставляют соответствующие результаты. • В самом последнем выпуске разработчики добавили виджет NavigationRail, который специально разработан для настольных компьютеров и планшетов.
FFI
Команда Dart усердно работала над полировкой интерфейса внешних функций (FFI), который является отличным средством повышения скорости интеграции с платформой. Для API на основе библиотеки dart:ffi предоставляется прямой механизм привязки к нативному коду. Среда выполнения Dart предоставляет возможность выделить память в куче (heap), которая поддерживается объектом Dart, и выполнять вызовы динамически связанных библиотек. Для простого примера из Windows, вот фрагмент кода для вызова традиционного API Win32 MessageBox()полностью с кодом Dart:
typedef MessageBoxNative = Int32 Function( IntPtr hWnd, Pointer lpText, Pointer lpCaption, Int32 uType); typedef MessageBoxDart = int Function( int hWnd, Pointer lpText, Pointer lpCaption, int uType); final user32 = DynamicLibrary.open('user32.dll'); final win32MessageBox = user32.lookupFunction('MessageBoxW'); void showMessageBox(String message, String caption) => win32MessageBox( 0, // No owner window Utf16.toUtf16(message), // Message Utf16.toUtf16(caption), // Window title 0 // OK button only ); … showMessageBox('Test Message', 'Window Caption'); // call just like any other Dart function
Вызов Win32 MessageBox API из кода Dart. В этом коде разработчики предоставляют typedef, который представляют сигнатуру метода как в его нативном, так и в Dart-представлении. Определив их, можно загрузить библиотеку динамической компоновки Windows (DLL), которая обеспечивает реализацию функции через метод lookupFunction(), который отображает сигнатуру функции Dart на базовую собственную точку входа. Наконец, необязательно добавляем простую идиоматическую оболочку, чтобы сделать ее легко доступной из другого кода Dart, что приводит к чему-то вроде этого:
Простой пример приложения для Windows, использующий Win32 MessageBox APIКонечно, вам не нужно делать эту работу самостоятельно, есть вероятность, что кто-то уже проложил путь к API, который вы хотите использовать.
Обновление модели плагина
Сам по себе Flutter имеет небольшое ядро. Вместо того, чтобы нести лишнюю заботу о базовой платформе, плагины и пакеты (как непосредственно от команды Flutter, так и от более широкой экосистемы участников) обеспечивают интеграцию с базовыми операционными системами. Однако, поскольку Flutter все больше поддерживает мобильные устройства, веб-сайты и настольные системы, разработка плагина для каждой поддерживаемой платформы становится все более сложной задачей. Скорее всего, для плагина потребуются вклады разных авторов, имеющих опыт работы с каждой платформой. Полезный метод - определить общий интерфейс, как часть основного модуля, который каждая платформа может реализовать независимо. Разработчики недавно адаптировали схему для плагинов, чтобы упростить интеграцию разработки платформ с несколькими авторами. В рамках этой работы теперь можно явно объявить, какие платформы поддерживаются плагином. Также разработчики начали создавать некоторые из основных плагинов, используя эту модель, и вы найдете несколько ранних примеров интегрированной модели в репозитории flutter/plugins. Обратите внимание, что API плагинов для Windows и Linux все еще находятся в движении, поэтому, хотя разработчики поощряют исследование, они не готовы к поддержке общего назначения в настоящее время. Также они работают над добавлением тегов платформы Desktop на pub.dev.
Работает на Windows: Win32 и UWP
Один интересный аспект работы, которую разработчики выполняют в Windows - это эксперименты с различными архитектурными подходами. На любой платформе Flutter встроен в небольшое приложение хост-контейнера (“embedder”), используя аналогичный подход к игровым движкам, таким как Unity. Этот встроенный в платформу механизм внедрения обеспечивает точку входа, координирует с базовой операционной системой доступ к таким службам, как поверхности рендеринга, доступность и ввод, и управляет циклом событий сообщения. Windows предлагает два разных подхода для создания этого устройства для внедрения. Во-первых, зрелая модель программирования Win32 может использоваться для создания точки входа для содержимого Flutter - это обеспечивает максимальную обратную совместимость с платформами, такими как Windows 7 и создает стандартный EXE-файл, который ожидают многие разработчики. И наоборот, современная модель приложения UWP является рекомендуемым подходом для Windows 10 и предлагает интригующие возможности для расширения поддержки Flutter для таких устройств, как Xbox или будущая операционная система Windows 10X. Разработчики неофициально работают с разными участниками, чтобы исследовать различные решения и с удовольствием поддерживают тесное сотрудничество с Microsoft для создания высококачественного решения. Они полагают, что семейство устройств Surface, включающее в себя Android и Windows, предлагает Microsoft замечательную платформу для создания прекрасных нативных приложений, охватывающих весь их портфолио.
Весело с рабочим столом
Эта работа остается в техническом обзоре, а API и инструменты еще не стабильны. Разработчики по-прежнему отслеживают большую часть работы, которую хотят выполнить до того, как перейти к стабильной поддержке настольных компьютеров, включая улучшенную поддержку доступности и локализации. Если вы хотите попробовать это, вам нужно быть на канале разработки. Windows и Linux доступны только в ветке master, где происходит активная разработка Flutter. macOS доступен в ветке dev, которая немного более стабильна, но не рекомендуется для производственного использования. Вы можете переключать каналы с помощью flutter channel master или flutter channel dev, а затем использовать одну из следующих команд, чтобы включить поддержку используемой платформы: C:\flutter> flutter config --enable-windows-desktop$ flutter config --enable-macos-desktop$ flutter config --enable-linux-desktop Разработчики уже увидели, как некоторые авантюрные разработчики начинают использовать Flutter на рабочем столе для создания приложений. Одним из первых настольных приложений Flutter macOS, которое они увидели, является Sharezone. Студент-планировщик, ориентированный на образовательный рынок Германии, который начинал его как мобильное приложение, но недавно добавил веб-версии и версии для настольных компьютеров. Sharezone Schulplan - приложение для студентов, преподавателей и родителей, чтобы отслеживать домашние задания, учебные группы и расписание.
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.
- Почему я занимаюсь программированием
- JavaScript - Как извлечь параметр из URL
- Обзор бесплатного видеоредактора VSDC Free Video Editor
По статье задано0 вопрос(ов)
Дружим Flutter с С# и С++
На Flutter`е очень удобно и хорошо пишутся интерфейсы для пользователей. Но использовать Dart для решения алгоритмических задач тяжко и неэффективно. Семейство Си справляется гораздо лучше и позволяет легко распараллеливать вычисления. Кроме того, за многолетнюю историю С++ и С# обзавелись множеством полезных библиотек, не все из которых имеют аналоги во Flutter.
Зная про существование библиотеки FFI для Flutter, что позволяет даже синхронно запускать Си-шный код, я решил закопаться в эту тему и попробовать объединить наследие Си и их эффективность с удобным фреймворком. Учитывая то, что в интернете маловато информации про использование FFI, особенно с C#, я решил поделиться своим опытом "построения мостов" на примере двух приложений в этой статье.

Немного теории
FFI - это foreign function interface, или же интерфейс внешних функций. Это механизм, позволяющий программе на одном языке вызывать функции, написанные на другом. В случае с Flutter-ом, это возможность вызывать из Dart функции Си из скомпилированных библиотек. Функции, что можно вызвать из вне обязательно перед компиляцией должны быть помечены как внешние и являться статическими.
Библиотека FFI позволяет работать с внешними функциями на всех популярных платформах. Но в этой статье я проверял работу библиотеки только для Windows, однако, не думаю, что на других платформах должны быть из ряда вон выходящие осложнения.
Работа с симуляцией на С++
Моя первая курсовая работа была написана на плюсах и выводила результаты в виде bmp файлов. Когда же подошло время второй курсовой, я понял, что явно нужно сделать хоть какой-то интерфейс, отличный от проводника Windows. Переписывать тысячи строк кода такое себе удовольствие. Кроме того, Dart - всё ещё однопоточный язык, а распараллелить вычисления так же легко, как с openMP, не получится. Так что было решено создать программу по такому образу: С++ выполняет шаги симуляции, хранит в себе все данные и состояние, а Flutter поддерживает симуляцию и получает состояние симуляции, отображая его на интерфейсе.
Благо, есть документация о том, как создать ffi плагин. В ней можно легко найти команду:
flutter create --template=plugin_ffi --platforms=android,ios,linux,macos,windows ffigen_app
В созданном плагине даже сразу есть example, который можно изучить, чтобы понять, как с точки зрения кода работает вызов функции.
Однако здесь же меня ждала первая проблема - код Dart-а в этом шаблоне написан с использование ffigen. Кодогенерация всегда хорошо, но было болезненно осознавать, что она работает только для чистого Си. Так что пришлось самостоятельно записывать каждый вызов функций, а также править сгенерированный CMake файл.
С последним достаточно легко. Необходимо вписать все необходимые файлы с кодом (например, main.cpp можно не включать), указать заголовочный файл, а также подключить необходимые пакеты (например, openMP, чтобы работали его директивы). Вот пример из моего проекта.
Перед тем, как начинать работу с функциями, необходимо настроить само подключение к библиотеке. Для этого достаточно написать путь к ней и динамически открыть её с помощью DynamicLibrary.open(pathToLib/libName.dll) . Именно из этого объекта с помощью метода lookup мы будем вызывать функции при помощи их названия на стороне С++.
Но и этого недостаточно, ибо только простые типы данных можно передавать между языками. То есть аргументы функции и возвращаемый тип в обоих языках обязан быть простым. Для передачи структурированных данных, необходимо воспользоваться указателями и структурами. В Dart необходимо создать класс, наследованный от Struct , а также делать его аналог в коде С++, после чего работать с этим классом через Dart класс Pointer . Причём, обязательно нужно сохранять порядок полей структур в обоих языках:
Пример структуры в Dart:
final class IntArray extends Struct < external Pointerdata; @Int32() external int length; >
Пример этой же структуры в С++:
typedef struct < int *data; int length; >IntArray;
Здесь показан пример создания массива. Другие структуры создаются аналогично. Как пример, структура модели локации, что используется в симуляции. Так как я не хотел бы в бизнес-логике приложения работать с классом Pointer, то для каждой структуры пишем класс без указателей.
Стоит упомянуть, что указатель в Dart создаётся намного более проблематично, нежели в С++. Для создания необходимо подключить уже не только библиотеку dart:ffi , но и пакет package:ffi/ffi.dart , для того, чтобы воспользоваться классом Arena , с помощью которого можно выделять память:
- Создаём объект для выделения памяти
Arena arena = Arena(); - Создаём нужные указатели, например, таким образом:
Pointer townsPriority = arena.allocate(length * sizeInt); - После того, как созданные указатели перестали быть нам нужны, очищаем память:
arena.releaseAll();
Теперь, когда есть все необходимые структуры данных и подключение к библиотеке, настало время писать глобальные внешние функции и функции, что вызывают внешние функции.
На стороне С++ я создал отдельный файлик FlutterAdapter.cpp, который хранит всё для работы с внешними функциями. Из необычного в нём только FFI_PLUGIN_EXPORT перед теми функциями, которые являются внешними. Аналогично FFI_PLUGIN_EXPORT нужно написать и в заголовочном файле перед объявлением функций. Кроме того, в нём ещё необходимо сделать два действия. Во-первых, добавить в начало:
#if _WIN32 #define FFI_PLUGIN_EXPORT __declspec(dllexport) #else #define FFI_PLUGIN_EXPORT #endif
А во-вторых, обернуть все объявления в extern "C" <> . Это ключевое слово необходимо для сохранности кода при переводе С++ и Си. Однако, его добавление может вызвать проблемы из-за того, например, отсутствия поддержки template у функций.
Согласен с мыслью, что легче один раз увидеть, чем пытаться прочитать теорию, так что для стороны Dart-а я просто покажу примеры: вот пример кода с получением указателя, а вот пример кода с передачей аргументов в С++.
Но и это ещё не всё! Так как Dart однопоточный, то при вызове длительной функции на стороне С++ интерфейс на Flutter-е "прикажет долго жить" и зависнет. Для того, чтобы этого не происходило, необходимо настроить работу с отдельным изолятом. Это достаточно много сложного кода, который необходимо было повторять для каждой такой функции (причём эту работу должен был делать ffigen), но у меня получилось при помощи парочки костылей настроить вызов изолята для любой функции. Подобные вызовы функции выглядят таким образом:
Future makeStep(int stepCount) async < return await executeInIsolate( _makeStepForIsolate, ); > void _makeStepForIsolate(Map args) < final execute = lookup>('execute') .asFunction(); return execute(args['stepCount']); >
Всё выглядит так костыльно из-за того, что в SendPort можно передавать только верхнеуровневые функции. Для этого отдельно и объявляется _makeStepForIsolate. Аргументы записываются в Map для того, чтобы сохранить возможность передать абсолютно любую функцию внутрь executeInIsolate . Ждём макросы в Dart.
Для тех, кто хочет разобраться, каким же образом создаётся изолят и выполняются функции в отдельном потоке, вот в этом файле я оставил достаточно много комментариев.
Перед подведением итогов хочу ещё сказать о том, что было достаточно проблематично писать этот проект из-за проблем с дебагом. Писал код Flutter-а в VScode и иногда через него же делал правки в код плюсов. Во-первых, при компиляции и запуске программы ошибки C++ выводятся в debug console, но на другой кодировке, из-за чего большинство ошибок мне приходилось гуглить по её номеру, ибо текст ошибки представлял собой кучу вопросов. Скорее всего это из-за русского языка. Настройки CMake в vs-code не помогли. Во-вторых, я заметил проблему, что при создании функции с 20-ю и более аргументами программа завершается без вывода ошибки.
Подведём итог. Для построения мостов между языками необходимо: настроить CMake, на стороне С++ пометить внешние функции и скомпилировать код в dll, на Dart подключиться к этой библиотеке и вызвать функцию по названию, при необходимости создать на двух языках одинаковые структуры и передавать указатели на них, и при тяжеловесности функции создаём отдельный изолят для вызова.
Документооборот на C#
Была задача для десктопа: оптимизировать заполнение определённых типов документов, чтобы их можно было делать не через Office, а через удобную специальную программу. Конечно же, я сразу вспомнил C#, ибо пару раз подобное уже писал на нём. Но желание экспериментировать с FFI заставило меня пересмотреть мой выбор в сторону сразу двух языков. Да и, кроме того, на момент создания программы для Flutter не было нормальной библиотеки для работы с Word, а заказчик хотел иметь возможность редактировать и Word и Excel.
Настрадавшись с прошлым проектом, было решено в этом минимизировать точки соприкосновения двух языков. Как следствие этого - программа имеет лишь две внешние функции, аргументы которой являются json-ами. При вызове метода makeFile в одном большом json передаётся информация о всех необходимых структурах. Очень удобно, хоть, конечно, нужно понимать, что это чуточку мешает производительности. Кроме того, чтобы не было проблем с форматом обязательно нужно воспользоваться json.encode на стороне Dart-а и не запутаться в кодировках. Напомню, что Utf8 не поддерживает кириллицу. Так что подготовка аргументов на стороне Dart выглядит вот так:
final json = json.encode(data.toJson()); final pointer = json.toNativeUtf16(); final result = dartFunc(pointer);
Так что на сей раз абсолютно вся работа с FFI на стороне Dart поместилась в один файлик.
Если в прошлый раз сразу же создавался простой CMake файл, в котором нужно было заполнить парочку строчек, чтобы всё работало по нажатию одной кнопки, то на сей раз у меня была только минимальная информация из интернета. К сожалению, мои поиски ни к чему не привели. Однако, это и не особо нужно, ведь всю работу по компиляции можно выполнить при помощи обычного скрипта.
Касательно создания самого dll файла. .NET Core поддерживает AOT компиляцию, как и Dart, но только с помощью определённого компилятора и парочки команд dotnet. Все шаги были проделаны на .net 6.0. Не могу точно сказать, работают ли команды на более высокой версии. Буду рад, если в комментариях кто-нибудь напишет, получилось ли на более высокой.
Для начала я создал обычное консольное приложение C# через VS. Далее через консоль добавляем необходимый компилятор командой:
dotnet add package Microsoft.DotNet.ILCompiler -v 7.0.0-*
Теперь проект готов к тому, чтобы его наполнили кодом и библиотеками. В этой программе для работы с excel использовал epplus, что подключил через nuget. Внешние функции также выделяем в отдельный файлик, делаем статическими и помечаем атрибутом, который хранит в себе имя, которое мы будем искать через lookup :
Пользуемся Marshal.PtrToStringUni , помня о кодировке и используя встроенную десериализацию, чтобы получить готовые к работе модели. Как раз из-за json формата передачи появилась проблема с неймингом полей передаваемых моделей, так как стили в Dart и C# разные. Я решил, что пусть уж C# потерпит это нарушение, так что названия полей сделал точно как в Dart, то есть с маленькой буквы:
public string name
Тестируем программу в режиме консольного приложения, но для компиляции обязательно меняем тип проекта на библиотеку классов. Чтобы получить dll файл билдим проект и пишем в консоль:
dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r win-x64 -c release(или debug)
Спустя достаточно долгое время работы компилятора в папке bin/Release/net.6.0/win-x64/publish появился нужны dll файл. Внутри него сразу включены все нужные ему библиотеки, так что достаточно воспользоваться только им.
Подведём итог. На сей раз достаточно лишь: создать проект и подключить компилятор, пометить внешние функции, скомпилить dll и перенести в нужное место, в Dart подключиться к библиотеке и передать в вызов функции json.
Заключение
По итогу у меня получились два приложения, с разными алгоритмическими языками и с разными подходами к построению связи с Flutter-ом. В целом, полученных на практике знаний вполне достаточно для того, чтобы без страха в следующий раз подключать код семейста Си в проект.
Плюсами такого подхода считаю увеличение производительности и расширение пула библиотек, ибо теперь помимо pub у нас есть все библиотеки С++ и C#.
К минусам я могу отнести повышенный вес приложения, увеличение времени компиляции проекта и сложность отладки.



