Макросы Haxe. Автоматическое встраивание ресурсов (assets embedding) 2.

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

Первым делом, мы попробуем автоматизировать макрос для автодополнения. Автодополнение выполняет все макросы, чтобы получить точные данные о всех полях класса. Но в случае автодополнения часть операций макроса можно опустить. Макрос может сам узнать, запущена сейчас компиляция или просто автодополнение:

Context.defined("display")

Context.defined принимает один параметр, который может быть передан компилятору при помощи -D define_name или задан самим компилятором автоматически. Например для js сборки Context.defined(“js”) вернет true. Context.defined("display") вернет true, когда выполняется автодополнение.

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

var display = Context.defined("display");

var data = display ? null : File.getContent(file);

Видно, что в случае автодополнения, не будет читаться содержимое файла ассета. Это дало мне примерно 0.1 секунды прироста во времени работы автодополнения в случае 4 ассетов. Время замерял с помощью директивы --times, переданной компилятору. А если замерять отдельно выполнение самого макроса (-D macro_times), то макрос с оптимизацией ускорился в 10 раз, с 0.2 секунды, до 0.02! Выводы делайте сами.

Мы уже сделали немало, но давайте пойдем дальше. А дальше мы немного углубимся в macro reification. Для начала начнем с нового синтаксиса:

var complexType = macro : flash.display.BitmapData;

Обратите внимание на двоеточие после macro. Теперь complexType хранит в себе один из консрукторов enum-а ComplexType (haxe.macro.Expr), в данном случае TPath(p:TypePath):

TPath({ name => BitmapData, pack => [flash,display], params => [] })

“Ага!” - скажете вы, - “Да это же почти то же, что мы делали “руками” в методе getKind.” Ну так давайте использовать это. Для начала, правда, сделаем дополнительную функцию для вычисления комплексного типа, которая пригодится дальше и изменим метод getKind:

// комплексный тип
static function getComplexType(type:AssetType):ComplexType {
        return switch (type) {
                case AImage: macro : flash.display.BitmapData;
                case ASound: macro : flash.media.Sound;
        }
}

Ну а теперь вычислим базовый тип вида TDClass( ?superClass : TypePath, ?interfaces : Array, ?isInterface : Bool ). Обратите внимание, тут снова TypePath, как и у TPath:

// Базовый тип
static function getKind(type:ComplexType):TypeDefKind {
        return switch (type) {
                case TPath(p): TDClass(p);
                default:
                        Context.error("can't find asset type", Context.currentPos());
                        null;
        }
}

Надеюсь, в этом методе все понятно. Мы берем комплексный тип, предполагаем, что он TPath, считываем из него TypePath и оборачиваем в TDClass. Все остальные случаи невозможны, т.к. мы точно знаем какие комплексные типы будут использованы, но на всякий случай я добавил вывод ошибки - Context.error, который выведет ошибку так, будто она была сделана на месте вызова макроса в коде. В данном случае ошибка будет не очень понятна и удобна, а вот когда макрос вызывается в коде, то это бывает очень удобно.

Не скажу, что новый getKind стал сильно красивее предыдущего, но мне он такой больше нравится. Меньше страшных и пугающих фигурных скобок и enum-ов, но все равно мы от них не избавились и не сможем.

Поехали дальше. Попробуем заменить:

{ expr :EConst(CString("data:" + data)), pos :filePos }

на “реифицированный” вариант:

macro $v{"data:" + data}

Ура, работает! На самом деле странно, т.к. у нас Position самодельный, а в macro нельзя указать position и он берет Context.currentPos(). Но, т.к. position учитывается только при дебаге, а мы вряд ли будем дебажить ассеты, то проблем не будет. А кто не понял, что за $v{} в коде, обратитесь к коду первой статьи и дополнительным методам, о которых я писал во второй статье.

Вернемся теперь к методу getComplexType. Я ведь говорил, что он нам еще пригодится. Улучшим наше автодополнение ещё раз. Кто запускал прошлый пример, тот видел, что старое автодополнение напротив атрибутов Assets писало тип Unknown<0>, теперь же настало время вписать туда более внятный тип:

var ct = getComplexType(type);

kind : FVar(ct, { expr : ENew( { pack : ["assets"], name : getPrefix(type) + name, params : [] }, getArgs(type)), pos : pos } ),

Наблюдательный читатель заметит, что если раньше первый параметр у FVar был null, то теперь он равен комплексному типу. И теперь автодополнение сразу знает тип переменных и подскажет их нам:

Самое время отказаться от префиксов в названиях переменных, правда для классов я решил их все таки оставить и сделать все немного красивее:

От префиксов, как видно, я не отказался полностью, а оставил их там, где без них не удалось бы получить валидный идентификатор (snd1, snd2). Можно, конечно, еще проверять на уникальность, но пока мы работаем с одним каталогом, файловая система делает это за нас.

Еще можно заинлайнить утилитарные методы, такие как getMetaName или getComplexType, что еще немного ускорит работу макроса.

Напоследок, я собирался сделать встраивание шрифтов, но не тут то было. Закралась ошибочка, с которой я не смог справится. Но Haxe не был бы Haxe-ом, если бы мы не могли решить задачу иначе.

Допустим, что у вас шрифты встроены уже в готовую флешку, если это не так, то можно взять hxswfml и быстренько это сделать. Да и вообще, давайте уже сделаем встраивание флешек автоматически, без этих долгих -swf-lib в hxml файле. И на этот раз это займет всего три строчки кода:

if (type == ASWF) {
        Compiler.addNativeLib(file);
        continue;
}

Ну, или четыре, если считать скобки. Compiler.addNativeLib - новый для вас метод, который встраивает swf или swc, точно так же, если бы мы написали -swf-lib file. Класс haxe.macro.Compiler вообще очень полезный и интересный, стоит заглянуть в него и почитать документацию по нему.

Осталось встроить текстовые файлы, и на этом, думаю, хватит. С текстовыми файлами вообще просто. Создаем переменные типа String и сохраняем в них содержимое файла:

if (type == AText) {
        res.push( {
                name : varName(type, p.file),
                access : [APublic, AStatic],
                doc : 'file: "$file"',
                kind : FVar(ct, macro $v{data} ),
                pos : pos,
        });
        continue;
}

Я практически скопировал код встраивания изображений или звуков, за исключением того, что содержимое переменной заданно иначе - macro $v{data}. Да и поле doc заполнил, на самом деле я сделал это везде, что добавило еще больше информации в автодополнение:

Теперь точно все. Сегодня мы больше всего времени уделили автодополнению и это понятно. Макросы были бы обычными шаблонами, если бы не столь тесная работа с компилятором. Еще мы оптимизировали работу макроса и добавили встраивание текстовых и swf(swc) файлов. Все это делает из нашего пробного макроса вполне юзабельный инструмент для работы над flash проектами.

Исходный код всего урока лежит на гитхабе. На самом деле, я многое изменил и доработал, о чем я не писал в статье по ряду причин, так что взгляните на весь макрос в целом.

Отдельная благодарность Александру Хохлову и Александру Кузьменко за помощью в написании статьи.