Пока вы рождались, ходили в школу, заканчивали учебу и выходили на свою первую работу, на свете существовал совершенно особенный набор компиляторов, о котором крайне мало известно на просторах РФ.
Именно о нем пойдет сегодняшний рассказ.
Amsterdam Compiler Kit
Врядли среди читателей обнаружится аксакал живой пользователь этого удивительного проекта:
The Amsterdam Compiler Kit is a venerable piece of software that dates back to the early 1980s. It was originally written by Andrew Tanenbaum and Ceriel Jacobs as a commercial product; for many years it was also used as Minix’ native toolchain. After eventually failing as a commercial project, it was made open source under a BSD license in 2003 when it looked like it was going to be abandoned and the code lost.
Сочетание «начало 80х» и «коммерческий продукт» оставляет мало шансов на появление пользователей ACK в родных краях, поскольку в 80е еще вовсю жил СССР и вопрос покупки иностранного программного обеспечения был мягко говоря неактуальным.
Теперь подробнее, что там внутри и почему оно до сих пор шевелится представляет интерес:
The ACK contains compilers for ANSI C, K&R C, Pascal, Modula-2, Occam 1, and a primitive Basic. It contains code generators for a large number of architectures, mostly 8 and 16 bit machines; there are also a set of generic optimisation, linker and librarian tools.
В принципе стандартный набор языков для тех лет, но есть нюанс:
It contains assembler and linker support for: 6500, 6800, 6805, 6809, ARM, i80, Z80, Z8000, i86, i386, 68000, 68020, NS32016, S2650, SPARC, VAX, PDP11 and VideoCore IV.
Это уже несет определенный «вау-эффект», причем как для тех, так и для этих лет, поскольку даже для популярных clang и gcc столь широкая поддержка различных архитектур решается весьма нетривиально - путем форков и неофициальных патчей.
В мейнстриме и готовых пакетах столь дикого набора архитектур разумеется нет, при этом поддержку устаревших архитектур еще и регулярно ломают, а некоторые вообще удаляют.
Современная версия ACK, разрабатываемая с 2003 года как открытый проект, поддерживает следующие платформы:
И ACK дает вам возможность скомпилировать приложение в современном окружении в 2025м году, которое будет работать на этом.
Чтобы у вас не сложилось впечатление, будто ACK это только лишь про плешивых дедов пожилых программистов и их древние игрушки, покажу как выглядит заявленный выше VideoCore IV:
Как видите это уже вполне себе современная плата, используемая в различных устройствах.
Minix
Отдельно стоит упомянуть историю с Minix — той самой операционной системой, созданной тем самым Таненбаумом для обучения нерадивых студентов сложной теме разработки операционных систем.
В почтовой рассылке, посвященной этой ОС когда-то давно некий Линус Торвальдс впервые представил свой известный проект, вызвавший эпический архитектурный срач дискурс, ныне являющийся историческим событием.
Дело в том, что ACK когда-то был основным системным компилятором в Minix, при этом являясь коммерческим продуктом — поставлялся в виде готовых бинарников:
The ACK has been used as the standard Minix compiler for years. While the ACK was still commercial, this was done by distributing binaries; when it get opened, a version was forked off and is now used as part of the Minix base build.
Форк с поддержкой Minix мне был не особо интересен, поэтому искать не стал, тем более что в современной Minix 3 используется вполне стандартный clang.
Однако на поддержке столь широкого набора архитектур возможности ACK не заканчиваются и чтобы добить окончательно нежную психику современных разработчиков, процитирую следующий абзац:
Each language comes with its own runtime, so if you’re a C programmer you also get a libc. Compared to gcc, it is far smaller, faster and easier to port.
Стоило догадаться об этом, прочитав список поддерживаемых архитектур и прикинув как оно вообще может работать, но тем не менее.
Так что ACK это уникальный, редкий и необычный проект, позволяющий творить запредельную дичь вроде кросс-компиляции из FreeBSD в MS-DOS подручными средствами, которую вы могли видеть в шапке статьи.
Ниже я опишу процесс сборки и использования этого необычного проекта.
Сборка
Собирать буду по традиции на FreeBSD 14, поэтому часть требуемых шагов несколько отличается от стандартных.
Проект старый, разработка в git ведется давно, поэтому внутри репозитория присутствует множество разных веток, не актуальных для обывателя.
Чтобы не выкачивать всю эту дичь, я использовал ключ --depth -1, с которым будет выгружена только ветка по-умолчанию:
Таким образом собирать мы будем текущую на момент написания статьи версию:
ACK 6.0 is a ground-up reworking of the whole compiler suite, with a lot of the more archaic features removed.
Сборка проекта.. весьма своеобразна, поскольку основана на скриптах Python и немного Lua. Как гласит описание:
The version 5.0 build mechanism has been completely rewritten (twice).
И видимо это еще не конец.
Для сборки нужен достаточно банальный набор инструментов:
любой ANSI C компилятор (автор использовал GCC)
flex и yacc
GNU make (gmake)
Lua с библиотекой lua-posix
Python 3.4 и выше
~2Гб свободного места
В трекере проекта и пул-реквестах есть сообщения от камрадов, использующих ACK на OpenBSD, так что врядли будут проблемы в куда более популярных Linux, Windows и MacOS.
Запускается сборка стандартным образом — вызовом GNU Make в корне проекта:
gmake
Поскольку автор собирал на FreeBSD, которая имеет определенную специфику в именовании инструментов, появится такая ошибка:
Происходит это из-за того, что lua во FreeBSD имеет постфикс версии:
Так что надо отредактировать Makefile в корне проекта и поменять значение переменной LUA=, добавив версию:
Следующая ошибка также специфична для FreeBSD, поскольку gcc у нас тоже с постфиксом версии:
Несмотря на документацию, которая утверждает что актуальный компилятор должен подхватываться через стандартную переменную окружения CC=, нашлось место в скриптах сборки, где были прямо забиты названия используемых бинарников:
Нужный файл называется ack/build/ab.mk и почему-то несмотря на название и расположение — не является генерируемым.
По аналогии с lua, добавляем постфикс версии и сохраняем:
После этого заново запускаем сборку и ждем, никаких других ошибок при сборке замечено не было.
Итоговый размер после завершения сборки, со всеми временными файлами получился размером в 1.7Гб, что несколько больше заявленного в требованиях:
По-умолчанию ACK устанавливается в каталог /opt/pkg/ack, поэтому запускаем из корня проекта:
mkdir -p /opt/pkg/ack gmake install
Перед установкой будут запущены тесты, но далеко не все:
Итоговый каталог bin выглядит следующим образом:
Хотя основные бинарники находятся в ack/lib/ack:
Теперь переходим к самому интересному — к запуску и работе с ACK, это будет действительно весело.
В репозитории проекта находится каталог examples, где лежат примеры более-менее сложной логики на Си, Паскале и Бейсике, которые точно собираются и работают с помощью ACK.
Один из таких примеров под названием mandelbrot.c , выводящий в консоль с помощью символа * фрактал Мандельброта вы можете лицезреть в работе на заглавной картинке к статье.
Но поскольку мне был интереснее сам процесс компиляции и запуска приложений на разных экзотических архитектурах из древних времен нежели специфика каждой конкретной платформы, не стал заморачиваться сложной логикой, взяв в качестве эталона классический «Hello world!» на Си:
#include <stdio.h>
int main(void) { printf("Hello, alex0x08 \n"); return 0; }
И собственно ниже покажу сборку и запуск этой нестареющей классики под крайне экзотические (по современным меркам) архитектуры.
Компиляция FreeBSD->MS-DOS
Компиляцию в COM-файл с последующим запуском можно увидеть на заглавной картинке, поэтому ниже покажу компиляцию в EXE под DOS:
Поэтому для работы нужен запущенный DPMI-резидент — т. н. «расширитель памяти», который можно взять например тут.
Так это выглядит в записи:
Как видите запуск осуществлялся в известном эмуляторе DOS под названием Dosbox, установленном из пакетов FreeBSD.
Компиляция для CP/M
Продолжая исторический угар, показываю сборку и запуск под CP/M, напоминаю что это операционная система из 1970х (старше автора) а компьютеры, на которых она работала выглядели так:
Kaypro II
Тут надо сделать небольшое отступление и рассказать про эмулятор CP/M, поскольку его в пакетах FreeBSD не нашлось — пришлось собирать руками.
При сборке будет описанная выше проблема с номером версии в названии исполняемых файлов компилятора GCC — стандартная для FreeBSD, поэтому необходимо в файле RunCPM/Makefile.posix в переменную СС= добавить номер версии:
Сама сборка запускается командой:
gmake posix build
Но это еще не все приключения, после сборки необходимо подготовить рабочее пространство — специальный каталог из которого будет запускаться эмулятор:
Круто, но недостаточно, поскольку среди поддерживаемых ACK систем есть:
pdpv7 produces PDP/11 V7 Unix binaries
Думаю вы догадываетесь, что пройти мимо такого было невозможно, поэтому автор убил еще неделю показываю нечто действительно удивительное.
Компиляция для Unix v7 и PDP-11 в 2025м году (!)
Чтение этого абзаца прибавляет 100 баллов к инженерным навыкам.
Напоминаю как выглядел PDP-11:
Вот так выполняется компиляция из ACK для этого древнего монстра:
/opt/pkg/ack/bin/ack -mpdpv7 -O hello.c -o hello
Как ни странно и неожиданно, но стандартная утилита file, присутствующая во всех UNIX-системах с незапамятных времен честно показывает тип:
Apout — Simulate PDP-11 Unix a.out binaries
Для проверки я сначала запустил полученный бинарник на этом:
This program is a user-level simulator for UNIX a.out binaries. Binaries for V1, V2, V5, V6, V7, 2.9BSD and 2.11BSD can be run with this simulator. The user-mode PDP-11 instructions are simulated, and TRAP instructions are emulated by calling equivalent native-mode system calls.
Собирается оно под FreeBSD одной командой, поскольку внешних зависимостей нет:
export CC=gcc13 gmake
После сборки в корне проекта появится бинарник apout, так выглядит в работе запуск нашего «Hello world»:
Но разумеется сильно круче было бы попробовать запустить в реальном симуляторе PDP с Unix v7 на борту, что я и сделал.
SIMH
Эмуляцию PDP как впрочем и множества других исторических систем обеспечивает известный проект Open SIMH. Нужный нам Unix v7 заявлен на главной странице проекта в качестве ключевого примера:
For example Version 7 Unix, released in 1979, runs unchanged today on SimH.
В этот раз эмулятор присутствовал в готовом виде среди пакетов FreeBSD, так что хотя-бы его не пришлось собирать из исходников.
Дальше необходимо создать конфигурационный файл эмулятора:
set cpu u18 set cpu idle attach rl0 unix_v7_rl.dsk attach rl1 hello.tar boot rl0
Сохраните файл как simh-pdp11.ini, в том же самом каталоге v7 , куда был распакован образ диска.
Теперь надо создать tar-файл с собранным бинарником "Hello world" приложения:
cd ~ tar cvf hello.tar hello cp hello.tar ~/v7/
Итоговый набор файлов должен выглядеть как-то так:
Запускаем эмулятор:
pdp11 simh-pdp11.ini
После появления приглашения в виде символа @ вводим:
boot
Появится древний предок современного Grub, загрузчик:
Вводим:
rl(0,0)rl2unix
Появится приглашение в виде символа # , что означает запуск Unix v7 в однопользовательском режиме:
Нажмите Ctrl - D для начала работы в многопользовательском режиме:
Появится хорошо знакомое любому юниксоиду приглашение авторизации. Введите root в качестве логина и пароля:
При первом запуске будет необходимо выполнить ряд дополнительных шагов. Создаем каталог для временных файлов:
mkdir /tmp
Создаем ссылки на устройства:
cd /dev make rl
Таким образом со стороны запущенной в эмуляторе Unix v7 будет доступно устройство, эмулирующее ленту и можно будет добраться наконец до собранного на хосте бинарника:
cd /tmp tar xvf /dev/rrl1
В результате в файловой системе появится тот самый файл hello, собранный на FreeBSD с помощью ACK:
Наконец сам запуск:
Ну кто еще вам спрашивается покажет такую красоту?
Эпилог
ACK имеет отличную портируемость и расширяемость, поэтому существует столь интересный форк этого проекта:
This fork of the Amsterdam Compiler Kit supports the Cray X-MP supercomputer and the COS operating system platform.
Его тоже удалось собрать и запустить, но ввиду невероятной сложности самобытности COS, история будет уже в отдельной статье. Следите за анонсами, как говорится.
Статья была опубликована на Хабре, оригинал как обычно в нашем блоге, все желающие получить обои с автографом (и не умеющие пользоваться нейросетями) должны будут повторить все описанные в статье шаги и прислать скриншот с работающим «Hello <username>» под Unix v7 на PDP, где username — ваш ник.
На относительно простом примере показываю как можно сделать программу «снова великой».
*разумеется это нейросетевая Дженна а не реальная, которая про рефакторинг ничего не знает.
Исходный код отрефакторенной версии выложен на Github.
Задача
Допустим есть некий софт, созданный еще «при царе Горохе» неким гордым но умным одиночкой, которого с тех пор никто не видел. Софт живой и с пользователями, которые приносят прибыль, поэтому его надо как‑то развивать и поддерживать.
В попытке расширить команду разработки, вы начинаете нанимать новых разработчиков, но раз за разом происходит одна и та же ситуация:
проработав месяц-два, нанятые программисты в ужасе убегают в закат.
Кто‑то при увольнении намекает на причины такого поступка, в диапазоне от «ваш проект попахивает» до надо «срочно все переписать». После примерно десятого убежавшего программиста, вы наконец начинаете задумываться, что возможно с проектом действительно что‑то не так и стоит провести этот самый «рефакторинг».
Так это обычно начинается.
Образец
В качестве образца для этой статьи был взят один интересный но малоизвестный широкой публике проект JPC:
The fast x86 PC emulator in 100% pure Java
Самый настоящий эмулятор старого x86-компьютера, реализованный без всяких нативных частей — на чистой Java!
Не очень большой (~6500 файлов с исходным кодом), но имеет стадию «внутренней генерации» — часть исходного кода создана не вручную а путем запуска кодогенератора по метаданным, что довольно часто встречается у больших проектов с историей, причем на любом языке.
Еще к сожалению JPC немного заброшен, что для статьи только в плюс поскольку добавляет реалистичности — именно в таком состоянии чаще всего пребывают проекты, которые просят «привести в чувство».
Текущее состояние
После переезда проекта на Github, список коммитов выглядит следующим образом:
Мягко говоря негусто.
Никаких тестов в проекте нет, по всей видимости тестировалось все вручную с помощью молитвы, еще судя по исходному коду — далеко не весь функционал является рабочим, что также характерно для проектов в «пред‑рефакторинговом» состоянии.
Думаю теперь очевидно почему для этой статьи был выбран именно JPC — несмотря на редкость решаемой задачи, для рефакторинга это самый типичный клиент.
Собирается сей чудо-проект с помощью.. Makefile, что для мира Java является дичью и извращением редкостью:
make application
Примерно как собирать проект на QT с помощью Gradle или (еще лучше) — sbt.
Предложите как-нибудь коллегам и посмотрите на реакцию, некоторые точно перестанут с вами здороваться за руку.
Для сборки необходимо указать путь к JDK в переменной PATH, при этом сборка проходит успешно даже с последними версиями (автор использовал OpenJDK 21).
Готовое приложение JPCApplication.jar появится в корне проекта после завершения сборки, но на этом хорошие новости заканчиваются:
Собранное приложение отказывается запускаться, что мы исправим чуть ниже.
Стадия первая: новый скелет
Первым делом, как и в реальном боевом проекте, необходимо избавиться от любого «самопала», задействованного при сборке. Причина, почему этот шаг критически важен на самом деле не так очевидна:
статические анализаторы — главный иструмент рефакторинга, крепко привязаны к структуре проекта и стандартным средствам сборки
Разумеется существуют варианты и с произвольной структурой проекта, но эффективность рефакторинга будет заметно ниже. Поэтому автор сделал стандартный (для своей практики) «финт ушами»:
перевел сборку проекта на Apache Maven, максимально широко поддерживаемый средствами анализа кода, CI-системами и средами разработки.
Реализовать такую миграцию в данном случае оказалось очень просто, поскольку JPC совсем не использует внешние библиотеки. Все что я сделал — раскидал ресурсы и исходный код в стандартную для Maven структуру каталогов:
Исходный код был перенесен из каталога src в src/main/java, ресурсы — в src/main/resources. Также был добавлен очень простой pom.xml, описывающий минимальные шаги сборки проекта:
Это было убрано, поскольку точно такой же параметр запуска зашит еще и в код:
Наследие «былых времен», которое также достаточно часто встречается в устаревших проектах — во времена Java 1.5 и апплетов было модным использовать собственные атрибуты в манифесте.
Стадия вторая: удаление ненужного
Как в практически любом долгоживущем проекте, в JPC есть свои «внутренние утилиты» — отдельные программы, написанные для задач внутренней автоматизации.
Это та самая «грязная рабочая поверхность», которую не видит конечный пользователь.
При проведении рефакторинга, трогать внутренние утилиты стоит в последнюю очередь и в самом крайнем случае, поскольку правильность их работы проверять тяжело (ни тестов ни документации для внутренних утилит обычно нет в природе), зато они сильно влияют на общую работоспособность проекта.
В JPC внутренние утилиты реализованы в виде отдельных классов в пакете «tools» и нескольких шелл-скриптов в корне проекта.
И то и другое я просто не стал переносить в новую версию, также я убрал часть исходного кода эмулятора, отвечающего за отладку (пакет org.jpc.debugger) — по той же самой причине.
был убран импорт класса org.jpc.debugger.LinearMemoryViewer а используемая статичная функция translateLinearAddressToInt перенесена в класс PC.
Все эти действия позволили сократить кодовую базу проекта практически вдвое, что сильно упростило следующий шаг рефакторинга.
Стадия третья: критические проблемы
Наконец мы подошли непосредственно к самому рефакторингу, который я буду проводить с помощью среды разработки Intellij Idea.
Первый запуск анализатора дает следующий результат:
24 критических ошибки и ~ 37 тысяч предупреждений — не так уж плохо, по сравнению с тем что бывает на свете.
Смотрим глубже и видим, что все 24 ошибки — действительно самые критичные, поскольку из-за них проект может перестать собираться в самом ближайшем будущем:
Так что эти места стоит рефакторить в первую очередь, пока проект хотя-бы собирается из исходников.
Есть и хорошая новость:
Как видно из скриншота выше, большая часть критичных ошибок гнездится в классе JPCApplet, который используется для запуска приложения в режиме Java-апплета — ныне устаревшей технологии, когда-то работавшей с помощью плагина для браузера.
Поскольку плагин более официально не поддерживается — вся технология приказала долго жить и у обычных пользователей не встречается, так что класс можно удалить.
Но все несколько сложнее, поскольку еще есть вложенные классы, один из которых используется снаружи (org.jpc.j2se.JPCApplication):
JPCApplet.PlayPausePanel pp = new JPCApplet.PlayPausePanel(this);
Я просто перенес этот класс по месту использования, что позволило наконец удалить JPCApplet из проекта целиком.
Получилось минус 13 критических ошибок.
Еще один источник проблем — класс LinkBorder также можно удалить, поскольку он использовался лишь из удаленного JPCApplet.
Что дало еще минус три критических ошибки.
Дальше смотрим класс org.jpc.emulator.peripheral.Mixer, который забит предупреждениями от анализатора буквально через каждую строчку, однако вносить массовые правки пока не стоит — «всемогущая» Idea временами ошибается и это именно такой случай.
Анализатор ругается (в первую очередь) на конструктор new Float(), поскольку его прямое использование объявлено устаревшим, а в новых версиях Java стоит использовать Float.valueOf() в качестве замены.
Но как только вы замените конструктор, анализатор подскажет еще несколько оптимизаций, так что конечный вариант будет достаточно сильно отличаться:
Ругается анализатор на уникальный метод stop(), который был отмечен как устаревший еще до того как я начал писать на Java:
'stop()' is deprecated since version 1.2 and marked for removal
Примерно до версии 1.8 использование данного метода еще можно было как‑то оправдать наличием устаревших библиотек, в нынешних реалиях этот метод — просто еще один способ «выстрелить себе в ногу»:
Stopping a thread causes it to unlock all the monitors that it has locked.
Так что в коде использование этого метода точно стоит заменить на стандартный .interrupt() :
if (runner.isAlive()) { runner.interrupt(); }
Блок try-catch также можно спокойно убрать, поскольку SecurityException не выбрасывается в новых версиях Java при попытке остановки нити.
На этом все критические проблемы в проекте решены и получен минимальный практический смысл от всей затеи:
убраны места, которые могут сломать сборку проекта в новых версиях Java
Стадия четвертая: ошибки выполнения
Пришло время наконец попробовать запустить нашего «франкенштейна».
Сборка разумеется завершится успешно (не зря же старались), но при запуске будет выбрасываться все та же ошибка поиска ресурсов:
В оригинальной версии JPC, часть ресурсов (например образы биоса) загружались только из jar‑файла, часть (образы дисков) — только снаружи, из каталога resources, при этом каталог с ресурсами был общим.
Сию дичь необходимо пресечь и сделать в более адекватном стиле, например как это реализовано в движке знаменитого Quake:
сначала ищем внешний файл, если не найден — ищем в ресурсах, если не найден в ресурсах — падаем с ошибкой
За чтение образа BIOS отвечает вот такой метод в классе org.jpc.emulator.motherboard.Bios:
private staticfinalbyte[] getBiosData(String image) throws IOException { InputStream in = Bios.class.getResourceAsStream(image); if (in == null) { thrownew IOException("resource not found: " + image); } try { ByteArrayOutputStream bout = new ByteArrayOutputStream();
while (true) { int ch = in.read(); if (ch < 0) { break; } bout.write((byte) ch); }
В принципе за такую реализацию уже можно начинать бить, спасает лишь факт, что столь идиотсткое побайтовое чтение работает исключительно с ресурсами, которые уже находятся в памяти.
File f = new File(image); if (f.exists() && f.isFile() && f.canRead()) return Files.readAllBytes(f.toPath());
f = new File("resources",image); if (f.exists() && f.isFile() && f.canRead()) return Files.readAllBytes(f.toPath());
final URL u = Bios.class.getResource(image); if (u == null) thrownew IOException("resource (bios) not found: %s".formatted(image));
try (InputStream in = u.openStream()) { return in.readAllBytes(); } }
Логика переделана на возможности современной Java 17, поэтому кода стало сильно меньше, также были добавлены проверки на наличие ресурса:
по полному пути,
по частичному (предполагается что файл находится в каталоге resources),
поиск внутри jar приложения.
Но при следующей попытке запуска получаем еще одно исключение, уже в другом месте:
Причиной является искусственная проверка:
if (!(cl instanceof URLClassLoader)) thrownew IllegalStateException();
Когда-то давно системный загрузчик классов действительно наследовался от URLClassLoader, так что проверка бы отработала.
Несмотря на то, что подобные искусственные проверки служат вообщем‑то хорошей цели раннего обнаружения проблем, временами разработчики перебарщивают и пытаются контролировать то что контролю не поддается.
Однако одним лишь удалением проверки дело не ограничилось — необходимо почистить еще один метод, реализующий «закат солнца вручную»:
if (!dir.equals(thisDir)) continue; resources.add(name); }
jarStream.close(); } catch (IOException e) { e.printStackTrace();} } InputStream stream = context.getResourceAsStream(directory); try { if (stream != null) { Reader r = new InputStreamReader(stream); StringBuilder sb = newStringBuilder(); char[] buffer = newchar[1024]; try { while (true) { int length = r.read(buffer); if (length < 0) { break; } sb.append(buffer, 0, length); } } finally { r.close(); }
for (String s : sb.toString().split("\n")) { if (context.getResource(directory + s) != null) { resources.add(s); } } } } catch (IOException e) { LOGGING.log(Level.INFO, "Exception reading images directory stream", e); }
return resources.iterator(); }
Тут происходит поиск доступных образов дисков путем последовательного перебора всех файлов внутри .jar с приложением.
С учетом того что .class файлов внутри ~6500 — такое решение мягко говоря «не оптимально».
Вообще говоря любой поиск ресурсов через перебор во время работы приложения является медленным, это и есть основная причина медленного запуска любого приложения на (например) Spring Boot.
Поскольку в проекте используется очень небольшое количество образов диска и нет вариантов по резкому увеличению их количества, я просто зашил названия в код:
privatestatic Iterator<String> getResources(String directory) { final List<String> resources = new ArrayList<String> (Arrays.stream(IMAGES).toList()); final File f = new File(directory);
if (!f.exists() || !f.isDirectory()) { return resources.iterator(); }
final File[] files = f.listFiles();
if (files == null) { return resources.iterator(); } for (File ff: files) { resources.add(directory + ff.getName()); }
return resources.iterator(); }
Метод getResources() используется для отображения списка доступных образов дисков через меню приложения, все внутренние образы (зашитые в.jar) добавляются в этот список автоматически.
После столь примитивной правки, приложение стало запускаться визуально быстрее даже на мощном современном ноутбуке, так что не стоит недооценивать силу простых решений ;)
Хотя всех правок выше оказалось недостаточно, следующая остановка — класс org.jpc.support.ArrayBackedSeekableIODevice, который (внезапно) играет ключевую роль в проекте.
Метод configure() отвечает непосредственно за загрузку образов дисков и дискет:
File f = new File(spec); if (f.exists() && f.isFile() && f.canRead()) { imageData = Files.readAllBytes(f.toPath()); length = imageData.length; return; }
f = new File("resources",spec); if (f.exists() && f.isFile() && f.canRead()) { imageData = Files.readAllBytes(f.toPath()); length = imageData.length; return; } final URL u = ArrayBackedSeekableIODevice.class.getResource(spec); if (u == null) thrownew IOException("resource (image) not found: %s" .formatted(spec));
На этой стадии была проведена самая настоящая «коммерческая оптимизация» — доведен до ума функционал актуальный конечным пользователям.
Это уже не стандартные сказки про «технический долг» и «плохую архитектуру», а вполне себе осязаемый результат, который можно потрогать.
Так что вас за такое-то скотство, проведенное с рабочим проектом уже точно не уволят ;)
Стадия пятая: массовые правки
Все описанное выше — обязательные базовые части, без которых рефакторинг вообще не может состояться как согласованный с бизнесом и оплаченный процесс. Но можно зайти дальше — в действительно рисковую зону, где ваши действия могут иметь не всегда предсказуемые последствия:
массовые и сквозные правки исходного кода, во всем проекте целиком
Рабочая область выглядит как-то так:
Собственно все «желтенькое» на скриншоте ниже — места для рефакторинга, заботливо подсказанные средой разработки:
К сожалению на практике все несколько сложнее чем подсказывает Idea и просто нажимать «Alt + Shift + Enter» на каждую подсказку не стоит:
Все потому, что в проекте активно используется Reflection API для загрузки и обращения к методам класса необычными способами:
Что сводит анализаторы исходного кода с ума, поэтому примерно половина методов в проекте десктоп-приложения, не использующего никакие IoC-контейнеры отмечено как неиспользуемые:
Скотство?
Конечно скотство, но и в реальных больших и старых проектах такое тоже будет в обязательном порядке — когда‑то использование Reflection API считалось модным и молодежным явлением, убрать которое "под капот" смогли только те самые IoC‑контейнеры вроде Spring.
Следующим примером кода, нуждающегося в массовой зачистке является использование анонимных классов:
Лямбды появились еще в Java 8 и с тех пор уже нет никакого здравого смысла их игнорировать — они здорово сокращают объем кода:
В любом legacy-проекте, особенно если это приложение для десктопа такого будет очень и очень много:
Следующий повод для массовых правок — прямой результат ручной разработки, без использования средств проверки и анализа кода:
Хороший пример, подсказанный анализатором:
Разумеется это не является критической проблемой, поскольку эти модификаторы ничего не делают, но таких мест очень много и в сумме они дают ненужное увеличение объема кодовой базы.
Следующие две проблемы — также частые гости устаревших проектов:
Точно также как и с ненужными модификаторами в интерфейсе, всего лишь занимают место и увеличивают объем кода.
Хотя пример выше это совсем уж старый код, поскольку метод Arrays.asList () появился еще в Java 7 — былинные времена далекого прошлого, как можно было его сохранить до сих пор — загадка.
Эпилог
Если вы никогда не видели JPC то стоит посмотреть, поскольку он в свое время несколько расширил мнение о возможности Java, в первую очередь в плане производительности — тема о которой много и сильно шутили еще 10 лет назад.
Ну а если перед вами стоит задача провести подобный рефакторинг — стадии с первой по четвертую фактически являются руководством к действию.
Массовые правки я бы с ходу делать не рекомендовал — очень уж высокие риски, что что‑то пойдет не так.
Еще в реальном проекте процесс рефакторинга скорее всего сильно затянется, поэтому вам придется делать промежуточные срезы и синхронизировать ваш рефакторинг с обычной разработкой, о чем стоит помнить до начала всего действа.
Казалось бы, какое отношение «хипстерские скрипты для веб» могут иметь к жестким реалиям встариваемых систем, со всей их низкоуровневой работой и ограниченными ресурсами?
Увы, но реальность в очередной раз оказалась куда интересней предубеждений, так и появилась на свет эта статья.
Картинка для привлечения внимания, была выложена на ЛОР.
Что это и зачем
Начну как обычно с цитаты:
mruby is the lightweight implementation of the Ruby language complying with part of the ISO standard. mruby can be linked and embedded within your application.
Словом, это такая особенная реализация языка Ruby, с упором на встраивание и встраиваемые системы — да да, тот самый «кровавый embedded», где царствует чистый C, ссылочная арифметика, malloc() и прочие кошмары и ужасы для современного разработчика.
И вдруг в этом царстве Аида появляетесь вы весь в белом и пишете что-то такое, высокоуровневое:
extend Yeah::DSL set port: 3000
get '/hi/{name}' do |name| "Привет #{name}" end
ENV['SHELF_ENV'] = 'production'
puts "Запуск.." __main__ [0]
И.. оно просто работает:
Запуск собранного бинарника. Обратите внимание на текст на русском.
И даже вот так:
Ответ сервера в браузере. Обратите внимание на текст на русском.
Круто?
Сколько там пудов соли нужно скушать, чтобы так просто работать с юникодом из чистого С, тем более в embedded среде?
Кстати вся эта «радость хипстера» еще и собирается в очень небольшойбинарник:
2 Мегабайта на все про все.
Внутри будет «все и сразу»:
MRuby, все используемые библиотеки и само приложение.
Хотя на ЛОРе заметили, что «640кб хватит на всех» это как‑то многовато будет, все же напомню что мы живем в мире копеечных 128Гб флешек и битва за каждый байт свободного места уже не так актуальна как 10 лет назад.
Вполне допускаю, что подобное приложение показывает веб‑интерфейс в вашем домашнем роутере, показывает меню в телевизоре или крутит рекламу в автобусе — словом находит применение в большинстве мест, где используются встраиваемые системы.
Поддерживаемые платформы
К сожалению не удалось найти одним списком все поддерживаемые MRuby платформы, поэтому ограничусь только конкретными найденными примерами.
Поскольку «малинки» в очередной раз под рукой не оказалось, было решено реализовать тестовый проект на банальном x86 — была собрана вся цепочка разработки, включая фреймворки, был реализован «Hello world» в виде веб‑приложения, работающего на встроенном веб‑сервере.
Все манипуляции производились на неподдерживаемой никем и нигде FreeBSD, так что скорее всего описанных ниже проблем со сборкой в более обычном Linux не будет.
Для тестового проекта использовалось вот это «чудо»:
Если кратко, то это своеобразная попытка реализовать «мини‑Rails, работающий на мини‑Ruby». Вполне себе успешная, надо отметить.
А теперь самое важное:
Фреймворки для mruby представляют собой надстройку, которая в процессе собирает сам mruby и добавляет себя в собираемые бинарники.
Звучит сложно и выглядит страшно, но для embedded-среды является привычным делом.
Так что нам будет нужно получить бинарники mruby и mrbc с упакованным внутрь фреймворком yeah и всеми библиотеками, а затем использовать этот билд для сборки уже своего приложения.
Для сборки фреймворка Yeah! требуется внешний «большой» Ruby и rake, будут работать как 2.x так и 3.x версии.
Поскольку по‑умолчанию собирается только компилятор mirbc, без интерактивной консоли (mirb) и интерпретатора (mruby), чего не хватит для нормальной разработки конечного приложения, добавляем в файл build_config.rb:
Эта команда автоматически скачает зависимые репозитории, в том числе нужную ветку самого mruby. На данной стадии у автора появлялись две ошибки.
Первая — про заголовочный файл mingw.h:
In file included from /opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/memory.c:34: /opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/mman.h:15:10: fatal error: _mingw.h: No such file or directory 15 | #include <_mingw.h> | ^~~~~~~~~~ compilation terminated. rake aborted!
В файле mman.h есть вот такая строка:
/* All the headers include this file. */ #ifndef _MSC_VER #include <_mingw.h> #endif
Переменная _MSC_VER не задается при сборке на FreeBSD, поэтому срабатывает вариант по умолчанию — для Windows и MinGW. В качестве исправления, я просто закомментировал этот блок, не заморачиваясь дальнейшими изысканиями.
Вторая ошибка также достаточно банальна и происходит из-за разницы в реализации функции mmap:
/opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/mman.h:52:9: error: conflicting types for 'mmap'; have 'void *(void *, size_t, int, int, int, OffsetType)' {aka 'void *(void *, long unsigned int, int, int, int, unsigned int)'} 52 | void* mmap(void *addr, size_t len, int prot, int flags, int fildes, OffsetType off); | ^~~~ In file included from /opt/work/tmp/mruby-yeah/mruby/build/repos/host/mruby-r3/r3/src/memory.c:27: /usr/include/stdio.h:444:10: note: previous declaration of 'mmap' with type 'void *(void *, size_t, int, int, int, __off_t)' {aka 'void *(void *, long unsigned int, int, int, int, long int)'} 444 | void *mmap(void *, size_t, int, int, int, __off_t); | ^~~~ rake aborted!
В этом же файле mman.h заменяем:
void* mmap(void*addr, size_t len, int prot, int flags, int fildes, OffsetType off);
на:
void* mmap(void*addr, size_t len, int prot, int flags, int fildes, __off_t);
И повторно запускаем сборку.
Если сборка прошла успешно то в папке build/host/bin будут готовые бинарники:
ls ./mruby/build/host/bin/ mirb mrbc mruby Теперь с их помощью запускаем сборку уже нашего тестового приложения:
Если все пройдет успешно, в текущей папке появится финальный бинарник test_program, который я запускал в самом начале.
Эпилог
Думаю изложенного материала хватит для того чтобы те из читателей, кто занимается встраиваемыми системами попробовали MRuby для своих задач, благо автору данная штука видится крайне перспективной.
Также как и правительству Японии, которая этот проект финансирует.
Просто потому что убирает целый класс проблем, связанных с разработкой прикладных систем на чистом Си — управление памятью, юникод, строки и так далее.
С нетерпением жду отзывов о реальном использовании.
Рассказываю как мы сделали самые крутые визитки на Диком Западе в отечественной ИТ-индустрии.
Внимание на код - он полностью рабочий!
Все началось когда автор наткнулся на одну интересную статью где эксперт по 3D-технологиям вместил специально оптимизированный и обфусцированный код рейтрейсера на C++ в размеры своей визитки.
Мы позеленели от зависти тоже захотели себе что-то такое, но поскольку занимаемся все же серверами а не 3D-графикой и больше Java, чем C++ — решили что будет круто уместить на обратной стороне нашей визитки простейший HTTP-сервер на Java.
Вместе с запуском и компиляцией.
Еще при наличии графического окружения будет запущен браузер.
Плюс немного криптографии для защиты от подделки.
Весь код уместился в 18 строк, выровненных по ширине так чтобы влезть в размеры визитки:
Вбиваете код с визитки в любимый редактор, сохраняете файл как vcard.sh и запускаете:
Локально запустится простейший HTTP-сервер, который отдаст текстовую страничку с нашими контактами. При наличии GUI — запустится еще и браузер по-умолчанию, с автоматическим открытием страницы этого сервера.
И все это в 18 строк кода.
Да, еще будет нужен любой Linux/BSD/MacOS/Solaris и любая версия JDK начиная с 1.8 на машине.
Поддержку запуска на Windows делать не стал (хотя это и возможно технически), но можно спокойно запустить в WSL .
Чтобы вы не мучились с вводом кода с картинки, вот текстовая версия:
Тут используется связка из заголовочного shell-скрипта и слегка обфусцированного кода на Java. Еще я не стал кодировать весь блок на Java полностью в HEX-строку, чтобы визуально оставалось ощущение исходного кода.
Это shebang, стандартное для Unix указание на используемый интерпретатор, про него и так все знают. Дальше происходит создание временного каталога в /tmp и присваивание его имени переменной в скрипте:
t=$(mktemp -d);
Затем получение скриптом собственного имени с полным путем:
e=$(realpath $0);
Чтение скриптом самого себя, с отрезанием первых 4х строк - чтобы получить блок кода на Java:
sed '1,4d' $0
Дальше начинается pipe, в котором результат предыдущей команды передается на вход следующей:
Результат всех преобразований записывается в файл Yo.java, в том самом временном каталоге.
Малоизвестная опция -XDignore.symbol.file отключает предупреждение об использовании системных классов JDK (com.sun.net.httpserver.*) в проекте — в 1.8 версии классы встроенного в JDK HTTP-сервера еще считались системными.
Запуск с передачей полного пути оригинального скрипта для последующего его чтения из Java-кода:
java -cp . Yo $e
Сам код после деобфускации и форматирования выглядит уже вот так:
Тут уже большая часть логики вполне очевидна, поэтому раскрою лишь два самых сложных фрагмента.
Криптография
Когда я только начинал думать над реализацией этой штуки, уже было ясно что нужен какой-то неочевидный контроль целостности:
исходный код очевидно будут пересылать через сообщения, в виде постов или по почте, что легко его сломает.
Поэтому хотелось хоть какую-то защиту от подделки содержимого, чтобы компьютерные дети не добавили патч Брамина в самое интересное место, а индийский паренек не подменил авторство и мои контакты на свои, ради строчки в резюме.
Задачу усложнял факт передачи открытых исходников и ограничение по размерам, но видимо получилось:
static String ED = "1AtzGU0uq7J7DHPdjdJJ5JJDiwQi8mElIDOjuRK0DEU=";
На каждую попытку как-то подменить содержимое (включая заголовок) будет выдаваться вот такая ошибка:
Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. at java.base/com.sun.crypto.provider.CipherCore.unpad(CipherCore.java:981) at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1062) at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853) at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202) at Yo.main(Yo.java:6)
Получается код сам себя защищает от подделки.
0x7f000001
Вторым неочевидным моментом является вот такой странный адрес хоста:
String h = "0x7f000001";
Который используется при формировании ссылки для открытия браузером:
Desktop.getDesktop().browse(new URI("http://" + h + ":" + p));
Такое применение однозначно говорит о том что адрес очень даже стандартный, поскольку проходит как стадию валидации на стороне Java при формировании объекта URI, так и валидацию на стороне запускаемого браузера.
Это просто нотация, вариант написания IP-адреса 127.0.0.1, обозначающего loopback (петлю) — внутренний интерфейс, к которому можно подключиться локально, а не из сети.
Вот тут больше примеров различных вариантов написания IP-адресов, уверен — удивит даже бывалых админов.
P.S.
Статья была опубликована на Хабре, более фривольный оригинал статьи находится в нашем блоге, где мы подробно рассказываем об ужасах разработки, вгоняя в краску даже опытных и бывалых.
Буднично рассказываю как локализовать обычное корпоративное приложение на нечеловеческие языки: Клингонский и Р’льех.
На этом скриншоте куда больше реального приложения чем кажется на первый взгляд.
Эээ.. думаю стоит начать с демонстрации результата — той самой нереальной локализации, ради которой все это и затевалось, чтобы всем сразу "все стало понятно".
Так выглядит версия на клингонском:
Обратите внимание на даты — это настоящий Stardate.
А вот так выглядит версия на Р'льех:
«Cthulhu fhtagn!» на JSF, CDI и JPA. Сложно сказать какая часть предложения напугает сильнее.
Ну и наконец банальный английский:
Вот так выглядит в работе переключение локализации:
Да, это самое обычное веб-приложение на Java, работающее в обычном браузере.
Но только с локализацией на клингонский и Р'льех.
Матчасть
Чтобы вы смогли оценить сложность задачи «локализации на язык которого нет», стоит для начала рассказать как происходит обычная локализация — на обычные человеческие языки.
Возьмем для примера классику в виде русско‑английской локализации, вот что необходимо реализовать в этом случае:
Определение текущей локали
Переключение локали
Хранение локализованных строк
Отображение локализованных данных
Данный функционал подразумевается как минимальный, когда речь заходит о локализации ПО, причем большая часть всей этой логики уже реализована в любом современном инструментарии и все что нужно сделать для поддерживаемых языков — «включить и использовать».
Вот так например выглядит хранение локализованных строк:
Это абсолютно стандартный способ, поддерживаемый как самим JDK так и всем прикладным ПО на Java
Также легко и просто оперировать обычным человеческим языком со стороны прикладного кода, например вот так выглядит получение локали из кодового названия:
Locale locale = Locale.forLanguageTag("en_US");
Где en — это указание на английский а US — на страну США.
Не менее легко происходит и переключение между языками (в данном случае в Jakarta Faces):
Но вся эта благодать быстро заканчивается, стоит только выйти за границу реальности поддерживаемых локалей и попытаться использовать «то чего нет».
Язык которого нет
Символы несуществующих фантастических языков предсказуемо отсутствуют в официальной таблице символов Unicode, их нет в списке поддерживаемых средствами разработки и нет в браузере.
Что означает невозможность какой-либо работы «из коробки» с таким языком — без специальных шагов.
Но прежде хотелось бы немного рассказать о самих фантастических языках, выбранных для локализации — чтобы у вас появилось некоторое представление куда может завести фанатизм и любовь к хардкору.
В мире где настоящие человеческие языки отмирают по сотне в день по мере ухода из жизни последних носителей, кто-то специально учит вымышленный!
Поскольку большинство фанатов клингонского — самые разнообразные гики, хорошо дружащие с техникой и матчастью, было и есть множество попыток протащить вымышленный язык куда только можно.
Например в ядро Linux:
In September 1997, Michael Everson made a proposal for encoding KLI pIqaD in Unicode, based on the Linux kernel source code. The Unicode Technical Committee rejected the Klingon proposal in May 2001
September 1997: first Unicode proposal for pIqaD.1 May 2001: Rick McGowan submits Proposal to Reject Klingon May 2001: Proposal to reject Klingon adopted by UTC (minutes) November 2016: New Proposal for Encoding Klingon, showing lots of examples of usage July 2020: Another New Proposal for Encoding Klingon. This one uses the correct “Klingon” names for the letters. August 2021: Request to Remove Klingon from Non-Approval List, made in accordance with Ken Whistler’s suggestion from 2016, linked above.
Как видите фанаты "Star Trek" крайне упертые товарищи, которые уже второй десяток лет продолжают упорно осаждать двери офиса по адресу:
611 Gateway Blvd. Suite 120
в Сан‑Франциско CA 94 080, где и располагается «The Unicode Consortium». Кстати вы также можете позвонить в консорциум Unicode на их офисный номер:
+1-408-401-8915
и поинтересоваться почему клингонский до сих пор не включен в официальный набор символов — дело же важное.
Удивительно (или нет), но в Microsoft тоже любят клингонский, настолько что добавили его поддержку в свой онлайн-переводчик:
Именно его я использовал для клингонского перевода.
При таком интересе технически продвинутой общественности, очень быстро появились готовые TTF-шрифты, использующие PUA область:
Since then several fonts using that encoding have appeared, and software for typing in pIqaD has become available
Это важный момент, поскольку такой шрифт позволяет комбинировать символы клингонского со всеми остальными, например одним шрифтом можно отобразить и английский и клингонский.
Вот так выглядит клингонский алфавит:
Обратите внимание на соответствие одного глифа клингонского сразу нескольким на английском — это влияет на реализацию транслятора (см. ниже).
Р'льех
С языком древнихР’льех все обстоит куда проще — этот также полностью выдуманный язык, приверженцы которого живут под водой и к счастью мало интересуются продвижением своего фантастического языка в широкие массы.
С названием есть небольшая неточность:
Cthuvian, which is also called R'lyehian, is a fictional language created by H. P. Lovecraft in "The Call of Cthulhu" and expanded upon by various authors.
Дословный перевод — «ктулхский» или «р'льехский», что (да простят меня подводные боги) показалось не очень благозвучным.
Поэтому я использовал термин Р'льех, который на самом деле означает иное:
Н'ЯРЛАФОТЕП — отличное название для нового проекта, не находите?
Доступные TTF-шрифты для Р'льех не используют PUA-область Unicode, поэтому применение такого шрифта превратит все символы в месиво:
Обратите внимание на поле ввода - текст в нем визуально на Р'льех, хотя введены символы английского.
Но если переключиться на клингонский, будет виден ввод символов на нормальных языках:
В этом и заключается главная сила PUA-области и ее главная фишка.
Будете создавать локализацию на древнеегипетский или руническое письмо викингов — обязательно используйте шрифт с PUA-областью.
Так выглядит проект из среды разработки.
Тестовый проект
Для статьи был специально выбран самый «тру‑энтерпрайз» стек, чтобы показать насколько далеко продвинулись технологии локализации. Это не какие-то околонаучные экспериментальные языки или малоизвестные специализированные фреймворки и не дикий «low level» с песьеголовыми программистами на С, это самый настоящий технологический мейнстрим — тот вид разработки и набор технологий, с которыми вы (если занимаетесь разработкой) сталкиваетесь каждый день:
представьте любимый клиент-банк с локализацией на клингонском.
Разумеется будет много специфики именно для Java и выбранных технологий, но описанные идеи и подходы очень даже применимы и для большинства других языков и решений.
Вот что в меню:
JakartaEE 10, который в девичестве назывался JavaEE а в далеком детстве J2EE.
В качестве сервера приложений был взят IBM OpenLiberty — современный открытый потомок большой IBM Websphere Application Server, который IBM ныне продвигает в светлое корпоративное будущее как платформу для разработки микросервисов.
Технически тестовый проект представляет собой веб‑приложение (WAR), которое разворачивается на сервере приложений и по полной использует его ресурсы — все как в золотые годы JavaEE.
Но чтобы не загонять читателей в классические мытарства с установкой и развертыванием — был добавлен автозапуск приложения с автоматическим развертыванием (как в Spring Boot).
Внутри классика корпоративной разработки:
JPA, CDI, JSF и новое Servlet API 6 — уже полностью на аннотациях.
Все прямо как на настоящей работе в банке, где деньги платят.
И сейчас мы будем локализовывать все это на выдуманный язык из фантастического сериала 1970х.
Но прежде опишу стандатное — сборку и запуск.
Сборка
Для сборки используется обычный Apache Maven и последняя версия JDK (22+), забираем проект из репозитория:
Готовое приложение будет находиться в каталоге target:
В каталоге liberty находится распакованный сервер приложений Open Liberty, с установленным внутрь нашим приложением — за все эти радости отвечает специальный плагин (см. ниже).
Запуск
Как уже упоминалось выше, наш замечательный проект предназначен для запуска и работы на сервере приложений IBM Open Liberty.
Разумеется вы можете сходить по ссылке выше, прокрутить страницу вниз до раздела Releases, скачать версию 24.0.0.6+ с профилем Jakarta EE 10, развернуть и затем установить туда наше приложение.
Для настоящего развертывания в корпоративной среде обычно и делают. По крайней мере делали до эры докера.
Но поскольку у нас тут технологическое демо, я посчитал что все эти шаги по развертыванию будут слишком сложными и добавил в сборку специальный плагин для автоматического развертывания и запуска.
Одной командой:
mvn liberty:dev
Произойдет скачивание IBM Open Liberty, распаковка, настройка, установка внутрь нашего приложения и немедленный запуск.
Вот так это выглядит из среды разработки Intellj Idea:
Начну с самого главного вопроса — с отображения символов несуществующего фантастического языка. Взгляните:
Нет это не галлюцинации или фотошоп, это установленный правильный TTF-шрифт клингонского в системе.
На скриншоте выше стандартный gedit, в настройках которого был задан клингонский шрифт для отображения основной части. Как видите использование PUA‑области Unicode в шрифте позволяет неплохо дружить символы обычного и фантастического языков.
Если приглядитесь — увидите сглаживание, работающее даже для глифов клингонского.
К сожалению для Р'льех не нашлось шрифта, использующего PUA‑область Unicode, поэтому при отображении происходит замена всех символов глифами Р'льех:
Тут все служат подводным богам, без исключений.
К сожалению нехватило времени для разработки с нуля шрифтов двух несуществующих языков, поэтому были взяты готовые.
Для клингонского:
Klingon pIqaD Mandel takes the Klinzhai or Mandel font glyphs (really a different alphabet from the KLI’s Standard pIqaD) and refits them for use as pIqaD.
I created this font based on the description by H.P. Lovecraft. Click here to download the Rlyehian font package, which includes two version of the font and a guide to understanding its use.
Но для полноты картины, все же расскажу как происходит разработка новых шрифтов, если вдруг вам понадобится локализовать проект скажем на дотракийский.
FontForge
Уже достаточно давно и успешно существует отличный открытый редактор шрифтов:
FontForge is a FOSSfont editor which supports many common font formats. Developed primarily by George Williams until 2012, FontForge is free software and is distributed under a mix of the GNU General Public License Version 3 and the 3-clause BSD license.[2] It is available for operating systems including Linux, Windows,[3] and macOS,[4] and is localized into 12 languages
Редактор мощный и доступный практически для любых ОС — его возможностей точно хватит с запасом, по крайней мере для стадии прототипирования и любительской работы со шрифтами.
Вот так выглядит клингонский шрифт, открытый в этом редакторе:
Обратите внимание на фразу «Private Use Area» — она означает что глифы клингонского расположены именно в PUA‑области.
Вот так выглядит процесс редактирования отдельного символа:
Имейте ввиду что это долгий и утомительный процесс, особенно если речь про разработку шрифта с нуля.
А вот так для сравнения выглядит шрифт для Р'льех:
Как видите тут не используется PUA и заменяются символы ASCII, с самого начала таблицы.
Для полного погружения, вот так выглядит редактирование одного из этих стильных глифов:
И ведь кто-то сидел и рисовал это. Воистину воля подводных богов безгранична.
Разумеется, можно было потратить какое‑то время и перенести глифы Р'льех в PAU‑область, что позволило бы использование шрифта по аналогии с клингонским — параллельно с другими языками.
Но к сожалению я не верю в Ктулху обладаю достаточным запасом времени и сил, так что оставил как есть.
На самом деле есть еще одна важная причина — показать вам два подхода к локализации, а не один:
второй вариант реализации шрифта с полной заменой всех символов на безумные иероглифы чем-то фантастическим (без использования PAU-области) встречается куда чаще.
Его точно стоит учитывать, поскольку скорее всего именно с таким шрифтом вы и столкнетесь, пытаясь работать с фантастическими языками.
Отображение в браузере
Отдельно опишу как происходит отображение этих фантастических языков в браузере — поскольку мы используем веб, а не отдельное десктоп-приложение.
Все современные браузеры поддерживают регистрацию и использование пользовательских шрифтов на странице — это мягко говоря не новость.
Регистрация TTF‑шрифта происходит путем использования CSS‑стиля и специальной директивы font‑face:
Сложно выглядящая директива #resource[''] на самом деле уже часть парсера страниц JSF — EL-выражение, преобразующее относительный путь к указанному ресурсу в полный.
А вот так выглядит задание отдельных стилей для использования наших фантастических шрифтов:
.klingon { font-family: 'Klingon'; }
Эти стили применяются выборочно, для включения фантастического шрифта при включенной перекодировке у сообщения:
Если сообщение было написано на клингонском pIqaD — оно будет пропущено через транслятор (см. ниже) и при отображении будет использован клингонский TTF‑шрифт.
Таким образом сохраняется обратная совместимость с другими языками и остается возможность ввода на обычном английском.
Но это решение только для отдельных блоков сообщений, ведь есть еще глобальное переключение выбранной локали:
Для решения этой задачи, используется вот такая логика:
Звездочка (*) означает что указанный шрифт должен быть применен ко всем элементам на странице, что и дает вот такой эффект глобальной локализации всего:
Также тут задается фоновая картинка в немного странном формате:
klingon.jpg.xhtml
На самом деле файл называется klingon.jpg и находится в каталоге webapp/resources, а постфикс .xhtml — особенность работы ресурсов в JSF, он нужен для правильной работы, хотя и выглядит полной дичью.
Переходим к следующей важной теме.
Транслятор
При локализации на несуществующий и неподдерживаемый язык существует еще одна проблема:
необходимо как-то работать с локализованным на такой язык текстом из стандарного окружения.
Конечно можно попробовать ставить шрифты, поддерживающие ваш фантастический язык в каждый используемый редактор, каждый терминал и среду разработки — да, это будет работать (см. ниже).
Но с точки зрения промышленной разработки это плохой путь — любая ошибка приведет к тому что вы не сможете увидеть локализованный текст вообще, либо он будет отображаться неправильно.
Если очень повезет, то пойдя этим путем можно получить что-то такое:
Круто, но слишком сложно и не подходит для массовой разработки — когда задействовано много разработчиков.
Есть способ лучше. Дело в том что ни один, даже трижды фантастический язык не существует в вакууме — для него в обязательном порядке создается:
Транслитера́ция (лат. trans- «через; пере-» + littera — «буква») — точная передача знаков одной письменности знаками другой письменности[1][2], при которой каждый знак (или последовательность знаков) одной системы письма передаётся соответствующим знаком (или последовательностью знаков) другой системы письма.
Даже если речь про например дотракийский — выдуманный сценаристами язык кхала Дрого из «Игры Престолов», к нему все равно в качестве приложения идет транслитерация на английском — актерам надо как-то учить произношение.
Более того, такая транслитерация существует и для самих человеческих языков, причем видимо для всех (исключений пока не встречал).
Например есть широко известный вариант написания кириллицы с помощью символов латиницы:
Нет людей в рунете старше 30ти, которые бы его никогда не видели.
Собственно транслит встречается до сих пор — стоит только сломаться мультиязычному вводу на вашем компьютере или телефоне и все — вам тоже придется его использовать.
Именно транслитерацию в латинские символы мы и будем использовать.
Да это «Гамлет» на клингонском — а что вы знаете о фанатизме?
pIqaD
Вариант написания клингонского латинскими символами называется pIqaD, конечно же он куда более широко распространен и популярен чем те сложные клингонские иероглифы, которые я с таким трудом отображал выше.
Думаю не стоит упоминать, что при такой популярности есть и устоявшиеся правила транслитерации и (что куда более важно) — готовые наработки. Очень быстро были найдены и готовые трансляторы, самый популярный (из открытых) выглядит вот так:
На основе его исходного кода (на Javascript) была написана моя реализация на Java, с помощью которой вот такие строковые ресурсы:
Превращаются во время работы приложения в те самые фантастические иероглифы:
Как видите тут происходит достаточно простая замена символов согласно таблице подстановки, с латинских на Unicode из PAU‑области — все внешне сложное, на самом деле устроено очень просто.
Один из немногих оригиналов документов на Р'льех.
К сожалению (или к счастью — в зависимости от контекста), фантастический язык Р’льех из миров Лавкрафта куда менее популярен, поэтому получилось найти всего один рабочий транслятор:
Using the digital serpent's package, you can translate english to the language of the "old ones" Spread aimgr'luh
Занимается им некий китайский DevOps-инженер (надеюсь не в рамках должностных обязанностей), сам транслятор и написан на Python:
Важным моментом является другой принцип работы — вместо транслитерации символов происходит подстановка слов или даже целых фраз:
Вся логика была портирована в мой проект, мою реализацию транслятора для Р'льех можно посмотреть вот тут. Разумеется с таким подходом в виде зашитого и очень небольшого словаря, нет возможности реализовать перевод технических терминов:
у меня честно нет идей как могут выглядеть слова «Авторизация», «Назад» или «Сохранить» на языке древних.
Поэтому транслятор Р’льех используется только для ввода текста — чтобы найти истинных последователей показать как это работает.
Но перейдем к следующей интересной теме.
Нереальная локаль
Следующей проблемой при работе с фантастическими языками является их регистрация в системе — в том языке, платформе или фреймворке, который вы используете.
Это нужно в первую очередь для того, чтобы как‑то сигнализировать внутри приложения о том что используется такой фантастический язык и проводить соответствующую подстройку — например вызывать тот самый транслятор, описанный выше.
Тут может быть огромное количество вариантов, проблем и подводных камней, поскольку такой разработкой мы выходим за рамки обыденного поддерживаемого. И при возникающих проблемах вам скорее всего никто не поможет — кроме нас разумеется.
Но для Java весь процесс более-менее отработан, описан и предсказуем:
в Java у локалей есть поддержка т. н. «variant» — специальной вариации языка, которая может быть сколь угодно нестандартной.
Сама локаль остается системной (в данном случае — английской), но при этом к ней добавляется специальный постфикс, означающий что используется «вариация»:
Поскольку такие variants являются частью официального API, они поддерживаются всем прикладным ПО и библиотеками (за редкими исключениями).
В том числе они используются в механизме работы ResourceBundle:
Если включить «variant» в название файла с ресурсами — он будет найден и загружен при выборе локали с таким «variant». К сожалению стандартной реализации ResourceBundle оказалось недостаточно — хотелось получить перекодированный клингонский сразу из ресурсов, поэтому я сделал свою:
package com.Ox08.experiments.kligon; import jakarta.annotation.Nonnull; import jakarta.faces.context.FacesContext; import java.util.Enumeration; import java.util.Locale; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; /** * Extended resource bundle, used to inject Klingon glyphs if Klingon locale * used * * @Author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a> */ publicclass KlingonedResourceBundle extends ResourceBundle { public KlingonedResourceBundle() { setParent(ResourceBundle.getBundle("i18n.messages", FacesContext.getCurrentInstance().getViewRoot().getLocale())); } @override publicfinalvoid setParent(ResourceBundle parent) { super.setParent(parent); } @override protectedObject handleGetObject(@Nonnull String key) { // here will be extracted and substituted value finalObject v = parent.getObject(key); if (!(v instanceofString vstring)) { return v; } LOG.log(Level.INFO, "handleGetObject : {0}", vstring); // current locale final Locale l = FacesContext.getCurrentInstance().getViewRoot().getLocale(); // check if its Klingon and transliterate to glyphs if ("KLINGON".equals(l.getVariant())) return KlingonTranslator.transliterate(vstring); // .. and for Rlyeh if ("RLYEH".equals(l.getVariant())) return RlyehTranslator.translate(vstring);
Основное действие происходит в методе handleGetObject() ,сейчас разберу логику этого метода по шагам, благо она будет повторяться и в других местах.
Первым шагом происходит вызов такого же метода, но из родительского класса — для получения еще не перекодированного текстового шаблона:
final Object v = parent.getObject(key);
Затем происходит отбраковка по возвращаемому типу — мы работаем только со строками и все остальные варианты пропускаем:
if (!(v instanceofString vstring)) { return v; }
Дальше происходит получение текущей локали пользователя:
final Locale l = FacesContext.getCurrentInstance() .getViewRoot().getLocale();
Что несколько неправильно с точки зрения архитектуры большой системы, но достаточно для демо проекта.
Затем в зависимости от значения «variant» вызывается перекодировщик для клингонского:
if ("KLINGON".equals(l.getVariant())) return KlingonTranslator.transliterate(vstring);
или для Р'льех:
if ("RLYEH".equals(l.getVariant())) return RlyehTranslator.translate(vstring);
Регистрация кастомной реализации ResourceBundle задается в файле с настройками Jakarta Faces (webapp/WEB-INF/faces-config.xml):
.. <resource-bundle> <!-- Note that 'base name' points to specific class, not to .properties file --> <base-name>com.Ox08.experiments.kligon.KlingonedResourceBundle</base-name> <var>msgs</var> </resource-bundle> ..
Там же указывается список поддерживаемых локалей, с учетом «variants»:
Но это еще не все интесное и необычное, что хотелось бы раскрыть в рамках статьи.
Валидация данных
Как каша без масла протеина или водка без закуски — не бывает корпоративных приложений без валидации данных.
В Jakarta EE (как и в ее предшественнике JavaEE) для автоматической валидации входных данных используются механизмы из спецификации JSR 303 «Bean Validation».
В самом простом случае это выглядит как аннотирование полей класса:
.. @size(min = 3, max = 255) privateString title; // a title @NotBlank(message = "{validation.message.not-blank}") @Lob @column(length = Integer.MAX_VALUE) privateString message; // message, stored as CLOB in database, //so size is almost unlimited @size(min = 3, max = 30) @email privateString author; // author's email ..
Когда такой класс попадает в качестве входящего аргумента метода класса, управляемого CDI‑окружением, срабатывает автоматическая валидация и в интерфейсе появляются сообщения об ошибках:
Если ошибка имеет привязку к конкретному полю, за ее отображение отвечает отдельный блок:
<h:message for="f_message" errorClass="msg" />
если нет — она отображается через «глобальную свалку»:
Вместо текста сообщения, тут указан некий код, который автоматически заменяется на текст из специального ResourceBundle:
Согласно спецификации JSR303 название для бандла должно быть именно ValidationMessages.
Вся эта логика является частью спецификации JSR303 и вообщем-то отлично работает без вашего участия — до тех пор пока не появляется необходимость сотворить какую-нибудь дичь.
К сожалению поддержка несуществующих языков в текстах сообщений об ошибках является именно такой дичью:
Текст красненьким — та самая валидация JSR303. На клингонском.
Поэтому придется немного подумать.
После долгих поисков и изучения документации, все же был найден способ вклиниться в процесс получения текстов сообщений с ошибками валидации:
Message interpolators are used by the validation engine to create user readable error messages from constraint message descriptors.
В итоге была написана собственная реализация такого «интерполятора»:
package com.Ox08.experiments.kligon; import jakarta.validation.MessageInterpolator; import jakarta.validation.Validation; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; /** * Custom JSR 303 Message Interpolator, used to inject Klingon glyphs * into JSR303 validation * * @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a> */ publicclass JSR303KlingonMessageInterpolator implements MessageInterpolator { // we need to have existing MessageInterpolator, // to being used as parent privatefinal MessageInterpolator delegate; public JSR303KlingonMessageInterpolator() { // take default implementation from JSR303 configuration this.delegate = Validation.byDefaultProvider() .configure().getDefaultMessageInterpolator(); } @Override publicString interpolate(String string, Context cntxt) { LOG.log(Level.INFO, "interpolating {0}", string); // without specified locale - just pass interpolation to delegate return delegate.interpolate(string, cntxt); } @Override publicString interpolate(String string, Context cntxt, Locale locale) { LOG.log(Level.INFO, "interpolating {0} with locale: {1}", newObject[]{string, locale.toLanguageTag()}); // here will be extracted and substituted value finalString result = delegate.interpolate(string, cntxt, locale); // check for Klingon locale and transliterate to glyphs if ("KLINGON".equals(locale.getVariant())) return KlingonTranslator.transliterate(result);
if ("RLYEH".equals(locale.getVariant())) return RlyehTranslator.translate(result);
Основная магия логика заключается вот в этих строках:
.. finalString result = delegate.interpolate(string, cntxt, locale); // check for Klingon locale and transliterate to glyphs if ("KLINGON".equals(locale.getVariant())) return KlingonTranslator.transliterate(result);
if ("RLYEH".equals(locale.getVariant())) return RlyehTranslator.translate(result);
return result;
Как видите, локаль поступает на вход метода в готовом виде — ее не надо определять из контекста JSF, а вот в этом месте происходит получение оригинальной строки из файла с текстовыми строками:
final String result = delegate.interpolate(string, cntxt, locale);
Дальше в зависимости от наличия «variant» у локали, текст либо пропускается через транслятор либо отдается «как есть».
Регистрация кастомного интерполятора также имеет свою специфику — она происходит в отдельном XML-файле:
Который находится в файле src/main/resources/META-INF/validation.xml
Последней интересной темой, достойной освещения в рамках статьи про локализацию будут фантастические даты.
Заметьте — не просто фантастический формат отображения а целый календарь!
Фантастические даты
Никогда не задумывались какой смысл закладывается в дату?
Что такое на самом деле 2024й год?
Фактически это означает что прошло 2024 года с рождения Иисуса Христа (по новому летоисчислению), что возможно не очевидно некоторым представителям молодого поколения, но вполне достаточно для жизни и работы цивилизации.
А что если вам надо использовать альтернативную систему расчета времени?
Миллион лет от последнего динозавра?
40 000 лет бесконечной войны?
Озадачившись данным вопросом, я решил что неплохо было бы реализовать для фантастического языка еще и фантастическое летоисчисление. И использовать его для обычного корпоративного приложения, да.
A stardate is a fictional system of time measurement developed for the television and film series Star Trek. In the series, use of this date system is commonly heard at the beginning of a voice-over log entry, such as "Captain's log, stardate 41153.7.
Именно ее поддержку я и решил реализовать:
За основу был взят фанатский проект с реализацией StarDate на куче разных языков, оригинальный код был сильно уменьшен и почищен.
Но одной только реализации кастомного календаря оказалось мало — нужен еще один класс-конвертер, реализующий непосредственно конвертацию дат с этим календарем:
package com.Ox08.experiments.kligon; import jakarta.faces.component.UIComponent; import jakarta.faces.context.FacesContext; import jakarta.faces.convert.Converter; import jakarta.faces.convert.FacesConverter; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; importjava.util.Date; import java.util.Locale; /** * A custom converter for StarDate * @author alex0x08 */ @FacesConverter(value = "stardateConverter") publicclass StarDateConverter implements Converter<Date> { @Override public Date getAsObject(FacesContext fc, UIComponent uic, String string) { final Locale l = fc.getViewRoot().getLocale(); if ("KLINGON".equals(l.getVariant())) return StarDate.parseStarDate(string).getDate();
return Date.from(ZonedDateTime.parse(string, DateTimeFormatter.ISO_DATE_TIME .withZone(ZoneId.systemDefault())).toInstant()); } @Override publicString getAsString(FacesContext fc, UIComponent uic, Date t) { final Locale l = fc.getViewRoot().getLocale(); if ("KLINGON".equals(l.getVariant())) return StarDate.newInstance(t).toString(); return DateTimeFormatter.ISO_DATE_TIME .withZone(ZoneId.systemDefault()) .format(t.toInstant()); } }
Активируется этот конвертер автоматически благодаря наличию аннотации:
@FacesConverter(value = "stardateConverter")
И автоматически же применяется для всех полей с типом Date, проходящих через бины, управляемые CDI.
Внутри уже традиционная логика получения текущей локали:
final Locale l = fc.getViewRoot().getLocale();
Затем при наличии клингонского «variant» происходит либо преобразование из объекта в строку с учетом кастомного календаря:
if ("KLINGON".equals(l.getVariant())) return StarDate.parseStarDate(string).getDate();
либо из строки в объект (также с учетом StarDate):
if ("KLINGON".equals(l.getVariant())) return StarDate.newInstance(t).toString();
На этом красивая история о нереальном подходит к концу, подведем итоги.
Итоги и выводы
В современных реалиях и с использованием современных инструментов нет серьезных препятствий для локализации на любые неведомые языки — искусственные или настоящие.
Отсутствие официальной поддержки «из коробки» в инструментах разработки и даже отсутствие символов в таблице символов Unicode — не является проблемой для настоящего джедая.
Первым шагом необходимо разработать или найти готовый TTF‑шрифт для вашего языка и проверить его отображение в системе и браузере — если планируется веб‑разработка.
Следующим шагом необходимо реализовать либо взять готовые правила транслитерации вашего фантастического языка символами существующего — кириллицей, латиницей и так далее. И написать соответствующий транслятор символов.
Вся дальнейшая работа сведется к включению транслятора в ключевых местах проекта.
Автор, как и наверное большинство разработчиков, считал Golang всего лишь новомодной корпоративной игрушкой, призванной подсадить широкие программисткие массы на очередную технологию «корпорации добра» — создавался этот язык внутри Гугла и для задач Гугла, которые разумеется сильно отличаются от обывательских.
Поэтому когда мне показали работу Golang с WinAPI «из коробки» я был сильно удивлен — в более серьезных языках вроде C/C++ работа c внутренностями Windows всегда выглядела куда более монструозной. Так и родилась эта замечательная статья.
Что мы будем в этот раз творить:
Desktop-приложение с настоящим интерфейсом, с учетом реалий Windows, которое запустит встроенный вебсервер, с методом REST API на ассемблере.
Еще будет загрузка графического файла и установка его в качестве обоев — через WinAPI. Плюс небольшой обход файрвола, чтобы не показывался вот этот раздражающий экран с предупреждением:
Он всегда меня бесил, а то что меня бесит — я отключаю.
Надеюсь описанное в статье удивит даже опытных разработчиков на Golang.
Собственно так выглядит наш сегодняшний герой в действии:
Обратите внимание на отключенные кнопки «закрыть» и «развернуть» — даже это оказалось не так просто сделать на чистом WinAPI
Я использовал последнюю на момент написания версию 1.22.5, но язык столь бурно развивается, что не удивлюсь если выйдет более новая версия еще до завершения статьи.
Открытый проект в Visual Studio Code с установленным плагином для Golang
Теперь самое интересное:
для сборки проекта использовались не обычные Makefile и не шелл-скрипты — так характерные для проектов на «гошечке», а целая отдельная внешняя система сборки — Magefile.
Ставится она множеством разных способов, я использовал вот такой:
Если сборка прошла успешно, в текущем каталоге будет файл ungoogled-go.exe, который можно свободно перемещать и запускать на пользовательских компьютерах — он полностью статичный и не зависит от установленного Golang.
Опционально можно запустить:
mage generate
Этой командой запустится генерация файлов add.s и stub.go — для метода на ассемблере. Стоит также отметить, что конечная и отладочная сборка немного отличаются, разделение происходит путем проброса параметра:
-X main.DebugMode=false
Которым изменится значение глобальной переменной — флагом отладочного режима, который в свою очередь немного влияет на поведение программы.
Теперь начинаем разбираться, как же оно все работает.
Невероятный факт № 5668 : не каждый Windows-программист знает как скомпилировать программу из консоли.
Приложение Windows
Если попробовать собрать и запустить в Windows классический «Hello world» на C:
#include <stdio.h> int main() { printf("Hello, World!"); return 0; }
Вместо ожидаемого пустого графического окна запустится страшная черная консоль как на снимке выше. Это происходит потому что в Windows для графических программ используется другая точка запуска (entry point):
Every Windows program includes an entry-point function named either WinMain or wWinMain.
И если уж жизнь вас заставила разрабатывать на Go под Windows, еще и с графическим интерфейсом, то стоит «гошечке» об этом сообщить, добавив флаг в параметры ldflags.:
-H windowsgui
Целиком это выглядит так:
go build -ldflags "-H windowsgui"
Помимо этого, я указываю режим сборки exe:
-buildmode=exe Build the listed main packages and everything they import into executables. Packages not named main are ignored.
Для того чтобы получить в итоге сборки один большой и переносимый запускаемый exe файл.
Так выглядит «официальный Hello World» на C++ и WinAPI
Golang и WinAPI
Стоит пояснить читателям, в чем вообще заключается сложность работы с WinAPI. Для примера возьмем официальный «Hello world» на C++ под Windows:
А все потому что 90% кода даже в столь простом приложении не имеют никакого отношения к C++, а являются структурами, макросами или функциями самого WinAPI.
От C++ тут только примитивные типы (int) и управляющие конструкции (case, while).
Поэтому задача как-то серьезно взаимодействовать с WinAPI (дальше чем разовый вызов какой-то функции) — всегда была, есть и будет сложной. А разработка под Windows является отдельной специальной дисциплиной, чемпионы которой запросто могут забыть обычный C/C++ вообще и всю разработку (даже серверную) вести на инструментах WinAPI.
Но вернемся к нашей «гошечке».
Go далеко не C++ и является экзотикой в мире Windows-разработки, по крайней мере за пределами кампусов Google.
Но внезапно оказалось, что поддержка WinAPI в нем очень даже неплоха.
Взгляните как выглядит вызов WinAPI функции для установки обоев на Golang:
И оно даже работало. Но только объем кода очень быстро вырос до былинных размеров и никак не влезал в масштаб статьи.
Поэтому от такого подхода пришлось отказаться, оставив лишь работу с системным треем.
Все остальное я отдал на откуп готовым библиотекам. В частности построение окон и обработку событий были реализованы через библиотеку Windigo. — хотя это по-сути лишь набор готовых биндингов для функций WinAPI.
Вот так выглядит в работе демо-приложение на Windigo:
Собственно тут показаны все основные радости Windigo, доступные без долгих часов камлания над документацией
func main()
Запуск приложения Go согласно спецификации начинается с функции func main() в пакете main:
A complete program is created by linking a single, unimported package called the main package with all the packages it imports, transitively. The main package must have package name main and declare a function main that takes no arguments and returns no value.
Первая же строка внутри main() нашего проекта нуждается в пояснении:
runtime.LockOSThread()
Этот вызов из пакета runtime нужен для того чтобы все goroutines (легковесные потоки Go) выполнялись в отдельных системных потоках каждый.
В нашем случае это необходимо для взаимодействия с системным потоком, отвечающим за графический интерфейс:
A goroutine should call LockOSThread before calling OS services or non-Go library functions that depend on per-thread state.
Следующим шагом происходит вызов функции, отвечающей за построение графического интерфейса:
mainWindow = newMyWindow()
Разберем как формируются и связываются графические элементы, в нашем проекте за это отвечает функция:
С помощью константы co.CS_NOCLOSE отключается кнопка закрытия окна:
CS_NOCLOSE 0x0200 Disables Close on the window menu.
Ну и дальше задается заголовок и размеры создаваемого окна — тут все просто. Зато сложно чуть ниже:
if DebugMode == "false" { // ID of icon resource, see resources folder // does not work in debug mode opts = opts.IconId(101) }
Тут указывается иконка окна в виде числового ID ресурса, файл с ресурсами minimal.syso был взят из демо-проекта Windigo:
A syso file, ready to use, that contains the icon and the manifest. Just place it at the root folder of your project. You can load the icon using the resource ID 101.
Следующим шагом происходит вызов сложной цепочки инициализации окна:
// create main window wnd := ui.NewWindowMain(opts)
В конце которой вызывается известная функция WinAPI CreateWindowEx, используемая для создания нового графического окна.
Если пользователь нажал кнопку «Yes» (т.е подтвердил операцию), происходит завершение работы HTTP-сервера:
if httpSrv != nil { if err := httpSrv.Close(); err != nil { fmt.Printf("HTTP close error: %v", err) } }
Закрытие главного окна приложения:
me.wnd.Hwnd().DestroyWindow()
И завершение работы:
os.Exit(0)
Следущим шагом из функции main мы загружаем иконку, используемую в трее:
var trayIcon win.HICON
// Load icon // in debug mode, there are no resources available, so we need to load // icons from FS if DebugMode == "false" { trayIcon = win.HICON( win.GetModuleHandle(win.StrOptNone()).LoadImage( win.ResIdInt(101), co.IMAGE_ICON, 16, 16, co.LR_DEFAULTCOLOR, )) } else { trayIcon = win.HICON( win.GetModuleHandle(win.StrOptNone()).LoadImage( win.ResIdStr("gopher.ico"), co.IMAGE_ICON, 16, 16, co.LR_DEFAULTCOLOR|co.LR_LOADFROMFILE, )) }
Используется разная логика для режима отладки и запуска финального бинарника, потому что в готовом приложении иконка будет находиться в ресурсах — специальном файле, упакованном вместе с приложением.
А во время отладки либо запуска вроде:
go run main.go
ресурсов не будет, поэтому придется загружать иконку непосредственно с файловой системы.
Дальше мы настраиваем дополнительные обработчики, в первую очередь добавляем обработку на закрытие главного окна приложения:
// close systray on main window destroy mainWindow.wnd.On().WmDestroy(func() { if tray != nil { tray.Dispose() } })
При закрытии главного окна, произойдет и автоматическое закрытие трея — не будет эффекта потерянной инонки, когда приложение уже закрылось, а его иконка до сих пор отображается в трее.
Дальше мы вешаем обработчик на активацию главного окна, для того чтобы поймать момент полной готовности и отображения и запустить сервер:
var configured = false // check for action that runs only once
mainWindow.wnd.On().WmActivate(func(p wm.Activate) { // we need to run our handler logic only once at start if configured { return } configured = true go startServer() })
Столь отложенный старт необходим для большей интерактивности:
метод startServer () пишет сообщения в «графический лог», если он не будет полностью инциализирован — сообщения пропадут.
Проблема заключается в том что этот обрабочик будет запускаться и на повторную активацию (например после сворачивания окна) — чтобы логика не отрабатывала повторно стоит проверка на переменную configured, которая работает в качестве флага «инициализация завершена».
Ну и сам запуск HTTP-сервера происходит через отдельный поток, чтобы не блокировать работу обработчика:
go startServer()
Последним мы добавляем инициализацию трея по событию создания главного окна:
// action on windows create // runs once mainWindow.wnd.On().WmNcCreate(func(p wm.Create) bool { // create systray tray := systray.CreateSysTray() // set handler on icon click - just focus on main window systray.SetTrayClickHandler(func() { systray.ShowWindow(uintptr(mainWindow.wnd.Hwnd()), systray.SW_SHOWNORMAL) })
tray.SetIcon(uintptr(trayIcon)) tray.SetTooltip("Tiny Server: click me to show main window.")
return true })
Нужно это по той простой причине что только на этой стадии появляется настоящий window handle:
mainWindow.wnd.Hwnd()
С помощью которого возможно взаимодействовать с окном:
systray.ShowWindow(uintptr(mainWindow.wnd.Hwnd())
До этого момента (т.е. до вызова обработчика) HWND нашего окна будет пустым. Наконец финальный шаг в функции main() это запуск блокирующего цикла обработки cобытий:
mainWindow.wnd.RunAsMain()
После вызова этого метода, приложение начнет реагировать на события вроде нажатия клавиш или кликов мышкой.
Теперь разберем работу с системным треем — как пример работы с чистым WinAPI.
Вот так это выглядит в Windows 11
Работа с системным треем
Разумеется есть способ проще:
взять одну из готовых библиотек, тем более что есть универсальные — сразу для Windows, MacOS и Linux и всей кучи разных сред окружения.
Но фана ради и пользы обучения для, был избран более сложный путь — закат солнца вручную взаимодействие с системным треем только через WinAPI.
Все функции, относящиеся к этой задаче находятся в пакете systray:
Благодаря этому флагу можно создать невидимое окно и заставить его обработчик принимать сообщения системного трея:
// this is main window function // see https://learn.microsoft.com/en-us/windows/win32/api/winuser/... func wndProc(hWnd uintptr, msg uint32, wParam, lParam uintptr) uintptr { switch msg { case TrayIconMsg: nmsg := LOWORD(uint32(lParam)) // if user clicked on tray icon if nmsg == WM_LBUTTONDOWN { // if callback function exist if trayClickCallback != nil { trayClickCallback() } } case WM_DESTROY: PostQuitMessage(0) default: r, _ := DefWindowProc(hWnd, msg, wParam, lParam) return r } return 0 }
Да, это все тот же старый добрый WndProc , описанный выше в статье и хорошо знакомый любым Windows-разработчикам.
Блок внутри:
.. case TrayIconMsg: nmsg := LOWORD(uint32(lParam)) // if user clicked on tray icon if nmsg == WM_LBUTTONDOWN { // if callback function exist if trayClickCallback != nil { trayClickCallback() } } ..
Отвечает за обработку сообщений системного трея.
Функция, которая отрабатывает по событию клика левой кнопки мыши (WM_LBUTTONDOWN) выглядит вот так:
Словом, уровень интеграции с WinAPI и легкости его применения поражает воображение.
Лог с интерфейсом
Лог
Он же «журнал работы» — отображает события в приложении в центральной части рабочей области. Логика записи выглядит следующим образом:
// appends to UI log func appendToLog(message string) { // could be no window yet if mainWindow == nil || mainWindow.txtName == nil { fmt.Println(message) return } // window could be not visible yet // and attempt to add message will raise an exception if !mainWindow.txtName.Hwnd().IsWindowVisible() { fmt.Println(message) return } // get current text txt := mainWindow.txtName.Text() // to avoid overflow if len(txt) > 512 { txt = "" } b := strings.Builder{} b.WriteString(txt) // append existing text b.WriteString(message) // append new message b.WriteString("\r\n") // this is Windows, so \r\n, not \n ! // and finally set updated text (yep, there is no append, sorry) mainWindow.txtName.SetText(b.String()) }
Кроме достаточно очевидного пропуска записи в случае неполной инициализации, тут есть еще вот такая логика:
if !mainWindow.txtName.Hwnd().IsWindowVisible() { fmt.Println(message) return }
Нужно это потому, что CEdit не даст изменить текст внутри если сам компонент еще не отображается, а попытка вызова метода API изменения текста вызовет ошибку.
Также внезапно (хотя для кого как) оказалось что стандартный компонент Windows для ввода не поддерживает логику добавления (append) — только полную замену всего текстового блока:
// get current text txt := mainWindow.txtName.Text() // to avoid overflow if len(txt) > 512 { txt = "" } b := strings.Builder{} b.WriteString(txt) // append existing text b.WriteString(message) // append new message b.WriteString("\r\n") // this is Windows, so \r\n, not \n ! // and finally set updated text (yep, there is no append, sorry) mainWindow.txtName.SetText(b.String())
Поэтому с точки зрения современной разработки это выглядит как колхоз, но увы — таковы реалии WinAPI.
Встроенный HTTP-сервер
В составе Golang идет готовый встраиваемый HTTP-сервер (пакет «net/http»), с примитивами обработчиков для типовых действий.
С его помощью удалось минимальными силами реализовать весь тестовый функционал, метод инициализации и запуска встроенного HTTP-сервера выглядит вот так:
// firewall bypass does not work correctly in debug mode if DebugMode == "false" { server.AddAppFirewallRule() appendToLog("Added firewall rule..") } // create request multiplexer, see https://pkg.go.dev/net/http#ServeMux mux := http.NewServeMux() // test assembler method mux.HandleFunc("/asmtest", server.TestAsmMethod) // upload & set wallpaper image mux.HandleFunc("/upload", server.UploadHandler) // default handler mux.HandleFunc("/", server.IndexHandler)
// if this is production mode - bind to all interfaces if DebugMode == "false" { httpSrv = &http.Server{ Addr: ":8090", Handler: mux, } } else { // otherwise - bind to localhost (firewall bypass // does not work in debug mode) httpSrv = &http.Server{ Addr: "localhost:8090", Handler: mux, } }
appendToLog(fmt.Sprintf("Server started at %s", httpSrv.Addr)) // set logging handler server.SetMessageLogHandler(appendToLog) httpSrv.ListenAndServe() // here will be lock }
Разберем что тут происходит, первым шагом идет запись в лог:
// firewall bypass does not work correctly in debug mode if DebugMode == "false" { server.AddAppFirewallRule() appendToLog("Added firewall rule..") }
Как это работает подробно разобрано ниже, пока замечу что этот обход не работает в режиме отладки, поэтому тут и стоит такая странная на первый взгляд проверка.
Следующим шагом происходит инстанциация мультиплексора запросов:
// test assembler method mux.HandleFunc("/asmtest", server.TestAsmMethod) // upload & set wallpaper image mux.HandleFunc("/upload", server.UploadHandler) // default handler mux.HandleFunc("/", server.IndexHandler)
Т.е. по какой ссылке будет отвечать каждый обработчик.
Логика всех обработчиков разобрана чуть ниже, а пока пройдем дальше по логике инициализации HTTP-сервера:
// if this is production mode - bind to all interfaces if DebugMode == "false" { httpSrv = &http.Server{ Addr: ":8090", Handler: mux, } } else { // otherwise - bind to localhost (firewall bypass // does not work in debug mode) httpSrv = &http.Server{ Addr: "localhost:8090", Handler: mux, } }
Вся эта простыня нужна по той простой причине что в Golang нет тернаров, т.е нельзя сделать логику одной строкой вроде:
Addr = DebugMode? "localhost:8090" : ":8090"
Как это было бы в Java или Typescript.
Поэтому надо было либо делать отдельную функцию, отдающую адрес, внутри которой вставлять проверку на DebugMode, либо сделать как на примере выше — два повторяющихся блока.
Дальше по логике происходит установка обработчика логирования:
// set logging handler server.SetMessageLogHandler(appendToLog)
Со стороны пакета сервера он вызвается вот так:
// logs message with callback on UI func logMessage(message string) { if messageLogCallback != nil { messageLogCallback(message) } else { fmt.Println(message) } }
А вот так выглядит пример конечного использования:
logMessage(fmt.Sprintf("Background changed to %s", dst.Name()))
Наконец последним шагом происходит запуск самого HTTP-сервера:
httpSrv.ListenAndServe() // here will be lock
Обратите внимание что httpSrv объявлен как глобальная переменная:
var ( tray *systray.TrayIcon httpSrv *http.Server mainWindow *MyWindow //You can only set string variables with -X linker flag. From the docs: DebugMode = "true" )
Это нужно чтобы иметь возможность остановить HTTP-сервер при завершении работы (graceful shutdown):
if httpSrv != nil { if err := httpSrv.Close(); err != nil { fmt.Printf("HTTP close error: %v", err) } }
Обход файрвола
И не надо так подозрительно смотреть — речь про вполне себе документированное API, позволяющее пропустить вот такое откровенно дурацкое подтверждение:
Был добавлен еще в Windows 7 с официальной целью: выбешивать пользователей.
Как и в случае с интерфейсом, было принято волевое решение использовать готовый пакет с биндингами:
This is a package for controlling the Windows Filtering Platform (WFP), also known as the Windows firewall.
С его помощью вся логика свелась к вот такой простой функции, взятой из issue в Github проекта и немного переделанной:
Не буду детально расписывать эту довольно сложно воспринимаемую логику — слишком уж тут много специфики WFP, читать устанете.
Если есть желание погрузиться в тему — вам в помощь вот такая замечательная статья от авторов пакета, где расписано в деталях внутренее устройство WFP и работа с его API.
Но если в кратце — тут происходит создание и применение нового правила фильтрации, которое разрешает входящие соединения для приложения, из которого выполняется вызов API.
Golang и ассемблер
Чтобы сразу закрыть все вопросы по поводу адекватности использования ассемблера из Go, вот вам небольшая цитата:
This example is taken from the AES package of the standard Go library. It makes use of Go Assembly to leverage Intel’s hardware support for AES, calling the AES-NI CPU instructions that can perform a “round” of encryption or decryption of the AES algorithm.
Да, как только начинается большая криптография и множественные вычисления на слабом железе — сразу с дальней полки достается пыльная книга по ассемблеру.
Разумеется не было открытием что язык, с самого своего начала имевший компилятор в нативный код умеет вызывать вставки на ассемблере. Открытием была легкость и простота с которой это делается.
Не задумывались почему маскоты Go и Plan 9 так похожи?
Еще одним открытием оказались торчащие уши Plan 9:
The assembler is based on the input style of the Plan 9 assemblers, which is documented in detail elsewhere. If you plan to write assembly language, you should read that document although much of it is Plan 9-specific
А разгадка проста — один из авторов Go когда‑то работал над Plan 9:
func main() { TEXT("Add", NOSPLIT, "func(x, y uint64) uint64") Doc("Add adds x and y.") x := Load(Param("x"), GP64()) y := Load(Param("y"), GP64()) ADDQ(x, y) Store(y, ReturnIndex(0)) RET() Generate() }
Это отдельная программа на Go, которая при запуске генерирует ассемблерный код в файле asm/add.s:
// Code generated by command: go run asm.go -out asmtest/add.s -stubs asmtest/stub.go. DONOT EDIT.
#include "textflag.h"
// func Add(x uint64, y uint64) uint64 TEXT ·Add(SB), NOSPLIT, $0-24 MOVQ x+0(FP), AX MOVQ y+8(FP), CX ADDQ AX, CX MOVQ CX, ret+16(FP) RET
А также заголовочный файл на Go в файле stub.go:
// Code generated by command: go run asm.go -out asmtest/add.s -stubs asmtest/stub.go. DO NOT EDIT. package ungoogled // Add adds x and y. func Add(x uint64, y uint64) uint64
Затем данные файлы линкуются с основным приложением, а вызов метода с ассемблерной вставкой выглядит вот так:
// a test API method to call function with Assembler inside func TestAsmMethod(w http.ResponseWriter, req *http.Request) {
Просто для демонстрации возможностей, к достаточно стандартному функционалу по загрузке файлов был приделан вызов WinAPI функции для смены обоев на рабочем столе.
Сама форма загрузки выглядит максимально стандартно:
Затем она зашивается в приложение с помощью go:embed:
//go:embed upload.html var uploadTemplate string
Т.е. во время запуска приложения, переменная uploadTemplate будет содержать HTML-шаблон выше для загрузки картинки — зашивание происходит во время сборки.
За загрузку картинки отвечает функция:
func uploadFile(w http.ResponseWriter, r *http.Request) { .. }
Внутри стандартная скучная логика разборки multipart-формы и обработки загруженного файла - ее нет смысла описывать, зато дальше происходит кое-что интересное:
// build full path to image imagePath, err := windows.UTF16PtrFromString(dst.Name())
Тут происходит формирование ссылки на UTF-16 строку, содержающую полный путь к загруженному файлу.
Затем эта ссылка используется для вызова API:
// call WinAPI to change wallpaper to just uploaded image _, _, err = procSystemParamInfo.Call(20, 0, uintptr(unsafe.Pointer(imagePath)), 0x001A) // check for errors, respond 500 if any if err, ok := err.(syscall.Errno); ok { if err != 0 { fmt.Println("Error :") fmt.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } }
Вызывается функция SystemParametersInfoW, которая на самом деле используется для очень большого количества разных действий:
Retrieves or sets the value of one of the system-wide parameters. This function can also update the user profile while setting a parameter.
Нужный нам для смены обоев actionName называется SPI_SETDESKWALLPAPER который и указывается при вызове.
Эпилог
Разумеется я такой не один и уже достаточномногоразработчиков по всему миру делятся своим опытом разработки на Go и WinAPI. Надеюсь эта статья также добавит читателям восторгов и позволит взглянуть на любимый инструмент под другим углом.
Хотите довести до дурки любимого преподавателя компьютерных наук или навсегда прослыть «особенным» среди коллег, сразу после немедленного увольнения?
Ниже патентованный метод, с которым вас точно не забудут.
Вы наблюдаете самый настоящий ящик Пандоры, который автор для вас любезно приоткрыл.
Электронная дичь
Ладно, допустим вы не имеете отношения к ИТ или не занимаетесь непосредственно разработкой ПО. Возможно вы начинающий веб‑разработчик, умеющий только в лендинги и сайты‑визитки — словом вы никогда прежде не слышали об экзотерических языках программирования и их ярчайшем представителе:
Brainfuck — один из эзотерических языков программирования, придуман Урбаном Мюллером (нем. Urban Müller) в 1993 году, известен своим минимализмом. Название языка можно перевести на русский как вынос мозга, оно напрямую образовано от английского выражения brainfuck (brain — мозг, fuck — вынос), т. е. заниматься ерундой. Язык имеет восемь команд, каждая из которых записывается одним символом. Исходный код программы на Brainfuck представляет собой последовательность этих символов без какого-либо дополнительного синтаксиса.
Понимаю, тяжело воспринять фразу «экстремальный минимализм» по отношению к языку программирования, поэтому вот вам небольшая иллюстрация:
Brainfuck почти не используется для практического программирования (за исключением работ отдельных энтузиастов), а используется преимущественно для головоломок и задач для соревнований.
Теперь, если вы занимаетесь разработкой и более‑менее подкованы в программировании — вернитесь обратно к картинке выше и задумайтесь: чтоименно автор тут для вас приготовил:)
Транспилеры и компиляторы
Минутка матчасти, прежде чем мы с вами погрузимся в бездну бесконечного ужаса разработки, для лучшего хоть какого-то понимания этой сложной темы.
A source-to-source translator, source-to-source compiler (S2S compiler), transcompiler, or transpiler is a type of translator that takes the source code of a program written in a programming language as its input and produces an equivalent source code in the same or a different programming language. A source-to-source translator converts between programming languages that operate at approximately the same level of abstraction, while a traditional compiler translates from a higher level programming language to a lower level programming language.
Максимально упрощая, транспилер — такой специальный компилятор, для превращения исходного кода на одном языке в исходный код на другом языке.
Очень часто используется на практике, в том числе в современных и сверхпопулярных языках вроде Java или.NET.
Чаще всего такую технику применяют для выдачи «желаемого за действительное», например с помощью транспилера реализованы ваши любимые генерики и замыкания в Java.
Да это «та самая программа с помощью которой создаются другие программы», в изначальном смысле этого слова.
Практически любой компилятор — очень сложная программа, даже использование которой требует определенной подготовки. Ну а задача создания компилятора с нуля — предмет для изучения в высших технических заведениях и удел исключительно опытных профессионалов.
В теории.
Теперь совместите в воображении безумный эзотерический язык программирования и связку из транспилера и компилятора.
Именно эту дичь мы сейчас и будем творить.
Но все же объясню чуть подробнее, для обывателей:
программировать на самом Brainfuck безумно тяжело и ничего сложнее «Hello, World» у вас не выйдет пока вы не рехнетесь, но если использовать транспилер, — становится возможно писать код уже на С (или С‑подобном языке) и гораздо более интересные вещи.
Полный вариант занимает ~1.6Mb одной строкой, представляте как обрадуется любимый преподаватель такой оригинальной работе?
Главное чтобы вас потом не нашли.
Теперь перейдем к инструментарию для наших веселых шалостей.
Гримуар
Поскольку программисты по большей части ребята веселые, оказалось что для языка Brainfuck реализовано очень много всего интересного. Ниже будет небольшой обзор самых отбитых инструментов, которые мы будем использовать во имя Луны для создания тестового проекта.
Вся эта радость отлично встраивается например в Python‑скрипт работающий с нейросетями, тем самым добавляя новую грань смысла слову «blackbox», которым так любят прикрывать свои провалы ML‑разработчики.
К сожалению этот проект очень далек до завершения:
For instance, BFPY can only translate arithmetic operations (addition, subtraction, multiplication, floor division and power). There is still a lot of features to implement for it to be a proper Python runtime alternative
Поэтому если у вас есть лишний $1млн и ненависть к человечеству — можете проинвестировать в этот замечательный проект. Деньги пойдут автору на лечение в лучшей дурке.
Brainfix
Следующий проект зашел немного дальше в своем безумии, поэтому именно с его помощью мы с вами и сделаем тестовое приложение:
BrainFix is a compiler/language that takes a C-style language (although the syntax slowly evolved to something very close to Javascript for some reason) and compiles this into BrainF*ck, an esoteric programming language consisting of only 8 operations.
Для проверки корректности, вставляем полученный код Brainfuck в онлайн-интерпретатор:
Чего только не бывает в интернете.
Там еще многоинтересныхпримеров, но большие программы будут ощутимо долго компилироваться в код на Brainfuck, имейте ввиду.
Но едем дальше.
BFC
Как лаконично описывает проект его автор: «an industrial‑grade Brainfuck compiler» и он при этом совсем не врет:
bfc includes an extensive range of optimisations. This page discusses the techniques used, and gives examples of BF programs that are transformed in each case.
Написана эта штука разумеется на Rust (неужели вы сомневались?) и сейчас мы будем ее пенетри.. ээ использовать.
Поскольку дело происходит на FreeBSD (неужели я забыл об этом упомянуть?), с помощью переменной окружения RUSTFLAGS необходимо указать путь /usr/local/lib, который почему‑то не учитывается сборщиком cargo.
Также на машине должен быть сам Rust и 14я версия LLVM.
Зато теперь вы тоже знаете как использовать Rust на FreeBSD, надеюсь эти знания помогут вам построить успешную карьеру в ИТ-индустрии а не наставят на путь экстремального ИТ-терроризма.
В результате успешной сборки в каталоге ./target/debug появится готовый к использованию бинарник bfc:
Предупреждения компилятора, которые не решился исправлять даже автор
Что же мы будем делать с оптимизирующим компилятором Brainfuck? Разумеется использовать для всего хорошего и замечательного:
Это тот самый пример с «Hello world!» сгенерированный выше из С‑подобного кода, который с помощью замечательного оптимизирующего компилятора превратился в настоящее приложение:
Только что собранное, нативное приложение на Brainfuck
Итого
Повторим еще раз всю цепочку для лучшего запоминания:
Код на С‑подобном языке → транспилер Brainfix → компилятор Bfc → готовое приложение
В чем отличие от обычной разработки:
Вы спокойно сдаете в качестве лабы код на Brainfuck, который будет компилироваться в запускаемый бинарник и вполне себе подпадать под термин «исходный код».
Разумеется ваш любимый преподаватель будет в полном восторге, пытаясь в этом коде разобраться, в то время как вы никаких сложностей даже не увидите, поскольку настоящий код с читаемой логикой останется на уровне транспилера.
Правда весело?
Примерно из-за таких милых шалостей мои бывшие преподы до сих пор меня помнят мою фамилию.
Ну а если вас попросят написать тестовое задание, вставив в постановку фразу «на любом известном вам языке» — теперь вы знаете как и на чем его делать.
Само собой вся изложенная информация приведена исключительно в развлекательных целях, не стоит применять подобные технологии на практике в реальных проектах.
Рассказываю об одном весьма необычном инструменте, способном удивить даже очень изысканную публику. Поскольку с его помощью можно быстро и безболезненно.. сменить пол любимой программе для Linux. Добро пожаловать, снова.
Вдумчивое чтение содержимого этих терминалов может привести в дурку, я предупредил.
The Witchcraft Compiler Collection
Сей замечательный проект — отличное доказательство существования черной магии и колдовства тому, как мало на самом деле мы знаем об устройстве собственных программ.
По крайней мере лично автор весьма смутно представляет, как эта адская штука вообще работает, несмотря на весь свой опыт и многолетнюю практику:
WCC is a collection of compilation tools to perform binary black magic on the GNU/Linux and other POSIX platforms.
Да да, "черная магия" тут дословная цитата из описания проекта, а не выдумки или особенности перевода.
Если вкратце, WCC это такой набор очень специфичных утилит для препари.. исследования чужих программ.
Информации об этой штуке в сети крайне мало, фактически помимо проекта на Github, существует лишь одна интересная презентация, показанная на конференции Defcon, слайды из которой также использовались в статье.
Так что материал получился весьма редким.
Сборка и инвольтация эгрегора
К сожалению проект успел немного устареть, хотя и присутствует в виде готовых пакетов во многих дистрибутивах Linux. Однако в Mageia, которую автор использовал ради колдовской ступы на логотипе в качестве тестовой среды, WCC почему-то не оказалось, так что пришлось собирать руками.
Согласно описанию, необходимо установить следующие пакеты:
capstone, glibc, libbfd, libdl, zlib, libelf, libreadline, libgsl, make
Однако libbfd ныне стал частью пакета binutils и отдельно более не поставляется, а binutils скорее всего уже установлен по-умолчанию.
для завершения сборки необходимо изменить паметры, передаваемые линковщику.
Что я и проделал в файле src/wcc/Makefile:
После исправления, сборка заканчивается успешно и в каталоге bin появляются готовые к использованию "колдунские" приложения:
Колдовской набор.
Теперь можно начинать плести заклинания.
Колдунство первое: превращаем приложение в библиотеку
Нет это не шутка и не прикол, это та самая "черная магия" от системной разработки, которой не научат на курсах по вайбкодингу.
Согласно описанию, все достаточно просто, хоть и необычно - примерно как обнаружить у девушки из Тайланда кадык:
Transforming an ELF executable binary into an ELF shared library.
Последовательность заклинаний шагов выглядит так:
Превращение утилиты cat в разделяемую библиотеку.
После вызова этих нечистивых команд, то что было рождено программой для Linux, внезапно становится разделяемой библиотекой. А узревший такое в живую быдлокодер уезжает в дурку, отсыпаться и отдыхать.
Теперь рассказываю как это колдунство работает, внимание на экран:
Из презентации WCC для Defcon.
На скриншоте выше видно, что по факту в файле изменился лишь один байт, точнее поле заголовка ELF64:
Из заголовочных файлов формата ELF.
Хотя назначение этого поля не является секретом и присутствует даже в Википедии, помимо официального руководства, до столь затейливого его применения никто на моей памяти не доходил. Кроме авторов WCC.
При вызове, первым шагом происходит откат работы линковщика:
The primary use of wcc is to "unlink" (undo the work of a linker) ELF binaries, either executables or shared libraries, back into relocatable shared objects. The following command line attempts to unlink the binary /bin/ls (from GNU binutils) into a relocatable file named /tmp/ls.o
Пример вызова:
wcc -c /bin/ls -o /tmp/ls.o
По идее уже на этой стадии должен быть получен «relocable file», с которым может работать обычный gcc. Например должна отрабатывать команда:
И в результате действительно получается разделяемая библиотека, которую можно спокойно линковать с вашим приложением:
file /tmp/ls.so /tmp/ls.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=68d93a1d888eb560b8842 55a59c37cd6be0adddd, not stripped
Пример использования такой библиотеки, созданной с помощью темного ритуала из чужой программы показан на этом слайде:
Вставляет покруче Некрономикона.
Но едем дальше.
Получение имени пользователя радикальным способом - через рефлексию в плеере VLC (!)
Колдунство второе: рефлексия для.. чистого С
Следующий уровень безудержного колдовского веселья показан на скриншоте выше, но разумеется нуждается в пояснениях:
The witchcraft shell accepts ELF shared libraries, ELF ET_DYN executables and Witchcraft Shell Scripts written in Punk-C as an input. It loads all the executables in its own address space and makes their API available for programming in its embedded interpreter.
Вообще этот абзац - один сплошной "майндфак" для всех, кто хоть немного знаком с языком С, в лучших традициях Лавкрафта погружающий разум несчастных быдлокодеров во мрак ультрахардкора.
Хотя авторы WCC на голубом глазу заявляют:
This provides for binaries functionalities similar to those provided via reflection on languages like Java.
до рефлексии уровня Java/.NET тут все же очень далеко и вызывать столь интересным образом методы в чужих библиотеках получается далеко не всегда.
Теперь рассказываю, как можно попробовать сие темное колдунство в действии. Интерпретатору wsh из состава WCC скармливается разделяемая библиотека или ELF-приложение:
wsh /usr/sbin/apache2
К сожалению стабильность работы вызывает вопросы, поэтому тут 50/50 — колдунство может сработать, а может выпасть segmentation fault. Если был передан бинарник — автоматически произойдет его «библификация», описанная выше.
Если сработало, появится сообщение зелеными буквами на черном фоне:
loading of libified binary succeeded
И можно будет пытаться вызывать:
a = ap_get_server_banner() print(a)
Код выше — отдельный язык, такая своеобразная помесь С и Lua, с весьма поэтическим названием:
The resulting API, a powerful combination of lua and C API is called Punk-C
Авторы решили не заморачиваться с типами данных, поэтому и родилась на свет эта дикая помесь ужа с ежом С и Lua.
Так выглядит более сложный пример использования:
#!/usr/bin/wsh
-- Computing a MD5 sum using cryptographic functions from foreign binaries -- (eg: sshd/OpenSSL)
Для демонстрации работы необходимо вначале загрузить одну из библиотек, реализующих функции MD5-хеширования, что и было немедленно проделано:
Считаем MD5-хеш с помощью метода, вызванного через рефлексию из бинарника sshd.
Как видите все работает и действительно отображается MD5-хеш для введенной строки, вычисленный с помощью вызова функции из бинарника sshd.
Без исходников и без линковки.
Но это далеко не все колдовские приколы, которые позволяет творить WCC.
Колдунство третье: восстановление флагов сборки
Хотя авторы WCC опять несколько преувеличивают, рассказывая про возможность восстановления абсолютно всех флагов сборки:
When compiling C code, it is often required to pass extra arguments to the compiler to signify which shared libraries should be explicitly linked against the compile code. Figuring out those compilation parameters can be cumbersome. The wldd commands displays the shared libraries compilation flags given at compile time for any given ELF binary.
На практике же речь идет только о библиотеках:
Ключи сборки mplayer.
Так что речь больше про удобство использования, чем про уникальный функционал.
Колдунство четвертое: генератор заголовков
Последнее в этой статье, но далеко не последнее по важности и применимости:
The wcch command takes an ELF binary path as a command line, and outputs a minimal C header file declaring all the exported global variables and functions from the input binary. This automates prototypes declaration when writing C code and linking with a binary for which C header files are not available.
Вот тут действительно респект и жертвоприношение уважение, поскольку столь мощное колдунство может сильно помочь в разработке:
Генерация заголовочного .h файла из бинарника sshd. Без исходного кода.
Но все же стоит помнить про ограничения технологии и не ждать 100% корректности получаемых заголовков.
В качестве иллюстрации, так выглядит небольшая часть заголовков методов, полученных этим генератором:
придется долго вычищать вручную, чтобы полученный заголовочный файл можно было использовать.
Резюмируя
WCC это по истине необычный и редкий проект, к сожалению (или к счастью) неизвестный широкой программерской публике.
Хотя его использование требует серьезной подготовки и определенных навыков, а работа утилит часто нестабильна - эффект в виде зарева полыхающих пердаков быдлокодеров простых разработчиков выходит эпический.
Если вы занимаетесь серьезной разработкой на C/C++ под Linux и ненавидите человескую расу, думаю стоит ознакомиться с этой штукой поближе, чтобы включить WCC в свои темные планы и нечестивые эксперименты.