alex0x08

alex0x08

Очень давно в этом вашем ойти. Видел сеть до интернета, помню интернет без фейсбука. До сих пор пишу код. ТГ/VK/Instagram/LinkedIn/Youtube/Rutube/Dzen: alex0x08 https://0x08.ru/ https://blog.0x08.ru/
Пикабушник
в топе авторов на 662 месте
614 рейтинг 9 подписчиков 0 подписок 17 постов 6 в горячем
3

Разработка на Java без всего

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

Снова показываю как вести разработку «голыми руками» — без IDE, документации и даже интернета. На этот раз с помощью «пользовательской» Ubuntu Linux и OpenJDK.

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

Жертвам «слабой памяти» посвящается.

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

Видео

На этот раз для большего угара помимо статьи было записано и видео, где показан весь процесс «полевой разработки» на Java, с одним только JDK:

Также ролик можно посмотреть на VK Video и Youtube.

Тестовое окружение

Для большей чистоты эксперимента был взят Live-образ Ubuntu Desktop 24.04.3 LTS, записан на флешку, флешка вставлена в один из рабочих ноутбуков, который затем с нее был загружен.

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

Из инструментов у нас будет лишь текстовый редактор и JDK.

И все.

Что будем писать

Самое простое что можно написать в таких полевых условиях — реверс-шелл HTTP-сервер. На самом деле написать можно много чего, особенно если посмотреть в каталог demo внутри OpenJDK:

Набор демо-проектов из состава OpenJDK 24

Набор демо-проектов из состава OpenJDK 24

Здесь и далее скриншоты из другой системы (Manjaro), чтобы не заморачиваться с их перебрасыванием из Live-системы и добавлением в статью. Тем не менее на видео все описываемые в статье шаги и весь код вбиваются каноничным способом — полностью вручную, на чистой системе, загруженной с Live USB.

Демо

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

Так выглядит демо-проект Notepad, реализующий простейший текстовый редактор:

Окно отладки справа — часть демо‑проекта.

Окно отладки справа — часть демо‑проекта.

Так выглядит демо Metalworks, с простейшей реализацией мульти-оконной системы (MDI):

Обратите внимание на меню Theme, даже у демо-проекта есть скины!

Обратите внимание на меню Theme, даже у демо-проекта есть скины!

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

Все демо-проекты содержат исходный код в архивах src.zip и собираются без внешних зависимостей.

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

Ручная разработка

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

// разумеется я не помню названий абсолютно всех
// импортируемых классов, поэтому тут стоит '*'
import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class MyWebServer {

static void handle(Socket s) {
// метод getId() устарел, поэтому его использование в
// последних версиях JDK выдает предупреждение
System.out.println("Thread: %d"
.formatted(Thread.currentThread().getId()));

// самое сложное место, которое удалось повторить на записи
// далеко не с первой попытки
try(PrintWriter out = new PrintWriter(s.getOutputStream());
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream()));) {
// поскольку используется чтение и запись строк а не байт - читаем
// строку целиком, т.е. до символа \n
String l = in.readLine();
// тут просто показываем в консоль
System.out.println(l);
// этим простым способом читаем только строку запроса,
// которая идет первой, пропустив все заголовки
// \r\n (пустая строка) - признак завершения запроса
while (l==null || l.isEmpty() || "\r\n".equals(in.readLine()));

// тут мы 'в лоб' сравниваем строку HTTP-запроса целиком
// так она выглядит до работы парсера
if ("GET /test HTTP/1.1".equals(l)) {
// поскольку мы реагируем только на один url '/test'
// формируем ниже статичный ответ
String data = "Hello from alex0x08 at "+ new Date();
// так выглядят стандартные поля ответа в 'raw' виде, без обработки
out.println("HTTP/1.1 200 OK");
// 'close' дает указание браузеру разорвать соединение
// с сервером сразу после получения данных
out.println("Connection: close");
// поскольку мы отдаем строку - ставим MIME тип 'text/plain'
out.println("Content-Type: text/plain");
// опционально отдаем размер данных
out.println("Content-Length: " + data.length());
// пустая строка - признак начала блока с данными
out.println();
// отдаем сами данные
out.println(data);
} else {
// во всех остальных случаях формируем ответ 404
out.println("HTTP/1.1 404 Not Found");
out.println("Connection: close");
out.println();
}
// нужно обязательно вызывать поскольку PrintWriter кеширует данные
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
// в любом случае закрываем клиентский сокет
try {s.close();} catch (Exception ee) {}
}
}
// стартовый метод приложения
public static void main(String[] args) throws Exception {
System.out.println("Starting..");
// тоже сложное место, которое было непросто ввести по памяти
ExecutorService p = Executors.newFixedThreadPool(10);
// создание 'серверного' сокета, который будет прослушивать
// указанный порт
// поскольку хост не указан - будут прослушиваться все (0.0.0.0)
ServerSocket ss = new ServerSocket(8089);
// бесконечный цикл, который нужен тк метод accept() - блокирующий
// и выход из него произойдет после получения входящего подключения
while (true) {
// получен клиентский сокет
Socket s = ss.accept();
// запуск асинхронной обработки
p.execute(() -> handle(s));
}
}
}

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

Данный исходный код реализует простейший многопоточный веб-сервер на Java, который отвечает лишь на один URL /test и отдает заранее заданную строку с датой.

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

Компиляция выполняется как и в записи всего одной командой:

javac -cp . MyWebServer.java

После чего появится один единственный class-файл c совпадающим именем. Поскольку пакеты не использовались, для запуска достаточно указать в качестве classpath текущий каталог:

java -cp . MyWebServer

Но это все лирика и понты.

Когда кончается человеческая память

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

Для примера, автор при записи видео столкнулся с таким в двух местах:

длинные классы-обертки над потоками (stream) сокета и сложное название статичного метода, создающего экземпляр ExecutorService.

И то и другое получилось правильно ввести далеко не с первой попытки.

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

Невероятно но факт:

подсмотреть названия системных классов и методов можно.. в самом JDK!

Вот это поворот!

В последних версиях JDK появилась интересная утилита jimage, которая находится в каталоге bin (там же где и главные бинарники java и javac).

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

Листинг системных классов JDK с полными именами.

Листинг системных классов JDK с полными именами.

Правда знание полного имени класса не всегда помогает, поскольку в JDK много вложенных системных классов, которые по идее вызывать снаружи не надо.

Команда для запуска:

jimage list $JAVA_HOME/lib/modules |less

Где переменная JAVA_HOME указывает на каталог с установленной JDK:

Так вы увидите названия всех системных классов, но что делать с методами?

Вытаскиваем сигнатуры методов

Тут тоже есть решение, поскольку эта же самая утилита позволяет распаковывать jmod-файлы в которых находятся системные классы JDK:

jimage extract --dir=/opt/src/tmp $JAVA_HOME/lib/modules

А еще одна утилита javap позволяет посмотреть метаданные class-файла, в том числе сигнатуры всех методов:

cd /opt/src/tmp/jre
javap java.base/java/nio/Bits.class

Так выглядит результат:

Вот этого уже с запасом хватит для полевой разработки в условиях крайнего Севера.

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

Исходники JRE

Если вам совсем повезет, в каталоге JDK/lib будет находиться файл src.zip, внутри которого будут исходники всех системных классов JRE:

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

В распакованном виде:

Внутри находится исходный код всех классов Java, используемых в JDK:

cat java.base/java/io/Bits.java |less

Для примера, так выглядит исходный код класса java.io.Bits, который мы просматривали выше с помощью javap:

Как видите тут есть все, включая комментарии.

Как видите тут есть все, включая комментарии.

Эпилог

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

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

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

Показать полностью 10 1
21

Snapd, 100% загрузка cpu и баг ядра

Серия Мы не пишем в техподдержку

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

Тот самый баг, смотрит на вас с экрана.

Тот самый баг, смотрит на вас с экрана.

Вводная

Эмм с чего бы такого начать, чтобы не испугать раньше времени и не заставить устанавливать *BSD.

Есть на свете одна компания, которой мы помогаем с ИТ и есть у нее несколько виртуальных серверов на Ubuntu Linux, используемых для половых утех разработки и тестирования.

Ubuntu там использовалась нормальной (для сервера) LTS‑версии, но в какой‑то момент — в погоне за патчами безопасности ее обновили до текущей.

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

Все происходило летом 2025 года, поэтому речь про версию 25.04 Ubuntu Linux, которая использует ядро 6.14 (запомните этот важный момент):

Баг

Однажды сисадмин компании-заказчика заметил слишком частую и сильную нагрузку на CPU, создаваемую процессом snapd, который является частью пакетного менеджера Snap.

Скриншот был взят из сети, поэтому «шакальего» качества;)

Скриншот был взят из сети, поэтому «шакальего» качества;)

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

Но в этот раз 100% загрузка CPU происходила.. строго по расписанию:

Нет, это не осеннее обострение, дело происходило летом.

Нет, это не осеннее обострение, дело происходило летом.

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

Отдельно порадовал ответ ИИ:

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

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

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

Чуть ниже по переписке видно, что баг особо ярко проявляется на ноутбуке, работающем от батареи:

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

На счастье, на машине осталась сборка 6.14 версии ядра с патчами от Xanmod, которая использовалась для статьи про l9ec.

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

Наконец источник проблем был найден:

Чуть ниже по переписке обнаружился и тестовый код на C, демонстрирующий проблему, вот такой:

#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/wait.h>

int main() {
int e = epoll_create1(0);
struct epoll_event event = {.events = EPOLLIN};
epoll_ctl(e, EPOLL_CTL_ADD, 0, &event);
const struct timespec timeout = {.tv_nsec = 1};
epoll_pwait2(e, &event, 1, &timeout, 0);
}

А так это выглядит в действии:

Обратите внимание на загрузку CPU и блокировку выхода из приложения

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

Патч целиком находится тут, место исправления выглядит как-то так:

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

Текущее состояние

Формально проблема была решена еще летом этого года, патч попал в mainline и пакет с обновлением ядра от команды Ubuntu:

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

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

При подозрении на описанный баг — попробуйте собрать тестовое приложение (см. выше) и запустить в своем окружении.

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

В исправленном ядре тестовое приложение немедленно завершится.

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

Позже обновили уже штатными средствами дистрибутива до текущей актуальной версии.

Эпилог

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

1. Linux — могила, *BSD - сила

Шучу, разумеется неподготовленным пользователям в BSD-системы лучше не лезть совсем, но задуматься (или хотя-бы просто знать) о реалиях функционирования Linux все же стоит. Чтобы факт выноса мозга ядру из прикладного ПО не стал для вас неприятным сюрпризом.

2. Граница между прикладкой и системной разработкой весьма абстрактна

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

3. Любой уважающий себя сисадмин и DevOps должны знать С

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

И поверьте моему печальному опыту:

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

4. Считайте деньги, хотя-бы иногда

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

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

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

P.S.

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

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

Творим дичь с помощью инструментов веб-разработки

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

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

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

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

Наш волшебный дикий веб

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

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

Самые продвинутые из читателей вспомнят PWA или какой-нибудь React Native с Flutter — предел полета фантазии обычного разработчика.

Что плохо:

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

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

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

Дичь первая: HTMLang

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

They were laughing that HTML was not a real programming language... WHO"S LAUGHING NOW!!11

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

Вот так выглядит реализация знаменитого FizzBuzz:

<!DOCTYPE html>
<html>
<head>
<title>FizzBuzz</title>
</head>
<body>
<h3>Look into the DevTools Console</h3>
<htmlang style="display: none;">
<call target="console.log"><s>Generating FizzBuzz...</s></call>
<for><v>i</v> <n>1</n> <n>100</n>
<cond>
<when>
<eq>
<mod>
<v>i</v>
<n>15</n>
</mod>
<n>0</n>
</eq>
<call target="console.log">
<s>FizzBuzz</s>
</call>
</when>
<when>
<eq>
<mod>
<v>i</v>
<n>3</n>
</mod>
<n>0</n>
</eq>
<call target="console.log">
<s>Fizz</s>
</call>
</when>
<when>
<eq>
<mod>
<v>i</v>
<n>5</n>
</mod>
<n>0</n>
</eq>
<call target="console.log">
<s>Buzz</s>
</call>
</when>
<else>
<call target="console.log"><v>i</v></call>
</else>
</cond>
</for>
</htmlang>
<script src="./HTMLang.js"></script>
</body>
</html>

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

Кстати кто там рассказывал на лекциях про «декларативный язык разметки» и «общую неполноценность»?

HTML (от англ. HyperText Markup Language — «язык гипертекстовой разметки») — стандартизированный язык гипертекстовой разметки документов для просмотра веб-страниц в браузере.

Зря старались, автор этого проекта тем временем спокойно пишет в консоль тегами HTML:

<call target="console.log">
<s>FizzBuzz</s>
</call>

А все потому, что не надо нанимать системных программистов, прошедших полноценное обучение по дисциплинам CS (вроде курса по разработке компиляторов) для работы штатным говночистом разработчиком в обычном корпоративном проекте.

Дичь вторая: HTML-as-programming-language

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

Но только он зашел в этом процессе несколько дальше предыдущего.

Как вам например функция на чистом HTML:

<def multiplyFunction returns=int> <!-- You can create functions -->
<param a type=int/>
<param b type=int/>
<return>a * b</return>
</def>

<def main>
<var result type=int>
<!-- Create variables -->
<multiplyFunction>
<!-- and store the result of the function in the variable -->
<param>5</param>
<param>6</param>
</multiplyFunction>
</var>
</def>

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

Но к сожалению уже слишком поздно, поэтому делюсь откровениями:

Замечательный пайплайн с вызовом компилятора HTML, правда?

Замечательный пайплайн с вызовом компилятора HTML, правда?

Да, вы все правильно поняли — это самый настоящий компилятор из HTML в нативный ELF64.

А сейчас совсем поплохеет:

To write code for Adruino/AVR microcontrollers, (Arduino UNO for example) you need to put a DOCTYPE tag in your HTML file.

For example:

<!DOCTYPE avr/atmega328p>

Да, это была оригинальная задумка автора — разработка для микроконтроллеров на HTML, я ничего не придумываю.

К слову, небольшая магия с #include <stdio.h> на скриншоте выше была необходима как раз потому, что компилятор предназначен для микроконтроллеров и не добавляет в генерируемый код на С этот стандартный для обычной ОС заголовок.

Вот так выглядит эта <a href="https://pikabu.ru/story/tvorim_dich_s_pomoshchyu_instrumentov_vebrazrabotki_14099356?u=https%3A%2F%2Fwww.arduino.cc%2F&t=%D0%B6%D0%B5%D0%BB%D0%B5%D0%B7%D0%BA%D0%B0&h=b1ac8ec1b0c44147da9621c7949df1a76be5a0ec" title="https://www.arduino.cc/" target="_blank" rel="nofollow noopener">железка</a>, если никогда не видели.

Вот так выглядит эта железка, если никогда не видели.

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

А мы тем временем переходим к следующему замечательному проекту.

Дичь третья: ass-js

Таким названием и не менее характерным логотипом, авторы честно намекают на суть проекта:

Assembler implemented in JavaScript

Полагаю вы еще никогда не устанавливали компилятор ассемблера с помощью npm? Что ж, все когда‑нибудь бывает в первый раз:

npm i ass-js

Так выглядит классический «Hello, world»:

import {X64} from '../src/index';

const asm = X64();
asm.code(_ => {
_('db', 'Hello World!\n');
_('mov', ['rax', 1]); // 0x48, 0xC7, 0xC0, 0x01, 0x00, 0x00, 0x00
_('mov', ['rdi', 1]);
_('lea', ['rsi', _('rip').disp(-34)]);
_('mov', ['rdx', 13]);
_('syscall');
_('ret');
});

console.log(asm.toString());

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

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

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

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

Зато есть отдельный туториал по разбору примера с «Hello world», где по шагам разобрано как оно работает.

Еще нашелся замечательный вопрос к автору:

I was looking at your project and I couldn't figure out a reason as to why I would (and what, rather) implement with this.

Который как бы намекает на уровень треша и угара в этом проекте. Но едем дальше — к следующему отбитому уникальному проекту.

Дичь четвертая: ts2c

Тут все просто и очевидно:

Produces readable C89 code from JS/TS code.

Собственно все кроме смысла существования этого замечательного проекта — понятно и очевидно. Как-то так выглядит весь пайплайн:

Если вам вдруг будет нужен транспилер из Typescript в чистый Си — берите и пользуйтесь, благо проект очень даже рабочий:

npm install -g ts2c

Работает кстати и из браузера:

<script src="https://unpkg.com/typescript"></script>
<script src="ts2c.bundle.js"></script>
<script>
var cCode = ts2c.transpile("console.log('Hello world!')");
alert(cCode);
</script>

Есть даже онлайн версия:

Несмотря на то что автор честно пишет о куче недоработок:

Work in progress: it works, but only about 70% of ES3 specification is currently supported: statements and expressions - 95%, built-in objects - 17%.

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

Дичь пятая: nerd

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

По описанию, обещающему бесконечные ништяки:

Javascript's God Mode. No VM. No Bytecode. No GC. Just native binaries.

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

Как нетрудно догадаться, вместо нормального JavaScript тут тоже что-то свое божественное:

NerdLang is a substract of JS with some additions, focus on efficiency.

И это «свое» скажем так застряло в далеком прошлом:

Supporting EcmaScript 3 standard

На минуточку, 3я редакция стандарта вышла еще в далеком 2000м году.

А сам проект пытается в который раз «натянуть сову на глобус» и залезть туда, где последовательно обломали клыки все крупные корпрорации уровня Google:

Nerd is a JavaScript native compiler aiming to make JavaScript universal, Nerd is able to compile native apps for Windows, Mac, Linux, iOS, Android, Raspberry, STM32 and more.

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

Пайплайн (присутствует на титульном скриншоте) выглядит вот так:

Автор настолько суров, что запихал инстукции сборки и линковки модуля работы с сокетами в package.json:

{
..
"version": "0.0.1",
"nerd":
{
"env": ["std"],
"read_only": [],
"lib":
{
"win32":
[
"-D_WIN32_WINNT=0x0600",
"-Wno-narrowing",
"-D_GNU_SOURCE",
"-I{__EXTERN__}/libuv/include/",
"-I{__EXTERN__}/libuv/src/",
"-D_CRT_SECURE_NO_DEPRECATE",
"-D_CRT_NONSTDC_NO_DEPRECATE",
"{__EXTERN__}/libuv/src/*.h",
"{__EXTERN__}/libuv/src/*.c",
"{__EXTERN__}/libuv/src/win/*.h",
"{__EXTERN__}/libuv/src/win/*.c",
"-I {__MODULE__}/httplib/uWS/",
"-I {__MODULE__}/httplib/uSockets/",
"{__MODULE__}/httplib/uSockets/*.c",
"{__MODULE__}/httplib/uSockets/crypto/*.c",
"{__MODULE__}/httplib/uSockets/eventing/*.c",
"-DLIBUS_NO_SSL",
"-DUWS_NO_ZLIB",
"-fpermissive",
"-w",
"-lm",
"-ladvapi32",
"-liphlpapi",
"-lpsapi",
"-lshell32",
"-luser32 ",
"-luserenv",
"-lwsock32",
"-lws2_32"
]
}
}
}

Увидев вот такой package.json, знакомый веб-разработчик решил навсегда уйти из профессии и теперь пасет коз в горах Кавказа. Ну а я всего лишь не рискнул адаптировать такое для сборки под Linux, так что вы останетесь без примера запуска HTTP-сервера на этом чудище.

Дичь шестая: lemon

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

Lemon is a framework for building Javascript runtime software, built on the Chrome V8 Javascript Engine.

Мне он понравился своей предельной простотой (по сравнению со всеми остальными проектами) и легкостью встраивания.

Специально показываю скрипт сборки целиком:

CXX = g++
V8 = engine/lib/v8
define INCLUDE
$(V8)/include
engine/Core.cpp
engine/Environment.cpp
engine/Lemon.cpp
engine/StaticHelpers.cpp
engine/ObjectCreator.cpp
endef
define APP
app/*.cpp
endef
define LIB
$(V8)/out/x64.release/obj/
endef
define OBJ
v8_monolith
endef
export INCLUDE
export APP
export LIB
export OBJ
build:
(CXX) -I " class="formula inline">$INCLUDE $$APP -L $$LIB -l $$OBJ -std=c++0x -pthread -o lemon

И.. это все.

Настолько простую сборку V8 вижу впервые, честно.

Оно действительно собирается одной командой:

Ниже показано как выглядит двойной «Hello, world», в котором есть как часть на JavaScript так и часть на C++ — немного подумав объединил два примера из документации в один.

App.js:

version();
console.log("Превед из JS");
helloworld();

App.hpp:

#ifndef APP
#define APP

#include "../engine/Lemon.hpp"
using v8::Context;

class App : public Lemon {
public:
void Start(int argc, char* argv[]);
void SetupEnvironment();
};
#endif

App.cpp:

#include "App.hpp"

using namespace v8;

static void Log(const FunctionCallbackInfo<Value>& args) {
HandleScope scope(args.GetIsolate());
String::Utf8Value str(args.GetIsolate(), args[0]);
const char* cstr = StaticHelpers::ToCString(str);
fprintf(stdout, "%s", cstr);
fprintf(stdout, "\n");
fflush(stdout);
}
static void HelloWorld(const FunctionCallbackInfo<Value>& args) {
printf("Превед из C++ \n");
}
void App::SetupEnvironment() {
this->CreateGlobalMethod("helloworld", HelloWorld);
}
void App::Start(int argc, char* argv[]) {

for (int i = 1; i < argc; ++i) {
// Get filename of the javascript file to run
const char* filename = argv[i];
// Create a new context for executing javascript code
Local<Context> context = this->CreateLocalContext();
// Enter the new context
Context::Scope contextscope(context);
this->CreateGlobalObject("console")
.SetPropertyMethod("log", Log)
.Register();
// Run the javascript file
this->RunJsFromFile(filename);
}
}

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

Спросите с чего столько радости?

Потому что это самый настоящий V8, не самопал с реализацией ECMAScript «в переводе Гоблина», а именно тот самый движок, который используется в браузере Chrome — со всеми оптимизациями и наворотами.

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

Словом берите на вооружение, пригодится.

Одной строкой

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

js-ziju

Compile javascript to LLVM IR, x86 assembly and self interpreting

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

JS-ASM

JavaScript Assembler x86-16

Генерирует готовые COM-файлы времен DOS, но под эмулятором Dosbox они работать отказались.

Duktape

Duktape — embeddable Javascript engine with a focus on portability and compact footprint

Более продвинутый и известный аналог Lemon, который я нашел слишком поздно и не успел посмотреть.

clangor

«clang ported to js» — можно сразу в цитаты добавлять.

Сломанная и сильно устаревшая сборка, но сам проект — очень крутой, поскольку это полноценный компилятор Clang, вытащенный в веб.

Вот тут есть онлайн версия.

llvm.js

LLVM compiled to JavaScript using Emscripten

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

Она же в готовом виде онлайн.

jssat

Compile JS into LLVM IR - JavaScript Static Analysis Tool

Вот тут находится статья про этот проект, но мне собрать так и не удалось.

js2cpp

A toy js -> c++ compiler written in coffeescript. Uses escodegen to write c++ and tern to figure out types.

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

js-to-c

Compiled implementation of Javascript, targeting C (for fun)

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

Letter

Letter is a compiler project built in TypeScript using LLVM node bindings.

Очень интересный, но к сожалению устаревший проект — требует 13й LLVM и старую же версию Node.js.

Подружить с новыми версиями LLVM и Node не удалось.

CaptCC

A tiny C compiler written purely in JavaScript.

Еще один интересный, но неработающий проект — сам компилятор отработал, но ассемблерный код отказался собираться.

Эпилог

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

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

Либо умеете быстро бегать и имеете запасное гражданство.

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

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

Plexus P/20: самый редкий UNIX-сервер

Серия Некромантия старой школы

Который был оживлен и запущен в эмуляторе, спустя 40 лет после банкротства компании‑создателя. Самая редкая операционная система и самое редкое железо из всего что автор когда‑либо видел за жизнь.

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

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

Plexus

Всего было создано около 2500 таких машин, производила их с 1980 по 1988 небольшая калифорнийская компания Plexus Computers:

3833 North First Street San Jose, California, 95134, United States (408)943-9433

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

Еще кто-то выложил в сеть отсканированное руководство администратора для одной из последних моделей Plexus P/90:

Вот так выглядит один из последних сохранившихся образцов, Plexus P/75:

Кнопка запуска вблизи:

Обратите внимание на бежевые панели - это не пластик а самое настоящее железо.

Обратите внимание на бежевые панели - это не пластик а самое настоящее железо.

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

Вот так выглядит еще один из доживших образцов попроще — Plexus P/35:

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

Еще один отсканированный обзор:

Действующий Plexus P/20

В 2024м году одному компьютерному энтузиасту из Калифорнии удалось заполучить Plexus P/20 в свои лапы и вернуть его к жизни. Весь процесс оживления, а также детальный обзор этой машины он записал в серию видео, которые затем выложил на Ютуб.

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

About Plexus Computer, Inc. : It started operating in 1981 and in 1988 sold its Unix and hardware side of their business to Motorola. By this time they 've already gone up to the 68020 processor with up to 48 MB of RAM and adopted the VMEbus in their P/95 product. They kept on developing their expensive and niche software imaging products under chapter 11 protection (bankruptcy) until 1989 when they finally sold the remaining software assets to "Recognition Equipment".

Сочетание фраз up to 48 MB of RAM и 1988 год думаю заставит дернуться не один читательский глаз.

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

So cool to see one of those again! I worked for a Circuit City location years ago that in the 2000s was still running serial terminals over a PLEXUS server. I do know it was based on 68030 CPUs, with a pair of 160MB SCSI drives in mirror. But it so happened that I was the only non-corporate person who had root level login on the command line. The rest of the store was based on a locked menu for sales, inventory management, etc. I had just quit consulting and as the ONLY person on site with Unix certificates, I got saddled with emergency on-call. It never happened, but oh that brings back memories!

Обратите внимание на объемы: диски по 160 мегабайт в 1988м году (!), времена ленточных накопителей и дискет на 360 килобайт.

Эмулятор Plexus P/20

https://github.com/Spritetm/plexus_20_emu

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

This emulator emulates most aspects of a Plexus P/20 system: you can boot from a hard disk image, log into UNIX and play around. The things currently unsupported are the tape drive, floppy drive, and any Multibus cards.

Если вы простой обыватель, далекий от UNIX и программирования, но каким‑то удивительным образом дочитали до этого места — у эмулятора существует веб‑версия, которая позволит увидеть UNIX System 5.2 в работе без описанных ниже приседаний со сборкой из исходников и запуском.

Мы же пойдем как обычно путем хардкора — соберем и запустим все собственными руками. Проект эмулятора свежий, поэтому спокойно и без ошибок собирается самым обычным штатным clang на FreeBSD:

cc -v
FreeBSD clang version 18.1.6 (https://github.com/llvm/llvm-project.git llvmorg-18.1.6-0-g1118c2e05e67)
Target: x86_64-unknown-freebsd14.2
Thread model: posix
InstalledDir: /usr/bin

Поэтому проблем со сборкой в линуксе не будет.

Но в Windows/Mac так просто проект не соберется — соответствующих ветвлений в исходниках нет.

Забираем проект, внутри всего одна ветка — текущая:

git clone https://github.com/Spritetm/plexus_20_emu.git

Собираем:

cd plexus_20_emu
gmake

Эмулятор маленький, собирается очень быстро, после сборки появится готовый бинарник emu:

Для работы эмулятора необходимы два ROM-файла и образ диска, все три находятся в соседнем репозитории с информацией по Plexus P/20.

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

git clone https://github.com/misterblack1/plexus-p20.git
cd plexus-p20
cp ROMs/U1*-MERGED.BIN ../plexus_20_emu/
cp disk/plexus-sanitized.img.gz ../plexus_20_emu/
gunzip ../plexus_20_emu/plexus-sanitized.img.gz

В результате в корневом каталоге эмулятора у вас должно появиться три файла: два.BIN и один.img. Их имена зашиты в код эмулятора, поэтому переименовывать нельзя.

Запускаем эмулятор:

./emu

После прохождения тестов оборудования появится стадия single mode:

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

init 2

Появится диалог задания даты и времени, думаю очевидно что тут есть проблема 2000 и задать можно только дату до 1996го года.

Затем пойдут проверки файловой системы:

И наконец появится приглашение авторизации:

Введите root, затем вместо ввода пароля нажмите <Enter>, появится приглашение командной строки от суперпользователя, с полным доступом:

Добро пожаловать в самый редкий UNIX на свете:

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

Вывод корневых каталогов:

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

Для попадания можно использовать.. стандартный su:

su adrian

Или же зайти под ним из приглашения (пароля нет):

В системе есть cat и работают пайпы:

И даже есть вот такой артефакт:

Вывод содержимого классического /etc/passwd, до сих пор присутствующего во всех UNIX-системах:

Как видите в системе используется C shell (csh), расположенный в весьма нестандартном месте:

/usr/plx/csh

К сожалению man-ы (системные руководства и справка по командам) повреждены:

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

Есть vi, grep, find, bison и компилятор Си:

Удивительная система из далекого прошлого, о которой я абсолютно ничего не знал пока не наткнулся на видео от Адриана.

P.S.

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

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

Одноклассовый энтерпрайз

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

В пригороде далекого города Нью-Дели жил простой индийский паренек со сложным именем Чандракант. Любил он маму, Кришну и общаться с волшебными говорящими грибами.

Три грани безумия на одной картинке.

Три грани безумия на одной картинке.

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

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

— О юный Чандракант! — молвил гриб. — Помни что «вера без дел мертва». Будет непросто. Враг хитер и коварен, сражаться предстоит тайно и его же оружием..

Прошли годы, затем десятилетия.

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

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


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

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

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

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

Аннотации

У современных языков вроде Java и C# есть такая замечательная вещь как аннотации — метаданные, которые содержат различные метки и дополнительные настройки, связанные с конкретным полем, методом или классом.

Выглядит это обычно так:

@Configuration
@EnableCaching
public class CacheConfig {
@bean
public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(
javax.cache.CacheManager cacheManager) {
return hibernateProperties -> hibernateProperties
.put(ConfigSettings.CACHE_MANAGER, cacheManager);
}
..
}

Все что начинается с символа @ — те самые аннотации, содержащие определенную единицу смысла, в данном случае посвященную настройке Spring Boot.

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

/**
* Сущность 'заказ'
* @since 1.0
* @Author 0x08
*/
@entity
@table(name = "strm_orders")
@indexed
public class Order extends AbstractAuditingEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "order_seq")
@SequenceGenerator(name = "order_seq")
@GenericField
private Long id;
@column(name = "customer_id", nullable = false)
private long customerId;
@column(name = "sender_id", nullable = false)
private long senderId;
@Enumerated(EnumType.STRING)
@column(name = "order_status", nullable = false)
private OrderStatus orderStatus = OrderStatus.NEW;
..

В примере выше, аннотация @Column, которой помечено практически каждое поле класса, отвечает за связывание этого поля с колонкой в таблице базы данных. Атрибут name собственно хранит название колонки.

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

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

Неожиданный но предсказуемый результат поиска по словам "SOLID rocks"

Неожиданный но предсказуемый результат поиска по словам "SOLID rocks"

SOLID

В современном программировании есть такой термин SOLID — акроним, каждая буква которого обозначает отдельную концепцию:

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

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

Особенно когда волшебный говорящий гриб нарекает избранным и готовит к великой миссии — мстить «белым варварам» за годы угнетения их же собственным высокотехнологичным оружием.

Та самая корпоративная IBM Websphere, на которой работал неповторимый оригинал.

Та самая корпоративная IBM Websphere, на которой работал неповторимый оригинал.

Часть 0. Архитектурный джихад

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

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

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

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

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

Что никак не уменьшает убойность архитектурных талантов смелого Чандраканта (да не тронет GC его классы), помноженных на мощь его веры в святое дело борьбы с «белыми угнетателями» путем сжигания их мозгов.

Оригинальный проект представлял собой связку из нескольких десятков JEE-приложений, разворачиваемых на «большой» IBM Websphere и соответственно Java 8, но ради упрощения мы адаптировали реализацию для более современной Jakarta 10, со всеми наворотами новых версий Java (см. ниже).

Начнем демонстрацию с чего‑то более‑менее безобидного, хоть как‑то попадающего в рамки адекватности:

..
@WebFilter("/*")
@WebListener
public class Foo implements Filter,ServletContextListener{
// важная переменная
private int someImportantValue = 42;
@override
public void contextInitialized(ServletContextEvent sce) {
final ServletContext sc = sce.getServletContext();
// прокидывание в контекст выполнения собственный инстанс
sc.setAttribute("foo", this);
}
..

@override
public void doFilter(ServletRequest sr,
ServletResponse sr1, FilterChain fc)
throws IOException, ServletException {
final ServletContext sc = sr.getServletContext();
// вытаскивание своего же инстанса из контекста
Foo foo = (Foo)sc.getAttribute("foo");
if (foo.someImportantValue == 42) {
// дальнейшая обработка
..
}
}
..
}

В примере выше храбрый индийский архитектор Чандракант с помощью двух аннотаций, общего контекста и молитв Кришне использовал одну и ту же копию основного объекта в контексте двух разных сущностей в Servlet API (Filter и Listener), с разным жизненным циклом.

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

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

@Entity
@Table(name = "t_foo")
@NamedQueries({
@NamedQuery(name = "Foo.fetchAll",
query = "SELECT f FROM Foo f order by f.id desc")
})
@Named
@RequestScoped
public class Foo {
@Id
@SequenceGenerator
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "default_gen")
private Long id;
@size(min = 3, max = 255)
@pattern(regexp = "[a-zA-Z0-9._ -?!]+")
private String title;
..

private transient Foo instance;

@transient
@PersistenceContext
private EntityManager em;

..

public List<Foo> fetchRecords() {
return this.em.createNamedQuery("Foo.fetchAll",Foo.class).getResultList();
}
..
}

Если вы занимались разработкой для J2EE/JEE/Jakarta и видели все эти JPA, JTA, JSF в работе — уже должны были бежать за валидолом. Но если ужасы корпоративной Java-разработки обошли стороной, наверное стоит немного объяснить суть.

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

  • в качестве сущности (Entity) JPA, отвечающей за связывание с таблицей в базе данных;

  • в качестве бина CDI, который может быть использован непосредственно со страницы JSF.

Cо страницы это выглядит это как-то так:

<p>
<ui:repeat value="#{foo.fetchRecords()}" var="record">
<div style="padding-left: 2em; font-style: italic;">
<h:outputText value="#{record.title}"/>
</div>
..
</ui:repeat>
</p>

Но на самом деле тут все еще веселее — посмотрите на поле instance:

private transient Foo instance;

Которое великий индийский гуру использовал в качестве.. DTO!

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

Таких интересных классов в проекте было не один и не два, а около 700 — целая армия автономных бинов, каждый из которых отвечал только за себя от начала и до конца.

Временами бины вызывали друг-друга, временами делали это по сети, иногда — через очереди сообщений.

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

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

..
@WebMethod
@Post
@path("addMessage")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_PLAIN)
@Transactional(Transactional.TxType.REQUIRED)
public String addMessage(
@FormParam("title") String title,
@FormParam("author") String author,
@FormParam("message") String message) {
..
}

..

Если вы отличаете SOAP от REST и тем более знаете что такое JAX-WS и JAX-RS — с кода выше уже должно было морально поплохеть, поскольку сие есть самое натуральное оскорбление чувств верующих в высокие архитектурные принципы.

Для непричастных объясняю:

API состоит из методов, вызываемых удаленно, вебсервис — вариация API, вызываемая через веб, с использованием классических протоколов веба: HTTP/HTTPS.

REST и SOAP — два разных стандарта вебсервисов, один использует JSON, другой — XML (если упрощенно) для обмена сообщениями между клиентом и сервером. JAX‑WS и JAX‑RS — два разных стандарта для разметки методов вебсервиса с помощью аннотаций.

Так что в примере выше один и тот же метод используется одновременно в вебсервисе SOAP и как метод RESTful вебсервиса.

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

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

Или атрибут ServletContext.

Или через статический синглтон.

Или через контекст EJB.

Или по сети.

Или через очереди сообщений (JMS).

Гордый сын индийского народа любил разнообразие.

Резюмируя:

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

Такова сила и мощь индийской инженерной школы.

Часть 1. Одноклассовый энтерпраиз

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

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

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

Затем мы смогли развернуть столь упоротое приложение на всех основных серверах приложений, реализующих API Jakarta 10:

  • WildFly

  • Open Liberty / IBM WebSphere Liberty

  • Eclipse GlassFish

  • Payara

Так оно выглядит в работе (основной экран):

Для оформления на этот раз был взят индийский CSS-микрофреймворк с интересным для отечественного слуха названием.

Учим хинди вместе с автором:

FWIW, "choṭā" means "small" in Hindi

Так выглядит основной функционал гостевой книги — добавление новой записи в действии:

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

Теперь показываю работу с API, напоминаю это все еще один и тот же класс на Java.

JAX-WS

Вызов JAX-WS (SOAP) из клиента на Python:

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

from zeep import Client
# ссылка на wsdl файл
client = Client('http://localhost:9080/madjavaee-1.0.1-RELEASE/MegaBeanService?wsdl')
# вызов тестового метода
result = client.service.doPing()
print(result)

# создание объекта DTO
factory = client.type_factory('http://madjavaee.experiments.Ox08.com/')
message = factory.megaBean(title='new title', author='test@test.com', message='test message')
# вызов метода API для добавления записи
result = client.service.addMessage(message)
print(result)

# получить записи гостевой через API
result = client.service.fetchRecords()
print(result)
# получить количество записей через API
result = client.service.fetchRecordsCount()
print(result)

JAX-RS

Так выглядят в действии вызовы вебсервиса JAX-RS (REST) с помощью curl и браузера:

Команда для добавления поста с помощью curl:

curl -H "Content-Type: application/json" -X POST http://localhost:9080/madjavaee-1.0.1-RELEASE/api/addMessage -d '{"title":"test title2","author":"user@test.com", "message":"some message"}'

Часть 2. Психотронное оружие

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

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

Но сначала немного статистики:

~600 строк из которых ~200 — комментарии, итого ~400 строк на все приложение.

Смотрим и наслаждаемся шедевром:

package com.Ox08.experiments.madjavaee;
// Common Java
import java.io.*;
import java.util.*;
import java.util.logging.*;
// CDI
import jakarta.enterprise.context.*;
import jakarta.inject.*;
// JPA
import jakarta.persistence.*;
import jakarta.persistence.criteria.*;
// JSR 303 Validation API
import jakarta.validation.constraints.*;
// JSF
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.*;
// JSR 375
import jakarta.security.enterprise.AuthenticationStatus;
import jakarta.security.enterprise.authentication.mechanism.http.*;
import jakarta.security.enterprise.credential.*;
import jakarta.security.enterprise.identitystore.*;
// Servlet API
import jakarta.servlet.*;
import jakarta.servlet.annotation.*;
import jakarta.servlet.http.*;
// JTA
import jakarta.transaction.Transactional;
// JAX-RS
import jakarta.ws.rs.core.*;
import jakarta.ws.rs.ext.*;
import jakarta.ws.rs.*;
// JAX-WS
import jakarta.jws.*;
/**
* This is single class CRUD application, based on recent Java EE stack.
* @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a>
*/
// ordinary JPA entity annotations
@Entity
@Table(name = "t_records")
@NamedQueries({
@NamedQuery(name = "MegaBean.getAllRecords",
query = "SELECT m FROM MegaBean m order by m.id desc")
})
// CDI bean annotation, which used to register instance of this class as CDI managed bean
// This is required for EntityManager injection
@Named
// Java Faces annotation, required to trigger JSF initialization on some servers
@jakarta.faces.annotation.FacesConfig()
//(version = FacesConfig.Version.JSF_2_3) - deprecated in Faces 4.0 and upper
// JSF annotation, required to bypass jsr299 validation see WebContainer.validateJSR299Scope
@Dependent
//@ApplicationScoped or @RequestScoped are not allowed, because of @WebFilter/@WebListener annotations presence
// Servlet 3.0 annotations
// Servlet Filter - another instance of this class will be registered as servlet filter
@WebFilter("/*")
// One more instance will be registered as servlet context listener, to be used as initialization point.
// All because we can't use @ApplicationScoped and @Observes here
@WebListener
// See JSR375 spec for details
@CustomFormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage = "/index.xhtml?login=true",
useForwardToLogin = false,
errorPage = "/index.xhtml?login=true&error=true"
)
)
// used only when embedded IdentityStore in use
@jakarta.annotation.security.DeclareRoles({"admin", "user", "demo"})
// JAX-RS annotations
@ApplicationPath("api")
@jakarta.ws.rs.Path("")
// this is required for ExceptionMapper
@jakarta.ws.rs.ext.Provider
// JAX-WS binding (SOAP) Warning: conflicts with JAX-RS on OpenLiberty and Wildfly!
//@WebService
public class MegaBean extends Application implements Serializable,
jakarta.servlet.Filter,
ServletContextListener,
// Because OpenLiberty/IBM Websphere Liberty does not support combination of
// CustomFormAuthenticationMechanismDefinition and HttpAuthenticationMechanism,
// I was required to remove HttpAuthenticationMechanism interface
// Custom IdentityStore does not work without @ApplicationScoped on OpenLiberty
IdentityStore, // see JSR375
ExceptionMapper<Exception> {
public MegaBean() {
// call for JAX-RS parent class
super();
/*
* we need to set some default values to bypass JSR 303 bean validation for
* JAX-RS bean, otherwise, JAX-RS service will not work.
*/
this.author = "no@no.org";
this.createdAt = new Date();
this.message = "no no no";
this.title = "test title";
}
/**
* We need to have an instance of this class as DTO - to transfer data from
* html form
*/
private transient MegaBean current;
/**
* This class is also a CDI managed bean, remember? So here we will inject
* EntityManager
*/
@Transient
@PersistenceContext(unitName = "megaPU")
private EntityManager em;
/**
* Security context maybe null when JAAS API was not initialized, so it's wrapped with @Instance
*/
@Transient
@Inject
private jakarta.enterprise.inject.Instance<jakarta.security.enterprise.SecurityContext> securityContext;
@Transient
@Context
private ServletContext servletContext;
/**
* Ordinary JPA fields
*/
@Id
@SequenceGenerator(name = "default_gen", sequenceName = "w_default_pk_seq")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "default_gen")
private Long id; // unique id, this sequence will be created automatically too
@Size(min = 3, max = 255)
@Pattern(regexp = "[a-zA-Z0-9._ -?!]+")
private String title; // used also as 'login' field for auth
@Size(min = 3, max = 30)
@Email
private String author; // used also as 'password' field for auth
@Lob
@Column(length = Integer.MAX_VALUE)
@NotBlank(message = "message may not be blank")
private String message; //message body, CLOB/TEXT/BLOB type will be used in database
@Column(name = "created_date", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
@NotNull
protected Date createdAt;
/**
* this called from JSF page to clean up fields on page reload
*/
@WebMethod(exclude = true)
public void init() { resetFields(this); }
/**
* Each and every interface methods should be implemented and marked with
* Annotation WebMethod(exclude = true) used to avoid bug in Apache CXF (Wildfly/OpenLiberty)
* <a href="https://issues.apache.org/jira/browse/CXF-4916">...</a>
* Method 'contextDestroyed' is part of ServletContextListener interface, so must be implemented
*/
@WebMethod(exclude = true)
@Override
public void contextDestroyed(ServletContextEvent sce) {
// not used, but required
}
/**
* Part of ServletContextListener API, used on app start/reload
*/
@Override
@WebMethod(exclude = true)
// transactional is required to let EntityManager do his job
@Transactional(Transactional.TxType.REQUIRED)
public void contextInitialized(ServletContextEvent sce) {
final ServletContext sc = sce.getServletContext();
// due to CDI vs servlet conflict
sc.setAttribute("mega", this);
// we can make it only here due to stackoverflow error in eclipselink
this.current = new MegaBean();
// reset fields back to nulls - to have working JSR303 validation
resetFields(this.current);
// populate JSF version details
addVersionEnv(sc);
// try to add some initial data if database is empty
try {
if (fetchRecordsCount() == 0) {
//create test entity
final MegaBean r = new MegaBean();
r.setCreatedAt(new Date()); r.setAuthor("system@test.org"); r.setMessage("Test message"); r.setTitle("Test title");
em.merge(r);
LOG.info(String.format("automatically added default record: %d", r.getId()));
}
} catch (Exception e) {
LOG.log(Level.WARNING,
String.format("Exception on startup: %s", e.getMessage()), e);
}
}
/**
* JSF bean method, used to save form (from itself)
*/
@WebMethod(exclude = true)
@Transactional(value=Transactional.TxType.REQUIRED,rollbackOn = Exception.class)
public String save() {
// set creation date&time
current.setCreatedAt(new Date());
em.merge(current);
// this is required to reset form fields
this.current = new MegaBean();
resetFields(this.current);
// does redirect
return "/index.xhtml?faces-redirect=true";
}
/**
* Does login action from JSF page
* @throws IOException
* if God was not on our side
*/
@WebMethod(exclude = true)
public void login() throws IOException {
// we need to re-use 2 existing fields, present in this class: 'author for username and 'title' for password
final Credential credential = new UsernamePasswordCredential(author, new Password(title));
final FacesContext facesContext =FacesContext.getCurrentInstance();
final ExternalContext ec = facesContext.getExternalContext();
// should not happen, this is used to avoid class cast
if (!(ec.getRequest() instanceof HttpServletRequest req)
|| !(ec.getResponse() instanceof HttpServletResponse res)) {
ec.getRequestMap().put("login", "true");
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login failed", null));
return;
}
// check if JAAS initialized
if (!securityContext.isResolvable()) {
LOG.warning("SecurityContext cannot be resolved!"); return;
}
// try to authenticate programmatically
final AuthenticationStatus status = securityContext.get()
.authenticate(
req, res, AuthenticationParameters.withParams().credential(credential));
if (status == null) {
LOG.warning("JAAS not initialized!"); return;
}
LOG.fine(String.format("auth status: %s",status));
switch (status) {
case SEND_CONTINUE: {
facesContext.responseComplete();
break;
}
case SEND_FAILURE: {
ec.getRequestMap().put("login", "true");
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login failed", null));
break;
}
case SUCCESS: {
putCurrentUser(current);
LOG.info(String.format("logged in as %s",current.author));
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Login succeed", null));
// after redirect there will be full page reload
ec.redirect(ec.getRequestContextPath() + "/index.xhtml?ok=true");
break;
}
case NOT_DONE:
facesContext.responseComplete();
break;
}
}
/**
* Does logout action from JSF page
*/
@WebMethod(exclude = true)
public String logout() throws ServletException {
final FacesContext facesContext =FacesContext.getCurrentInstance();
final ExternalContext ec = facesContext.getExternalContext();
// check for impossible state
if (!(ec.getRequest() instanceof HttpServletRequest req)) {
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Logout failed", null));
return "";
}
req.logout();
ec.invalidateSession();
return "/index.xhtml?faces-redirect=true";
}
/**
* JPA Entity fields
* -------------------------------------------------------------------------------------------
*/
@WebMethod(exclude = true)
public String getAuthor() { return author; }
@WebMethod(exclude = true)
public void setAuthor(String author) { this.author = author; }
@WebMethod(exclude = true)
@jakarta.json.bind.annotation.JsonbTransient
public MegaBean getCurrent() {
return current;
}
@WebMethod(exclude = true)
public Date getCreatedAt() {
return createdAt;
}
@WebMethod(exclude = true)
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
@WebMethod(exclude = true)
public Long getId() {
return id;
}
@WebMethod(exclude = true)
public void setId(Long id) {
this.id = id;
}
@WebMethod(exclude = true)
public String getTitle() {
return title;
}
@WebMethod(exclude = true)
public void setTitle(String title) { this.title = title; }
@WebMethod(exclude = true)
public String getMessage() { return message; }
@WebMethod(exclude = true)
public void setMessage(String message) { this.message = message; }
/**
* JAX-RS & JAX-WS Methods
* -----------------------------------------------------------
* Each method serves for both APIs
* Ping is a test method, which respond plain text
*/
@GET
@jakarta.ws.rs.Path("ping")
@Produces(MediaType.TEXT_PLAIN)
@WebMethod
public String doPing() { return "pong: " + System.currentTimeMillis(); }
/**
* Adds new message to guestbook from API
* @param dto
* new message data
* @return
*/
@WebMethod
@POST
@jakarta.ws.rs.Path("addMessage")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
// for JAX-WS only
@Transactional
public String addMessage(MegaBean dto) {
LOG.info(String.format("prepare to add record %s , %s , %s", author, title, message));
final MegaBean r = new MegaBean();
r.setCreatedAt(new Date()); r.setAuthor(dto.author);
r.setMessage(dto.message); r.setTitle(dto.title);
// for JAX-RS, EntityManager should be injected
if (em!=null && em.isJoinedToTransaction())
return addMessageImpl(r);
// otherwise, take EntityManager from servlet context
// note: access to servletContext from JAX-RS will trigger exception:
// RESTEASY003880: Unable to find contextual data of type: jakarta.servlet.ServletContext
else {
final MegaBean mb = (MegaBean) servletContext.getAttribute("mega");
return mb.addMessageImpl(r);
}
}
/**
* This 'black magic' is required, because JAX-WS does not allow transaction injection on service method
*/
@Transactional(Transactional.TxType.REQUIRED)
@WebMethod(exclude = true)
public String addMessageImpl(MegaBean r) {
try {
r=em.merge(r);
LOG.info(String.format("saved record %d", r.id));
return String.format("Message added: %d %n", r.id);
} catch (Exception e) {
LOG.log(Level.WARNING, e.getMessage(), e);
return String.format("Error on saving: %s", e.getMessage());
}
}
/**
* Uses Criteria API to retrieve count of records
*/
@WebMethod
@GET
@jakarta.ws.rs.Path("recordsCount")
@Produces(MediaType.TEXT_PLAIN)
public Long fetchRecordsCount() {
final EntityManager em = selectEm();
final CriteriaBuilder qb = em.getCriteriaBuilder();
final CriteriaQuery<Long> cq = qb.createQuery(Long.class);
cq.select(qb.count(cq.from(MegaBean.class)));
return em.createQuery(cq).getSingleResult();
}
/**
* API method to retrieve all guestbook records
*/
@WebMethod
@GET
@jakarta.ws.rs.Path("records")
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
public List<MegaBean> fetchRecords() {
return selectEm().createNamedQuery("MegaBean.getAllRecords", MegaBean.class).getResultList();
}
/**
* API method to get currently authenticated user details
*/
@GET
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@jakarta.ws.rs.Path("details")
@WebMethod(exclude = true)
public Response userDetails(@Context SecurityContext sc) {
final java.security.Principal p = sc.getUserPrincipal(); // see sc.getCallerPrincipal() in Jakarta EE;
return p != null ? Response.ok(p.getName()).build() :
Response.status(Response.Status.UNAUTHORIZED).build();
}
/**
* Methods below are required , due to re-use of same class for both JAX-WS
* and JAX-RS
* -------------------------------------------------------------------------------------------------------
*/
// part of IdentityStore API, not used
@Override
@WebMethod(exclude = true)
public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
return Collections.emptySet();
}
@Override
@WebMethod(exclude = true)
public int priority() {
return 100;
}
@Override
@WebMethod(exclude = true)
public Set<ValidationType> validationTypes() {
return DEFAULT_VALIDATION_TYPES;
}
@WebMethod(exclude = true)
@Override
public void init(FilterConfig filterConfig) { }
@WebMethod(exclude = true)
@Override
public void destroy() { }
/**
* this filter is used to redirect from / to actual jsf page
*/
@Override
@WebMethod(exclude = true)
public void doFilter(ServletRequest sr, ServletResponse sr1, FilterChain fc)
throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) sr;
LOG.info(String.format("got request: %s", request.getRequestURI()));
// required for correct characters encoding
request.setCharacterEncoding("UTF-8");
final String p = request.getRequestURI(),
cp = request.getServletContext().getContextPath();
String url = p;
if (p.startsWith(cp)) url = p.substring(cp.length());
if ("/".equals(url) && sr1 instanceof HttpServletResponse hsr)
hsr.sendRedirect(cp + "/index.xhtml");
else fc.doFilter(sr, sr1);
}
/**
* Custom JSR375 validation
* Used in combination with IdentityStore
* @param credential
* @return
*/
@Override
@WebMethod(exclude = true)
public CredentialValidationResult validate(Credential credential) {
// should not happen
if (!(credential instanceof UsernamePasswordCredential userCredential))
return CredentialValidationResult.INVALID_RESULT;
final String login = userCredential.getCaller();
LOG.info(String.format("called validate for %s", login));
if (!USERS.containsKey(login))
return CredentialValidationResult.INVALID_RESULT;
final Map<String, Object> user = USERS.get(login);
// dumb password check
if (!userCredential.compareTo(login, (String) user.get("password")))
return CredentialValidationResult.INVALID_RESULT;
LOG.info(String.format("user %s validated", login));
return new CredentialValidationResult(login, new HashSet<>(Arrays.asList((String[]) user.get("roles"))));
}

/**
* JAX-RS exception handler
*/
@Override
@WebMethod(exclude = true)
public Response toResponse(Exception e) {
LOG.log(Level.WARNING, String.format("Exception on call : %s", e.getMessage()), e);
return Response.status(400).entity(e.getMessage()).type("text/plain").build();
}
// !! required for YASSON parser, otherwise exception will raise:
// Error accessing getter 'getEnclosingConstructor' declared in 'class java.lang.Class'
@Override
@WebMethod(exclude = true)
@jakarta.json.bind.annotation.JsonbTransient
public Set<Class<?>> getClasses() { return Collections.emptySet();}

/*
remove from JAX-RS/JAX-WS output
*/
@Override
@WebMethod(exclude = true)
@jakarta.json.bind.annotation.JsonbTransient
public Set<Object> getSingletons() { return Collections.emptySet();}
/*
remove from JAX-RS/JAX-WS output
*/
@Override
@WebMethod(exclude = true)
@jakarta.json.bind.annotation.JsonbTransient
public Map<String,Object> getProperties() { return Collections.emptyMap();}
/**
* Clean fields for provided instance
* @param m
* bean instance
*/
private void resetFields(MegaBean m) {
m.setAuthor(null); putCurrentUser(m);
m.setCreatedAt(null); m.setId(null);
m.setMessage(null); m.setTitle(null);
}
/**
* JAX-RS and JAX-WS APIs have different lifecycle, for JAX-WS, an EntityManager will be injected by CDI,
* but for JAX-RS is not (not for all servers).
* So we need some selection logic here
*/
private EntityManager selectEm() {
// if EntityManager was not injected
if (this.em!=null) return this.em;
// take instance from servlet context
return ((MegaBean) servletContext.getAttribute("mega")).em;
}
/**
* Get current user from principal
* @return
* current user's name
*/
public static String getCurrentUser() {
final FacesContext ctx = FacesContext.getCurrentInstance();
// if there is no faces context - could happen if current bean was not created by JSF
if (ctx==null || ctx.getExternalContext()==null) return null;
// get principal (the standard way) from current context
final java.security.Principal p = ctx.getExternalContext().getUserPrincipal();
return p == null ? null : p.getName();
}
/**
* Set current user's name to author field of our bean instance
* @param instance
* an instance of MegaBean, used as DTO
*/
private static void putCurrentUser(MegaBean instance) {
final String username = getCurrentUser();
if (username!=null)
instance.setAuthor(username);
}
/**
* Reads JSF version details and store as attribute of ServletContext
* @param sc
*/
private static void addVersionEnv(ServletContext sc) {
final Package facesPackage = FacesContext.class.getPackage();
final StringBuilder sb = new StringBuilder();
if (sc.getServerInfo() !=null)
sb.append(sc.getServerInfo());
if (facesPackage.getImplementationVersion()!=null)
sb.append(facesPackage.getImplementationVersion());
sc.setAttribute("versionLine", sb.toString());
LOG.info(sb.toString());
}
// credentials store, not used under Wildfly/OpenLiberty
private static final Map<String, Map<String, Object>> USERS = new TreeMap<>();
static {
final Map<String, Object> admin_user = new HashMap<>();
admin_user.put("password", "admin");
admin_user.put("roles", new String[]{"admin", "user", "demo"});
USERS.put("admin@test.org", admin_user);

final Map<String, Object> s_user = new HashMap<>();
s_user.put("password", "user");
s_user.put("roles", new String[]{"user"});
USERS.put("user@test.org", s_user);
}
// ordinary JUL logger, will not be serialized/persisted
private static final Logger LOG = Logger.getLogger("MEGA");
}

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

Часть 3. Препарируя дичь

Начнем с жемчужины индийской архитектурной мысли — использования JPA Entity и работы с Entity в одном и том же классе:

@Entity
@Table(name = "t_records")
@NamedQueries({
@NamedQuery(name = "MegaBean.getAllRecords",
query = "SELECT m FROM MegaBean m order by m.id desc")
})
@Named
..
// ниже в этом же классе
@Transient
@PersistenceContext(unitName = "megaPU")
private EntityManager em;
..
// еще ниже в этом же классе
final MegaBean r = new MegaBean();
r.setCreatedAt(new Date()); r.setAuthor("system@test.org");
r.setMessage("Test message"); r.setTitle("Test title");
em.merge(r);
..

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

Но работает тем не менее все довольно просто:

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

Да, оказывается «так можно было».

Аннотация @Entity регистрирует класс в качестве entity JPA, @Table указывает на конкретную таблицу, @NamedQueries и @NamedQuery описывает именованное JPQL-выражение для получения записей из базы - все как в других, нормальных проектах.

А затем начинается чистая шиза:

в этом же классе указывается аннотация @Named, которая превращает класс в управляемый бин CDI, с возможностью связывания зависимых полей.

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

Вставляет сюда:

@Transient
@PersistenceContext(unitName = "megaPU")
private EntityManager em;

Аннотация @Transient нужна для того чтобы скрыть вставляемое через CDI поле от механизма, отвечающего за сохранение данных в JPA.

Servlet API

Следующий интересный с точки зрения клинической психиатрии блок аннотаций отвечает за надругательство над Servlet API:

@Dependent
@WebFilter("/*")
@WebListener

Конечно же так тоже делать нельзя, более того — если попытаетесь комбинировать @RequestScoped,@SessionScoped или @ApplicationScopedи аннотации Servlet API получите отлуп а приложение упадет при установке.

Единственная причина, по которой эта дичь вообще работает — «волшебная» аннотация @Dependent, про которую никто (из моих коллег) никогда не слышал.

Согласно официальному описанию:

The default scope if none is specified; it means that an object exists to serve exactly one client (bean) and has the same lifecycle as that client (bean).

Во всех остальных случаях (для всех остальных scope) будет выбрасываться ошибка при установке. Разгадка находится в методе validateJSR299Scope, аналог которого есть в любой реализации Jakarta API.

Так что суммнарно аннотации @Named и @Dependent позволяют использовать класс в качестве бина для Jakarta Faces и одновременно использовать аннотации из Servlet API — на одном и том же классе.

Тут помимо аннотаций становятся нужны интерфейсы:

..
public class MegaBean extends Application implements Serializable,
jakarta.servlet.Filter,
ServletContextListener,
..

Поэтому у класса появляются обязательные методы, которые необходимо реализовать. Для интерфейса ServletContextListener это метод contextInitialized, ради которого собственно интерфейс и использовался:

..
@Override
@WebMethod(exclude = true)
@Transactional(Transactional.TxType.REQUIRED)
public void contextInitialized(ServletContextEvent sce) {
final ServletContext sc = sce.getServletContext();
// due to CDI vs servlet conflict
sc.setAttribute("mega", this);
// we can make it only here due to stackoverflow error
// in eclipselink
this.current = new MegaBean();
// reset fields back to nulls - to have working JSR303 validation
resetFields(this.current);
// populate JSF version details
addVersionEnv(sc);
// try to add some initial data if database is empty
try {
if (fetchRecordsCount() == 0) {
//create test entity
final MegaBean r = new MegaBean();
r.setCreatedAt(new Date()); r.setAuthor("system@test.org");
r.setMessage("Test message"); r.setTitle("Test title");
em.merge(r);
LOG.info(String.format("automatically added default record: %d", r.getId()));
}
} catch (Exception e) {
LOG.log(Level.WARNING, String.format("Exception on startup: %s", e.getMessage()), e);
}
}
..

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

Кастомная авторизация

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

полностью программная настройка авторизации, силами одних лишь аннотаций.

Конечно после красот Spring Boot все это смотрится как жалкая пародия уже не так мощно, но не забываем что Jakarta это API, у которого есть разные реализации.

И все они обязаны поддерживать вот такое:

..
@CustomFormAuthenticationMechanismDefinition(
loginToContinue = @LoginToContinue(
loginPage = "/index.xhtml?login=true",
useForwardToLogin = false,
errorPage = "/index.xhtml?login=true&error=true"
)
)
// used only when embedded IdentityStore in use
@jakarta.annotation.security.DeclareRoles({"admin", "user", "demo"})
..

Верхняя аннотация «лошадиного размера» отвечает за настройку механизма авторизации — указывает на использование form-based авторизации с дополнительными настройками.

Нижняя @DeclareRoles описывает набор ролей, используемых приложением.

К сожалению аннотация @DeclareRoles работает только в сочетании с IdentityStore, который актвируется не всеми серверами приложений.

Отбитый API

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

На класс на самом деле навешано два разных набора аннотаций, первая отвечает за инициализацию бина в качестве вебсервиса JAX-WS (старый добрый SOAP с XML):

@WebService

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

@WebMethod(exclude = true)
public String getAuthor() { return author; }

Также будут пропущены все статичные поля.

Второй набор аннотаций отвечает за инициализацию JAX-RS и с ним все несколько сложнее:

@ApplicationPath("api")
@Path("")
@jakarta.ws.rs.ext.Provider

Помимо стандартных @ApplicationPath и @Path, которые вы точно видели в официальных примерах и более нормальных проектах, тут используется аннотация @Provider.

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

@Override
@WebMethod(exclude = true)
public Response toResponse(Exception e) {
LOG.log(Level.WARNING, String.format("Exception on call : %s", e.getMessage()), e);
return Response.status(400).entity(e.getMessage()).type("text/plain").build();
}

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

Часть 4. Сборка и деплой отбитой дичи

Проект целиком выложен в репозиторий на Github, сборка осуществляется с помощью обычного Apache Maven:

mvn clean package

Для сборки и запуска использовалась стандартная OpenJDK 21, которую поддерживают все используемые в статье серверы приложений.

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

Самый простой способ увидеть наше «чудо-приложение» в действии — запустить специальный шаг Maven:

mvn liberty:run

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

В качестве СУБД на всех серверах приложений использовался встраиваемый Apache Derby, за исключением Payara, где по-умолчанию используется H2.

Open Liberty / IBM Websphere Liberty

Проект OpenLiberty это открытая реализация сервера приложений, активно разрабатываемая IBM. Ее коммерческая версия IBM Websphere Liberty позиционируется как замена «большой» Websphere и основа всех будущих продуктов IBM, создаваемых на базе Websphere.

Вся основная разработка и развитие происходят в Open Liberty, затем переносятся в IBM Websphere Liberty, для которой потом оказывается коммерческая и долговременная поддержка.

Все описанные шаги по развертыванию актуальны и применимы для IBM Websphere Liberty.

Для статьи использовалась Open Liberty версии 25.0.0.5 с профилем Jakarta 10, скачать архив со сборкой можно по ссылке.

Так выглядит наша «адская гостевая» будучи запущенной в Open Liberty:

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

К сожалению OpenLiberty не подхватывает программный IdentityStore, который мы реализовывали в самом бине ради авторизации.

Для обоих действий надо изменить файл server.xml в каталоге:

wlp/usr/servers/defaultServer/server.xml

где wlp — корневой каталог распакованного Open Liberty.

Для регистрации тестового пользователя необходимо добавить блок:

<basicRegistry id="basic" realm="WebRealm">
<user name="admin@test.org" password="admin"/>
<group name="admin">
<member name="admin"/>
</group>
</basicRegistry>

Для пула подключений к базе:

<dataSource id="DefaultDataSource">
<jdbcDriver libraryRef="phlegethLib"/>
<properties.derby.embedded createDatabase="create"
databaseName="shuggDB"/>
<containerAuthData password="y'hah" user="tharanak"/>
</dataSource>

<library id="phlegethLib">
<file name="${server.config.dir}/lib/global/jdbc/derby.jar"/>
<file name="${server.config.dir}/lib/global/jdbc/derbyshared.jar"/>
</library>

Также будет необходимо скопировать JDBC-драйвер для Derby в указанный выше каталог:

wlp/usr/servers/defaultServer/lib/global/jdbc

WAR-файл с нашей адской гостевой копируется в каталог dropins:

cp /opt/work/serial-experiments/madjpa/target/*.war wlp/sr/servers/defaultServer/dropins/

Запущенное приложение доступно по адресу:

http://localhost:9080/madjavaee-1.0.1-RELEASE/index.xhtml

Как уже упоминал выше:

Текущая версия OpenLiberty не дает использовать одновременно JAX-RS и JAX-WS вебсервис, основанный на одном и том же классе.

Хотя в предыдущих версиях прокатывало.

Поэтому придется либо закомментировать аннотацию @WebService для отключения JAX-WS либо набор аннотаций JAX-RS:

@ApplicationPath("api")
@Path("")
@jakarta.ws.rs.ext.Provider

Еще OpenLiberty не умеет генерировать WADL-файл с описанием JAX-RS сервиса, поэтому ссылка на него будет битой.

Wildfly

Wildfly (в девичестве JBoss) — один из самых известных серверов приложений с очень долгой историей. Хотя это изначально открытый проект, как и в случае с разработкой IBM (Websphere Liberty) существует отдельный коммерческий продукт на его основе — Red Hat JBoss Enterprise Application Platform.

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

Для статьи использовался Wildfly 36.0.1.Final, архив с которым можно скачать по ссылке.

Так выглядит наше чудо-приложение в работе под управлением Wildfly:

Установка приложения на Wildfly заметно проще — достаточно скопировать WAR-файл с приложением в каталог:

wildfly/standalone/deployments

Поддержка Apache Derby, JDBC-драйвер и пул подключений по-умолчанию уже присутствуют.

К сожалению текущая версия Wildfly (на момент написания статьи) также не активирует программный IdentityStore — тестовый домен JASPIC убрали в настройке по-умолчанию.

Поэтому тестовых пользователей придется создавать вручную с помощью скрипта bin/add-user.sh

Запущенное приложение доступно по адресу:

http://localhost:8080/madjpa/index.xhtml

Eclipse Glassfish

Еще один очень известный проект с длинной историей, некогда разрабатываемый самой компанией Sun Microsystems в качестве эталонной реализации J2EE.

Для статьи использовалась версия 7.0.25 с профилем Jakarta EE Platform, скачать архив со сборкой можно по ссылке.

Так выглядит развертывание и запуск нашего приложения в Glassfish:

Запускается сервер командой bin/startserv

Glassfish поддерживает и похватывает при установке программный IdentityStore (будут работать встроенные в бин учетки) и пул по-умолчанию с Apache Derby и сочетание JAX-WS и JAX-RS вебсервисов на одном и том же классе.

И даже генерацию WADL-файла он тоже поддерживает:

Единственная яркая дичь — за каким-то хреном в последних версиях перестал автоматически запускаться сервер Apache Derby, теперь для его запуска надо запустить консоль управления asadmin и выполнить команду:

start-database

Результат выполнения:

Payara

Наконец последний в сегодняшнем списке и наименее известный в наших краях проект Payara:

When commercial support for GlassFish ended in 2014, Payara Server was created as a fully-supported drop-in replacement. Payara Services was born in 2016 to offer support solutions for the application server.

Да, теперь вы тоже знаете что у Glassifsh оказывается были и коммерческие пользователи, по зову которых появилась эта самая "Payara".

Для статьи использовался Payara Server 6.2025.5 с профилем Full, скачать сборку можно по ссылке.

Запускается командой:

bin/asadmin start-domain

Так выглядит в действии развертывание и запуск нашего приложения:

Тут все совсем хорошо и никаких дополнительных шагов не требуется совсем, база (H2) запускается по-умолчанию, программный IdentityStore подхватывается автоматически из бина и определяются оба типа вебсервисов — JAX-RS и JAX-WS, которые спокойно работают одновременно.

Эпилог

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

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

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

Но если столкнетесь с таким на практике и захотите сохранить психику ваших программистов в девственном виде — теперь будете знать кому написать.

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

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

OSF/1 в эмуляторе1

Серия Некромантия старой школы

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

Так это выглядит в работе.

Так это выглядит в работе.

История

OSF/1 это еще одна редкая UNIX-система из далекого прошлого, которую вы врядли могли наблюдать в живую:

OSF/1 is a variant of the Unix operating system developed by the Open Software Foundation during the late 1980s and early 1990s. OSF/1 is one of the first operating systems to have used the Mach kernel developed at Carnegie Mellon University, and is probably best known as the native Unix operating system for DEC Alpha architecture systems.

Прямой участник корпоративных Unix‑разборок 90х известных как Unix Wars, но в отличие от A/UX (которую автор оживил и запустил в прошлом выпуске) OSF/1 не канула в лету в бурные 90е а будучи переименованной в TRUE64 UNIX поддерживалась аж до 2012го года.

Вот вам и «динозавр», внезапно отказавшийся умирать.

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

IBM used OSF/1 as the basis of the AIX/ESA operating system for System/370 and System/390 mainframes.[12] Intel Paragon supercomputers used a version of OSF/1 featuring the Mach 3.0 kernel.[13] OSF/1 was also ported by Kendall Square Research to its proprietary microarchitecture used in the KSR1 supercomputer.

Суперкомпьютер KSR1 Единственное <a href="https://pikabu.ru/story/osf1_v_yemulyatore_14080919?u=https%3A%2F%2Fglobal.museum-digital.org%2Fsingleimage%3Fimagenr%3D2310146&t=%D1%84%D0%BE%D1%82%D0%BE&h=4b7b079fa2995c9379cb8f99799979f7e7eb5c34" title="https://global.museum-digital.org/singleimage?imagenr=2310146" target="_blank" rel="nofollow noopener">фото</a> в приличном разрешении этой редкой машины.

Суперкомпьютер KSR1 Единственное фото в приличном разрешении этой редкой машины.

А также попытку адаптации компанией Apple в качестве замены A/UX:

Apple Computer intended to base A/UX 4.0 for its PowerPC-based Macintoshes upon OSF/1,[11] but the project was cancelled and PowerOpen deprecated.

Которая была отменена вместе с самой A/UX.

OpenBSD/luna88k running inside GXemul, on an emulated SGI O2 running OpenBSD/sgi (on a FreeBSD/amd64 host). Теперь попробуйте осознать прочитанное.

OpenBSD/luna88k running inside GXemul, on an emulated SGI O2 running OpenBSD/sgi (on a FreeBSD/amd64 host). Теперь попробуйте осознать прочитанное.

Эмулятор

Полностью оно называется Gavare's eXperimental Emulator за авторством Anders Gavare и согласно официальному описанию с сайта проекта, считает себя фреймворком для виртуализации:

GXemul is a framework for full-system computer architecture emulation, mostly written in 2003-2005. Several real machines were implemented within the framework, consisting of processors (ARM, MIPS, Motorola 88K, PowerPC, and SuperH) and surrounding hardware components such as framebuffers, interrupt controllers, busses, disk controllers, and serial controllers. The emulation is working well enough to allow several unmodified "guest" operating systems to run.

По сути это такой сильно разросшийся «pet project», созданный одним энтузиастом ради эмуляции особо редких систем:

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

Сборка

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

В этот раз все действия производились на обычном Mageia Linux, без BSD и прочих изысков, с использованием стандартного GCC:

Несмотря на строчку из документации по сборке:

This should work on most Unix-like systems, with few or no modifications to the source code. The basic requirement is a reasonably modern C compiler (C99).

Эмулятор написан все же на C++ а не на чистом С, поэтому для сборки требуется компилятор C++, который ныне устанавливается отдельным пакетом.

Но в остальном автор не врет:

GXemul does not require any additional third-party libraries to build.

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

  • X11 headers and libraries: for graphical framebuffer emulation.

Так что ничего дополнительно устанавливать не придется.

С версии 0.6 разработка переехала на Github, откуда мы и заберем последние правки исходников (ветка master):

git clone https://github.com/BitEdits/gxe.git

Главная проблема с этим эмулятором — неправильная работа с некоторыми эмулируемыми ОС без специального ключа сборки --debug:

When compiling on Linux via a modern GCC please bear in mind that the emulator wont work correctly if compiled with -O optimization flags. Please use -debug configure option to disable such optimization

Что выяснилось далеко не с первой попытки.

Изучаемая OSF/1 как раз из числа проблемных, поэтому попытка запуска в версии эмулятора, собранного без этого волшебного ключа выдает kernel panic:

Теперь вы тоже видели как выглядел kernel panic в 1992м, поздравляю.

Теперь вы тоже видели как выглядел kernel panic в 1992м, поздравляю.

Согласно официальному описанию, ключ --debug всего лишь отвечает за отладочную сборку:

configure for a debug build (turn off optimizations)

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

DEBUG=YES

Скрипт кстати не из известного пакета autotools а полностью собственной разработки. После каждой правки configure скрипта или шаблонов Makefile.skel (см. ниже) необходимо перезапускать настройку вызовом:

./configure

Теперь запускаем make для запуска сборки и ждем приключений.

Первая ошибка не заставит себя ждать:

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

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

В файле src/main/Makefile.skel в конец значения параметра CXXFLAGS необходимо дописать указание на версию спецификации --std=c++14:

CXXFLAGS=$(CWARNINGS) $(COPTIM) $(DINCLUDE) -std=c++14

Заново запускаем configure и сборку:

./configure
make

Следующая ошибка также связана с изменением в поведении компилятора:

И да, эта ошибка также обоснована и по хорошему код стоит отрефакторить. Когда-нибудь потом.

А на сейчас я просто отключил предупреждение, добавив в файл src/main/promemul/Makefile.skel параметр -Wno-narrowing:

CXXFLAGS=$(CWARNINGS) $(COPTIM) $(DINCLUDE) -Wno-narrowing

Повторно запускаем configure и сборку:

./configure
make

В этот раз сборка должна завершиться успешно и в корне проекта появится бинарник gxemul:

Запуск

К сожалению не удалось найти установочный диск OSF/1, поэтому я использовал готовый образ диска, в котором ОС уже была развернута.

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

Команда запуска выглядит так:

./gxemul -e 3max -X -d osf1_mips.img -j vmunix

Где -e 3max указание на тип эмулируемой машины, 3max это например DECstation 5000/200 (3MAX):

Ключ -X указывает использовать графический фреймбуфер, без него вы не увидите графического приглашения. Ключ -d указывает на путь к образу диска (в архиве по ссылке выше) а вот -j это уже особая уличная магия:

-j name  set the name of the kernel;

for DECstation emulation, this passes the name to the bootloader, for example:  --j netbsd  (NetBSD/pmax) 

-j bsd  (OpenBSD/pmax)

-j vmsprite  (Sprite/pmax)  

-j vmunix  (Ultrix/RISC)

Войти в систему можно под учетной записью root с паролем Jaguar64.

Новый пользователь создается в терминале xterm, с помощью команды adduser:

Изменить пароль можно стандартной командой passwd.

Сеть

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

Вам остается только вторая половина — настроить сеть в системе 1992 года.

Как уже описал выше, эмулятор сам создает виртуальный сетевой адаптер, который со стороны гостевой OSF/1 называется ln0.

Должна отрабатывать команда:

ifconfig ln0

В выводе должно быть видно текущее состояние адаптера.

Также эмулятор самостоятельно создает исходящий адаптер с IP-адресом 10.0.0.254, который также является маршрутизатором.

Эти детали отображаются в консоли при запуске эмулятора:

simulated network: 10.0.0.0/8 (max outgoing: TCP=100, UDP=100) simulated gateway+nameserver: 10.0.0.254 (60:50:40:30:20:10) simulated nameserver uses real nameserver 192.168.1.1

Так выглядит запрос состояния устройства со стороны гостевой OSF/1:

Как только назначим IP-адрес 10.0.0.2 виртуальному адаптеру, должна отрабатывать команда pingк хосту:

Таким образом обеспечен сетевой доступ к хосту из гостевой ОС.

Но чтобы заработал доступ еще и наружу — в интернет, необходимо включить маршрутизацию в гостевой ОС (со стороны хоста это NAT):

В этот момент автор наткнулся на свежую статью 2024го года от Kevin Read, где случились два важных открытия:

  • в интернете есть более современная версия OSF/1 2.0 (да оно официально так называется),

  • существует и подробно описана стандартная настройка сети.

Шаги настройки описанные выше в правильной версии закрываются командой netsetup:

Так вы получите базовую настройку сети и маршрутизацию, но вот с DNS все несколько сложнее. Отвечает за настройку работы резолвера команда bindsetup:

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

Enter the default BIND domain name []: localdomain

Enter your choice [c]: c

Enter the host name of a BIND server: vigor

Enter the Internet address for 10.0.0.254. []: 10.0.0.254

# affected: /etc/rc.config, /etc/hosts

Would you like to run svcsetup now to edit the /etc/svc.conf file (y/n) [y] ?

Enter your choice(s).

For example "0 3 5" [no default] : 2 "hosts" database [2]: 3

# affected: /etc/svc.conf

# hosts: local,bind

Главное это задать IP-адрес хоста, который отвечает за DNS.

В результате всех этих манипуляций должен отрабатывать ping из гостевой OSF/1 и с использованием доменного имени:

OSF/1 2.0

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

Отличить можно по качеству картинки - в 2.0 версии больше цветов. Учетная запись в этой копии: root/test123

Команда запуска также несколько отличается:

./gxemul -X -e 3max -d osf1-2.0-root-full.img -d osf1-2.0-opt.img -M 128 -j genvmunix

Еще тут в каталоге /usr/local/src находится куча полезного софта в виде исходников, для примера удалось запустить сборку bash:

К сожалению не до конца:

Хотя Kevin пишет что should compile fine:

The installed MIPS C compiler is good enough to compile some basic things, and gcc 2.9.5 should compile fine. Apparently binutils is broken, but ld is installed anyway.

Думаю реальная проблема в лицензиях, которые в те времена выдавались на компиляторы в коммерческих UNIX-системах.

P.S.

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

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

Retro68: если вам скучно жить на свете

Серия Некромантия старой школы

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

Видимо по этой причине, одним мрачным осенним днем мне захотелось создать графическое приложение на современном C++ под.. классический Mac из 90х.

В действии.

История

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

Cпишем на плохую погоду и рептилоидов с Нибиру:

рассказываю и показываю как создавать приложения на современном C++ для классического Apple Macintosh начала 90х.

Примерно такого:

Macintosh Quadra 700. Компьютер как компьютер.

Macintosh Quadra 700. Компьютер как компьютер.

The Macintosh Quadra 700 is a personal computer designed, manufactured and sold by Apple Computer from October 1991 to March 1993.

Сам я никогда не видел такие машины в живую, поскольку в годы когда они выпускались был совсем зеленой школотой и жил в далеких сибирских пердях. Хотя даже живи я посреди Нью-Йорка в 90е — врядли бы смог позволить компьютер за почти шесть штук баксов:

The Quadra 700 originally had a list price of US$5,700

Такое и сегодня далеко не для всех.

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

Поэтому, то что опишу ниже — точно не имеет никакого практического применения:

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

Это на тот случай, если вы вдруг ожидаете от статьи чего-то большего.

Весь сетап целиком.

Весь сетап целиком.

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

A GCC-based cross-compilation environment for 68K and PowerPC Macs. Why? Because there is no decent C++17 Compiler targeting Apple's System 6. If that's not a sufficient reason for you, I'm sure you will find something more useful elsewhere.

Разве можно было пройти мимо столь вдохновляющего описания?

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

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

Матчасть

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

Хотя именно <a href="https://pikabu.ru/story/retro68_esli_vam_skuchno_zhit_na_svete_14072507?u=https%3A%2F%2Flinuxdevices.org%2Ftiny-linux-mips-system-runs-on-3-watts%2F&t=%D1%8D%D1%82%D0%BE%D1%82%20%D0%B4%D0%B5%D0%B2%D0%B0%D0%B9%D1%81&h=3f102c8688b75f6dcd0978108dd586021fafe38e" title="https://linuxdevices.org/tiny-linux-mips-system-runs-on-3-watts/" target="_blank" rel="nofollow noopener">этот девайс</a> на самом деле весьма мощный.

Хотя именно этот девайс на самом деле весьма мощный.

Да, на фото выше самый настоящий компьютер:

c процессором, памятью и портами ввода-вывода — все как положено.

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

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

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

смартфоне, роутере, соковыжималке, встраиваемой платформе умного дома, коптере или еще каком боевом роботе.

Или на другой операционной системе — сборка на линуксе под Windows это тоже вполне себе кросс-компиляция.

Тулчейн для кросс-компиляции обычно устанавливается (и даже собирается) отдельно, часто бывая очень сложным. Еще он содержит компиляторы, линкеры, ассемблеры для «вражеской» архитектуры и многое другое, не менее интересное.

В некоторых случаях тулчейн включает и части целевой системы:

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

Без которых создавать что-то сложнее «Hello, world» с помощью кросс-компиляции было бы проблематично.

Так выглядит тот самый "Hello,world", собранный через кросс-компиляцию и запущенный в эмуляторе с MacOS 7.

Так выглядит тот самый "Hello,world", собранный через кросс-компиляцию и запущенный в эмуляторе с MacOS 7.

Retro68

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

a gcc-based cross-compiler for classic 68K and PPC Macintoshes

Во-первых «gcc-based», что означает определенную совместимость с нормальными и современными компиляторами, а также отсутствие экзотических проблем, характерных для редких и/или устаревших компиляторов, вроде bcc.

Во-вторых тут две разных целевых архитектуры: 68k и PowerPC.

А значит можно собирать софт для всей линейки Маков с начала 90х и до начала 2000х — до MacOS X Tiger, последней поддерживающей архитектуру PowerPC.

Но пожалуй самое важное это поддержка трех разных UI-фреймворков на целевых системах:

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

Типа такого:

Так выглядит демо с диалогом, созданное при помощи кросс-компиляции.

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

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

Терминал

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

Консоль появилась только в <a href="https://pikabu.ru/story/retro68_esli_vam_skuchno_zhit_na_svete_14072507?u=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FMacOS&t=MacOS%20X&h=5fffa674be624100557b82e8803e28d255798bb0" title="https://en.wikipedia.org/wiki/MacOS" target="_blank" rel="nofollow noopener">MacOS X</a>, в 2001м году.

Консоль появилась только в MacOS X, в 2001м году.

Поэтому тот факт, что классическое консольное приложение на С:

#include <stdio.h>

int main(int argc, char** argv)
{
printf("Hello, world.\n");
printf("\n(Press Return)\n");
getchar();
return 0;
}

возможно собрать в графическое, с таким псевдо-терминалом в комплекте:

Тоже является заслугой тулчейна Retro68 и ключем линковки при сборке:

LDFLAGS=-lRetroConsole

Сборка на FreeBSD

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

  • Linux, Mac OS X or Windows (via Cygwin)

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

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

Всегда так делаю.

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

Ну а мы как обычно пойдем путем хардкора — соберем весь тулчейн на FreeeBSD с нуля.

Кстати форк Retro68 с поддержкой сборки на FreeBSD выложен в отдельном репозитории. Первым делом надо установить ряд инструментов для разработки и системных библиотек:

pkg install cmake gmp mpfr mpc boost-all bison flex texinfo ruby

Забираем исходники и поскольку репозиторий большой — выгружаем без истории коммитов (ключ depth):

git clone --depth 1 https://github.com/autc04/Retro68.git

Выкачиваем зависимые репозитории:

git submodule update --init
git pull
git submodule update

Теперь надо немного изменить скрипт сборки, для учета особенностей FreeBSD.

Первым делом заменяем вызовы make на gmake, поскольку FreeBSD использует свою версию утилиты make, несовместимую с GNU-версией. Которая конечно также присутствует в пакетах, но только называется gmake.

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

пробросить указание на использование библиотек из каталога /usr/local, куда устанавливаются все внешние с точки зрения системы библиотеки в FreeBSD.

Например для строки:

$SRC/binutils/configure --target=m68k-apple-macos --prefix=$PREFIX --disable-doc

необходимо дописать в конец:

CFLAGS=-I/usr/local/include LDFLAGS=-L/usr/local/lib

Таким образом переменные CFLAGS и LDFLAGS будут переданы скрипту configure в качестве аргумента при вызове.

Чуть сложнее со сборкой GCC (да, мы тут собираем компилятор из исходников), строка:

$SRC/gcc/configure --target=m68k-apple-macos --prefix=$PREFIX \
--enable-languages=c,c++ --with-arch=m68k --with-cpu=m68000 \
--disable-libssp MAKEINFO=missing

Тут уже нельзя использовать переменные окружения CFLAGS и LDFLAGS из-за особенностей сборки GCC и необходимо передавать пути для ключевых библиотек специальными аргументами:

--with-mpc=/usr/local --with-mpfr=/usr/local --with-gmp=/usr/local --with-isl=/usr/local --with-libiconv-prefix=/usr/local

Дописав их в конец вызова configure.

Таким же образом необходимо поправить еще два места в скрипте сборки: раз и два.

После чего можно запускать сборку.

Запускается она из отдельного каталога:

mkdir ../Retro68-build
cd ../Retro68-build
../Retro68/build-toolchain.bash

После весьма продолжительной (даже на мощном компьютере) и жутко выглядящей сборки, по накалу страстей на экране напоминающей знаменитый make buildworld из FreeBSD:

..все упадет на вот такой «страшной» ошибке:

Для исправления ситуации, надо всего лишь изменить один import в файле portable_endian.h , в этом месте:

#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__)

# include <sys/endian.h>

Суть проблемы в том что расположение заголовка endian.h поменялось и теперь его необходимо включать без указания каталога sys:

# include <endian.h>

Исправляем импорт и перезапускаем сборку.

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

На этот раз падение произойдет на стадии сборки из cmake:

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

удалением импорта этого заголовка из файла PEFTools/MakePEF.cc

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

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

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

скопировал файл gelf.h из libelf/include в каталог Elf2Mac, и поменял ссылки на этот заголовок из глобального на локальный.

Было:

#include <gelf.h>

Стало:

#include "gelf.h"

Поменять ссылки необходимо во всех файлах проекта Elf2Mac где встречается данный импорт. Наконец последняя ошибка сборки, связанная с особенностями FreeBSD:

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

Для исправления достаточно добавить:

#include <netinet/in.h>

в файл LaunchAPPL/Client/TCP.cc

Так выглядит успешное завершение сборкиRetro68:

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

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

Эмулятор

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

Такова сила бренда, созданного Стивом Джобсом.

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

Basilisk II is an Open Source 68k Macintosh emulator.

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

И внезапно добавлен JIT (Just-In-Time Compiler).

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

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

pkg install sdl2 autoconf automake mpfr gmp

Теперь забираем исходники:

git clone https://github.com/kanjitalk755/macemu.git

Собираем:

cd macemu/BasiliskII/src/Unix
$ ./autogen.sh
$ gmake

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

Для того чтобы запускать MacOS7 в эмуляторе, нужен загружаемый образ диска с установленной системой (или ее инсталлятор) и ROM-файл.

Образ диска можно взять например тут или тут, ROM-файл например отсюда или отсюда. Еще пара вариантов.

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

Напомню, что для этой статьи я эмулировал модель Quadra 600:

Для эмулятора использовался вот такой ROM-файл:

Указав в разделе «Volumes» путь к образу диска с MacOS7, затем в разделе «Memory/Misc» путь к ROM-файлу, можно наконец запускать эмулятор:

Теперь переходим к самому веселому — к разработке.

Разработка с помощью Retro68

Собственно после сборки Retro68, у вас будет все что нужно для разработки конечного софта, поскольку в каталоге build-target/Samples будут собранные примеры приложений:

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

Для сборки во всех случаях используется cmake, исходники находятся в каталоге Retro68/Samples.

Чтобы собрать проект с HelloWorld отдельно, выполняем:

сd /opt/src/Retro68/Samples/HelloWorld
mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=/opt/src/Retro68-build/toolchain/m68k-apple-macos/cmake/retro68.toolchain.cmake
gmake

Результат сборки:

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

Достаточно указать путь к dsk-файлу, который находится в каталоге build и запустить эмулятор:

На рабочем столе появится иконка с названием, совпадающим с названием примера:

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

Двойной клик по файлу HelloWorld запустит наше приложение.

Примеры приложений

Как уже упоминал, в составе Retro68 есть довольно много демонстрационных приложений.

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

Автор тулчейна постарался, поэтому все примеры логичны, просты и понятны.

Retro68/Samples/Raytracer

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

В проекте на самом деле присутствуют сразу две реализации, одна на С, другая на C++, видимо для сравнения во время работы.

Так выглядит финальный результат рендеринга:

Проект ценен в первую очередь как демонстрация расчетной логики и операций с числами с плавающей точкой.

Retro68/Samples/Dialog

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

В работе:

Retro68/Samples/WDEF

Демонстрация работы с многооконными интерфейсами (MDI), создание окна с кастомным оформлением и работа с ресурсами окон.

Так это выглядит в действии:

Кратко по остальным примерам, реализующим какую-то специфическую логику.

Retro68/Samples/MPWTool

Реализует расширение для MPW (Macintosh Programmer's Workshop), точнее для MPWShell, с обработкой запуска оттуда и корректного возвращения в консоль.

Retro68/Samples/SharedLibrary

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

Retro68/Samples/SystemExtension

Пример простейшего расширения для самой операционной системы MacOS 7, в данном случае вся видимая логика заключается в показе новой иконки при запуске ОС.

Retro68/Samples/Launcher

Реализует запуск сторонних приложений через перетаскивание (Drag&Drop).

"Masters of Hardcore" - вбейте в поиск если не жалко уши и соседей.

"Masters of Hardcore" - вбейте в поиск если не жалко уши и соседей.

Настоящий хардкор

Вы же не думали, что я успокоюсь на банальной сборке тулчейна с эмулятором под FreeBSD и запуске пары тестов?

Праздник должен продолжаться:

Clapkit (CLassic APplication KIT) is a framework for developing applications for Classic Mac OS (System 7.x - Mac OS 9.x), basically a C++/object-oriented wrapper for Macintosh Toolbox functions.

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

Так это выглядит:

Картина неизвестного питерского художника "Мак под маком".

Картина неизвестного питерского художника "Мак под маком".

Так выглядит код:

#include <ckApp.h>

int main() {

CKApp* app = CKNew CKApp();

app->CKNewMsgBoxNote("Hello world!", nullptr, "OK",
nullptr, [app](int button) {
app->CKQuit();
});
// Run loop: without this, your app will quit as soon as it launches.
while (!app->CKLoop(5));
delete app;
return 0;
}

Так выглядит мое тестовое приложение на современном C++, собранное с помощью cmake на FreeBSD и запущенное на MacOS 7 из 90х:

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

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

Ну что дорогие читатели, ударим шизой по осенней летней хандре?

MPW

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

Разумеется на маках всегда была собственная среда разработки:

Macintosh Programmer's Workshop (MPW) is a software development environment for the Classic Mac OS operating system, written by Apple Computer.

Ее вполне можно использовать до сих пор, благо образов дисков в сети хватает. Именно так я и советую поступать (т.е. использовать MPW) в случае любого более-менее серьезного проекта, поскольку где и когда закончатся возможности кросс-компиляции и вы опустите руки — предсказать не берусь.

Как-то так MPW выглядит в эмуляторе:

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

В Retro68, в каталоге InterfacesAndLibraries находится файл Readme.md, содержимое которого явно намекает на дальнейшие действия:

Find a copy of Apple's Universal Interfaces 3.x (preferrably 3.4) and put them here.

Вообще «Find a copy» это такой толерантный и социально приемлемый синоним старого доброго «скачать варез», прямо из 90х. Потому как купить официально старое ПО уже не представляется возможным, если подобное вдруг придет в голову. Даже если компания-разработчик существует (как в случае Apple), старые версии своих продуктов она так просто не продаст.

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

И репутация компании.

Поэтому сообщаю по секрету, что «найти» эти самые универсальные интерфейсы можно например тут:

wget https://henlin.net/images/Interfaces\&Libraries.zip
unzip "Interfaces&Libraries.zip" -d /opt/src/Retro68/InterfacesAndLibraries

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

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

JS-версия загружается довольно долго, браузер может подвисать.

JS-версия загружается довольно долго, браузер может подвисать.

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

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

Так что расскажу про связку из Nuklear, QuickDraw и Retro68 в другой раз, чтобы не раздувать эту статью до совсем уж космических размеров.

А пока для затравки, показываю эту связку в действии:

Не пугайтесь размеров элементов интерфейса в эмуляторе - игрался с зумом, проиграл.

Не пугайтесь размеров элементов интерфейса в эмуляторе - игрался с зумом, проиграл.

Но вернемся к фреймворку ClapKit, благо история с ним еще не закончена.

После распаковки заголовков из MPW, необходимо перезапустить сборку самого тулчейна Retro68:

cd Retro68-build
../Retro68/build-toolchain.bash

И наконец можно приступать к половым утехам с самим ClapKit. Забираем исходники:

git clone https://github.com/macinlink/clapkit.git

Сборка построена на cmake, но перед тем как собирать Clapkit, необходимо указать путь к Retro68 в скрипте сборки:

# Ensure Retro68 toolchain is set BEFORE defining project
set(CMAKE_TOOLCHAIN_FILE "/opt/src/Retro68-build/toolchain/m68k-apple-macos/cmake/retro68.toolchain.cmake")

и чуть ниже:

# Define Retro68 path properly
set(RETRO68_PATH /opt/src/Retro68-build/toolchain/m68k-apple-macos)

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

Для исправления достаточно добавить в include/ckTypes.h два импорта:

#include <Quickdraw.h>
#include <Events.h>

В репозитории проекта находится готовый Showcase с демонстрацией возможностей фреймворка, на базе которого я и делал свое тестовое приложение (см. ниже).

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

Напоминаю, что все это создано с помощью кросс-компиляции и затем запущено в целевой MacOS 7.

Напоминаю, что все это создано с помощью кросс-компиляции и затем запущено в целевой MacOS 7.

Выглядит заметно веселее, чем примеры из самого Retro68, согласитесь.

Тестовый образец

Я не хотел сразу пускаться во все тяжкие и портировать Doom/Quake/Cyberpunk 2077 под винтажные Маки из 90х — надо же оставить место воображению, поэтому мой тестовый образец для этой статьи это в первую очередь пример автономного, отделяемого приложения с минимальной логикой.

Которое собирается без привязки к иерархии каталогов Clapkit или Retro68.

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

Так мое приложение выглядит в работе на MacOS 7:

Все что оно делает:

отображает меню «Yo» в Finder, с единственным пунктом «Run me», по клику на который вызывается модальный диалог с двумя кнопками. По нажатию на «Quit» произойдет завершение работы программы.

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

На современном С++, под FreeBSD темной питерской ночью.

Так выглядит основной код на C++, файл main.cpp:

#include "main.h"
#include <ckMenu.h>

CKTestAlexs* app;

int main() {
// класс переименован ради тестов
app = CKNew CKTestAlexs();
// false - не использовать стандартные пункты меню
CKMenuBar* menuBar = CKNew CKMenuBar(false);
// добавляем свой пункт меню в Finder
CKMenu* menuTests = CKNew CKMenu("Yo");
menuBar->AddMenu(menuTests);
// затем элемент в выпадающем списке
CKMenuItem* item = CKNew CKMenuItem("Run me", 'R', [&item](CKEvent e) {
// по клику произойдет отображение стандартного
// модального диалога
app->CKNewMsgBoxNote("Welcome to old school!",
"Hello world", "Mkay", "Quit", [](int button) {
// если нажали Quit - завершить работу приложения.
if (button == 0) {
app->CKQuit();
}
});
});
// связывание элемента с пунктом меню
menuTests->AddItem(item);
// связывание меню с приложением
app->CKSetMenu(menuBar);
// бесконечный цикл ожидания для отрисовки,
// без которого приложение сразу завершится
while (!app->CKLoop(5));
delete app;
return 0;
}

Класс описан в заголовке main.h, декларации методов я убрал за ненадобностью:

#include <ckApp.h>

class CKTestAlexs : public CKApp {
};

Так выглядит скрипт сборки для cmake, как можно заметить тут происходит автоматическое скачивание родительского фреймворка Clapkit и наложение патча с пропущенными импортами (описаны выше):

cmake_minimum_required(VERSION 3.16)

# Ensure Retro68 toolchain is set BEFORE defining project
set(RETRO68_PATH "/opt/src/Retro68-build/toolchain/m68k-apple-macos")
set(CMAKE_TOOLCHAIN_FILE "${RETRO68_PATH}/cmake/retro68.toolchain.cmake")

project(clapkit VERSION 1.0)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Include Retro68 headers
include_directories(${RETRO68_PATH}/include)

# Set default build type to Release if not specified
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type (Debug or Release)" FORCE)
endif()

set(CLAPKIT_LOCAL_PATH "${CMAKE_SOURCE_DIR}/../../")
option(USE_LOCAL_CLAPKIT "Use local checkout of clapkit instead of fetching from Git" ON)

if(USE_LOCAL_CLAPKIT AND EXISTS "${CLAPKIT_LOCAL_PATH}/CMakeLists.txt")
message(STATUS "Using local clapkit at ${CLAPKIT_LOCAL_PATH}")
add_subdirectory(${CLAPKIT_LOCAL_PATH} clapkit)
set(USE_LOCAL_CLAPKIT ON)
else()

set(cktypes_patch git apply ${CMAKE_SOURCE_DIR}/patches/cktypes.patch)

message(STATUS "Fetching clapkit from GitHub...")
include(FetchContent)
FetchContent_Declare(
clapkit
GIT_REPOSITORY https://github.com/macinlink/clapkit.git
GIT_TAG main
PATCH_COMMAND ${cktypes_patch}
UPDATE_DISCONNECTED 1
)
FetchContent_MakeAvailable(clapkit)
set(USE_LOCAL_CLAPKIT OFF)
endif()

# Add application
add_application(CKTestAlexs
TYPE "APPL"
CREATOR "CKTS"
main.cpp
cktest.r
)

# Debug vs Release settings
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Configuring Debug Build")
target_compile_definitions(CKTestAlexs PRIVATE kCKAPPDEBUG=1)
target_compile_definitions(clapkit PRIVATE kCKAPPDEBUG=1)

set_target_properties(CKTestAlexs PROPERTIES
COMPILE_FLAGS "-O0 -Wall -g -fdata-sections -ffunction-sections"
LINK_FLAGS "-Wl,--mac-single"
)

elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
message(STATUS "Configuring Release Build")
target_compile_definitions(CKTestAlexs PRIVATE NDEBUG) # Disable debugging in Release
target_compile_definitions(clapkit PRIVATE NDEBUG)

set_target_properties(CKTestAlexs PROPERTIES
COMPILE_FLAGS "-s -O1 -Wall -fmerge-all-constants -fmerge-constants -fstrict-aliasing -fomit-frame-pointer -fdata-sections -ffunction-sections -finline-functions"
LINK_FLAGS "-Wl,--mac-single -Wl,--mac-strip-macsbug"
)
endif()

# Link clapkit
target_link_libraries(CKTestAlexs PRIVATE clapkit)

Поскольку пути к Retro68 задаются внутри скрипта сборки, а фреймворк Clapkit автоматически скачивается из Github, сама сборка максимально упрощена:

mkdir build
cd build
cmake ..
gmake

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

Продолжение банкета

Ниже несколько интересных (но все еще социально одобряемых) идей, на тему что еще можно сделать с эмулятором винтажного Macintosh.

Для начала, можно довести накал программерского угара до предела — интегрировав Retro68 с полноценным IDE и отладчиком:

Developing vintage 68K Macintosh apps with CodeLite IDE, Retro68 and pce-macplus emulator

С автодополнением из среды разработки:

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

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

Тут можно найти отдельное видео с демонстрацией работы.

Следующая замечательная идея в моем списке это очевидная попытка использовать Rust для кросс-компиляции под старые Mac. Нет, автор не шутит, он действительно это сделал:

Through some luck and a little persistence I have actually managed to get Rust code running on classic Mac OS (I’ve tried Mac OS 7.5 and 8.1)

Репозиторий находится тут, проект достаточно свежий (2023 год) и вполне рабочий.

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

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

Кстати приложение сетевое, так что на Rust реализована не только работа с интерфейсом, но и с TCP/IP и даже HTTP протоколом.

Для операционной системы из 1994 года.

P.S.

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

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

Развлечения джентельменов: Binary Golf

Серия Хороший тамада и конкурсы интересные

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

Нет это не баг, это спецэффект!

Нет это не баг, это спецэффект!

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

Я ошибался.

То что описано в этой статье — за гранью понимания обычного разработчика и тем более обычного человека, далекого от ИТ. Честно и при всем желании не смогу объяснить даже подготовленным «молодым специалистам» от разработки, протирающим штаны в опенспейсе за шлепаньем форм для очередного корпоративного маркетплейса насколько круто то что описано ниже.

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

Добро пожаловать, снова: <a href="https://pikabu.ru/story/razvlecheniya_dzhentelmenov_binary_golf_14068864?u=https%3A%2F%2Fbinary.golf%2F&t=https%3A%2F%2Fbinary.golf%2F&h=24e67b8051d9cea0d28f96b2a437c4d76cc5f62e" title="https://binary.golf/" target="_blank" rel="nofollow noopener">https://binary.golf/</a>

Добро пожаловать, снова: https://binary.golf/

Любовь и байты

Однажды в хорошем пабе собрались несколько джентельменов от мира разработки:

демосценеры, реверс-инженеры, ИБ-аналитики малвари и разумеется яркие представители с «другой стороны».

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

─── Binary Golf Grand Prix 1──────────────────────//──

Welcome to the Binary Golf Grand Prix! This is a challenge for people who like to craft tiny binaries.

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

The goal of the Binary Golf Grand Prix is to challenge programmers to make the smallest possible binary that fits within certain constraints.

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

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

The binary you will craft will be the same executable when flipped backwards.

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

$ xxd some.random.exe | head -n 1
00000000: 4d5a 9000 0345 0000 0400 0000 ffff 0000 MZ...E..........

The entire binary will be reversed, and the first byte, will be the last byte:

4d5a 9000 0345 .... <-- reversed --> .... 4503 0090 5a4d

Критериев оценки в столь замечательном конкурсе два длина и толщина:

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

В оригинале:

Scores will be calculated based on both the size of the executable, as well asthe percentage of bytes executed when it is run.

Так что у джентельменов все серьезно.

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

BootNoodle: A Palindromic Bootloader for BGGP

Автор: xcellerator

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

Запускается в эмуляторе QEMU:

qemu-system-x86_64 bin/bootnoodle.bin

В работе:

Вот так выглядит сборка и создание «зеркальной» копии:

mkdir bin 2>/dev/null
nasm -f bin -o bin/bootnoodle.bin src/bootnoodle.asm
dd if=bin/bootnoodle.bin of=bin/tmp.bin bs=1 count=256
rm bin/bootnoodle.bin
perl -0777pe '$_=reverse $_' bin/tmp.bin > bin/tmp2.bin
cat bin/tmp.bin bin/tmp2.bin > bin/bootnoodle.bin
rm bin/tmp*

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

ns.bggp : Palindromic 64 bit ELF binary

Автор: netspooky

Статья с описанием: https://n0.lol/elf-palindrome-original/

Работа с оформлением, которую можно распечатать и повесить в рамку на стену, чтобы пугать окружающих быдлокодеров:

Он же в упакованном виде:

base64 -d <<< \
f0VMRgUP/zFIPLCQkJDrNAIAPgABAAAABAAAAAEAAAAcAAAAAAAAAAAAAAAAAAAAAQAAAEAAOAAB\
AAIA6wsAAAAAAADrCwAAAAAAADzrwDFIUFVQUFlTUFlQU1lQUFVQ6xiQkJCQkAUPlbZAIObBSMaJ\
D7LHiQAAAAG4AQAAAInHsg+JxkjB5iBAtpUPBZCQkJCQGOtQVVBQWVNQWVBTWVBQVVBIMcDrPAAA\
AAAAAAvrAAAAAAAAC+sAAgABADgAQAAAAAEAAAAAAAAAAAAAAAAAAAAcAAAAAQAAAAQAAAABAD4A\
AjTrkJCQsDxIMf8PBUZMRX8= > ns.bggp

Теперь о печальном:

This was tested and built on Ubuntu 20.04 with kernel 5.4.0-42-generic.

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

Вот так это должно было выглядеть:

$ ./build.sh
Executing initial binary...
PUPPYSPYPSYPPUP
00000000: 7f45 4c46 050f ff31 483c b090 9090 eb34 .ELF...1H&lt;.....4
00000010: 0200 3e00 0100 0000 0400 0000 0100 0000 ..>.............
00000020: 1c00 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0100 0000 4000 3800 0100 0200 eb0b 0000 ....@.8.........
00000040: 0000 0000 eb0b 0000 0000 0000 3ceb c031 ............&lt;..1
00000050: 4850 5550 5059 5350 5950 5359 5050 5550 HPUPPYSPYPSYPPUP
00000060: eb18 9090 9090 9005 0f95 b640 20e6 c148 ...........@ ..H
00000070: c689 0fb2 c789 0000 0001 b801 0000 0089 ................
00000080: c7b2 0f89 c648 c1e6 2040 b695 0f05 9090 .....H.. @......
00000090: 9090 9018 eb50 5550 5059 5350 5950 5359 .....PUPPYSPYPSY
000000a0: 5050 5550 4831 c0eb 3c00 0000 0000 000b PPUPH1..&lt;.......
000000b0: eb00 0000 0000 000b eb00 0200 0100 3800 ..............8.
000000c0: 4000 0000 0100 0000 0000 0000 0000 0000 @...............
000000d0: 0000 0000 1c00 0000 0100 0000 0400 0000 ................
000000e0: 0100 3e00 0234 eb90 9090 b03c 4831 ff0f ..>..4.....&lt;H1..
000000f0: 0546 4c45 7f .FLE.

Reversing...
Executing binary in reverse...
PUPPYSPYPSYPPUP
00000000: 7f45 4c46 050f ff31 483c b090 9090 eb34 .ELF...1H&lt;.....4
00000010: 0200 3e00 0100 0000 0400 0000 0100 0000 ..>.............
00000020: 1c00 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0100 0000 4000 3800 0100 0200 eb0b 0000 ....@.8.........
00000040: 0000 0000 eb0b 0000 0000 0000 3ceb c031 ............&lt;..1
00000050: 4850 5550 5059 5350 5950 5359 5050 5550 HPUPPYSPYPSYPPUP
00000060: eb18 9090 9090 9005 0f95 b640 20e6 c148 ...........@ ..H
00000070: c689 0fb2 c789 0000 0001 b801 0000 0089 ................
00000080: c7b2 0f89 c648 c1e6 2040 b695 0f05 9090 .....H.. @......
00000090: 9090 9018 eb50 5550 5059 5350 5950 5359 .....PUPPYSPYPSY
000000a0: 5050 5550 4831 c0eb 3c00 0000 0000 000b PPUPH1..&lt;.......
000000b0: eb00 0000 0000 000b eb00 0200 0100 3800 ..............8.
000000c0: 4000 0000 0100 0000 0000 0000 0000 0000 @...............
000000d0: 0000 0000 1c00 0000 0100 0000 0400 0000 ................
000000e0: 0100 3e00 0234 eb90 9090 b03c 4831 ff0f ..>..4.....&lt;H1..
000000f0: 0546 4c45 7f .FLE.

Comparing hashes...
c082d226c96b7251649c48526dd9766071fa5e59 ns.bggp
c082d226c96b7251649c48526dd9766071fa5e59 ns.bggp.R

Текст «PUPPYSPYPSYPPUP» это вывод приложения при запуске.

Viznut/PWP

Автор: viznut

Статья с описанием: http://viznut.fi/demos/vic20/vizpalapziv.html

Работа:

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

Entry name: VIZPALAPZIV Executable format: Commodore 8-bit PRG format on Commodore VIC-20 Number of bytes: 20 (including the 2-byte start address) Executed bytes: 18 (every byte except the start address, i.e. 90%) Bytes: 7c 00 8f 0f 90 25 48 48 73 a9 a9 73 48 48 25 90 0f 8f 00 7c

Да, это программа для Commodore, вот такой игрушечной машинки из 80х:

Которую мало кто из читателей имел возможность видеть в живую.

BGGP.COM

Автор: Boo Khan Ming

Работа:

Как гласит известная поговорка:

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

И код выше — лучшая тому иллюстрация.

Простой малазийский паренек сорвал куш — его код стал победителем этой специальной олимпиады в 2020 м году:

Hi! Nice to meet you everyone there! I am Wu (Boo Khan-Ming) from Malaysia. I am not a scene/demo coder. Just join this ASMCOMPO3 for fun.

Исходник на ассемблере под DOS:

JMP 103  EB01
RET  C3
MOV AX, B800  B800B8
MOV ES, AX  8EC0
MOV DI, 07D0  BFD007  
MOV AX, 9090  B89090
MOV ES:[DI], AX  268905
ADD AX, 2689  058926
NOP  90
NOP  90
MOV AX, D007  B807D0
MOV DI, 8EC0  BFC08E
MOV AX, B800  B800B8
RET  C3

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

При работе синий блок еще и мигает.

При работе синий блок еще и мигает.

Эпилог

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

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

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

Показать полностью 8
Отличная работа, все прочитано!

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества