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

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
9

Марсианские страсти: DOS, любовь и С++

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

У нас было 640Кб памяти, CGA-экран, 20-мегабайтовый диск и целых четыре мегагерца тактовой частоты. А еще старые пятидюймовые дискеты на 360кб. Не то чтобы это был необходимый набор для современного разработчика C++, но если уж начали коллекционировать дичь, то сложно остановиться..

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

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

Проклятая выдача

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

На меня эти алгоритмы тоже начали влиять, но.. особенным образом:

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

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

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

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

Технический ультрахардкор

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

Для простых обывателей поясняю:

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

Сотрудник компании Valve вместо работы над Half-Life 3 запускает компилятор, который собирает из набора файлов с исходниками «финальный билд», который затем упаковывается в инсталлятор и выкладывается на официальный сайт Steam.

Откуда его потом скачивают ушастые простые пользователи.

Теперь представьте:

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

C концами.

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

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

И находите на развалинах бывшего офиса Valve ту самую флешку с исходниками альфа-версии Half-Life 3.

Конечно прогресс в разработке программ к тому времени успел уйти далеко вперед, у вас в ходу на лунной базе теперь какие-нибудь C++-157 и clang-777, с встроенным Rust, тремя слоями виртуализации и сборщиком мусора, работающим через промпты LLM. А про язык C вы узнали лишь из лекции по истории, про исторический период между египетскими пирамидами и вторжением инопланетян.

Встает очевидный вопрос:

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

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

Нет, это не релизная версия.

Нет, это не релизная версия.

Несмотря на весь этот «апокалиптический треш», ситуация сама по себе — более чем реальная:

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

В первую очередь это мейнфреймы, которые до сих пор живее всех живых, вроде такого красавца от Unisys:

Далеко не только IBM занимается мейнфреймами до сих пор.

Далеко не только IBM занимается мейнфреймами до сих пор.

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

поддержка Windows 7/10, поддержка старых версий Android и iOS, обязательная работа на JRE 1.8+ и так далее и тому подобное.

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

Обратное портирование

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

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

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

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

приложение на современном C++, с современными фичами, созданное в современной ОС, которое работает в окружении из 1986 года!

Операционной системой, которую выпустили 40 лет назад выступит известная MS-DOS 3.20. Именно эта версия была выбрана ввиду одной интересной исторической особенности:

Version 3.20 – First retail release (non-OEM); Release date: July 1986

Это первая версия MS-DOS, продаваемая конечным пользователям напрямую от самой Microsoft, с полок магазинов.

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

"<a href="https://pikabu.ru/story/marsianskie_strasti_dos_lyubov_i_s_14059563?u=https%3A%2F%2Fblog.0x08.ru%2Fapricot-emulator-qdae&t=%D0%97%D0%B0%D0%B2%D0%BE%D0%B4%D0%BD%D0%BE%D0%B9%20%D0%90%D0%B1%D1%80%D0%B8%D0%BA%D0%BE%D1%81&h=3d437ee3ae93b69a9c4829f75b2a736753f3a53a" title="https://blog.0x08.ru/apricot-emulator-qdae" target="_blank" rel="nofollow noopener">Заводной Абрикос</a>"

"Заводной Абрикос"

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

Фото современное, таких машин в живом состоянии сохранилось довольно много.

Фото современное, таких машин в живом состоянии сохранилось довольно много.

Комната жениха

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

  • Компилятор C++ DigitalMars, который до сих пор официально поддерживает сборку под DOS;

  • Эмулятор 86Box для запуска DOS из 1986 года;

  • Утилиты GNU Mtools для работы с образами старых дискет.

И все это под FreeBSD 14, в качестве своеобразного гаранта качества:

если что-то работает на FreeBSD, оно однозначно будет работать и на Windows и в MacOS и в Linux и на Луне.

Цифровой Марс

Главный герой сегодняшнего праздника жизни — набор компиляторов от Digital Mars:

Digital Mars is an American computer software company founded by Walter Bright and based in Vienna, Virginia. It makes C, C++, and D compilers, and associated utilities such as an integrated development environment (IDE) for Windows and MS-DOS, which Digital Mars calls an integrated development and debugging environment (IDDE)

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

Во-первых Вена это не только столица Австрии, но еще и небольшой городок на севере США, на 15к жителей, где (внезапно) находится штаб-квартира серьезной софтверной компании, выпускающей эпические компиляторы аж с 1988 года:

In 1988, Zortech was the first C++ compiler to ship for Windows. PC Magazine ran a graphics benchmark and reported that most executables produced by Zortech ran faster than executables produced by Microsoft C 5.1 and by Watcom C 6.5.[3]

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

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

The company gained notice in the software development community for creating the D programming language. D resulted from Bright's frustration with the direction of the C++ language and from his experience implementing it.

Ребята до такой степени угорели по С++, что создали свой собственный язык, «по мотивам»:

resulted from Bright's frustration with the direction of the C++ language

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

  • Includes C++ templates, exception handling and rtti.

  • Most other compilers for 16 bit code were abandoned nearly a decade ago. Digital Mars has a modern compiler for 16 bits.

Все перечисленные выше радости — для официально поддерживаемой сборки под DOS и 16-битных приложений.

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

Сравните это с современными "запускаемыми архивами" с играми, по 100Гб в одном .EXE

Сравните это с современными "запускаемыми архивами" с играми, по 100Гб в одном .EXE

Это предельные размеры создаваемого приложения, в случае MS-DOS 3.20 мы будем использовать «s» модель памяти, поэтому размер финальной сборки не должен превышать 64Кб.

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

Компиляторы C и C++ от DigitalMars изначально были коммерческим продуктом и до сих пор существует коммерческая версия, включающая помимо компиляторов еще и собственную среду разработки (IDDE). Однако не так давно компиляторы стали бесплатным и открытым проектом, с исходниками на Github.

К сожалению проект пока не портируемый — собирается и работает исключительно под Windows.

Тем не менее, поскольку создавали его очень опытные люди — все отлично работает в Wine, что мы и будем использовать для его запуска на FreeBSD. Чтобы снова не раздувать статью — не стал заморачиваться сборкой компилятора из исходников (хотя это не очень сложный процесс), использовав готовую сборку версии 8.5.7.

Инсталлятора тут нет, в архиве сразу готовые бинарники, поэтому распаковываем и запускаем:

mkdir -p /opt/src/digitalmars/dm
cd opt/src/digitalmars/dm
wget http://ftp.digitalmars.com/Digital_Mars_C++/Patch/dm857c.zip
unzip dm857c.zip
wineconsole bin/dmc.exe

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

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

Внутри будут дополнительные бинарники, главный из которых это довольно известный EXE2BIN.COM, используемый для генерации COM-файлов из EXE.

С EXE2BIN есть один важный нюанс — он не запустится с помощью Wine, поскольку является DOS-приложением.

Чтобы использовать эту утилиту для создания COM-файлов — необходимо запускать ее с помощью DOSbox:

Теперь переходим к основному эмулятору.

Официальный логотип 86Box

Официальный логотип 86Box

86Box

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

86Box is an IBM PC system emulator that specializes in running old operating systems and software designed for IBM PC systems and compatibles from 1981 through fairly recent system designs based on the PCI bus.

Готовой сборки для FreeBSD пока нет (как и присутствия в портах) и еще год назад были проблемы, требующие ручных правок исходников:

Initially exclusive to Windows, it was ported to Linux in version 3.2 and macOS in version 3.4.

Но на момент написания статьи все поправлено и эмулятор отлично собирается из исходников без каких-либо проблем, а в инструкции по сборке появился раздел про FreeBSD.

Сборка

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

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

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

Сборка построена на cmake, но также использует ninja (для модулей).

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

pkg install cmake pkgconf freetype-gl sdl2 libspng openal-soft rtmidi qt5 libslirp fluidsynth libsndfile

Запуск вполне классический для cmake-проектов:

mkdir build
cd build
cmake ..

Готовое для запуска приложение эмулятора будет в build/src/86Box.

Но это еще не конец.

Для работы нужен еще и набор ROM-файлов (да, опять), который для текущей версии (из master-ветки) необходимо загружать из отдельного репозитория:

Для релизных версий эмулятора — ROM-файлы надо брать в другом месте, со страницы releases. Запускается эмулятор с указанием путей к каталогу с ROM-файлами следующим образом:

./src/86Box --rompath /opt/src/roms

После запуска эмулятора (по прямой аналогии с каким-нибудь VirtualBox) необходимо создать новую виртуальную машину, задав ей вот такие настройки:

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

Так выглядела обложка коробочной версии с MS-DOS. Помимо префикса, слово "Microsoft" встречается трижды ;)

Так выглядела обложка коробочной версии с MS-DOS. Помимо префикса, слово "Microsoft" встречается трижды ;)

MS-DOS

Образы самого MS-DOS достать в сети очень легко, например отсюда. Все они уже давно находятся в публичном пользовании (public domain), поэтому за их использование Microsoft вас не съест засудит.

Более того, даже исходный код некоторых версий MS-DOS был выложен в публичный доступ:

The original sources of MS-DOS 1.25, 2.0, and 4.0 for reference purposes

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

Нам нужен лишь первый:

Этот файл нужно выбрать в меню после запуска эмулятора:

После чего перезагрузить эмулируемую систему.

При запуске пойдет проверка памяти, затем MS-DOS будет просить ввести ей дату и время, достаточно нажать пару раз <Enter> для пропуска:

MS-DOS из 1986 года готов к работе.

MS-DOS из 1986 года готов к работе.

Mtools

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

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

Тот самый, с которого мы выше загружали образ MS-DOS.

Для генерации образов флоппи я использовал известный пакет GNU Mtools,

In addition to file access, it supports many FAT-specific features: volume labels, FAT-specific file attributes (hidden, system, ...), "bad block" map maintenance, access to remote floppy drives, Iomega ZIP disk protection, "secure" erase, display of file's on-disk layout, etc.

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

pkg install mtools

Так выглядят те самые пятидюймовые дискеты (5.25").

Так выглядят те самые пятидюймовые дискеты (5.25").

Работа с образом дискеты

Создаем пустой образ дискеты на 360кб:

mformat -C -t 40 -h 2 -n 9 -i floppy360k.img

Копируем файл с хоста внутрь этого образа:

mcopy -i floppy360k.img /opt/src/digitalmars/dm/bin/test.exe ::test.exe

Запускаем эмулятор 86box и указываем созданный образ в качестве второго в эмулируемой системе:

Переходим на диск B:

Обратите внимание на даты на файлах.

Обратите внимание на даты на файлах.

Тесты

А теперь будут танцы:

показываю функционал 21 века, работающий под MS-DOS из 20 века.

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

Первым делом я попробовал что-то собрать с RTTI и классами, вот такой довольно простой код:

#include <iostream.h>

class Base {
public:
virtual void test() {
cout << "this is base" << endl;
}
};

class Derived : public Base {
public:
void test() {
Base::test();
cout << "this is derived" << endl;
}
};

int main()
{

Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b);
if (d != NULL) {
// Successful cast
cout << "works" << endl;;
d -> test();
} else {
// Cast failed
cout << "cannot cast B* to D*" << endl;;
}
return 0;
}

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

dmc.exe -0 -ms -Ar -NV -o+space ..\..\test.cpp

Дальше копируем полученный EXE-файл в образ дискеты:

mcopy -i floppy360k.img /opt/src/digitalmars/dm/bin/test.exe ::test.exe

Ну и запускаем эмулятор.

Как видите все отлично работает в операционной системе 40-летней давности:

Покажите вашему преподу по C++.

Покажите вашему преподу по C++.

Следующим шагом проверил обработку ошибок:

#include <iostream.h>
#include <exception>

int main() {
try {
int age = 15;
if (age >= 18) {
cout << "Access granted - you are old enough.";
} else {
throw 505;
}
} catch (int myNum) {
cout << "Access denied - You must be at least 18 years old.\n";
cout << "Error number: " << myNum;
}
return 0;
}

Тут стоит уточнить, что поддержку обработки ошибок надо включать отдельно, ключом -Ae:

dmc -0 -ms -Ae -NV -o+space ex.cpp

Не поверите, но оно тоже работает:

Вот такие дела, конструкция try-catch на языке высокого уровня спокойно работает в MS-DOS образца 1986 года.

Еще больше балета

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

STLport 4.5.3 ported to Digital Mars C++ (requires Digital Mars C++ 8.32 or later)

Это отдельная, весьма своеобразная версия STL, совместимая с DigitalMars, доступная для скачивания в собранном виде (плюс исходники). Но собрана она для целевой платформы Win32, поэтому для сборки под DOS ее готовую версию использовать не получится, надо пересобирать самостоятельно.

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

Не поверите, но еще существует Boost (хоть и очень старый) с поддержкой компилятора DigitalMars:

Boost 1.30.0 for Digital Mars C++

Вот уж где раздолье и настоящий угар: писать под DOS c помощью самой жирной библиотеки для C++ на свете!

Чтобы окончательно добить вашу психику и чувство прекрасного, показываю несколько проектов «обратного портирования» STL — как раз для особых случаев, вроде истории с DigitalMars.

Первый проект:

A recreation of much of the modern C++ standard library in ISO C++98

https://github.com/DryPerspective/Cpp98_Library

Что именно из STL реализовано и примеры использования есть в Wiki проекта. Собирать с помощью dmc попробовать не успел, но выглядит многообещающе.

Второй проект:

A backport of C++11/14/17 features to earlier versions

https://github.com/chaossky/cppbackport

Тут основная цель это C++11, который DigitalMars поддерживает очень слабо, плюс будут проблемы с Windows-платформой:

Does it work on Windows? Sorry, POSIX mostly. Would love for some Windows devs to help.

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

Возвращение на Землю

Несмотря на описанное в статье, дела с компиляторами C/C++ от DigitalMars обстоят несколько печальней чем кажется. Во-первых этот компилятор очень сильно устарел и совместимость со стандартом языка осталась на уровне C++98.

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

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

Во-вторых этот компилятор по прежнему имеет серьезные проблемы с лицензированием:

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

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

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

Но для целей кросс‑компиляции под DOS (если вас жизнь заставит) — инструмент более чем интересный.

P.S.

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

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

Необычный заказ: разработка под CP/M

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

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

NE PUERO GLADIUM, DR. KRIGER. 04.03.2026

Три пары глаз повернулись ко мне в немом изумлении.

— Это длинная история, нам лучше присесть.

Все герои в сборе.

Все герои в сборе.

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

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

И тем странее выглядел этот заказ.

— Алекс, полагаю?

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

— Это проблема? Мне рекомендовали вас как опытных профессионалов..

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

АРХЕОТЕХ

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

CP/M 2.2 compilers, assemblers, and interpreters

CP/M, если кто вдруг не знает (коих среди читателей точно будет большинство), это такая операционная система из далекого-далекого прошлого, созданная аж в 1974 году. Разумеется давно и крепко забытая обывателями.

Для сравнения и понимания исторического периода:

в 1977м году вышел первый фильм Джорджа Лукаса по вселенной «Звездных Войн», а в 1975м закончилась война во Вьетнаме.

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

Чтобы вы смогли оценить объем и сложность проделанной работы:

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

C помощью дисководов, возрастом слегка за пятьдесят, типа такого:

<a href="https://pikabu.ru/story/neobyichnyiy_zakaz_razrabotka_pod_cpm_14058329?u=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FHistory_of_the_floppy_disk&t=8-%D0%B4%D1%8E%D0%B9%D0%BC%D0%BE%D0%B2%D1%8B%D0%B9&h=cd61eab45f544483eb7ee1c8de57cbbf55554ef9" title="https://en.wikipedia.org/wiki/History_of_the_floppy_disk" target="_blank" rel="nofollow noopener">8-дюймовый </a>(!) диск с дисководом начала 70х. Слева для сравнения "современная" 1'44 дискета.

8-дюймовый (!) диск с дисководом начала 70х. Слева для сравнения "современная" 1'44 дискета.

Представляете, чего стоило все это оживить, запустить и заставить работать?

Физические процессы, механические повреждения и влияние среды при этом никто не отменял.

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

От такого изобилия начала капать слюна и дергаться глаз:

Сообщения коммитов вроде "macos build" и "linux build" в одной строке с компиляторами из 70х невероятно доставляют.

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

Отдельный восторг у искушенной публики вызовет наличие скриптов сборки под Unix (Linux/Mac/BSD).

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

— Этот разговор будет долгим. Не найдется ли у вас чем освежиться?

..

Искусство искусством, но прежде чем погружаться по локоть в «кровавый ад разработки» 80х c линковщиками и компиляторами, стоит рассказать о технических реалиях тех лет — оборудовании и самой операционной системе CP/M. Будет интересно, даже если вы, дорогой читатель, сами не старше третьего GTA.

Аукционный дом <a href="https://pikabu.ru/story/neobyichnyiy_zakaz_razrabotka_pod_cpm_14058329?u=https%3A%2F%2Fwww.christies.com%2F&t=Christie%27s&h=668bd8bd8985e0826c77a55bac7d97b5689ffd47" title="https://www.christies.com/" target="_blank" rel="nofollow noopener">Christie's</a>, <a href="https://pikabu.ru/story/neobyichnyiy_zakaz_razrabotka_pod_cpm_14058329?u=https%3A%2F%2Fonlineonly.christies.com%2Fs%2Ffirsts-history-computing-paul-g-allen-collection%2Fgroup-nine-early-microcomputers-120%2F230058&t=%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B0%D1%8F%20%D0%BA%D0%BE%D0%BB%D0%BB%D0%B5%D0%BA%D1%86%D0%B8%D1%8F%20%D0%9F%D0%BE%D0%BB%D0%B0%20%D0%90%D0%BB%D0%BB%D0%B5%D0%BD%D0%B0&h=cb7e7057a8bda4da9e09231074fbf666bca6f0a7" title="https://onlineonly.christies.com/s/firsts-history-computing-paul-g-allen-collection/group-nine-early..." target="_blank" rel="nofollow noopener">частная коллекция Пола Аллена</a>.

Аукционный дом Christie's, частная коллекция Пола Аллена.

ЭПОХА МИКРО

Между огромными мейнфреймами, занимающими машинные залы и первым домашним PC с DOS а затем и Windows, был относительно краткий период рассвета (1970-1980) микрокомпьютеров:

A microcomputer is a small, relatively inexpensive computer having a central processing unit (CPU) made out of a single integrated circuit microprocessor.

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

Знаменитый <a href="https://pikabu.ru/story/neobyichnyiy_zakaz_razrabotka_pod_cpm_14058329?u=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FCommodore_128&t=Commodore%20128D&h=a8faf68b98c488a7565ecaeae3695fafd7f55edb" title="https://en.wikipedia.org/wiki/Commodore_128" target="_blank" rel="nofollow noopener">Commodore 128D</a>

Знаменитый Commodore 128D

Не менее знаменитый <a href="https://pikabu.ru/story/neobyichnyiy_zakaz_razrabotka_pod_cpm_14058329?u=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FTRS-80&t=TRS-80&h=562f4f5c590fd1e17a9c83a77d3c676ae9e688a7" title="https://en.wikipedia.org/wiki/TRS-80" target="_blank" rel="nofollow noopener">TRS-80</a>.

Не менее знаменитый TRS-80.

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

— Интересная история, крайне.. необычная.

Рассказ гостя определенно интриговал.

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

— Но все же, что конкретно мы должны реализовать? И под какую машину?

KAYPRO II

Одной из компаний, решивших поучаствовать в гонке первых домашних компьютеров была Kaypro Corporation:

Kaypro Corporation was an American home and personal computer manufacturer based in Solana Beach, California, in the 1980s

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

by mid-1983 the company was selling more than 10,000 units a month, briefly making it the fifth-largest computer maker in the world

Пятая в мире по продажам, рядом с Apple и IBM — в первом ряду, не где-то сбоку, причем с первого продукта.

Именно этот продукт — домашний компьютер Kaypro II был выбран в качестве полигона для восстановления процесса разработки под CP/M. И последующей сдачи этого фантастического проекта для столь особенного заказчика.

Только посмотрите на эту брутальную «няшу» в стильном металлическом корпусе:

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

— Видите ли, Алекс. У нас в.. организации есть один сотрудник. С наци.. ээ немецкими корнями и такой же педантичностью.

Мой гость сделал еще один глоток виски, явно наслаждаясь процессом.

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

— С нашей помощью, полагаю?

Создатель операционной системы CP/M <a href="https://pikabu.ru/story/neobyichnyiy_zakaz_razrabotka_pod_cpm_14058329?u=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FGary_Kildall&t=Gary%20Kildall&h=9f2249d31f7fb95184c772e94dd2cdfbf20db77e" title="https://en.wikipedia.org/wiki/Gary_Kildall" target="_blank" rel="nofollow noopener">Gary Kildall</a> за работой, 1978 год. Огромный ящик слева это.. дисковод.

Создатель операционной системы CP/M Gary Kildall за работой, 1978 год. Огромный ящик слева это.. дисковод.

CP/M

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

CP/M,originally standing for Control Program/Monitor and later Control Program for Microcomputers, is a mass-market operating system created in 1974 for Intel 8080/85-based microcomputers by Gary Kildall of Digital Research, Inc.

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

Зеленые буквы на черном фоне, за тридцать лет до выхода "Матрицы".

Зеленые буквы на черном фоне, за тридцать лет до выхода "Матрицы".

Кстати скриншот выше это тоже настоящий артефакт, который был сделан после случайной археологической находки — были обнаружены исходники четырех первых версий CP/M, примерно 1975-79 годов. Так вот на этом скриншоте — работающая CP/M версии 1.3.. собранная из найденных исходников!

..

— Несмотря на все наши таланты и умения, думаю не сможем помочь.

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

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

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

ЭМУЛЯЦИЯ

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

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

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

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

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

MYZ80

Начнем с самого отбитого хардкорного. Перед вами настоящий дедушка эмуляции — действующий эмулятор CP/M, написанный в 1991м году под MS-DOS:

MyZ80 does more than just emulate the CPU though, it's a complete Z80 computer emulation

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

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

Скачиваем и распаковываем:

wget http://www.z80.eu/downloads/myz80124.zip
mkdir myz80cpm && cd myz80cpm
unzip ../myz80124.zip

Внутри будет весьма необычный для современности набор файлов:

MYZ80.EXE — очевидный бинарник эмулятора, но проблема в том что это 16-битное приложение для DOS, запустить которое в современном окружении представляет проблему. Так что придется использовать еще один известный эмулятор — Dosbox, чтобы запустить внутри винтажный MYZ80. А так эта «матрешка» выглядит в работе:

Самое важное, ради чего были нужны заморочки аж с двумя эмуляторами — виртуальные диски, связанные с файловой системой DOS:

Поскольку Dosbox в свою очередь дает виртуализацию дисков непосредственно из хоста — получаем отличный способ быстро перебрасывать файлы в виртуальную CP/M.

Что важно при любой разработке с использованием эмулятора.

Собственно, то самое тестовое приложение EXMPL.COM на заглавной картинке было сначала скопировано в каталог с эмулятором:

cp ../../src/cpm_compilers/manx\ aztec\ c\ v106d/EXMPL.COM .

Затем импортировано в виртуальный диск CP/M:

И запущено уже в CP/M.

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

..

— Интересное предложение. Проучить коллегу по опасному инженерному делу, еще и за приличные деньги..

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

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

— Боюсь, уринотерапия тут не поможет — коллега все же имеет немецкие корни. У каждой нации свои.. культурные особенности.

— Ну да ладно. Обсудим конкретику?

MAME

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

С помощью Mame, мы получим почти настоящий Kaypro II, с оригинальным ROM и оригинальным образом загрузочной дискеты с CP/M — прямо из 80х.

Дальше нужно будет скачать загрузочный образ 5.25-дискеты с CP/M 2.2, например отсюда, затем специальные ROM-файлы для самого Kaypro II и его весьма специфической клавиатуры, отсюда.

Архивы называются kayproii.zip и kaypro10kbd.zip, их необходимо копировать как есть, не распаковывая. Создаем структуру каталогов:

mkdir kayproii-machine && cd kayrpoii-machine
mkdir roms disks

Копируем ROM-файлы:

cp ~/Downloads/kaypro*.zip ./roms/

Распаковываем образ дискеты:

7z e ~/Downloads/KAYPRO\ II\ 64k\ CPM\ vers\ 2.2.7z -o./disks/

Запускаем:

mame kayproii -w -rompath ./roms -flop1 disks/KAY64CPM.IMD

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

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

..

Да все просто.

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

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

Создать новую электронную жизнь в древнем компьютерном теле.

— Надеюсь, ваш коллега хотя-бы вводит код руками а не другим органом.

Гость посмотрев с удивлением, ненадолго задумался.

— Какая интересная мысль! Мне бы такое в голову не пришло. Я уже упоминал, что вы с ним похожи?

CPMTOOLS

Это весьма известный (среди некрофилов) набор инструментов для работы с файловыми системами CP/M, за авторством Michael Haardt. К сожалению работа над оригинальным проектом давно прекращена, так что мы будем использовать более современный форк:

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

./configure
make

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

Думаю даже по названию нетрудно догадаться об их предназначении.

Думаю даже по названию нетрудно догадаться об их предназначении.

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

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

cp ../../work/kayproii-machine/test2.dsk .

Проверяем что диск читается:

./cpmls -f kpii test2.dsk

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

./cpmcp -f kpii test2.dsk README 0:README.TXT

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

NTVCM

Наконец последний эмулятор CP/M, который тоже придется использовать, хотя и опосредованно:

Virtual CP/M Machine. Emulates CP/M and the 8080/Z80 on Linux, MacOS, Windows, and real-mode 8086 DOS to run CP/M .com files

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

Этот самый ntvcm придется собрать и добавить в переменную PATH, поскольку эмулятор создан тем же автором, что собрал репозиторий с компиляторами под CP/M и используется из скриптов для тестов и.. кросс-компиляции. Под CP/M, ага.

Шаги простые и очевидные:

Дальше просто добавляете этот каталог в переменную PATH:

export PATH=$PWD:$PATH

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

..

— Короче, коллега Кригер считает, что только он один в силах что-то написать для нашего Kaypro II. Что вообщем-то правда, поскольку остальные умеют все эти ваши компьютеры только включать и выключать. Обычно пинком.

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

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

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

Увиденное на фото заставило шевелиться последние остатки волос.

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

All 24 and 28 pins EPROM pinout 2708 - 27512. <a href="https://pikabu.ru/story/neobyichnyiy_zakaz_razrabotka_pod_cpm_14058329?u=http%3A%2F%2Fwww.z80.info%2F&t=%D0%9E%D1%82%D1%81%D1%8E%D0%B4%D0%B0&h=0538d55ccd2e382340c71e498d719cfb76e91df1" title="http://www.z80.info/" target="_blank" rel="nofollow noopener">Отсюда</a>.

All 24 and 28 pins EPROM pinout 2708 - 27512. Отсюда.

— Это распиновка EPROM, для Z80.

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

Тут гость привлек мое внимание, указав на одну из фотографий.

— Вот здесь, на стене было загадочное послание, оставленное.. аналогичным способом. Но его смысл мы так и не смогли разгадать.

Я пригляделся к фотографии:

640КБ ХВАТИТ НА ВСЕХ

— Нет идей что бы это могло значить?

Пол Аллен и юный Гейтс за четыре года до основания Microsoft.

Пол Аллен и юный Гейтс за четыре года до основания Microsoft.

НЕПРЕРЫВНОЕ ВЕСЕЛЬЕ

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

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

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

  • Сборка;

  • Тестовый запуск с помощью ntvcm;

  • Запуск с помощью myz80;

  • Запуск в Mame с образом настоящего Kaypro II;

  • Сеанс рукоблуд.. ээ переход к следующему примеру.

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

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

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

Aztec C

Спасая цитату из погибающей википедии:

Aztec C is a discontinued C programming language compiler for CP/M-80, MS-DOS, Apple II (both Apple DOS 3.3 and ProDOS), Commodore 64, early Macintosh, Amiga, and Atari ST. It was sold commercially by Manx Software Systems[1].

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

Aztec C — a programming language for a variety of platforms

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

Сложно из 2026 года оценивать цены 1980х, но судя по прайсу выше, Aztec C был явно не самым дорогим софтом, тем более для разработчиков. Но мы опять отвлеклись.

Переходим в каталог manx aztec c v106d и запускаем сборку примера:

./m.sh EXMPL

Да, обязательно КАПСЛОКОМ и обязательно без указания расширения, поскольку в скрипте идет вызов как компилятора, так и линковщика. Будет собран COM-файл для CP/M, который проще всего запустить с помощью эмулятора от автора репозитория:

ntvcm EXMPL.COM

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

Теперь запустим на втором эмуляторе — MYZ80, чтобы убедиться в корректности только что собранного приложения. Копируем созданный EXMPL.COM в каталог с эмулятором и запускаем его через Dosbox:

cp ../../src/cpm_compilers/manx\ aztec\ c\ v106d/EXMPL.COM .
dosbox MYZ80.EXE

Затем, уже из CP/M выполняем:

import EXMPL.COM

Этим действием, файл будет скопирован с хоста в виртуальный диск (дискету) CP/M, откуда его наконец можно будет запустить:

EXMPL

В действии:

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

./cpmcp -f kpii ../../work/kayproii-machine/test2.dsk ../cpm_compilers/manx\ aztec\ c\ v106d/EXMPL.COM 0:EXMPL.COM

Этим действием мы запишем наше собранное приложение EXMPL.COM в образ дискеты для Kaypro II. Для проверки выполняем:

./cpmcp -f kpii ../../work/kayproii-machine/test2.dsk

Должно быть видно содержимое образа дискеты, с новым файлом внутри:

Наконец запускаем максимально близкую к реальности эмуляцию компьютера Kaypro II в Mame:

mame kayproii -w -rompath ./roms -flop1 disks/KAY64CPM.IMD -flop2 test2.dsk

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

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

..

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

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

— Как же оно называется? Туда запихиваются такие плоские черные штуки? Д.. дди.. доо..

— Дисковод, для дискет. Работу необходимо сдать на 5.25 дискете, я уже понял.

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

(гомерический хохот за кадром)

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

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

Microsoft COBOL

Малоизвестный ныне факт, но компания Microsoft когда-то продавала компилятор COBOL:

Microsoft produced a COBOL compiler for CP/M. This product was also licensed to IBM as IBM Cobol Compiler.

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

./m.sh SQUARO

Для запуска помимо SQUARO.COM надо перебросить в эмулятор еще и файл RUNCOB.COM, иначе не заработает. Так этот пример выглядит в работе, на фоне (в редакторе) можно заметить исходный код на COBOL:

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

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

..

alex0x08: дарова!

alex0x08: нужны тайные знания ордена

alecv: излагай

alex0x08: надо программку одну запустить, на железе из 1987 года :)

..

Единственное фото дискеты с этим компилятором, которое удалось найти.

Единственное фото дискеты с этим компилятором, которое удалось найти.

Prospero Pascal

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

Prospero Pascal is a full-featured Pascal that includes everything you need to begin writing stand-alone programs for your Atari ST.

Компилятор Паскаля для 8-битных компьютеров с блекджеком и блудницами с дополнениями и доработками:

Prospero Pascal is a superset of ISO Standard Pascal. In addition to the standard features, this package offers a number of extensions including dynamic strings, single- and double-precision floating-point arithmetic, an assembler-level interface, and separate compilation of program segments. Moreover, Prospero Pascal offers a high degree of portability.

Динамические строки, вычисления с плавающей точкой, поддержка ассемблерных вставок и все это в 1983м году! На Паскале и под Atari, если кто вдруг не понял.

В качестве примера была взята реализация "симулятора жизни":

./m.sh LIFE

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

ntvcm PROLINK.COM LIFE,PASLIB/S

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

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

Borland Pascal 1.0

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

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

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

Это его первая релизная версия, с которой все началось.

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

Вот он, тот самый TURBO.COM, который позже превратится в TURBO.EXE:

Кусочек руководства пользователя с адресом «небольшой компании-стартапа» на просторах Калифорнии:

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

Для тестов, были скопированы TURBO.COM и HELLO.PAS на образ дискеты, который затем подключили в эмулятор. Так все действо выглядит в эмуляторе Mame:

НЕЛЕГКИЙ ВЫБОР

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

  • Первая релизная версия Borland Pascal;

  • Редкая версия Pascal, с крутыми (для 1983 года) фичами;

  • Microsoft COBOL, без комментариев.

И конечно же Aztec C — реализация языка С до его стандартизации, настоящий артефакт. Что выбрала для меня судьба?

И.. да, это Aztec C.

(бурные аплодисменты)

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

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

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

ПОСЛЕДНЯЯ ПРОВЕРКА

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

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

Ордена «Свидетелей Спектрума».

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

Тут было все:

Спектрумы, Амиги, старые рабочие станции Sun и HP, DEC и даже Alpha.

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

запуска собранного приложения для машины из 1987го на реальном компьютере 1987го.

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

Robotron 1715 — 8-разрядный персональный компьютер производства ГДР. Производился на заводе VEB Robotron.

Версия 1715M, сохраненная в ордене это «экспортный» вариант, когда-то давно поставляемый в Советский Союз. С величайшим почтением нашептывая молитвы загрузки, послушник в черном балахоне приблизился к постаменту и приложил ладонь к считывателю отпечатков пальцев. Защитное стекло начало медленно опускаться.

Наконец запуск и загрузка операционной системы SCP ( аналога CP/M, но из ГДР) были завершены. Появилось приглашение ввода команд:

А>

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

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

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

NE PUERO GLADIUM, DR. KRIGER. 04.03.2026

Что в переводе с латыни означает:

НЕ СТОИТ ДАВАТЬ БОЕВОЙ МЕЧ МАЛЬЧИКУ, ДОКТОР КРИГЕР.

Проверка удалась и написанное мой приложение для компьютеров 80х под CP/M действительно успешно запустилось на настоящем компьютере из 1987 года. Так это выглядело в живую:

Мое послание, запущенное на настоящем Роботроне.

Мое послание, запущенное на настоящем Роботроне.

P.S.

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

Показать полностью 36
Отличная работа, все прочитано!

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества