Взлом с помощью Dart

Dart REPL позволяет вам оценивать выражения и утверждения Dart в интерактивной оболочке. Прошло некоторое время с момента моего первого поста о Dart REPL (вам не нужно его читать, чтобы насладиться этим постом), а многие функции все еще отсутствуют. В частности, были бы очень полезны динамический импорт и поддержка объявлений верхнего уровня, поэтому давайте посмотрим, как их поддерживать.

Заявление об ограничении ответственности: я работаю в Google, но этот пост посвящен личному проекту. Я не в команде Dart и не связан с ней. Эта статья содержит только мое скромное личное мнение.

tl; dr: код и инструкции по запуску Dart REPL можно найти по адресу https://github.com/BlackHC/dart_repl.

Горячая перезагрузка

Для Flutter в виртуальную машину Dart добавлена ​​крутая новая функция: горячая перезагрузка. Есть занимательный видеоролик YouTube с Dart DevSummit, который объясняет и подробно демонстрирует его:

Горячая перезагрузка позволяет изменять код во время работы программы. Виртуальная машина Dart подберет внесенные вами изменения и попытается применить их, сохраняя при этом все работоспособность. А если нет, он скажет вам, почему. Это очень круто! В духе взлома Dart на великие дела, давайте подумаем, как мы можем использовать это для реализации новых функций.

Почему мы не можем импортировать новые библиотеки в текущую версию REPL?

REPL использует службу виртуальной машины Dart для оценки выражений. К сожалению, импорт библиотеки не является выражением в Dart, поэтому мы не можем просто оценить его в этом контексте. Однако мы можем изменить код песочницы REPL во время ее работы, чтобы импортировать новую библиотеку, а затем мы могли бы просто запустить горячую перезагрузку для обновления REPL. Это работает? На самом деле это \ o /

Но подождите: еще одна вещь, которую мы не можем сделать, оценивая выражения через службу виртуальной машины, - это создание новых классов и функций. Фактически, по этой причине ни одно из объявлений верхнего уровня Dart не может быть выполнено путем вычисления выражения.

Как мы можем разрешить объявления верхнего уровня?

Конечно, мы могли бы использовать ту же идею, что описана выше, для добавления нового класса или глобальной функции. Однако любой, кто какое-то время использовал IPython или аналогичный, знает, что вы склонны часто повторно объявлять один и тот же класс или функцию при повторении кода. Вы повторно выполняете слегка измененные версии одного и того же кода снова и снова, пока играете с ним.

Если бы мы просто добавили эти объявления в нашу библиотеку Dart песочницы, это потребовало бы от нас отслеживания того, что было объявлено, когда и где в файле, чтобы обновлять объявление, когда вы повторяете его. Это требует много логики и умного кода. К сожалению, он также легко сломался бы, если бы изменение класса нарушило совместимость со старым кодом или другими объявлениями. Это предотвратит горячую перезагрузку REPL и заставит пользователя перезапустить его :( Звучит сложно и хрупко: я думаю, не выигрышная комбинация!

Шип и цепи

Вместо этого, что, если бы мы могли переопределять одно и то же объявление верхнего уровня несколько раз без столкновения повторных объявлений? Это вообще возможно в Dart? Вы держите пари, что это так! Но не в той же библиотеке :) Dart позволяет вам импортировать библиотеку, а затем объявить класс, функцию или глобальную переменную, которая затеняет существующее объявление.

В этом примере не будет претензий к версии b.dart MyClass затенения a.dart, потому что они находятся в разных библиотеках, а локальное объявление в b.dart имеет приоритет над объявлением, импортированным из a.dart.

В общем, затенение - это то, что происходит, когда вы объявляете переменную, которая скрывает другую переменную из внешней области видимости. Например:

Можем ли мы использовать это? Чтобы исследовать это, я реализовал быстрый спайк здесь. Он не генерирует никакого кода. Скорее, это очень глупый пример, чтобы убедиться, что то, что, по нашему мнению, будет работать, действительно работает. Было бы неприятно потратить много времени на реализацию этого с помощью генерации кода только для того, чтобы обнаружить, что это могло никогда не сработать! В этом суть:

Это действительно работает! Мы можем создать цепочку библиотек, которые импортируют друг друга (а также экспортируют друг друга, потому что в противном случае символы будут доступны не везде). Затем пользователи могут переопределять символы так часто, как захотят. Очевидно, это может привести к тому, что старый код будет ссылаться на затененные символы, что может немного сбить с толку, но, по крайней мере, он не сломается. И любой, кто использовал IPython или аналогичный, тоже научился с этим жить. Это не может быть так плохо.

На диаграмме выше показано, как это работает: по мере добавления новых объявлений верхнего уровня создаются новые «ячейки» (библиотеки Dart), которые импортируют (и экспортируют) предыдущую ячейку. Последняя ячейка импортируется в библиотеку песочницы, которая используется в качестве среды выполнения для обычных выражений и операторов Dart. Файл песочницы редактируется на месте, а затем перезагружается с помощью горячей перезагрузки.

Видение рабочего процесса

Более того, если вы хотите постоянно обновлять код, ничего не затеняя, это тоже возможно: горячая перезагрузка уже позволяет использовать этот рабочий процесс в обычных программах Dart. Вы можете сделать то же самое в REPL. Вы можете редактировать свою замечательную библиотеку Dart amazing_dart_library.dart и импортировать ее в REPL, поэкспериментировать с ней, и пока вы это сделаете, вы можете редактировать код в выбранном вами редакторе и перезагрузить код REPL, когда захотите, вызвав reload(). Лучшее из обоих миров \ o /

Как это реализовать на практике?

Что ж, здесь мы взламываем Dart, так что давайте посмотрим: горячая перезагрузка еще не поддерживается vm_service_client, поскольку это такая новая функция, а спецификация службы еще не полностью завершена. Я начал писать запрос на вытягивание для Натали (сопровождающей), чтобы добавить для него поддержку, но на самом деле, как знают мои коллеги: производственный качественный код - это не мое, особенно в свободное время (извините, Натали!). Однако это не мешает нашему хакерскому приключению.

Pub, система управления пакетами Dart, не только поддерживает автоматическое разрешение ограничений версии и централизованное хранилище пакетов, но также позволяет использовать локальные пакеты или напрямую зависеть от GitHub. Обычно это не рекомендуется, потому что вы теряете большую часть того, что делает паб отличным, но здесь это работает: я просто разветвил vm_service_client в свой собственный клон GitHub и внес необходимые изменения. Вы можете найти код на странице https://github.com/BlackHC/vm_service_client/tree/reload_sources_poc. Впоследствии я изменил pubspec.yaml Dart REPL, чтобы он ссылался на мой клон GitHub вместо официальной версии:

И это все! Simplepub getin в терминале теперь обновляет Dart REPL для использования разветвленной версии.

Это позволяет действительно легко экспериментировать с чем угодно: вы можете разветвлять другие пакеты, чтобы попробовать что-то, и легко зависеть от них. И что самое интересное, я могу опубликовать это, и когда вы загрузите REPL для себя с помощью pub, он также получит код с GitHub. Очень легко взломать, но тем не менее можно поделиться! (Даже если это не рекомендуется для производственных пакетов в целом :)

Основная часть сочной логики - это генератор ячеек, который реализует очень простой механизм шаблонов в соответствии с тем, что мы обсуждали выше:

Функция горячей перезагрузки вызывается из REPL, когда требуется новый импорт:

Вот и все! Вы можете просмотреть все изменения в запросе на перенос: https://github.com/BlackHC/dart_repl/pull/2.
Я признаю, что код немного взломан и неаккуратен. Существует также довольно много несвязанного кода оболочки для передачи сообщений между REPL и песочницей в запросе на перенос. К сожалению, это немного запутывает основные изменения. Мне нужно посмотреть, как мы можем реорганизовать все это, чтобы снова сделать его более аккуратным и аккуратным ... но иногда просто быстрее наладить работу, чем писать лучший код и запросы на вытягивание. Извини за это!

Исходный код Dart REPL можно найти по адресу https://github.com/BlackHC/dart_repl. Помимо поддержки объявлений верхнего уровня, я также добавил поддержку встроенныхimport, loadPackage и reload команд. (Обратите внимание: для loadPackage требуются готовые к выпуску версии 1.24 для разработчиков Dart SDK. В противном случае это запрещено.) Все эти встроенные команды представляют собой тривиальные расширения, которые используют горячую перезагрузку. Наконец, чтобы загрузить новые пакеты из вашего локального кеша паба, я использую отличный пакет pub_cache.

Чтобы попробовать (и если вы установили Dart SDK), просто запустите:

pub global activate dart_repl
pub global run dart_repl

Спасибо, что дочитали статью до конца! Пожалуйста, дайте мне знать, что вы думаете :)

Здравствуйте,
Андреас