Русский
Русский
English
Статистика
Реклама

Delphi

Даешь свободную литературу! Или как я с политикой вуза боролся

25.04.2021 12:10:20 | Автор: admin

Доброго времени суток, хабровчане! Это мой первый пост на форуме, так что прошу строго не судить.

Коротко обо мне: студент, увлекаюсь электроникой, микроконтроллерами, и программированием. Однако, моя специальность ни коим образом не связана с It. Со мной покончено, переходим к сути.

Как и полагается любому техническому вузу в нашем есть куча интернет ресурсов, которыми вуз чрезмерно гордится. Однако есть оборотная сторона медали качество этих сервисов. А именно, если говорить про электронную библиотеку, о коей и пойдет речь в данной статье, то в ней напрочь отсутствует возможность скачивания pdf-версии нужной тебе методички, точнее она есть, но за это придется заплатить немало денЯк. Деньги далеко не маленькие (если говорить именно про цену за вузовские методички). Если же такой формат не устраивает, то можешь пользоваться онлайн библиотекой.

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

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

И теперь представьте картину: человек пытается подготовиться к контрольной по квантовой механике по методичкам преподавателя, объемом 700 страниц, где необходимый материал находится на 500, и может перелистывать по 5 страничек в минуту, и каждые 20 минут, его попытки приходится возобновлять. В общем, жесть. И вот после очередной неудачной попытки прочитать нужную главу, я решил, что пришло время положить конец данному произволу.

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

http://www.<название сайта>/plugins/<название просмоторщика>/getDoc.php?Id=<id книги>&page=<номер страницы>

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

  1. Необходимо получить id книги (или лучше ссылку на нее)

  2. Узнать количество страниц

  3. В самом простом цикле for пробежаться по всем страницам, загрузив их на компьютер

  4. Объединить фотографии в единый файл pdf

  5. Радоваться

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

Самая первая версия программы выглядела максимально убого: я использовал java и библиотеку selenium для работы с сетью. Приложение получилось явно не user-friendly: запускалась только из IDEA, в которой ручками необходимо было вставлять ссылку на каждую книгу, также ручками забивать количество страниц. Более того, самым убожеством был тот факт, что приложение полностью имитировало пользователя:

  • Открывался сайт в браузере

  • Далее проводился поиск полей для логина и пароля и их заполнение

  • Затем переход по ссылке с первой страницей книги

  • И сочетание клавиш CTRL+S, нажатие на Enter.

В общем фу! Нельзя так делать!

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

Но, не спешите бросаться помидорами, данный кошмар в дальнейшем был преобразован в весьма неплохой код. Я перешел на Delphi! Да, многие могут сказать, что язык устарел, и не обладает должным функционалам, однако в своей работе я его применяю постоянно. Обучился сему творению благодаря Михаилу Фленову (низкий поклон вам, Миша).

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

Рисунок 1 - Главная форма приложенияРисунок 1 - Главная форма приложения

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

Небольшое отклонение: логин на сайте представляет собой некоторый набор цифр, а пароль имя, написанной на русском языке. Однако, в программе WireShark на сайт отправлялся шестнадцатеричный код следующего формата: %D0% FF %D0% FF %D1% FF. Как позже выяснилось, символы FF кодировали буквы имени, а %D0% и %D1%, что то вроде контрольных байт. З.. Огромная просьба к знатокам, читающим эту статью, если это какой либо общеизвестный сетевой протокол, то не кидайтесь помидорами, а дайте, пожалуйста, ссылку, где можно прочитать про него. Но т.к. я не сталкивался с таким раньше, пришлось разбираться во всем самому.

Первым делом я обратил внимание на повторяющиеся символы %D1% и %D0%, которые, как я сразу и предположил, являются контрольными байтами, однако, оставалась загадкой, чем отличается %D0% от %D1%. Но это пока отложим, едем дальше. Как же шифруется пароль? А он шифруется достаточно просто, предположим, меня зовут Василий, давайте запишем мое имя в ACII символах:

82 (В) A0 (а) E1 (с) A8 (и) AB (л) A8 (и) A9 (й)

А на сайт отправляется следующая последовательность:

72 (В) 90 (а) 61 (с) 98 (и) 9B (л) 98 (и) 99 (й)

Хм, просматривается одна зависимость. А, точно! Если внимательно приглядеться, то у каждого символа, отправляемого на сайт, старший байт уменьшается на 1. Бум! Загадка разгадана.

Хотя, стоп, а почему же тогда вместо D1 (буква с), на сайт отправляется 61? А черт ее знает! Просто запомним, что для символов, лежащих в диапазоне от E0 до EF из старшего байта необходимо вычитать не 1, а 6. Собственно и все! А помните, я говорил про контрольный символ %D1%? Так вот, этот символ как раз таки ставится перед символами из данного диапазона. Ну, собственно и все. Далее привожу кусок кода, отвечающий за шифровку.

Код ""
 for i := 1 to length(password) do begin    temp := Ord(password[i]);    //Представили пароль в HEX виде    if (temp < 1088) or (temp > 1103) then // Значения символов E0 и EF        begin                 //Если вне этого диапазона, то %D0%pasBytes[i] := '%D0%' + IntToHex(((temp) - 896), 2); // + 128 - 1024 для шифрования   newPassword := newPassword + pasBytes[i];         end    elsebegin   //Иначе %D1%pasBytes[i] := '%D1%' + IntToHex(((temp) - 960), 2); // +64 -1024 для шифрования                   newPassword := newPassword + pasBytes[i];end; end;//1024 здесь вычитается потому, что в Delphi почему то ASCII символы начинаются с #400//Почему - не ясно 

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

%D0% 72 %D0% 90 %D1% 61 %D0% 98 %D0% 9B %D0% 98 %D0% 99

Собственно это и была самая сложная часть работы.

Далее, после авторизации, пользователь вставляет в строку ввода ссылку на необходимую книгу. Программа же в это время отправляет Get запрос на сервер, из ответа которого, затем в автоматическом режиме находит название книги, ее ID, и количество страниц. Все эти данные сохраняются в глобальные переменные. И, затем, после нажатия кнопки Download, в отдельном потоке отправляются Get запросы на сервер, из которых и забираются картинки с изображениями страниц. Которые в дальнейшем, с помощью библиотеки Synapse формируются в единый PDF файл.

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

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

Подробнее..

Рисуем интерференционную картину на JavaScript

18.06.2020 02:12:19 | Автор: admin
15 лет назад я пытался писать диссертацию на тему Оптоэлектронный метод определения шероховатости поверхности. В ходе работы активно использовались BRDF-функции и прочий замечательный математический аппарат для оптики. Был написан код и пара глав, но интерес пропал повяз в работе. Пару раз пытался заново начать, но, к сожалению, так и не нашел причины выключиться из семьи и работы на год или даже больше. В качестве побочной задачки решил для себя реализовать визуализацию какого-нибудь интересного оптического эффекта. Выбор пал на интерференцию (wiki: взаимное увеличение или уменьшение результирующей амплитуды двух или нескольких когерентных волн при их наложении друг на друга), как наиболее простую в реализации механику.

image


Первая версия была реализована на Delphi + OpenGL ещё в 2005 году, она предполагала манимацию смены фазы волны и состояла всего из 200 строк кода. Удивительно, но ее код до сих пор доступен в Кладовке.

Вернемся к более поздней версии, выполненной уже на JavaScript.


Физика отрисовки проста:
  1. Помещаем на страницу canvas и навешиваем событие onclick, которое запоминает координаты двух последних кликов мыши источников излучения: (x1, y1) и (x2, y2).
  2. Проходим в цикле по каждой точке canvas (x, y) и вычисляем евклидово расстояние до наших источников излучения: S1=SQRT((x1-x)^2+(y1-y)^2) и S2=SQRT((x2-x)^2+(y2-y)^2)
  3. Мы предполагаем, что наши источники излучают синусоидальный сигнал, поэтому можем легко вычислить амплитуду каждой точки: A(x, y)=sin(S1 * W)+sin(S2 * W), где W длина волны. Сюда можно еще добавить фазу, но это по желанию.
  4. Далее нормируем полученную амплитуду и получаем цвет пикселя: C(x,y)=A(x,y)*68+127
  5. ???
  6. Profit



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

image

Если вам интересно можете добавить реалистичности:


  1. Затухание сигнала.
  2. Анимацию изменения фазы.
  3. Отражение сигнала от плоскости (через аффинные преобразования легче всего).
Подробнее..

Пишем видеочат для локальной сети, или осваиваем WebRTC в 2020 году

04.08.2020 02:04:14 | Автор: admin
На фоне известных событий делать было нечего в рамках профессионального роста пытаюсь освоить WebRTC. Как известно, лучший способ освоения сделать что-нибудь хотя бы потенциально полезное. А заодно и поделиться-обменяться опытом создания и набивания шишек.

В качестве задачи было решено сделать простое приложение, позволяющее проводить аудиовидеозвонки между двумя (пока что) стационарными или мобильными устройствами в локальной сети без необходимости подключаться к Интернету. Установка и первоначальная настройка такого приложения должны быть простыми настолько, чтобы сколь-нибудь продвинутый эникей без проблем с этим справился и показал пользователям, как делать звонки, а при наличии соответствующих навыков мог бы сделать небольшие доработки в части дизайна и возможностей. Клиентом должно выступать любое устройство, оснащённое устройствами ввода-вывода мультимедиа и позволяющее запустить подходящий браузер (Firefox или Chrome тестил на майских, кажется, версиях).


Как это сделано


Как известно, технология WebRTC для связи между двумя абонентами предлагает использовать объект типа RTCPeerConnection, а главной задачей разработчика является организация обмена текстовой информацией (SDP-offer, SDP-answer, ICE-кандидат) между вызывающим и вызываемым абонентами. Другими словами, разработчику нужно сначала сделать текстовый чат с API для браузерного JavaScript и далее прикрутить к нему мультимедиа-часть события RTCPeerConnection и методы передачи и обработки приёма данных.

Выбор технологий для реализации и API текстового чата остаётся за разработчиком. Многие (и в частности Mozilla в своём официальном примере работы RTCPeerConnection) предпочитают использовать WebSocket API и соответствующий сервер например, на Node.JS. Но, учитывая нашу задачу сделать максимально просто для развёртывания, я решил для начала не переусложнять серверное приложение, тем более для доставки клиентскому устройству страниц и скриптов нужен был дополнительно Web-сервер. Поэтому API решил сделать на xmlHttpRequest с периодическими обращениями клиентов к тому же Web-серверу. Не могу сказать, что это работает идеально с точки зрения расхода ресурсов (и батарей) клиентского устройства и отсутствия тормозов, но работает точно, если при разработке учитывать некоторые нюансы. Возможно, в какой-нибудь следующей версии добавлю сервер WebSocket и переделаю соответствующим образом API, но не всё сразу.

Серверную часть было решено сделать на Lazarus под Windows; сетевые возможности обеспечивает пакет Synapse. В чём-то это, наверное, извращение, и пришлось серьёзно повозиться и набить несколько шишек, чтобы заставить всё работать так, как задумано. Но один exe, две dll (библиотеки OpenSSL), файлы самоподписанного SSL-сертификата и ключа к нему и немножко файлов конфигурации (плюс статика) позволяют сильно не заморачиваться с уровнем техники под сервер и способом запуска приложения. Первую версию этого сервера в 32-битном билде я тестил даже на Asus Eee PC 900 2009 года выпуска под Windows XP, хоть и не обошлось без чита в виде недавней замены штатного супермедленного SSD на более современный и объёмный. Это если говорить за производительность. А установка сервера представляет собой распаковку скачанного zip-архива в любую подходящую папку, редактирование JSON-файла конфигурации учётных записей пользователей и запуск exe-файла программы (в окне есть ещё кнопка, но можно указать параметр в командной строке, чтобы Web-сервер запустился сразу). Так или иначе, раздумываю и над более серьёзной серверной частью, благо опыт такой имеется. Всему своё время.

Помимо собственно организации API наш сервер отдаёт статические файлы для браузеров (Web-страницы логина и чата, стили, изображения, скрипты, рингтон). Вообще я старался по возможности обойтись без сторонних библиотек, но из-за того, что с дизайном и html-вёрсткой у меня не ахти, решил всё же воспользоваться jQuery.UI и соответственно jQuery, которые Web-сервер также отдаёт как статику. Все файлы статики лежат в отдельной подпапке папки программы; их, естественно, можно смотреть и даже менять при желании и наличии соответствующих навыков. В JavaScript код прокомментирован, по нему при необходимости можно и учиться.

Как организовать связь


Для организации связи главное подобрать и объединить в общую сеть клиентские устройства (компьютеры, ноуты, смартфоны, планшеты) и Windows-машинку с сервером (она же может выступать и клиентом). Из клиентских устройств я тестил несколько недорогих смартфонов выпуска последних нескольких лет на Android начиная с 7-й версии, а также компьютер и ноут на Windows 10, в т. ч. с двумя подключенными Web-камерами; они показали себя нормально. Первую версию я шутки ради протестил даже на Orange Pi One с Lubuntu (или Kubuntu, не помню уже с ходу) от производителя. На удивление, оно даже работало, хоть видео и тормозило, а страница чата открывалась невесть сколько времени (про загрузку системы и открытие браузера даже говорить не хочется).

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

Работает всё так. Пользователи заходят на серверную машинку браузером по протоколу https, используя её IP-адрес или доменное имя. Там они вводят свои логин-пароль и заходят на страницу чата со списком контактов. При щелчке по контакту открывается окно диалога с историей текстовых сообщений (к слову, сервер хранит её только в ОЗУ, в файл пока не умеет), полем для чата и формой аудиовидеозвонка с флажками для выбора аудио и (или) видео. Для совершения видеозвонка пользователь отмечает соответствующие флажки, нажимает кнопку звонка и подтверждает разрешение браузеру. У вызываемого абонента начинает пищать рингтон и открывается форма ответа с такими же флажками. После щелчка по кнопке ответа браузер также спросит разрешение на доступ к мультимедиа-устройствам. Затем открывается окно звонка.

Не могу сказать, что у меня большой опыт работы с ПО для видеоконференций, видеоконсультаций и т. д., но, например, в Google Hangouts на компьютере (как на мобильных устройствах, не знаю) я не увидел возможности включить себялюбимого на полный экран, что в теории может потребоваться на удалённых консультациях, когда тебе нужно хорошо видеть то, что ты показываешь своему собеседнику (например, через заднюю камеру смартфона). В этом чате в диалоге звонка я решил сделать две вкладки с видео собеседника и самого пользователя. С текущей версии на вкладке пользователя, помимо самого видео, есть поля для выбора камеры и микрофона; с менять их значения можно на лету в ходе беседы. Возможно, это окажется кому-то полезным.

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

Современные особенности работы и реализации WebRTC и вообще работы с мультимедиа на JavaScript


Здесь кратко; подробности можно посмотреть в комментариях в javascript-файле static/js/videoChat.js

  1. Chrome точно, плюс, возможно, и другие браузеры позволяют работать с getUserMedia только на сайтах, доступных по HTTPS
  2. Перечень устройств ввода аудио и видео можно получить только после успешного вызова getUserMedia
  3. Автоматический запуск проигрывания звука средствами JavaScript (через метод play() html-элемента video или audio) возможен только после того, как пользователь покажет активность на сайте например, щёлкнет по какому-то элементу управления.
  4. Окончание выполнения promise после setLocalDescription у вызывающего абонента нужно придержать хотя бы до того момента, как сервер чата отдаст вызываемому абоненту на обработку отправленный offer. Без этого RTCPeerConnection сразу начнёт отдавать ICE-кандидатов, которых вызываемый абонент до обработки полученного оффера добавить к себе не сможет.
  5. Для переключения устройств ввода на лету перед вызовом getUserMedia необходимо остановить все старые треки на RTCPeerConnection. Без этого, что бы пользователь ни выбрал в качестве устройства ввода, оно выбрано не будет.
  6. Во многих описаниях для мобильных устройств говорится про свойство facingMode для выбора фронтальной или задней камер. На самом деле не знаю, как в старых устройствах, но в этом чате на протестированных смартфонах переключение работает и без использования этого свойства. Но строго с учётом п. 5.


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

Шишка разработчика сетевых приложений на Lazarus


Пакет Synapse на текущий момент поддерживает только библиотеки OpenSSL версий 1.0.x; в 1.1 много чего уже реализовано по-другому, другие даже имена библиотек. Кроме того, просто поместить dll-ки в папку с программой недостаточно. Нужен также файл конфигурации (openssl.cnf), путь к которому задаётся через переменную окружения OPENSSL_CONF.

Где скачать


Дистрибутивы программы под Win32 и Win64 и исходники серверной части на Lazarus доступны на странице программы по ссылке www.lubezniy.ru/soft/videochat

P. S.: Кстати, кто-нибудь знает, каким образом можно с помощью Lazarus автоматизировать сборку из одних и тех же исходников двух разных exe под Win32 и Win64? Кросскомпилятор есть, но менять каждый раз опции в проекте не то чтобы правильно.
Подробнее..

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

17.08.2020 00:12:34 | Автор: admin


Добрый день, дорогие мастера своего дела!


Это моя первая статья, постараюсь быть интересным. Много лет назад (о ужас) когда я был супер энергичен, молод и глаза горели я хотел автоматизировать все что движется и не движется. Я чувствовал бескрайние свои возможности и был уверен в себе на 100% и более, передо мной не было не одной не решаемой задачи и все горело в руках. Однако с годами трэкбар от молодости к мудрости все двигался в сторону второго. Так со временем приходил опыт, который сын ошибок трудных и увы, там же гений парадоксов друг.

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

На дворе был 2012 год

К тому времени я был богат на опыт, репутацию и шрамы от ран, полученные неудачами и провалами. На одной из планерок я загорелся идеей внедрению электронной очереди в наше управление. На тот момент управление было расположено в 4-х этажном здании и механизм приема и оформления документов охватывал все этажи, где надо было по сути занимать как минимум 4 очереди на 1 дело. Зная все это я понимал что дело за малым. На следующей планерке я представил мой проект, который был из трех частей по автоматизации части приема граждан. Я представил готовое решение, по цене это было тогда примерно 3 средние зарплаты по городу, если говорить о цене для гос учреждения. Проект я представил на проекторе, посмотрев который всем понравилось, но как всегда это бывает денег нет.

Сдаваться и отчаиваться я не умел с детства. Получив благословение руководителя я отправился в вышестоящую инстанцию просить деньги и обосновывая потребность просить деньги на очередь. Хочется сказать что в 2012 году в наших ведомствах с ежедневным приемом граждан свыше 100 человек в день таких систем не было. Обстучав все пороги я все равно получил отказ по той же самой причине.

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

И вот я снова делаю все тоже самое, как в лучших фильмах про временные петли. И вот снова планерка, но уже все новые лица и 2016 года, и я снова толкаю речь о необходимости сие изобретения. Однако чему я как думаю немного научился (как в том же фильме Исходный код) надо чтото по малу менять, поэтому я сделал предложение от которого руководство не сможет отказаться я предложил, что я сделаю комплекс электронной очереди за 0 рублей. И вот тут я получил одобрение отовсюду и ото всех! Что же, небольшая победа, тоже победа! и Это было отличное начало для старта.

Такой позитивный настрой меня всегда портил. Как сказал один высокопоставленный человек художник должен быть голодным. Заряженный голодом, инициативой и бескрайним желанием я просто встал и пошел. Пошел в серверную и начал искать из чего делать. В качестве корпуса я использовал старый (еще белые когда то продавались) корпус системного блока. Ни о каком сенсоре дело идти не могло. Я не настолько крут, чтобы еще и сенсор делать своими руками. Поэтому экран я решил сделать простым, без сенсорного ввода. Функционал я задумывал максимально простым, являясь программистом уже с большим стажем для себя я поял то что программа должна помогать и быть инструментом а не человек для программы. В моем софте я всегда ставил на первое место однокнопочный интерфейс, системы где больше автоматизированного чем ручнотизированного, вот так и интерфейс терминальной части изначально имел всего 3 кнопки. Это потом он обрастет богатым функционалом и изяществом, а пока уже 2017 год.



С марта по май 2017 года я занимался созданием терминала. Одна из сложных задач это было организация печати. Любопытный факт, либо просто везение, я нашел на алиэкспрессе POS принтер со скидкой за 700 с небольшим рублей. Принтер брал только маленькие по ширине рулончики и не умел отрезать талоны. Да да, клиент посильнее мог вместо отрыва вытащить всю ленту. Благо я родился в 80-х и у меня было счастливое детство, опыт работы с железым конструктором позволил стать железным конструктором (шутка). Смострячив (более подходящего слова я не смог подобрать) систему смены ленты и прикрепив пополам разрезанное полотно от пилы по металлу мне удалось сделать систему отрезания путем потягивания талона вверх. Зубья пилы были расположены неровно, немного под углом, а направляющие детальки от железного конструктора заставляли вылезать бумаги вниз так, что взять ее модно было только оторвав. Вот такой трюк.

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

Я не упомянул что система называется ELECTRA.

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

Спустя некоторое время я смог справится с драйвером, своим софтом и подачей бумаги.

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

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

Работа над терминалом подходила к первому тестовому запуску, поэтому помалу назревал вопрос об эстетике.

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

К июню 2017 года терминал уже был как будто с военного завода по производству танков, утюгов и домофонов. Исполнен об был в 2 цветах: белый и синий и четырех тонах.



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

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

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

Конечно мне помогали и специалисты отдела работы с гражданами.

Тогда я молился всем и вся чтобы только на этой моей презентации не было сбоя. На работу я приехал в тот день в 6 утра, а работа начиналась с 8:48. Я перепроверял каждые несколько минут.

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

Теперь граждане спокойно сидят, ожидая свой номер, а специалисты координируют нагруженность так чтобы очередь была максимально продуктивной, например: если у человека вопрос на 40 минут а перед ним 4 по 2 минуте (только получить справку) то тони освобождаются и толпы нет.

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

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



Подробнее..

Recovery mode Немного про SOLID и суровое зомби-легаси

11.02.2021 22:07:58 | Автор: admin

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

Я систематически работаю с Delphi 7 (работа такая, чо). Поддерживаю и активно дорабатываю приложение, уходящее корнями в начало-середину нулевых и написанное в беспощадном императивно-процедурном стиле. Можно ли выжить ментально в подобном окружении? Ведь "единственный способ их победить -- не стать одним из них", не так ли?

Здесь на самом деле можно увидеть что-то вроде такого:

procedure TReport13Form.OnButton1Click(Sender: TObject);beginwith MainForm.WorkQuery.SQL dobeginAdd('Select ...');if ... thenAdd(', ...');Add('from ...');if ... thenAdd('join...');... // Ещё строк двадцать-тридцать подобного трэша.Add(Format('where id = %d and operDate = %s',[id, QuotedStr(DToS(DatePicker1.Date))]));

и прочие подобные непотребства.

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

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

1) Важное примечание: нисколько не ущемляю право старорежимных специалистов на работу в подобном стиле и соответствующую ментальность в условиях глубоко прикладной разработки. У себя на работе (компания НЕ производит ПО, разработка обслуживает интересы основного бизнеса) в этом смысле получил идеальное разграничение интересов: я веду несколько проектов, пишу как хочу, думаю о технологии и принципах проектирования, использую git(2). Мне хорошо. У остальных тоже всё работает. Они тоже молодцы. Дела идут, контора пишет.

2) Таки да, больше никто git не использует - так и не смог убедить. Я же без систем контроля версий процесса разработки не представляю ещё с тех пор, как пользовался CVS (git тогда ещё не родился) и читал новинку Кента Бека про "Экстремальное программирование".

Итак, смешение стиля неизбежно. Можно ли при этом хотя бы в активно дорабатываемой части "писать красиво"? И чтобы не получилось, как в мультике: "И так ходили они красиво весь день, а потом медвежонок сказал: Есть хочу!"? Думаю, что скорее да, даже несмотря на то, что часто приходится переключать мозги на ржавые рельсы ушедших времён, и это, несомненно, накладывает отпечаток. Но получится ли, к примеру, православный SOLID?

Что же, поищем ответ. За примерами далеко ходить не надо -- недавно написал микро-библиотеку для сохранения настроек внешнего вида формы (положение разделителей, порядок и расположение столбцов в таблице, при желании - прочие фантазии). В библиотеке VCL, концептуально разработанной в середине девяностых, с гибкостью отображения вообще проблемы. На самом деле не великая задача, пара часов работы и несколько классов, но радикально сократилось время на поддержку сохранения настроек внешнего вида. Буквально до нескольких строк кода. Попробую оценить её на соответствие культурному коду современного ООП.

Библиотеку, с благословения руководства, выложил на Битбакет под собственным копирайтом: https://bitbucket.org/danik-ik/layoutkeeper/

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

Комментарии по схеме:

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

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

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

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

Итак, пройдём по пунктам.

S. Single responsibility principle.

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

В самой библиотеке выделены интерфейсы и реализующие их абстрактные классы, обеспечивающие фреймворк. Конкретные реализации переопределяют минимальный набор событий фреймворка. Реализация отделена от фреймворка. Фреймворк отделён от реализации. Вроде, S.principle на месте.

O. Open-Closed principle.

Сразу скажу, что значимость в части Closed может нивелироваться в зависимости от условий (единоличное владение проектом, невеликий объём кодовой базы, адекватные регрессные тесты), но где же вы видели хорошо покрытое тестами легаси? К тому же я имел наглость назвать это творение библиотекой, что налагает ответственность в части обеспечения интересов неопределённого круга пользователей. О, этот вечный вопрос совместимости версий! Поэтому данный принцип одним махом выходит в цари горы.

В данном случае ключевым фактором становится наличие интерфейсов. Мало того, что именно в Delphi на интерфейсах работает чёрная магия подсчёта ссылок, так ещё и при желании можно без малейшего зазрения совести выкинуть даже приведённый в библиотеке фреймворк, и написать свою реализацию с нуля, но переиспользовать, например, процессоры, потому, что интерфейсы поделены в соответствии с принципом I. Впрочем, абстрактные классы, как правило, могут быть использованы как есть для реализации (Open) и не требуют при этом вмешательства в собственный код (Closed). Например, в реализации кипера мы можем использовать для хранения БД, или можем сохранять отдельные настройки для каждого из пользователей. При этом с точки зрения использующего библиотеку кода ничего не изменится (разве что где-то в одном месте поменяется фабрика).

L. Liskov substitution principle.

Самый трудный для понимания, потому, что чаще всего рассматривается на неправильном (личное мнение) примере: нам СНАЧАЛА предлагают спроектировать иерархию наследования (предлагают спроектировать сферические прямоугольник и квадрат в вакууме), и лишь ЗАТЕМ, при анализе решения, предлагают задаться вопросом, зачем это было надо, и соответствует ли решение высказанным "когда нибудь потом" пожеланиям. А тут всё (почти) просто: поведение наследника должно оправдывать ожидания от родителя. Если мы не ждём от прямоугольника, что у него будут независимо меняться ширина и высота (может, он вообще иммутабельный), то квадрат от него наследовать можно (в рамках исходной задачи).

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

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

По (б) - ежели пускаться во все тяжкие, то можно было бы сделать класс "сохраняемой формы" назвать его тоже TForm, унаследовать его от Forms.TForm и всегда вписывать модуль с новым классом в uses после Forms, чтобы использовался именно допиленный класс. Вы ещё не заплакали? Между тем, есть в проекте ряд компонентов, которые издревле именно так и завёрнуты (хитроподвывернуты?). И это реально работает, хотя и прямо нарушает принципы S и L.

I. Interface segregation principle.

Тут говорить особо не о чем: ни один из классов не стремится объять необъятное и даже не наследует более одного интерфейса. Собственно, это прямое следствие S. И собственно хранилище (ILayoutKeeper) полностью обособленно от "процессора" (ILayoutProcessor), который определяет ключ для хранилища и преобразует реальную раскладку в её строковое представление и обратно. Принцип не то, что соблюдается -- тут после S нужно ещё много нелепых телодвижений совершить, чтобы его нарушить.

D. Dependency inversion principle.

Хм. Считаю, что один только факт того, что библиотека была абсолютно безболезненно выделена как самодостаточный проект, говорит за соблюдение этого принципа. Если идёт сохранение в ini-файл, то здесь нет (и не должно быть, спаси, Господи) ссылки на MainForm, где где-то в публичной секции лежит эта инишка. Конкретный объект инжектится из пользовательского кода. Если будет, к примеру, сохранение в БД -- инструмент доступа к БД тоже будет передан извне. Библиотека вызывает методы объекта пользовательского кода (инишка), но не имеет зависимости от него (зависимость идёт строго в обратном направлении). Изменение пользовательского кода никоим образом не будет поводом для изменения библиотеки. В этом смысле D перекликается с S в современном понимании от дяди Боба, только на уровне библиотеки, а не класса - повод для изменения классов библиотеки может найтись только в ней же, в её логике. Зависимость от стандартной библиотеки, конечно же, зависимостью от пользовательского кода не является. Реализация "процессора" с зависимостью от коммерческой библиотеки EhLib вынесена в отдельный модуль и может использоваться исключительно по потребности, при наличии соответствующих компонентов в проекте.

Единственная имевшая место "грязная" зависимость -- модуль хелперов OeStrUtil, где многие функции работают в бизнес-контексте, и от которого для публичной версии я взял чистый по зависимостям огрызок. Можно было бы переписать разбор строк и на библиотеку из стандартной поставки (RxStrUtils.ExtractWord, к примеру), и я даже поначалу сделал это. Но что-то в полученном коде было настолько старорежимное, что я ничтоже сумняшеся стёр эту версию к свиньям собачьим. Пусть будет полезняшка в комплекте.

И как там было, когда я писал курсовые? Ах, да. Вывод.

В старорежимных проектах компромиссы практически неизбежны. Тем не менее, при желании остаётся возможность для стремления к лучшему. Самое суровое наследие ушедших времён часто позволяет создавать в себе "островки безопасности", которые могут постепенно разрастаться и со временем захватывают всё большую территорию в масштабах любимого болотца. А SOLID-принципы в определённой мере обладают синергетическим эффектом, обеспечивающим им взаимное влияние и поддержку. При этом ключевую роль играют взаимодополняющие S и I принципы, D и O необходимы для библиотек с неопределённым кругом пользователей (всё, что больше pet-проекта или действующего макета в натуральную величину), а L весь про то, что проектировать иерархию классов надо начинать только тогда, когда поймёшь, зачем эти классы вообще нужны (и что при этом может измениться, но это уже в зоне S и O).

Подробнее..

Delphi 7 на костылях автоматизация подготовки ресурсов

31.03.2021 18:20:59 | Автор: admin

Эпиграф: Пусть это вдохновит Вас на подвиг! (Бел Кауфман, Вверх по лестнице, ведущей вниз).

О костылях и велосипедах, неотъемлемой части современной некромантии.

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

Казалось бы, что может быть более простым и естественным, чем прозрачная, без нелепых телодвижений, работа с размещёнными в специально выделенной для этого папке ресурсами? А если среда разработки выпущена на границе тысячелетия?

Первым великим неудобством, с которым я столкнулся, подписавшись года четыре назад на сопровождение и активную доработку проекта на Delphi 7, было категорическое неудобство работы с входящими в проект относительно крупными SQL запросами. Проект обеспечивает отчётность перед поставщиками (десятки поставщиков, взаимодействие с которыми идёт через множество компаний-интеграторов), и этих запросов там, что гуталина у сторожа ну просто завались. Причём запросы эти изначально описывались прямо в тексте, вперемешку с кодом представьте мой восторг с учётом того, что запросы приходилось время от времени переносить в SSMS, исправлять и переносить обратно. А если вспомнить, что и Delphi, и в SQL используются одиночные кавычки, становится ещё печальнее.

Первая реакция на эту красоту была вполне предсказуемой: срочно отделить данные от кода! Идеальным (и очевидным) решением кажется создание структуры папок с файлами запросов, которые при компиляции автоматически попадали бы в ресурсы с соответствующими идентификаторами. При этом, однако, компилятору нужно явно предоставлять список ресурсов в виде отдельно сформированного *.rc файла с соответствующими именами для доступа, который надо ещё предварительно сформировать.

Однако, компиляция проекта из-под IDE Delphi 7 является чёрным ящиком без малейшей возможности прикрутить к ней хоть что-то своё. У неё просто нет ни одного хука, чтобы зацепить собственный обработчик. Современные версии работают с MS Build, но у меня-то этого нет! Конечно, для сборки продуктивной версии можно использовать батник и компилятор командной строки, где можно добавить любую предварительную обработку, но для запуска из-под IDE этот вариант не годится.

Ещё одна печалька оказалась в том, что файл ресурсов (*.rc) перекомпилируется только тогда, когда изменилась его собственная дата. То, что изменилась дата файла, на который он ссылается (то есть сам ресурс), компилятор не волнует никак. Плюс rc-файл ещё и создать надо! И очень, очень хочется делать это автоматически.

Но сначала появился костыль. До этого я работал с Дельфи лет восемь назад, и с ресурсами тогда совсем не сталкивался, зато свободно делал компоненты. Итак, для решения вопроса по-быстрому я набросал визуальный компонент, который можно было кинуть на форму и открыть текст в редакторе (в окончательной версии - вообще двойным кликом). Набросок был готов в течение пары часов, затем после нескольких подходов компонент был окончательно оформлен и разделён на две части (просто текст и SQL) ради синтаксической подсветки в редакторе кода. Ниже, на скриншоте, Вы можете увидеть аж четыре компонента (два из которых успели активно поработать), но Проект, хотя и обеспечил очень быстро возможность по-человечески работать с текстом запросов, был заброшен в силу присущих ему принципиальных недостатков.

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

  object SqlSALES: TSqlVar    SQL.Strings = (      'SET NOCOUNT ON'      ''      'IF OBJECT_ID(N'#39'tempdb..#Sales'#39', N'#39'U'#39') IS NOT NULL '      'DROP TABLE #Sales;'      ''      'IF OBJECT_ID(N'#39'tempdb..#ClientIDs'#39', N'#39'U'#39') IS NOT NULL '      'DROP TABLE #ClientIDs;'      ''      '-- '#1055#1072#1088#1072#1084#1077#1090#1088#1099': '#1076#1072#1090#1072' '#1086#1090', '#1076#1072#1090#1072' '#1076#1086', '#1075#1088#1091#1087#1087#1072      '-- '#1056#1077#1072#1083#1080#1079#1072#1094#1080#1103      'SELECT '              #9'OperDate,                                                  -- '#1044 +        #1072#1090#1072              ...

На это накладывался ещё один немаловажный недостаток, присущий самописным компонентам Дельфи в принципе: компонент не может быть зарегистрирован на уровне проекта, его надо явно и заранее устанавливать в IDE, но далеко не все это умеют. Если открыть проект в среде без соответствующих компонентов, то они, после гневного сообщения просто пропадут. В наше время, с сокращением количества опытных кадров, не гнушающихся работать с древнючей Delphi, это выглядит, как натуральная бас-факторная мина. Хотя и без того там этих мин хоть ведром черпай...

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

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

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

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

  2. ВАЖНО!!! изменение файла с запросом должно автоматически попасть в программу, при первой же компиляции (даже если не делать build)

  3. Обеспечить включение ресурсов в операцию контекстного поиска по проекту.

  4. Опционально ещё и шифровать для пущего пафоса. Требование проекта на уровне Очень желательно.

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

Обеспечение второго пункта разложилось на две половинки. Во-первых, при изменении исходных файлов требовалось сразу же, не дожидаясь перестроения списка, изменить дату .rc файла. Это инициирует запрос на перезагрузку файла при переключении в Delphi. Во-вторых, этот файл должен быть постоянно открыт в IDE, иначе компилятор не обращает внимания на то, что он изменился (он-то, наивный, предполагает, что я всё через IDE делаю). А так при переключении извне в IDE дата файла проверяется и задаётся вопрос о необходимости его перезагрузить.

Третий пункт (контекстный поиск) решается добавлением исходного файла в проект, до первого USES, в директиве компилятора вида:

{%File 'Res\SRC\SQL\Import\Sectors\leafSectors.sql'}

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

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

SomeProject.dpr

library SomeProject;{  Текст между тегами ниже генерируется и обновляется автоматически!  Этот фрагмент нужен для подключения исходных файлов ресурсов к поиску  текста по файлам проекта.   Для разового обновления запустить res/CompileRc/CompileAllResources.cmd  Для автоматического обновления запустить _Автообновление ресурсов.cmd  Пока работает автообновление, после каждого изменения исходных файлов в папке res\src  автоматически запускается res/CompileRc/CompileAllResources.cmd  Чтобы среда разработки автоматически подтягивала изменения, в ней должен быть  открыт в редакторе исходник 'Res\AutoGenerated.rc'. <= Можно поместить  курсор куда-нибудь сюда -----^^^^^^^^^^^^^^^^^^^^ и нажать Ctrl + Enter}{<AUTOGENERATED_RC>}{%File 'Res\SRC\SQL\DB_Updates.sql'}{%File 'Res\SRC\SQL\Foo\Sales.sql'}{%File 'Res\SRC\SQL\Foo\Stocks.sql'}...{$R 'Res\AutoGenerated.res' 'Res\AutoGenerated.rc'}{</AUTOGENERATED_RC>}uses...

AutoGenerated.rc (пример):

SQL_DB_Updates     RCDATA PREPARED\SQL_DB_UpdatesSQL_Foo_Sales     RCDATA PREPARED\SQL_Foo_SalesSQL_Foo_Stocks     RCDATA PREPARED\SQL_Foo_Stocks...

Галпфайл был простой до безобразия:

gulpfile.js

const { watch, series } = require('gulp');const spawn = require('child_process').spawn; function compileResources(cb) { var cmd = spawn('CompileRc\\CompileAllResources.cmd', [], {stdio: 'inherit'}); cmd.on('close', function (code) {   cb(code); });} exports.default = function() { compileResources(code=>{}) watch('Src/**', compileResources); // series(compileResources, ...)};

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

Кстати, пришлось его радикально править, когда пришлось обновить Галп после обновления Ноды. Текущие версии у меня такие:

>gulp --versionCLI version: 2.3.0Local version: 4.0.2>node -vv12.20.0

Впрочем, это уже не важно от Галпа я буквально в процессе написания статьи избавился.

Основной пакетник:

  • сразу же меняет дату сгенерированного файла

  • составляет список файлов в обслуживаемой папке, за вычетом исключений

  • запускает программку на Perl, которая преобразует сырой список файлов во все виды, в которых он употребляется, в том числе генерирует rc-файл, и формирует задание на подготовку (шифрование) для изменённых файлов

  • запускает подготовку ресурсов

  • выделяет из dpr ранее добавленный туда список ссылок. Если он изменился, то заменяет его

  • компилирует сформированный rc-файл

  • Информирует о времени запуска и завершения и об обслуживаемой папке (на случай одновременного запуска нескольких экзкмпляров.

И всё это запускается из отслеживающего изменения монитора.

CompileAllResources.cmd

@Echo offset BatchDir=%~dp0cd %BatchDir%touch ..\AutoGenerated.rcFOR %%i IN ("%BatchDir%..") DO (set target=%%~fi)echo.echo [%TIME%] STARTING: ===== %target% =====echo.if not exist %BatchDir%..\prepared\*.* md %BatchDir%..\prepared > nulcall %BatchDir%..\AutoCompileRc.Config.cmdFOR %%i IN ("%DprFile%") DO (set DprFileOnly=%%~nxi)call %BatchDir%WaitWhileRunned.cmd gitcd %BatchDir%..%BatchDir%bin\find SRC -type f | %BatchDir%bin\grep -E -v --file=%BatchDir%excludes.lst>%BatchDir%ResFiles.lst%BatchDir%bin\perl %BatchDir%CreateRc.pl %BatchDir%ResFiles.lst AutoGenerated.rc %BatchDir%RcSources.lst %BatchDir%PrepareIt.cmdcd %BatchDir%echo {$R 'Res\AutoGenerated.res' 'Res\AutoGenerated.rc'}>>RcSources.lstcall between.cmd %DprFile% "\{<AUTOGENERATED_RC>\}\s*" "\{<\/AUTOGENERATED_RC>\}">RcSourcesOld.lstfc RcSources.lst RcSourcesOld.lst>nulif errorlevel 1 (call ReplaceBetween.cmd %DprFile% RcSources.lst "\{\<AUTOGENERATED_RC\>\}\s*" "\{\<\/AUTOGENERATED_RC\>\}">%DprFileOnly%.tmpcopy %DprFileOnly%.tmp %DprFile%>nuldel %DprFileOnly%.tmp)echo Preparing updated and new files...cd ..call %BatchDir%PrepareIt.cmdecho Compiling resources...brcc32 AutoGenerated.rccd %BatchDir%echo.echo [%TIME%] DONE: ===== %target% =====echo.

Внутри он содержит сборную солянку технологий, использует линуксовские find и grep (под Виндой они ставятся вместе с git) и даже Перл. Каюсь, побаловаться захотелось. Забавный опыт, хотя и немного травматичный. Своей лаконичностью и непрозрачностью (вроде наличия переменной по умолчанию) он напомнил мне ассемблер:

CreateRc.pl

#!c:/Perl/bin/perlopen (IN, "<".$ARGV[0]) || die $!;      # Список исходных файлов для ресурсовopen (RC_ENC, ">".$ARGV[1]) || die $!;  # Формируемый исходник (.rc)open (LST, ">".$ARGV[2]) || die $!;     # Формируемый файл со списком  # исходников, который будет вставлен в проект между тегами  # {<AUTOGENERATED_RC>} и {</AUTOGENERATED_RC>}open (ENC_CMD, ">".$ARGV[3]) || die $!; # Формируемый пакетный файл (.cmd)  # для подготовки каждого изменённого файла (например, щифрование)while(<IN>){  split /\n/;  $File = $_;                     # Для каждого из исходных файлов  $File =~ s/\//\\/g;             # - приводим разделитель каталогов  $File =~ s/\n//;                # - исключаем перевод строки  $Name = $File;                  # Имя ресурса  $Name =~ s/^Src\\//i;           # - исключаем префикс (папка)  $Name =~ s/\\/_/g;              # - вместо разделителя каталогов подчёркивание  $Name =~ s/\..*$//;             # - исключаем расширение  $Dest = "PREPARED\\".$Name."";  $_ = $File;  $EncryptIt = ! /\\Bin\\/i;      # Ресурсы из папок и подпапок bin не шифруем  print RC_ENC $Name    , substr("                                        ", 1, 40 - length($Name))    , "    RCDATA  "    , $Dest    , "\r\n";  $_ = $File;  if (! /\\Bin\\/i) {    # Добавляем в проект текстовые ресурсы (здесь: формируем вставку в dpr файл)    # Ресурсы из папок и подпапок bin не считаем текстом и в проект не включаем    print LST        "{\%File 'Res\\"      , $File      , "'}\r\n";  }  # Только для новых или обновлённых файлов: добавляем команду на подготовку  # (шифрование либо просто копирование)  if (! (-f $Dest) || ( (stat $File)[9] > (stat $Dest)[9] ) ) {    if ($EncryptIt) {      # Шифрование: encrypt.cmd <источник> <приёмник> <имя ресурса в нижнем регистре>      # имя ресурса может быть использовано для генерации пароля.      # Еncrypt.cmd определяете самостоятельно.      print ENC_CMD "call encrypt.cmd "        , $File        , substr("                                        ", 1, 40 - length($File))        , " "        , $Dest        , " \""        , lc $Name        , "\"\r\n";    } else {      print ENC_CMD "copy "        , $File        , substr("                                        ", 1, 40 - length($File))        , " "        , $Dest        , ">nul\r\n";    }  }}close IN;close RC_ENC;close LST;close ENC_CMD;

В процессе многолетней эксплуатации вылезли забавные особенности. Для примера, обновление git (с последующим обновлением линуксовских утилит) как-то раз мою автоматизацию сломало. Конкретно, более новый grep отказался воспринимать список исключений как список регэкспов, по одному на строку. Find тоже что-то такое подбрасывал (по крайней мере, тот, что установлен глобально сейчас, уже не отрабатывает именно так, как ожидается). В результате пришлось зафиксировать их версию банально кинуть бинарники в репозиторий, чтобы потом не плакать, благо, они не шибко большие, и изменять я их не планирую. Было ли что-то подобное в связи с Перлом не помню, но на всякий случай и его туда же пихнул.

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

И уже после того, как взялся за эту статью, решил всё-таки избавиться от Галпа. Зачем мне (или тому, кто попробует это за мной повторить) неконтролируемая глобальная внешняя зависимость, имеющая ещё одну неконтролируемую внешнюю зависимость (я по node.js), и из возможностей которой используется откровенный мизер?

Результат проект FolderMonitor, написанный на Delphi (https://bitbucket.org/danik-ik/foldermonitor/src/master/). Собственно, именно он прописан как монитор по умолчанию в инсталляторе предыдущего проекта (да, я не только сделал из этого отдельный проект, но даже сдалал инсталлятор в виде пакетного файла, см. https://bitbucket.org/danik-ik/compilerc/src/master/README.md).

Инсталлятор проекта CompileRc работает с использованием git. Он создаёт ветку в репозитории проекта и добавляет туда необходимые модули и настроечные файлы. Вместо шифрования по умолчанию используется заглушка (копирование), что опзволяет добавлять ресурсы как есть. Вот скриншот репозитория тестового проекта в SmartGit, после добавления в него CompileRc (все три коммита ветки CompileRc, как и сама ветка, сформированы инсталлятором), запуска монитора и добавления нескольких файлов ресурсов (в рабочем дереве):

В основу монитора изменений был положен широко известный в пример, с некоторыми доработками по результатам эксплуатации. Во что он там обёрнут, можете глянуть в репозитории, если интересно, там кода всего ничего. Консольное приложение, в режиме ожидания управляется с клавиатуры (пуск/пауза/принудительный запуск).

Основа монитора:

(******************************************************************************  Ожидание (в отдельном потоке) изменений в папке, формирование  соответствующего события.  На основании широко известного примера:  https://webdelphi.ru/2011/08/monitoring-izmenenij-v-direktoriyax-i-fajlax-sredstvami-delphi-chast-1/  Проверяются события:  - изменение имени файла или папки  - изменение размера  - изменение времени последней записи ******************************************************************************)unit FolderMonitorCore;interfaceuses Classes, Windows, SysUtils;type  TFolderMonitorCore = class(TThread)    private      FDirectory: string;      FScanSubDirs: boolean;      FOnChange   : TNotifyEvent;      procedure DoChange;    public      constructor Create(ASuspended: boolean; ADirectory:string; AScanSubDirs: boolean);      property OnChange: TNotifyEvent read FOnChange write FOnChange;    protected      procedure Execute; override;  end;implementation{ TFolderMonitorCore }constructor TFolderMonitorCore.Create(ASuspended: boolean; ADirectory: string;  AScanSubDirs: boolean);begin  inherited Create(ASuspended);  FDirectory:=ADirectory;  FScanSubDirs:=AScanSubDirs;  FreeOnTerminate:=true;end;procedure TFolderMonitorCore.DoChange;begin  if Assigned(FOnChange) then    FOnChange(Self);end;procedure TFolderMonitorCore.Execute;var ChangeHandle: THandle;begin  // инициируем ожидание изменений, получаем соответствующий хэндл  ChangeHandle:=FindFirstChangeNotification(PChar(FDirectory),                                            FScanSubDirs,                                            FILE_NOTIFY_CHANGE_FILE_NAME+                                            FILE_NOTIFY_CHANGE_DIR_NAME+                                            FILE_NOTIFY_CHANGE_SIZE+                                            FILE_NOTIFY_CHANGE_LAST_WRITE                                            );  // Проверяем корректность инициации, иначе выбрасывается исключение{$WARNINGS OFF}  Win32Check(ChangeHandle <> INVALID_HANDLE_VALUE);{$WARNINGS ON}  try    // выполняем цикл пока не получим запрос на завершение    while not Terminated do    begin      { Важное отличие от оригинального примера: ожидание НЕ бесконечно,        периодически проверяется флаг выхода }      case WaitForSingleObject(ChangeHandle, 1000) of        WAIT_FAILED: Terminate; {Ошибка, завершаем поток}        WAIT_OBJECT_0: // Дождались изменений          begin            // Задержка страховка от повторной реакции на несколько изменений подряд            // в процессе единственного сохранения            // (при сохранении исходника в Sublime Text выскакивало стабильно).            // Проблема была следствием ключевого правила: если изменение происходит            // ПОСЛЕ запуска приложения, то по окончании приложение запускается повторно.            // Величина задержки подобрана пальцем в небо.            sleep(5);            WaitForSingleObject(ChangeHandle, 1); // изменение уже было, поэтому результат не проверяется            // Ещё одно отличие: завершение имеет приоритет перед событием изменения            if not Terminated then              Synchronize(DoChange);          end;        WAIT_TIMEOUT: {DO NOTHING}; // идём на следующий круг,                                    // либо завершаем по условию цикла       end;      FindNextChangeNotification(ChangeHandle);    end;  finally    FindCloseChangeNotification(ChangeHandle);  end;end;end.

Итог.

На сегодня работа в ресурсами выглядит следующим образом. Запросы хранятся в отдельных файлах и сгруппированы по папкам. Когда я собираюсь их править, запускаю монитор. Всю папку с исходниками открываю в редакторе (например, VS Code). Исправляю то, что надо, принудительно сохраняю (если полагаться на автосохранение при потере фокуса, и переключиться из редактора прямо в Delphi, то изменения не успеют дойти до rc-файла). В Delphi заранее и всегда открыт AutoGenerated.rc. Когда я переключаюсь в Delphi, дата rc-файла уже изменена монитором, и среда задаёт запрос на его перезагрузку. Любая последующая компиляция подхватывает произведённые изменения с первого раза. То есть, если упростить, то при запущенном мониторе: изменил исходный запрос (или что там в ресурсах лежит) сохранил переключился в Delphi Reload? Yes! запустил. При этом риск запуска с устаревшим вариантом ресурса практически отсутствует.

Ссылка на репозиторий проекта: https://bitbucket.org/danik-ik/compilerc/

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

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

P.S.

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

Подробнее..

Radix sort выжать всё

15.12.2020 22:17:55 | Автор: admin


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



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

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

Полагаю это связано с общей идеей отдать предпочтение быстрому программированию с использованием всё более мощных фреймворков и сверхвысокоуровневых / скриптовых языков программирования. Языки подобные Ruby или Python невероятно удобны для разработчика. Множество 'синтаксического сахара', я бы даже сказал 'Мёда', ускоряют разработку в разы, если не на порядки, но какой ценой. Как пользователя, меня напрягает низкая тепловая эффективность кода, про объёмы потребляемой памяти просто промолчу, однако главный ресурс человечества время. Оно бесследно исчезает в бесконечных абстракциях, похищается анализаторами кода, вычищается умными сборщиками мусора. Я не призываю вернуться в прошлое, отказавшись от благ современной разработки, писать дорогой код, лишь предлагаю задуматься над возможным устранением бутылочных горлышек производительности там, где это возможно в типовых задачах. Часто этого можно достичь, оптимизировав высоконагруженные участки кода.

Как одну из базовых задач оптимизации можно выделить сортировку. Тема настолько исследована, вдоль и поперёк, что, казалось бы, на этом пути трудно что-то обнаружить интересного. Однако мы попробуем.

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

Все алгоритмы, основанные на сравнениях, в общем случае решают задачу сортировки не лучше чем O(n * Log n). При больших n эффективность быстро снижается и изменить эту ситуацию не представляется возможным. Эту тенденцию можно исправить, отказавшись от методов, основанных на сравнении элементов. Наиболее перспективным мне видится алгоритм Radix sort. Его вычислительная сложность O(k * n), где k количество проходов по массиву. Если n достаточно велико, а k наоборот очень мало, то данный алгоритм выигрывает у O(n * Log n).
В современной архитектуре процессоров k практически всегда сводится к числу байт сортируемого числа. Так например, для DWord (int) k = 4, что весьма не много. Это некоторая потенциальная яма вычислений, которая обусловлена несколькими факторами:

  • Регистры процессора заточены под 8-битные операторы на аппаратном уровне
  • Используемый буфер подсчёта в алгоритме как раз укладывается в одну линию кэша L1 процессора. (256 * 4-байтных числа)

В истинности этого утверждения Вы можете попробовать убедиться самостоятельно. Однако на текущий момент времени делить по биту самый оптимальный вариант. Не исключаю, что когда кэш L1 процессоров разрастётся до 256 КБайт, более выгодным станет вариант делить по границе 2-байта.

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

В данном решении алгоритм состоит из нескольких этапов:

  1. Выделение памяти под временный массив, на фоне общего времени исполнения не самая дешёвая операция, как вариант можно вынести за пределы функции и подавать параметром
  2. Эффективный подсчёт по всем битам сразу за один проход с максимальной эффективностью кодогенерации
  3. Расчёт смещений, также для всех проходов сразу
  4. Собственно пошаговая сортировка по битам от младших к старшим (LSD), в виде отдельной функции

Алгоритм LSD применяем как более быстрый (по крайней мере, в моём варианте) за счёт более ровной обработки при различных флуктуациях входных данных.
Исходный полностью отсортированный массив является наихудшим случаем для алгоритма, так как данные всё равно будут полностью сортироваться. Зато на случайных или перемешанных данных Radix sort чрезвычайно эффективен.

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

typedef struct TNode {  //unsigned long long key;  unsigned int key;  //unsigned short key;  //unsigned  char key;  unsigned int value;  //unsigned int value1;  //unsigned int value2;} TNode;

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

Раннее я уже писал заметку с реализацией Radix sort на паскаль, однако сегрегация языков программирования набирает невиданные ранее темпы среди завсегдатаев данного ресурса. Поэтому код для данной статьи я решил частично переписать на 'си' как более 'правильный' распространённый язык в сообществе программистов (Хотя на выходе ассемблерная кодогенерация с Pascal местами практически идентичная). При сборке рекомендую применять ключ O2 или выше.

В отличие от прошлой реализации, применив указатели в адресации цикла вместо операций побитного сдвига, удалось получить ещё большую прибавку в 1-2% к скорости алгоритма.

C
#include <stdio.h>#include <omp.h>#include <time.h>#include <windows.h>#include <algorithm>//=============================================================typedef struct TNode {  //unsigned long long key;  unsigned int key;  //unsigned short key;  //unsigned  char key;  unsigned int value;  //unsigned int value1;  //unsigned int value2;} TNode;//=============================================================void RSort_step(TNode *source, TNode *dest, unsigned int n, unsigned int *offset, unsigned char sortable_bit){  unsigned char *b = (unsigned char*)&source[n].key + sortable_bit;  TNode *v = &source[n];  while (v >= source)  {    dest[--offset[*b]] = *v--;    b -= sizeof(TNode);  }}//=============================================================void RSort_Node(TNode *m, unsigned int n){  // Выделяем память под временный массив  TNode *m_temp = (TNode*)malloc(sizeof(TNode) * n);  // Заводим массив корзин  unsigned int s[sizeof(m->key) * 256] = {0};  // Заполняем массив корзин для всех разрядов  unsigned char *b = (unsigned char*)&m[n-1].key;  while (b >= (unsigned char*)&m[0].key)  {    for (unsigned int digit=0; digit< sizeof(m->key); digit++)    {      s[*(b+digit)+256*digit]++;    }    b -= sizeof(TNode);  }  // Пересчитываем смещения для корзин  for (unsigned int i = 1; i < 256; i++)  {    for (unsigned int digit=0; digit< sizeof(m->key); digit++)    {      s[i+256*digit] += s[i-1+256*digit];    }  }  // Вызов сортировки по битам от младших к старшим (LSD)  for (unsigned int digit=0; digit< sizeof(m->key); digit++)  {    RSort_step(m, m_temp, n-1, &s[256*digit] ,digit);    TNode *temp = m;    m = m_temp;    m_temp = temp;  }  // Если ключ структуры однобайтовый, копируем отсортированное в исходный массив  if (sizeof(m->key)==1)  {    TNode *temp = m;    m = m_temp;    m_temp = temp;    memcpy(m, m_temp, n * sizeof(TNode));  }  free(m_temp);}//=============================================================int main(){  unsigned int n=10000000;  LARGE_INTEGER frequency;  LARGE_INTEGER t1, t2, t3, t4;  double elapsedTime;  TNode *m1 = (TNode*)malloc(sizeof(TNode) * n);  TNode *m2 = (TNode*)malloc(sizeof(TNode) * n);  srand(time(NULL));  for (unsigned int i=0; i<n; i++)  {      m1[i].key = rand()*RAND_MAX+rand();      m2[i].key = m1[i].key;  }  QueryPerformanceFrequency(&frequency);  QueryPerformanceCounter(&t1);  RSort_Node(m1, n);  QueryPerformanceCounter(&t2);  elapsedTime=(float)(t2.QuadPart-t1.QuadPart)/frequency.QuadPart;  printf("The RSort:          %.5f seconds\n", elapsedTime);  QueryPerformanceFrequency(&frequency);  QueryPerformanceCounter(&t3);  std::sort(m2, m2+n,[](const TNode &a, const TNode &b){return a.key < b.key;});  QueryPerformanceCounter(&t4);  elapsedTime=(float)(t4.QuadPart-t3.QuadPart)/frequency.QuadPart;  printf("The std::sort:      %.5f seconds\n", elapsedTime);  for (unsigned int i=0; i<n; i++)   {    if (m1[i].key!=m2[i].key)     {      printf("\n\n!!!!!\n");      break;    }  }  free(m1);  free(m2);  return 0;}


Pascal
program SORT;uses  SysUtils, Windows;//=============================================================type TNode = record     key : Longword;     //value : Longword;end;type ATNode = array of TNode;//=============================================================procedure RSort_Node(var m: array of TNode);//------------------------------------------------------------------------------    procedure Sort_step(var source, dest: array of TNode; len : Longword; offset: PLongword; const num: Byte);        var b : ^Byte;            v : ^TNode;        begin          b:=@source[len];          v:=@source[len];          inc(b,num);          while v >= @source do          begin            dec(offset[b^]);            dest[offset[b^]] := v^;            dec(b,SizeOf(TNode));            dec(v);          end;        end;//------------------------------------------------------------------------------var // Объявляем массив корзин первым, для выравнивания на стеке    s: array[0..1023] of Longword =(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);    i : Longword;    b : ^Byte;    p : Pointer;begin  GetMem(p, SizeOf(TNode)*Length(m));  // Подсчёт корзин  b:=@m[High(m)];  while (b >= @m[0]) do  begin    Inc(s[(b+3)^+256*3]);    Inc(s[(b+2)^+256*2]);    Inc(s[(b+1)^+256*1]);    Inc(s[(b+0)^+256*0]);    dec(b,SizeOf(TNode));  end;  // Пересчёт смещений для корзин  for i := 1 to 255 do                               begin    Inc(s[i+256*0], s[i-1+256*0]);    Inc(s[i+256*1], s[i-1+256*1]);    Inc(s[i+256*2], s[i-1+256*2]);    Inc(s[i+256*3], s[i-1+256*3]);  end;  // Вызов сортировки по битам от младших к старшим  Sort_step(m, ATNode(p), High(m), @s[256*0], 0);                    Sort_step(ATNode(p), m, High(m), @s[256*1], 1);  Sort_step(m, ATNode(p), High(m), @s[256*2], 2);  Sort_step(ATNode(p), m, High(m), @s[256*3], 3);  FreeMem(p);end;//============================================================= procedure test();  const    n = 10000000;  var    m1: array of TNode;      i,j,k,j1,j2: Longword;    iCounterPerSec: TLargeInteger;    T1, T2: TLargeInteger; //значение счётчика ДО и ПОСЛЕ операции    begin    SetLength(m1,n);    for i := 0 to n - 1 do    begin      m1[i].key := Random(65536 * 65536);    end;    QueryPerformanceFrequency(iCounterPerSec);//определили частоту счётчика  QueryPerformanceCounter(T11); //засекли время начала операции  RSort_Node(m1);      QueryPerformanceCounter(T22);//засекли время окончания  WRITELN('1='+FormatFloat('0.0000', (T22 - T11)/iCounterPerSec) + ' sec.');//вывели количество секунд на выполнение операции    SetLength(m, 0);  end;  //------------------------------------------------------------------------------begin  test();  Readln();  exit;end.   


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

А если хочется ещё немного быстрее?

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

Поддавшись искушению получить дополнительную производительность, реализовал несколько вариантов сортировки на знакомом мне OpenCL. К сожалению у меня нет возможность проверить реализацию на топовых видеокартах, но на моей GEFORCE GTX 750 TI алгоритм проигрывал однопоточной реализации на CPU, за счёт того, что данные нужно отправить на видеокарту, а потом забрать обратно. Если не гонять данные по шине туда-сюда скорость была бы приемлемой, но всё равно не фонтан. Есть ещё одно замечание. В OpenCL нити выполнения не синхронны (рабочие группы выполняются в произвольном порядке, насколько мне известно, в CUDA это не так, поправьте, кто знает), что мешает написать в данном случае более эффективный код.

Код из серии: можно ли создать троллейбус обработку в OpenCL на Дельфи... но зачем?
Переписывать на 'Си' поленился, выкладываю как есть.
program project1;uses Cl, SysUtils, Windows, Math;//------------------------------------------------------------------------------function OCL_Get_Prog(context: cl_context; pdevice_id: Pcl_device_id; name: PChar; S:AnsiString):cl_kernel;var   Tex:PCHAR;      Len:QWORD;      PLen:PQWORD;      Prog:cl_program;      kernel:cl_kernel;      Ret:cl_int;begin   Tex:=@S[1];   Len:=Length(S);   PLen:=@LEN;   prog:=nil;   kernel:=nil;     // создать бинарник из кода программы     prog:= clCreateProgramWithSource(context, 1, @Tex, @Len, ret);     if CL_SUCCESS<>ret then writeln('clCreateProgramWithSource Error ',ret);     // скомпилировать программу     ret:= clBuildProgram(prog, 1, pdevice_id, nil, nil, nil);     if CL_SUCCESS<>ret then writeln('clBuildProgram            Error ',ret);     // создать объект ядра для исполнения программы     kernel:= clCreateKernel(prog, name, ret);     if  ret<>CL_SUCCESS then writeln('clCreateKernel            Error ',ret);     // удаляем программу     clReleaseProgram(prog);     OCL_Get_Prog:=kernel;end;//------------------------------------------------------------------------------var   context:cl_context;   kernel1, kernel2, kernel3, kernel4 :cl_kernel;   Ret:cl_int;   valueSize : QWord =0;   s0 : PChar;   platform_id:cl_platform_id;   ret_num_platforms:cl_uint;   ret_num_devices:cl_uint;   device_id:cl_device_id;   command_queue:cl_command_queue;   S1,S2,S3,S4 : AnsiString;   memobj1, memobj2, memobj3 :cl_mem;   mem:Array of LongWord;   size:LongWord;   g_works, l_works :LongInt;   iCounterPerSec: TLargeInteger;   T1, T2, T3, T4: TLargeInteger; //значение счётчика ДО и ПОСЛЕ операции   i,j,step:LongWord;procedure exchange_memobj(var a,b:cl_mem);var c:cl_mem;begin  c:=a;  a:=b;  b:=c;end;begin   //---------------------------------------------------------------------------   S1 :=   // Шаг сортировки 1 (Oбнуление массива счётчика)   '__kernel void sort1(__global uint* m1) {'+   ' uint g_id = get_global_id(0);'+   ' m1[g_id] = 0;'+   '}';   //---------------------------------------------------------------------------   S2 :=   // Шаг сортировки 2 (Подсчёт индексов по блокам)   '__kernel void sort2(__global uint* m1, __global uint* m2, const uint len, const uint digit) {'+   ' uint g_id = get_global_id(0);'+   ' uint size = get_global_size(0);'+   ' uint a = g_id / len;'+   ' uchar key = (m1[g_id] >> digit);'+   ' atomic_inc(&m2[key]);'+   ' atomic_inc(&m2[256 * a + key + 256]);'+   '}';   //---------------------------------------------------------------------------   S3 :=   // Шаг сортировки 3 (Расчёт смещения)   '__kernel void sort3(__global uint* m1, const uint len) {'+   ' uint l_id = get_global_id(0);'+   ' for (uint i = 0; i < 8; i++) {'+   '  uint offset = 1 << i;'+   '  uint add = (l_id>=offset) ? m1[l_id - offset] : 0;'+   '  barrier(CLK_GLOBAL_MEM_FENCE);'+   '  m1[l_id] += add;'+   '  barrier(CLK_GLOBAL_MEM_FENCE);'+   ' }'+   ' for (uint i = 1; i < 1024; i++) {'+   '  m1[i*256+l_id + 256] += m1[(i-1)*256+l_id + 256];'+   ' }'+   '  barrier(CLK_GLOBAL_MEM_FENCE);'+   ' if (l_id>0) {'+   '  for (uint i = 0; i < 1024; i++) {'+   '   m1[i*256+l_id + 256] += m1[l_id-1];'+   '  }'+   ' }'+   '}';   //---------------------------------------------------------------------------   S4 :=   // Шаг сортировки 4   '__kernel void sort4(__global uint* m1, __global uint* m2, __global uint* m3, const uint digit, const uint len) {'+   ' uint g_id = get_global_id(0);'+   ' for (int i = len-1; i >= 0; i--) {'+      // обратить внимание цикл обратный!   '  uchar key = (m1[g_id*len+i] >> digit);'+   '  m2[--m3[g_id*256 + key + 256]] = m1[g_id*len+i];'+   ' }'+   '}';   //---------------------------------------------------------------------------   // Получаем доступные платформы   ret := clGetPlatformIDs(1,@platform_id,@ret_num_platforms);   if CL_SUCCESS<>ret then writeln('clGetPlatformIDs          Error ',ret);   // Получаем доступные устройства   ret := clGetDeviceIDs(platform_id, CL_DEVICE_TYPE_GPU, 1, @device_id, @ret_num_devices);   if CL_SUCCESS<>ret then writeln('clGetDeviceIDs            Error ',ret);   clGetDeviceInfo(device_id, CL_DEVICE_NAME, 0, nil, valueSize);   GetMem(s0, valueSize);   clGetDeviceInfo(device_id, CL_DEVICE_NAME, valueSize, s0, valueSize);   Writeln('DEVICE_NAME:  '+s0);   FreeMem(s0);   // Создаём контекст для устройства   context:= clCreateContext(nil, 1, @device_id, nil, nil, ret);   if CL_SUCCESS<>ret then writeln('clCreateContext           Error ',ret);   // Создаём очередь команд для контекста и устройства   command_queue := clCreateCommandQueue(context, device_id, 0, ret);   if CL_SUCCESS<>ret then writeln('clCreateContext           Error ',ret);   //-------------------------------------------------------------   kernel1 := OCL_Get_Prog(context, @device_id, 'sort1', S1);   kernel2 := OCL_Get_Prog(context, @device_id, 'sort2', S2);   kernel3 := OCL_Get_Prog(context, @device_id, 'sort3', S3);   kernel4 := OCL_Get_Prog(context, @device_id, 'sort4', S4);   //-------------------------------------------------------------   size:=256*256*16*10;   g_works := size;   l_works := 256;   Randomize;   SetLength(mem, size);   for i:=0 to size-1 do mem[i]:=random(256*256*256*256);   // Создаём буферы для ввода вывода данных   memobj1 := clCreateBuffer(context, CL_MEM_READ_WRITE, size * sizeof(cl_uint), nil, ret);   if  ret<>CL_SUCCESS then writeln('clCreateBuffer1            Error ',ret);   memobj2 := clCreateBuffer(context, CL_MEM_READ_WRITE, size * sizeof(cl_uint), nil, ret);   if  ret<>CL_SUCCESS then writeln('clCreateBuffer2            Error ',ret);   memobj3 := clCreateBuffer(context, CL_MEM_READ_WRITE, (256+256*1024) * sizeof(cl_uint), nil, ret);   if  ret<>CL_SUCCESS then writeln('clCreateBuffer3            Error ',ret);   QueryPerformanceFrequency(iCounterPerSec); //определили частоту счётчика   QueryPerformanceCounter(T1); //засекли время начала операции   // Записываем данные в буфер   ret := clEnqueueWriteBuffer(command_queue, memobj1, CL_TRUE, 0, size * sizeof(cl_int), @mem[0], 0, nil, nil);   if  ret<>CL_SUCCESS then writeln('clEnqueueWriteBuffer      Error ',ret);   QueryPerformanceCounter(T2);//засекли время окончания   Writeln('write       '+FormatFloat('0.0000', (T2 - T1)/iCounterPerSec) + ' second.');//вывели количество секунд на выполнение операции   QueryPerformanceFrequency(iCounterPerSec); //определили частоту счётчика   QueryPerformanceCounter(T3); //засекли время начала операции   for step:=0 to 3 do   begin   //-------------------------------------------------------------   QueryPerformanceFrequency(iCounterPerSec); //определили частоту счётчика   QueryPerformanceCounter(T1); //засекли время начала операции   //-------------------------------------------------------------   // 1 ШАГ (очитска массива)   ret:= clSetKernelArg(kernel1, 0, sizeof(cl_mem), @memobj3 );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_1_1            Error ',ret);   i := 256+256*1024;   ret:= clEnqueueNDRangeKernel(command_queue, kernel1, 1, nil, @i, nil, 0, nil, nil);   if  ret<>CL_SUCCESS then writeln('clEnqueueNDRangeKernel_1    Error ',ret);   //-------------------------------------------------------------   clFinish(command_queue); // Ожидаем завершения всех команд в очереди   QueryPerformanceCounter(T2);  //засекли время окончания   Writeln('step 1      '+FormatFloat('0.0000', (T2 - T1)/iCounterPerSec) + ' second.');//вывели количество секунд на выполнение операции   QueryPerformanceFrequency(iCounterPerSec); //определили частоту счётчика   QueryPerformanceCounter(T1);  //засекли время начала операции   //-------------------------------------------------------------   // 2 ШАГ (заполнение счётчиков)   ret:= clSetKernelArg(kernel2, 0, sizeof(cl_mem), @memobj1 );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_2_1            Error ',ret);   ret:= clSetKernelArg(kernel2, 1, sizeof(cl_mem), @memobj3 );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_2_2            Error ',ret);   j := size div (1024);   ret:= clSetKernelArg(kernel2, 2, sizeof(j), @j );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_2_3            Error ',ret);   j:=step*8;   ret:= clSetKernelArg(kernel2, 3, sizeof(j), @j );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_2_4            Error ',ret);   i := size;   ret:= clEnqueueNDRangeKernel(command_queue, kernel2, 1, nil, @i, nil, 0, nil, nil);   if  ret<>CL_SUCCESS then writeln('clEnqueueNDRangeKernel_2    Error ',ret);   //-------------------------------------------------------------   clFinish(command_queue); // Ожидаем завершения всех команд в очереди   QueryPerformanceCounter(T2);//засекли время окончания   Writeln('step 2      '+FormatFloat('0.0000', (T2 - T1)/iCounterPerSec) + ' second.');//вывели количество секунд на выполнение операции   QueryPerformanceFrequency(iCounterPerSec);//определили частоту счётчика   QueryPerformanceCounter(T1); //засекли время начала операции   //-------------------------------------------------------------   // 3 ШАГ (пересчёт смещений)   ret:= clSetKernelArg(kernel3, 0, sizeof(cl_mem), @memobj3 );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_3_1            Error ',ret);   j := size;   ret:= clSetKernelArg(kernel3, 1, sizeof(j), @j );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_3_3            Error ',ret);   j := 256;   ret:= clEnqueueNDRangeKernel(command_queue, kernel3, 1, nil, @j, @j, 0, nil, nil);   if  ret<>CL_SUCCESS then writeln('clEnqueueNDRangeKernel_3    Error ',ret);   //-------------------------------------------------------------   clFinish(command_queue); // Ожидаем завершения всех команд в очереди   QueryPerformanceCounter(T2);//засекли время окончания   Writeln('step 3      '+FormatFloat('0.0000', (T2 - T1)/iCounterPerSec) + ' second.');//вывели количество секунд на выполнение операции   QueryPerformanceFrequency(iCounterPerSec);//определили частоту счётчика   QueryPerformanceCounter(T1); //засекли время начала операции   //-------------------------------------------------------------   // 4 ШАГ (сортировка)   ret:= clSetKernelArg(kernel4, 0, sizeof(cl_mem), @memobj1 );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_4_1            Error ',ret);   ret:= clSetKernelArg(kernel4, 1, sizeof(cl_mem), @memobj2 );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_4_2            Error ',ret);   ret:= clSetKernelArg(kernel4, 2, sizeof(cl_mem), @memobj3 );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_4_3            Error ',ret);   j:=step*8;   ret:= clSetKernelArg(kernel4, 3, sizeof(j), @j );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_4_4            Error ',ret);   j := size div (1024);   ret:= clSetKernelArg(kernel4, 4, sizeof(j), @j );   if  ret<>CL_SUCCESS then writeln('clSetKernelArg_4_5            Error ',ret);   i := (1024);  // глобально ядер   ret:= clEnqueueNDRangeKernel(command_queue, kernel4, 1, nil, @i, nil, 0, nil, nil);   if  ret<>CL_SUCCESS then writeln('clEnqueueNDRangeKernel_4    Error ',ret);   clFinish(command_queue);   // Меняем местами указатели на массивы после каждого шага   // Результат останется в memobj1   exchange_memobj(memobj2, memobj1);   //-------------------------------------------------------------   clFinish(command_queue); // Ожидаем завершения всех команд в очереди   QueryPerformanceCounter(T2);//засекли время окончания   Writeln('step 4      '+FormatFloat('0.0000', (T2 - T1)/iCounterPerSec) + ' second.');//вывели количество секунд на выполнение операции   //-------------------------------------------------------------   end;   QueryPerformanceCounter(T4);//засекли время окончания   Writeln('all not R/W '+FormatFloat('0.0000', (T4 - T3)/iCounterPerSec) + ' second.');//вывели количество секунд на выполнение операции   QueryPerformanceFrequency(iCounterPerSec);//определили частоту счётчика   QueryPerformanceCounter(T1); //засекли время начала операции   // Считываем данные из буфера   ret:= clEnqueueReadBuffer(command_queue, memobj1, CL_TRUE, 0, size * sizeof(cl_int), @mem[0], 0, nil, nil);   if  ret<>CL_SUCCESS then writeln('clEnqueueReadBuffer       Error ',ret);   QueryPerformanceCounter(T2);//засекли время окончания   Writeln('Read        '+FormatFloat('0.0000', (T2 - T1)/iCounterPerSec) + ' second.');//вывели количество секунд на выполнение операции    // Освобождаем ресурсы   clReleaseMemObject(memobj1);           // Высвобождаем память буфера   clReleaseMemObject(memobj2);   clReleaseMemObject(memobj3);   clReleaseKernel(kernel1);              // Высвобождаем объект исполнения   clReleaseKernel(kernel2);   clReleaseKernel(kernel3);   clReleaseKernel(kernel4);   clReleaseCommandQueue(command_queue);  // Высвобождаем очередь   clReleaseContext(context);             // Высвобождаем контекст устройства   //-------------------------------------------------------------   SetLength(mem, 0);   readln;end.  


Остался ещё один не опробованный вариант многопоточность. Уже используя только голый C и библиотеку OpenMP, я решил узнать какой эффект даст использование множества ядер CPU.

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

В итоге применил параллельную сортировку очень похожую на ту, что применялась на GPU. Вот с ней уже всё получилось на много лучше.

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

C
#include <stdio.h>#include <omp.h>//=============================================================typedef struct TNode {  //unsigned long long key;  unsigned int key;  //unsigned short key;  //unsigned  char key;  unsigned int value;  //unsigned int value1;  //unsigned int value2;} TNode;//=============================================================void RSort_Parallel(TNode *m, unsigned int n){  // Количество задействованных потоков  unsigned char threads = omp_get_num_procs();  // Выделяем память под временный массив  TNode *m_temp = (TNode*)malloc(sizeof(TNode) * n);  unsigned int *s = (unsigned int*)malloc(sizeof(unsigned int) * 256 * threads);  #pragma omp parallel num_threads(threads)  {    TNode *source = m;    TNode *dest = m_temp;    unsigned int l = omp_get_thread_num();    unsigned int div = n / omp_get_num_threads();    unsigned int mod = n % omp_get_num_threads();    unsigned int left_index  = l < mod ? (div + (mod == 0 ? 0 : 1)) * l : n - (omp_get_num_threads() - l) * div;    unsigned int right_index = left_index + div - (mod > l ? 0 : 1);    for (unsigned int digit=0; digit< sizeof(m->key); digit++)    {      unsigned int s_sum[256] = {0};      unsigned int s0[256] = {0};      unsigned char *b1 = (unsigned char*)&source[right_index].key;      unsigned char *b2 = (unsigned char*)&source[left_index].key;      while (b1 >= b2)      {        s0[*(b1+digit)]++;        b1 -= sizeof(TNode);      }      for (unsigned int i=0; i<256; i++)      {        s[i+256*l] = s0[i];      }      #pragma omp barrier      for (unsigned int j=0; j<threads; j++)      {        for (unsigned int i=0; i<256; i++)        {          s_sum[i] += s[i+256*j];          if (j<l)          {            s0[i] += s[i+256*j];          }        }      }      for (unsigned int i=1; i<256; i++)      {        s_sum[i] += s_sum[i-1];        s0[i] += s_sum[i-1];      }      unsigned char *b = (unsigned char*)&source[right_index].key + digit;      TNode *v1 = &source[right_index];      TNode *v2 = &source[left_index];      while (v1 >= v2)      {        dest[--s0[*b]] = *v1--;        b -= sizeof(TNode);      }      #pragma omp barrier      TNode *temp = source;      source = dest;      dest = temp;    }  }  // Если ключ структуры однобайтовый, просто копируем в исходный массив  if (sizeof(m->key)==1)  {    memcpy(m, m_temp, n * sizeof(TNode));  }  free(s);  free(m_temp);}//=============================================================int main(){  unsigned int n=10000000;  TNode *m1 = (TNode*)malloc(sizeof(TNode) * n);  srand(time(NULL));  for (unsigned int i=0; i<n; i++)  {    m1[i].key = rand()*RAND_MAX+rand();  }  RSort_Parallel(m1, n);  free(m1);  return 0;}


Надеюсь было интересно.

С уважением, Ваш покорный слуга, Rebuilder.

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

P.P.S.

Подробнее..
Категории: Алгоритмы , C , Delphi , Radix , Radixsort

Ansible-vault decrypt обходимся без Ansible

25.04.2021 02:04:46 | Автор: admin

Исходные данные

Дано:

  • конвейер CI/CD, реализованный, к примеру, в GitLab. Для корректной работы ему требуются, как это очень часто бывает, некие секреты - API-токены, пары логи/пароль, приватные SSH-ключи - да всё, о чём только можно подумать;

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

Требуется консольная утилита, которая:

  • занимает минимум места;

  • умеет расшифровывать секреты, зашифрованные ansible-vault;

  • не требует никаких внешних зависимостей;

  • умеет читать ключ из файла.

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

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

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

Начнём сначала

Итак, предположим, что у нас на Linux-хосте с CentOS 7 уже установлен Ansible, к примеру, версии 2.9 для Python версии 3.6. Установлен, конечно же, с помощью virtualenv в каталог "/opt/ansible". Дальше для целей удовлетворения чистого научного любопытства возьмём какой-нибудь YaML-файл, и зашифруем его с помощью утилиты ansible-vault:

ansible-vault encrypt vaulted.yml --vault-password-file=.password

Этот вызов, как можно догадаться, зашифрует файл vaulted.yml с помощью пароля, который хранится в файле .password.

Итак, что получается после зашифровывания файла с помощью утилиты ansible-vault? На первый взгляд - белиберда какая-то, поэтому спрячу её под спойлер:

Содержимое файла vaulted.yml
$ANSIBLE_VAULT;1.1;AES256613735363539633137393665366436613138616632663731303737306666343433373565363336643365393033623439356364663537353365386464623836640a356464633264626330383232353362636131353736383936656639623035303230613764323339313061613039666333383035656663376465393837636665300a633732313730626265636538363339383237306264633830653665343639303538636331373138663935666436613235366336663438376231303639666133633739623436303438623463323636336332643666663064393731363034623038653861373536643136393431636437346337323833333165386534353432386663343465333836643131643237313262386634396534383166303565306264303162383833643765613936373632626136663738363462626665366131646631663834316262663162353532366664386330323139643266636562653639306238653162316563613934323836303536613532623864303839313038336232616134626433353166383837643165643439363835643731316238316439633039

Ну а как именно эта белиберда работает "под капотом" - давайте разбираться.

Открываем файл /opt/ansible/lib/python3.6/site-packages/ansible/parsing/vault/__init__.py, и в коде метода encrypt класса VaultLib видим следующий вызов:

VaultLib.encrypt
 ... b_ciphertext = this_cipher.encrypt(b_plaintext, secret) ...

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

Смотрим в его метод encrypt:

VaultAES256.encrypt
@classmethoddef encrypt(cls, b_plaintext, secret):    if secret is None:        raise AnsibleVaultError('The secret passed to encrypt() was None')    b_salt = os.urandom(32)    b_password = secret.bytes    b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)    if HAS_CRYPTOGRAPHY:        b_hmac, b_ciphertext = cls._encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv)    elif HAS_PYCRYPTO:        b_hmac, b_ciphertext = cls._encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv)    else:        raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in encrypt)')    b_vaulttext = b'\n'.join([hexlify(b_salt), b_hmac, b_ciphertext])    # Unnecessary but getting rid of it is a backwards incompatible vault    # format change    b_vaulttext = hexlify(b_vaulttext)    return b_vaulttext

То есть перво-наперво генерируется "соль" длиной 32 байта. Затем из побайтного представления пароля и "соли" вызовом _gen_key_initctr генерируется пара ключей (b_key1, b_key2) и вектор инициализации (b_iv).

Генерация ключей

Что же происходит в _gen_key_initctr?

_gen_key_initctr:
@classmethoddef _gen_key_initctr(cls, b_password, b_salt):    # 16 for AES 128, 32 for AES256    key_length = 32    if HAS_CRYPTOGRAPHY:        # AES is a 128-bit block cipher, so IVs and counter nonces are 16 bytes        iv_length = algorithms.AES.block_size // 8        b_derivedkey = cls._create_key_cryptography(b_password, b_salt, key_length, iv_length)        b_iv = b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]    elif HAS_PYCRYPTO:        # match the size used for counter.new to avoid extra work        iv_length = 16        b_derivedkey = cls._create_key_pycrypto(b_password, b_salt, key_length, iv_length)        b_iv = hexlify(b_derivedkey[(key_length * 2):(key_length * 2) + iv_length])    else:        raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in initctr)')    b_key1 = b_derivedkey[:key_length]    b_key2 = b_derivedkey[key_length:(key_length * 2)]    return b_key1, b_key2, b_iv

Если по сути, то внутри этого метода вызов _create_key_cryptography на основе пароля, "соли", длины ключа и длины вектора инициализации генерирует некий производный ключ (строка 10 приведённого фрагмента). Далее этот производный ключ разбивается на части, и получаются те самые b_key1, b_key2 и b_iv.

Следуем по кроличьей норе дальше. Что внутри _create_key_cryptography?

_create_key_cryptography:
@staticmethoddef _create_key_cryptography(b_password, b_salt, key_length, iv_length):    kdf = PBKDF2HMAC(        algorithm=hashes.SHA256(),        length=2 * key_length + iv_length,        salt=b_salt,        iterations=10000,        backend=CRYPTOGRAPHY_BACKEND)    b_derivedkey = kdf.derive(b_password)    return b_derivedkey

Ничего особенного. Если оставить в стороне всю мишуру, то в итоге вызывается функция библиотеки OpenSSL под названием PBKDF2HMAC с нужными параметрами. Можете, кстати, самолично в этом убедиться, открыв файл /opt/ansible/lib/python3.6/site-packages/cryptography/hazmat/backends/openssl/backend.py.

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

Собственно шифрование

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

_encrypt_cryptography
@staticmethoddef _encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv):    cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)    encryptor = cipher.encryptor()    padder = padding.PKCS7(algorithms.AES.block_size).padder()    b_ciphertext = encryptor.update(padder.update(b_plaintext) + padder.finalize())    b_ciphertext += encryptor.finalize()    # COMBINE SALT, DIGEST AND DATA    hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND)    hmac.update(b_ciphertext)    b_hmac = hmac.finalize()    return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext)

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

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

Окончательное оформление файла

Возвращаемся к строкам 16-20 фрагмента VaultAES256.encrypt: три строки, содержащие "соль", подпись и шифртекст, склеиваются вместе, после чего снова преобразуются в шестнадцатеричное представление (комментарий прямо подсказывает, что это - для обратной совместимости).

Дальше дописывается заголовок (помните, тот самый - $ANSIBLE_VAULT;1.1;AES256), ну и, в общем-то, всё.

Обратный процесс

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

Понятно, что Python нам не подходит, иначе можно было и огород не городить: ansible-vault одинаково хорошо работает в обе стороны. С другой стороны, никто не мешает на базе библиотек Ansible написать что-либо своё - в качестве разминки перед "подходом к снаряду" я так и сделал, и о результате напишу отдельную статью.

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

Итак, нам понадобятся: FreePascal версии 3.0.4 (эта версия в виде готовых пакетов - самая свежая, нормально устанавливающаяся в CentOS 7), и библиотека DCPCrypt версии 2.1 (на GitHub). Интересно, что прямо вместе с компилятором (fpc) и обширным набором библиотек в rpm-пакете поставляется консольная среда разработки fp.

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

Часть кода, относящуюся к генерированию производного ключа (реализацию той самой функции PBKDF2), я нашёл в интернете, и поместил в отдельный модуль под названием "kdf".

Вот этот модуль собственной персоной:

kdf.pas
{$MODE OBJFPC}// ALL CREDITS FOR THIS CODE TO https://keit.co/p/dcpcrypt-hmac-rfc2104/unit kdf;interfaceuses dcpcrypt2,math;function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring;function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;implementationfunction RPad(x: string; c: Char; s: Integer): string;var  i: Integer;begin  Result := x;  if Length(x) < s then    for i := 1 to s-Length(x) do      Result := Result + c;end;function XorBlock(s, x: ansistring): ansistring; inline;var  i: Integer;begin  SetLength(Result, Length(s));  for i := 1 to Length(s) do    Result[i] := Char(Byte(s[i]) xor Byte(x[i]));end;function CalcDigest(text: string; dig: TDCP_hashclass): string;var  x: TDCP_hash;begin  x := dig.Create(nil);  try    x.Init;    x.UpdateStr(text);    SetLength(Result, x.GetHashSize div 8);    x.Final(Result[1]);  finally    x.Free;  end;end;function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;const  blocksize = 64;begin  // Definition RFC 2104  if Length(key) > blocksize then    key := CalcDigest(key, hash);  key := RPad(key, #0, blocksize);  Result := CalcDigest(XorBlock(key, RPad('', #$36, blocksize)) + message, hash);  Result := CalcDigest(XorBlock(key, RPad('', #$5c, blocksize)) + result, hash);end;function PBKDF1(pass, salt: ansistring; count: Integer; hash: TDCP_hashclass): ansistring;var  i: Integer;begin  Result := pass+salt;  for i := 0 to count-1 do    Result := CalcDigest(Result, hash);end;function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring;  function IntX(i: Integer): ansistring; inline;  begin    Result := Char(i shr 24) + Char(i shr 16) + Char(i shr 8) + Char(i);  end;var  D, I, J: Integer;  T, F, U: ansistring;begin  T := '';  D := Ceil(kLen / (hash.GetHashSize div 8));  for i := 1 to D do  begin    F := CalcHMAC(salt + IntX(i), pass, hash);    U := F;    for j := 2 to count do    begin      U := CalcHMAC(U, pass, hash);      F := XorBlock(F, U);    end;    T := T + F;  end;  Result := Copy(T, 1, kLen);end;end.

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

Однако от питонячьего модуля паскалевский отличается ещё и тем, что "снаружи" доступны только те функции/переменные, которые объявлены в секции interface. То есть по умолчанию внутри модуля ты можешь хоть "на ушах стоять" - снаружи никто не сможет вызвать твои внутренние API. Так устроен язык, а хорошо это или плохо - вопрос вкуса, поэтому оценки оставим в стороне (питонистам передают привет функции/методы, начинающиеся на "_" и "__").

Заголовочная часть

Код, как обычно, под спойлером.

Заголовочная часть ("шапка", header)
program devault;uses  math, sysutils, strutils, getopts, DCPcrypt2, DCPsha256, DCPrijndael, kdf;

Далее нам понадобится пара функций - hexlify и unhexlify (набросаны, конечно, "на скорую руку"). Они являются аналогами соответствующих функций Python - вторая возвращает строку из шестнадцатеричных представлений байтов входного аргумента, а первая - наоборот, переводит строку шестнадцатеричных кодов обратно в байты.

hexlify/unhexlify
function unhexlify(s:AnsiString):AnsiString;var i:integer;    tmpstr:AnsiString;begin  tmpstr:='';  for i:=0 to (length(s) div 2)-1 do    tmpstr:=tmpstr+char(Hex2Dec(Copy(s,i*2+1,2)));  unhexlify:=tmpstr;end;function hexlify(s:AnsiString):AnsiString;var i:integer;    tmpstr:AnsiString;begin  tmpstr:='';  for i:=1 to (length(s)) do    tmpstr:=tmpstr+IntToHex(ord(s[i]),2);  hexlify:=tmpstr;end;

Назначение функций showbanner(), showlicense() и showhelp() очевидно из названий, поэтому я просто приведу их без комментариев.

showbanner() / showlicense() / showhelp()
showbanner()
procedure showbanner();begin  WriteLn(stderr, 'DeVault v1.0');  Writeln(stderr, '(C) 2021, Sergey Pechenko. All rights reserved');  Writeln(stderr, 'Run with "-l" option to see license');end;
showlicense()
procedure showlicense();begin  WriteLn(stderr,'Redistribution and use in source and binary forms, with or without modification,');  WriteLn(stderr,'are permitted provided that the following conditions are met:');  WriteLn(stderr,'* Redistributions of source code must retain the above copyright notice, this');  WriteLn(stderr,'   list of conditions and the following disclaimer;');  WriteLn(stderr,'* Redistributions in binary form must reproduce the above copyright notice, ');  WriteLn(stderr,'   this list of conditions and the following disclaimer in the documentation');  WriteLn(stderr,'   and/or other materials provided with the distribution.');  WriteLn(stderr,'* Sergey Pechenko''s name may not be used to endorse or promote products');  WriteLn(stderr,'   derived from this software without specific prior written permission.');  WriteLn(stderr,'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"');  WriteLn(stderr,'AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,');  WriteLn(stderr,'THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE');  WriteLn(stderr,'ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE');  WriteLn(stderr,'FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES');  WriteLn(stderr,'(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;');  WriteLn(stderr,'LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON');  WriteLn(stderr,'ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT');  WriteLn(stderr,'(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,');  WriteLn(stderr,'EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.');  WriteLn(stderr,'Commercial license can be obtained from author');end;
showhelp()
procedure showhelp();begin  WriteLn(stderr,'Usage:');  WriteLn(stderr,Format('%s <-p password | -w vault_password_file> [-f secret_file]',[ParamStr(0)]));  WriteLn(stderr,#09'"password" is a text string which was used to encrypt your secured content');  WriteLn(stderr,#09'"vault_password_file" is a file with password');  WriteLn(stderr,#09'"secret_file" is a file with encrypted content');  WriteLn(stderr,'When "-f" argument is absent, stdin is read by default');end;

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

Переменные и константы
var secretfile, passwordfile, pass, salt, b_derived_key, b_key1, b_key2, b_iv,    hmac_new, cphrtxt, fullfile, header, tmpstr, hmac:Ansistring;    Cipher: TDCP_rijndael;    key, vector, data, crypt: RawByteString;    fulllist: TStringArray;    F: Text;    c: char;    opt_idx: LongInt;    options: array of TOption;const KEYLENGTH=32; // for AES256const IV_LENGTH=128 div 8;const CONST_HEADER='$ANSIBLE_VAULT;1.1;AES256';

Код

Ну, почти код - всё ещё вспомогательная функция, которая в рантайме готовит массив записей для разбора параметров командной строки. Почему она здесь - потому что работает с переменными, объявленными в секции vars выше.

preparecliparams()
procedure preparecliparams();begin  SetLength(options, 6);  with options[1] do    begin      name:='password';      has_arg:=Required_Argument;      flag:=nil;      value:=#0;    end;  with options[2] do    begin      name:='file';      has_arg:=Required_Argument;      flag:=nil;      value:=#0;    end;  with options[3] do    begin      name:='passwordfile';      has_arg:=Required_Argument;      flag:=nil;      value:=#0;    end;  with options[4] do    begin      name:='version';      has_arg:=No_Argument;      flag:=nil;      value:=#0;    end;  with options[5] do    begin      name:='license';      has_arg:=No_Argument;      flag:=nil;      value:=#0;    end;  with options[6] do    begin      name:='help';      has_arg:=No_Argument;      flag:=nil;      value:=#0;    end;end;

А вот теперь точно код самой утилиты:

Весь остальной код
begin  repeat    c:=getlongopts('p:f:w:lh?',@options[1],opt_idx);    case c of      'h','?' : begin showhelp(); halt(0); end;      'p' : pass:=optarg;      'f' : secretfile:=optarg;      'w' : passwordfile:=optarg;      'v' : begin showbanner(); halt(0); end;      'l' : begin showlicense(); halt(0); end;      ':' : writeln ('Error with opt : ',optopt); // not a mistake - defined in getops unit     end;  until c=endofoptions;  if pass = '' then // option -p not set    if passwordfile <> '' then      try        Assign(F,passwordfile);        Reset(F);        Readln(F,pass);        Close(F);      except        on E: EInOutError do        begin          Close(F);          writeln(stderr, 'Password not set and password file cannot be read, exiting');          halt(1);        end;      end    else      begin // options -p and -w are both not set          writeln(stderr, 'Password not set, password file not set, exiting');          showhelp();          halt(1);      end;  try    Assign(F,secretfile);    Reset(F);  except    on E: EInOutError do    begin      writeln(stderr, Format('File %s not found, exiting',[secretfile]));      halt(1);    end;  end;  readln(F,header);  if header<>CONST_HEADER then    begin      writeln(stderr, 'Header mismatch');      halt(1);    end;  fullfile:='';  while not EOF(F) do    begin    Readln(F,tmpstr);    fullfile:=fullfile+tmpstr;    end;  Close(F);  fulllist:=unhexlify(fullfile).Split([#10],3);  salt:=fulllist[0];  hmac:=fulllist[1];  cphrtxt:=fulllist[2];  salt:=unhexlify(salt);  cphrtxt:=unhexlify(cphrtxt);  b_derived_key:=PBKDF2(pass, salt, 10000, 2*32+16, TDCP_sha256);  b_key1:=Copy(b_derived_key,1,KEYLENGTH);  b_key2:=Copy(b_derived_key,KEYLENGTH+1,KEYLENGTH);  b_iv:=Copy(b_derived_key,KEYLENGTH*2+1,IV_LENGTH);  hmac_new:=lowercase(hexlify(CalcHMAC(cphrtxt, b_key2, TDCP_sha256)));  if hmac_new<>hmac then    begin    writeln(stderr, 'Digest mismatch - file has been tampered with, or an error has occured');    Halt(1);    end;  SetLength(data, Length(crypt));  Cipher := TDCP_rijndael.Create(nil);  try    Cipher.Init(b_key1[1], 256, @b_iv[1]);    Cipher.DecryptCTR(cphrtxt[1], data[1], Length(data));    Cipher.Burn;  finally    Cipher.Free;  end;  Writeln(data);end.

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

Стр.

Назначение

2-13

разбор параметров командной строки с отображением нужных сообщений;

14-34

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

35-44

попытка прочесть зашифрованный файл, указанный в параметрах;

Небольшой чит: по умолчанию имя файла (переменная secretfile) равно пустой строке; в этом случае вызов Assign(F, secretfile) в строке 36 свяжет переменную F с stdin

45-50

проверка наличия в файле того самого заголовка $ANSIBLE_VAULT;1.1;AES256;

51-57

читаем всё содержимое зашифрованного файла и закрываем его;

58-63

разбираем файл на части: "соль", дайджест, шифртекст - всё отдельно; при этом все три части нужно будет ещё раз прогнать через unhexlify (помните примечание в VaultAES256.encrypt?)

64-73

вычисление производного ключевого материала; разбиение его на части; расчёт дайджеста; проверка зашифрованного файла на корректность дайждеста;

74-83

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

Интересная информация для питонистов

Кстати, вы же слышали, что в Python 3.10 наконец-то завезли оператор case (PEP-634)? Интересно, что его ввёл сам BDFL, и произошло это примерно через 14 лет после того, как по результатам опроса на PyCon 2007 первоначальный PEP-3103 был отвергнут.

Собственно, теперь всё на месте, осталось собрать:

[root@ansible devault]# time fpc devault.pas -Fudcpcrypt_2.1:dcpcrypt_2.1/Ciphers:dcpcrypt_2.1/Hashes -MOBJFPC

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

Вывод компилятора
Free Pascal Compiler version 3.0.4 [2017/10/02] for x86_64Copyright (c) 1993-2017 by Florian Klaempfl and othersTarget OS: Linux for x86-64Compiling devault.pasCompiling ./dcpcrypt_2.1/DCPcrypt2.pasCompiling ./dcpcrypt_2.1/DCPbase64.pasCompiling ./dcpcrypt_2.1/Hashes/DCPsha256.pasCompiling ./dcpcrypt_2.1/DCPconst.pasCompiling ./dcpcrypt_2.1/Ciphers/DCPrijndael.pasCompiling ./dcpcrypt_2.1/DCPblockciphers.pasCompiling kdf.pasLinking devault/usr/bin/ld: warning: link.res contains output sections; did you forget -T?3784 lines compiled, 0.5 secreal    0m0.543suser    0m0.457ssys     0m0.084s

Вроде неплохо: 3,8 тысячи строк кода собраны до исполняемого файла за 0.6 сек. На выходе - статически связанный бинарник, которому для работы от системы требуется только ядро. Ну то есть для запуска достаточно просто скопировать этот бинарник в файловую систему - и всё. Кстати, я забыл указать его размер: 875К. Никаких зависимостей, компиляций по несколько минут и т.д.

Ах да, чуть не забыл самое интересное! Запускаем, предварительно сложив пароль в файл ".password":

[root@ansible devault]# ./devault -w .password -f vaulted.yml---collections:- name: community.general  scm: git  src: https://github.com/ansible-collections/community.general.git  version: 1.0.0

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

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

Хотите ещё Ansible? (осторожно, денежные вопросы!)

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

Если же хотите систематизировать и углубить свои знания Ansible - я провожу тренинги по Ansible, пишите мне в Telegram.

Подробнее..

Шахматы на Delphi. Как я изобретал велосипед

11.04.2021 00:10:54 | Автор: admin

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

Начало

Это случилось в 2009 году: я решил написать простую шахматную программу, чтобы попрактиковаться в разработке игрового AI. Сам я не шахматист и даже не сказать что любитель шахмат. Но задача для тренировки вполне подходящая и интересная. Кроме того, играя в шахматы на доске или в программе, мне всегда было любопытно, почему тот или иной ход сильнее другого. Хотелось возможности наглядно видеть всё дерево развития шахматной позиций. Такой фичи в других программах я не встречал - так почему бы не написать самому? Ну а раз уж это тренировка - то придумывать и писать нужно с нуля, а не изучать другие алгоритмы и писать их собственную реализацию. В общем, думаю, дня за три можно управиться и сделать какой-то рабочий вариант :-)

Первая версия

Обычно шахматные движки используют поиск в глубину с алгоритмом "ветвей и границ" для сужения поиска. Но это не очень-то наглядно, поэтому решено: мы пойдём своим путём - пусть это будет поиск в ширину на фиксированную глубину. Тогда в памяти будет полное дерево поиска, которое можно как-то визуализировать. А также выяснить: а) на какую глубину можно просчитать шахматную игру в рамках имеющихся ресурсов CPU и памяти, б) насколько хорошо или плохо будет играть такой алгоритм?

Надо сказать, что на тот момент у меня был 2-ядерный процессор с 2 или 4 Гб памяти (точно уже не помню), 32-битная винда и 32-битный компилятор Turbo Delphi Explorer. Так что если временем работы ещё можно было как-то пожертвовать, то доступная процессу память была ограничена 2Gb. Про PE flag, расширяющий user memory до 3Gb я тогда не знал. Впрочем, поскольку память кушают и система, и Delphi и другие программы - для шахмат, чтобы не уходить в своп, доступно менее гигабайта.

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

  • UI - основное окно, отрисовка доски с фигурами.

  • Игровая логика - составление списка возможных ходов, выполнение хода, детекция завершения игры.

  • AI:оценка - оценочная функция позиции.

  • AI:перебор - поиск в ширину через очередь.

  • UI:браузер - окно визуализации дерева поиска, в котором можно наглядно изучать как всё работает.

Выяснилось что:

  • Поиск на глубину 3 полухода работает быстро - меньше секунды, и расходует немного памяти - 5-15 Мб. А вот поиск на глубину 4 полухода работает уже довольно долго и расходует большую часть доступной памяти. В отдельных ситуациях памяти и вовсе не хватает.

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

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

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

Оценочная функция

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

В итоге пришел к примерно такому алгоритму:

  • Для каждого игрока:

    • Подсчитать стоимость фигур: конь - 3, ладья - 5 и т.д. Начальная стоимость пешки - 1, но она растёт по мере её продвижения.

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

    • Определение какие поля находятся под боем и кем. Это медленная операция - основная часть времени выполнения оценочной функции тратится именно здесь. Зато польза от неё колоссальная! Незащищённая фигура под боем на ходу противника - минус фигура. Защищённая - минус разность стоимости фигур. Это позволяет получить эффект углубления поиска на 1-2 уровня.

    • Если остался один король: штраф за расстояние от центра доски и штраф за расстояние до короля противника. Такая формула в эндшпиле заставляет AI стремиться прижать короля противника к краю доски, т.е. получить позицию, из которой можно найти возможность поставить мат.

  • Итоговая оценка = (white_rate - black_rate) * (1 + 10 / (white_rate + black_rate)). Эта формула делает разницу более значимой в эндшпиле, заставляя отстающего игрока избегать размена фигур, а ведущего - наоборот, стремиться к размену.

Углубление поиска

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

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

В итоге алгоритм получился такой:

  1. На первой стадии строится дерево с базовой глубиной 3 (при этом ветки со взятиями и шахами могут достигать заметно большей глубины).

  2. Оценка дерева алгоритмом минимакс.

  3. Если выполнены критерии принятия решения - выбирается ветка с наилучшей оценкой и алгоритм завершается.

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

  5. Переход к следующей стадии: добавляем вес всем листьям дерева и продолжаем поиск. После завершения переходим к п 2.

Критерии принятия решения:

  • Осталась единственная ветка - выбора нет.

  • Одна из веток имеет оценку существенно более высокую чем у остальных - выбираем её.

  • Истекло время на ход - выбираем ветку с наилучшей оценкой.

Кэширование

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

Всё просто: нужно вычислить хэш от позиции и если в кэше уже есть оценка с таким хэшем - использовать её вместо вычисления. А если нет - вычислить и сохранить в кэше оценок. Основная проблема - нужна достаточно качественная и быстрая хэш-функция. После множества экспериментов получилось написать приемлемую функцию, вычисляющую 64-битный хэш. Общее количество возможных шахматных позиций значительно больше 264, но количество позиций, оцениваемых в ходе партии значительно меньше 232, поэтому более-менее качественная хэш-функция должна свести вероятность хэш-коллизий к минимуму.

Процент "попаданий" в кэш в ходе игры получился в районе 30-45%, но в эндшпиле достигает 80-90%, что даёт ускорение почти в 5-10 раз, а следовательно позволяет увеличить глубину поиска. Неплохой выигрыш!

Что получилось?

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

  • AI работает в один поток - ресурс CPU задействован не полностью.

  • А что если предоставить больше памяти?

  • AI думает только во время своего хода. Почему бы не использовать время соперника для просчёта наиболее вероятных продолжений?

  • Главная слабость: детерминированность. Каждый ход рассматривается как отдельная задача, каждая позиция приводит к детерминированному решению. Достаточно выиграть один раз, чтобы запомнить последовательность ходов, которая всегда приведёт к победе. А играть с таким соперником неинтересно.

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

Дальнейшее развитие

Недавно наткнувшись на этот заброшенный проект, решил всё-таки привести его код в порядок и доработать. Доработки сделал такие:

  • Многопоточность: сейчас у меня уже 8-поточный, а не 2-поточный CPU, поэтому многопоточный вариант даёт серьёзную прибавку в скорости.

  • 64-битный режим: кроме возможности использовать больше памяти, было любопытно, будет ли алгоритм работать быстрее на архитектуре x64. Как ни странно, оказалось что нет! Хотя отдельные функции на x64 работают быстрее, в целом версия x86 оказалась на 5-10% быстрее. Возможно 64-битный компилятор Delphi не очень хорош, не знаю.

  • Больше памяти: даже в 32-битном режиме за счёт PE-флага расширения адресного пространства доступной памяти стало больше. Однако практика показала, что больше 1 Гб памяти все-равно не нужно - разве что для хранения "обрезанных" ветвей дерева. К усилению игры увеличение памяти не приводит.

  • Непрерывность поиска: теперь дерево поиска не создаётся с нуля каждый ход, а создаётся лишь в начале партии. Когда делается ход - неактуальные ветви дерева обрезаются а поиск продолжается с того состояния, которое было. В том числе и во время хода соперника - идёт проработка перспективных продолжений, поэтому когда соперник сделает ход, поиск продолжится не с нуля. Быть может и вовсе получится сразу же сделать ход. Момент времени, когда игрок сделает ход - это фактор случайности, который делает игру недетерминированной. Теперь уже нельзя запомнить выигрышную последовательность ходов.

  • Оценка дерева. Ввёл новый атрибут узла - качество оценки. Качество равно сумме качества всех прямых потомков, умноженное на коэффициент затухания. Т.е. качество оценки показывает проработанность ветки.

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

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

В результате этих доработок AI стал сильнее, и вполне уверенно обыгрывает старую версию игры. Я провел несколько партий против AI на chess.com и выяснил, что уровень моей программы примерно соответствует рейтингу 1800-1900. Прогресс есть, и это хорошо!

Программирование игрового AI - занятие чертовски затягивающее: всегда хочется добиться большего. И хотя у меня по прежнему есть масса идей для дальнейшего развития, наступает момент, когда надо остановиться. Думаю, он наступил. Однако если кто-либо желает - может взять мой код, побаловаться, поэкспериментировать, что-нибудь реализовать. Благо, сейчас Delphi доступен каждому благодаря бесплатной Community Edition, не говоря уже про бесплатный Free Pascal и Lazarus. Код проекта (а также скомпилированный exe-шник) можно взять тут: https://github.com/Cooler2/chess (для компиляции понадобится также кое что из https://github.com/Cooler2/ApusGameEngine). Спасибо всем, кто дочитал :-)

Подробнее..

Math Invasion. Мой долгострой

23.11.2020 16:15:20 | Автор: admin
Привет, народ!

Расскажу я вам свою историю о том, как я разрабатывал игру. Идея о том, чтобы скрестить shoot em up с математикой, мне пришла ещё в студенческие годы (Где-то в 2008 году).

Собственно, уже тогда я ещё делал попытки воплотить идею в жизнь. Для реализации своих целей, тогда я использовал язык программирования Delphi и, только осваиваемую мною, библиотеку GLScene. Как результат, у меня получилась игра видео которой Вы можете наблюдать ниже. Кстати, саму игру Вы можете скачать по данной ссылке. Запускается она через файл Project1.exe который находится в папке TestFireCursorProject19

С чего начинался Math Invasion


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

Лучшие времена не пришли.

Но, спустя 10 лет, объявилось желание воскресить прежнюю задумку уже в новом формате. К тому же, миру были подарены мощные инструменты для разработки игр. Моё внимание пало на Unity3D. По слухам, удобный инструмент для разработки игр в 2D. Как раз, то что было мне нужно. В 2019 году я приступил к разработке. Для написания кода я выбрал C#, так как с магией JavaScript я был знаком и не хотел портить себе нервы. Но ввиду того что я не был знаком с C#, я тратил на разработку больше времени чем могло уйти. И вот, спустя 2 месяца, имея на руках MVP, по причине нехватки времени для работы которая меня кормит, я забросил разработку ;-D

Прошёл ещё один год.

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

Первая версия на Unity3D
image

Я адаптировал игру под мобильное приложение. Внёс изменения в интерфейс и механику игры. Что бы игра не выглядела совсем сухо, я добавил ей дух соперничества, т.е. врага который хочет, что бы вы проиграли. Он то и шлёт на вас эти математические задачи и радуется каждой вашей ошибке. Отсюда и название Math Invasion (Математическое наступление). Мои знакомые сказали, что враг в игре это лишнее.

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

Релиз


Сейчас игра выложена в Play Market и любой желающий может опробовать её. В ней есть недостатки. В неё нужно добавлять дополнительные уровни. Добавить узбекский язык. Она сейчас на уровне чуть выше MVP. Я уже получаю фидбек и вношу изменения на его основе. Я добился того, что игра увидела свет.

Какой я вынес урок для себя?

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

Из песочницы Как оптимизировать блок проверок case

16.08.2020 16:17:46 | Автор: admin
Почему в delphi блок case работает медленно и как это исправить.

Делюсь способами оптимизации и хаками.

Почему case плох


Даже начинающие delphi-программисты знают как выглядит блок case. Однако не все знают, что для нашего процессора, он выглядит как множество if блоков.

Вот что видит программист:

procedure case_test(Index:Integer);begin  case Index of    0: writeln('Hello');    1: writeln('Habr!');  end;end;

А вот что видит процессор:

procedure case_test(Index:Integer);begin  Index:=0;  if Index = 0 then     writeln('Hello')  else      if Index = 1 then     writeln('Habr!');end;

К сожалению никакой магии за словом case не оказалось. Более того, слишком активное использование case может замедлить выполнение кода. Например если у Вас не 2 варианта проверок, а 50, 250 или ещё больше. Худшим решением для вас будет блок case.

Чем заменить case


Решение у этой проблемы есть. Само строение блока case подсказывает нам, что наши варианты должны быть достаточно прибраны, чтобы поместиться в перечисляемом типе данных например: Integer, Word, Byte, Enum или Char.

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

const   Data:Array[0..1] of String = ('Hello', 'Habr!');procedure case_test(Index:Integer);begin    writeln(Data[Index]);end;

Это работает когда в действиях внутри блока case меняется только один параметр. Но что делать если параметров несколько?

Чем заменить сложный case


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

type  TMyTextWord = record    Text:String;    NeedLinebreak:Boolean;  end;const  Data:Array[0..2] of TMyTextWord = (     (Text:'Hello'; NeedLinebreak:False),     (Text:' '; NeedLinebreak:False),     (Text:'Habr!'; NeedLinebreak:True)   );procedure case_test(Index:Integer);var  MyTextWord:TMyTextWord;begin    MyTextWord:=Data[Index];  write(MyTextWord.Text);  if MyTextWord.NeedLinebreak then writeln;end;

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

procedure case_test(Index:Integer);begin  case Index of    0: write('Hello');    1: write(' ');    2:     begin      write('Habr!');       writeln;       end;  end;end;

Таким образом мы сводим количество действий для выполнения любого из случаев в блоке case до минимума, одинакового для всех случаев. Хотя здесь не сильно видна разница т.к. один набор из 3-ех действий заменился другим. Но подумайте, что выполнится быстрее: 50 раз проверить, является ли переменная одним из чисел? или Получить по индексу из массива 1 параметр из 50 возможных?. Ответ очевиден.

И так мы пришли к тому что case не далеко ушёл от известного нам if.
А раз мы научились оптимизировать case почему бы не пойти дальше?

Чем заменить if


Допустим у нас case не использует настроек, которые можно было бы записать в обычный массив. Например такой case:

class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;  Data: Pointer);var  I:Integer;  PRec:Pointer;begin  PRec:=@Data;  for I:=0 to Length(Fields)-1 do  begin    case Fields[I].Kind of      tkUString,      tkWideString:  PString(PRec)^:=PString(Fields[I].Data)^;      tkInteger:     PInteger(PRec)^:=PInteger(Fields[I].Data)^;      tkInt64:       PInt64(PRec)^:=PInt64(Fields[I].Data)^;      tkFloat:       PDouble(PRec)^:=PDouble(Fields[I].Data)^;      tkEnumeration: PWord(PRec)^:=PWord(Fields[I].Data)^;    end;    IncPtr(PRec,Fields[I].Size);  end;end;

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

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

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

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

procedure SetString(A,B:Pointer); inline;procedure SetInt(A,B:Pointer); inline;procedure SetInt64(A,B:Pointer); inline;procedure SetDouble(A,B:Pointer); inline;procedure SetBool(A,B:Pointer); inline;implementationprocedure SetString(A,B:Pointer); begin PString(A)^:=PString(B)^; end;procedure SetInt(A,B:Pointer); begin PInteger(A)^:=PInteger(B)^; end;procedure SetInt64(A,B:Pointer); begin PInt64(A)^:=PInt64(B)^; end;procedure SetDouble(A,B:Pointer); begin PDouble(A)^:=PDouble(B)^; end;procedure SetBool(A,B:Pointer); begin PBoolean(A)^:=PBoolean(B)^; end;type  TTypeHandlerProc = reference to procedure (A,B:Pointer);var  TypeHandlers:Array[TTypeKind] of TTypeHandlerProc;class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;  Data: Pointer);var  I:Integer;  PRec:Pointer;begin  PRec:=@Data;  for I:=0 to Length(Fields)-1 do  begin    TypeHandlers[Fields[I].Kind](PRec, Fields[I].Data);    IncPtr(PRec,Fields[I].Size);  end;end;initialization  TypeHandlers[tkUString]:=SetString;  TypeHandlers[tkWideString]:=SetString;  TypeHandlers[tkInteger]:=SetInt;  TypeHandlers[tkInt64]:=SetInt64;  TypeHandlers[tkFloat]:=SetDouble;  TypeHandlers[tkEnumeration]:=SetBool;end.

Здесь мы создали массив с inline процедурами, т.е. вместо вызова этих процедур компилятор подставит их код в строку, из которой мы к ним обращаемся. А в остальном всё как раньше, обращаемся к массиву по индексу операции, и выполняем операцию указывая в круглых скобках параметры. Обработчик представлен в сокращенном варианте, т.к. обработка всех действий с типом TTypeKind выглядит громоздкой.

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

Если вы уверенный в себе программист и хотите максимальной оптимизации, но не хотите засорять модуль кучей примочек. Я представляю вам решение прямиком из тёмной стороны кодинга. Все кодеры вокруг говорят, что использовать goto не безопасно, что сам оператор устарел и в 90% случаях существуют решения без goto. Говорят что использование ассемблерных вставок доступно только злым хакерам. Но что будет, если мы пустимся во все тяжкие?

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

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

procedure TTestClass.Test(Fields: TArray<TField>; Data: Pointer);label  LS,LI,LI64,LF,LE,FIN;var  I:Integer;  PRec:Pointer;  ADR:Cardinal;  Types:Array[TTypeKind] of Cardinal;begin  FillChar(Types,Length(Types)*4,#0);  asm    lea EDX, [EAX].Types    mov [EAX-$4C+tkUString*4], offset LS    mov [EAX-$4C+tkWideString*4], offset LS    mov [EAX-$4C+tkInteger*4], offset LI    mov [EAX-$4C+tkInt64*4], offset LI64    mov [EAX-$4C+tkFloat*4], offset LF    mov [EAX-$4C+tkEnumeration*4], offset LE  end;  PRec:=Data;  for I:=0 to Length(Fields)-1 do  begin    ADR:=Types[Fields[I].Kind];    asm jmp ADR end;    LS:   PString(PRec)^:=PString(Fields[I].Data)^;   goto FIN;    LI:   PInteger(PRec)^:=PInteger(Fields[I].Data)^; goto FIN;    LI64: PInt64(PRec)^:=PInt64(Fields[I].Data)^;     goto FIN;    LF:   PDouble(PRec)^:=PDouble(Fields[I].Data)^;   goto FIN;    LE:   PByte(PRec)^:=PByte(Fields[I].Data)^;       goto FIN;    FIN:      IncPtr(PRec,Fields[I].Size);  end;end;

Трюк этого способа заключается в том, что компилятор delphi сам хранит адреса всех label и в ассемблерных вставках дает к ним доступ. Решение нашлось неожиданно просто[1]. Когда я искал как считать регистр EIP, в котором хранится адрес текущей исполняемой команды. Оказалось, что регистр считать нельзя, а вот адрес label'а как раз можно.

Ну а дальше всё просто, на ассемблере заполняем массив адресами нужных нам label'ов.
В цикле по массиву, берется каждый элемент. Тип элемента = индекс адреса его обработчика. Берем по индексу адрес обработчика, прыгаем на адрес, профит.

    ADR:=Types[Fields[I].Kind];    asm jmp ADR end;

Остались чисто формальности: после каждого блока ставим прыжок вконец цикла goto FIN;, чтобы не попадать на следующие label блоки.

Константа $4C это количество байт до начала блока с памятью массива, чтобы её вычислить можете записать в массив заполненный нулями mov [EAX], 1 и посмотреть в дебагере какая ячейка приняла это значение, количество ячеек от начала до неё * 4 и будет ваша константа.

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

References:

1. Хак с адресом EIP через label
Подробнее..
Категории: Оптимизация , Delphi , Case , If , Asm

Recovery mode JNI и Delphi. Использование Java методов при помощи JNI

15.08.2020 10:11:11 | Автор: admin
Всем доброго времени суток!

Сегодня мы рассмотрим такую тему, как использовать Java методы при помощи JNI.
На самом деле все очень просто. Давайте сразу начнем с примера:

Допустим у нас есть некое Java приложение на котором есть простая кнопка и при нажатии на эту кнопку будет исполняться некий код.
private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {                                                 File MyFile = new File("D:\\Sample\\text.txt");        MyFile.delete();    }                        


Как мы видим в событии клика на кнопку будет исполняться просто код для удаления файла.
На Java все выглядит ясно и просто, но как же это будет выглядеть на Delphi с использованием JNI. На самом деле все проще чем кажется.
Для этого нам нужно разобрать заглянуть и в класс File, который находится по адресу java.io.File. Из этого класса нам нужно:

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

И так приступим.
Самой удобной утилитой для разборки класса на чечки на мой взгляд является DirtyJOE. КИдаем туда класс File и в методах ищем нашу функцию. Находим ее Имя и Дескриптор. Переходим в Delphi и создаем там такую функцию.
function JVM_DeleteFile(JNIEnv: PJNIenv; FilePath: String): Boolean;

Обратим внимание в DirtyJOE что функция delete возвращает назад Boolean.
Тут все просто: Название функции и объявляем переменные.
Давайте объявим переменные для работы нашего кода:
var  FileClass: JClass;  Delete, Init: JMethodID;  FileObj: JObject;  Args: array[0..0] of JValue;

Это все то что далее мы будем искать и использовать.
Теперь давайте заполним Аргументы
Args[0].l:= WideToJString(jnienv, PwideChar(WideString(FilePath)));

Обратите внимание что JString мы передаем как JObject (По сути JString это и есть JObject)
Ну а далее мы будем получать класс и метод delete:
FileClass:= jnienv^.FindClass(jnienv, 'java/io/File');  Delete:= jnienv^.GetMethodID(jnienv, FileClass, 'delete', '()Z');

А теперь не мало важный этап. Обратите внимание что в Java коде мы создаем новый объект new File и в Delphi нам требуется сделать тоже самое:
Init:= jnienv^.GetMethodID(jnienv, FileClass, '<init>', '(Ljava/lang/String;)V');  FileObj:= jnienv^.NewObjectA(jnienv, FileClass, Init, @Args);

И нам по сути осталось только использовать сам метод delete:
jnienv^.CallBooleanMethod(jnienv, FileObj, Delete)

Но, так как вначе всего мы обратили внимание что функция delete возвращает Boolean, то давайте же сделаем проверку на возврат True и False:
if jnienv^.CallBooleanMethod(jnienv, FileObj, Delete) = 1 then  Result:= True  else  Result:= False;


Ну и давайте посмотрим на эту функцию в готовом виде:
function JVM_DeleteFile(JNIEnv: PJNIenv; FilePath: String): Boolean;var  FileClass: JClass;  Delete, Init: JMethodID;  FileObj: JObject;  Args: array[0..0] of JValue;begin  Args[0].l:= WideToJString(jnienv, PwideChar(WideString(FilePath)));  FileClass:= jnienv^.FindClass(jnienv, 'java/io/File');  Delete:= jnienv^.GetMethodID(jnienv, FileClass, 'delete', '()Z');  Init:= jnienv^.GetMethodID(jnienv, FileClass, '<init>', '(Ljava/lang/String;)V');  FileObj:= jnienv^.NewObjectA(jnienv, FileClass, Init, @Args);  if jnienv^.CallBooleanMethod(jnienv, FileObj, Delete) = 1 then  Result:= True  else  Result:= False;end;


Заключение.
На самом деле все проще чем кажется. С помощью JNI можно использовать любой Java метод. В этом примере, если вы внимательно смотрели то увидели что я использовал запись Аргументов типа JValue и как нужно их использовать. Это очень важный момент.
Подробнее..
Категории: Java , Delphi , Jni

BroKB Emulz русскоязычная клавиатура для эмуляторов DosBoxBochsLBochs на Android-телефоне

13.09.2020 02:19:21 | Автор: admin

Русскоязычная и для эмуляторов

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

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

BroKB Emulz - именно такая клавиатура:

  • вводимые символы - ТОЛЬКО латинские

  • отображаемые на клавишах символы - латинские или русские, переключение по кнопке Рус/Лат

Кроме того, для удобства ввода текстов клавиатура повторяет клавиатуру ПК, не нужно переключаться между буквами, спец.символами, цифрами по какой-нибудь клавише. Все клавиши на одном экране и можно, аналогично клавиатуре ПК, нажать шифт (клавиша Sh слева) единожды (клавиша станет SH) или дважды (клавиша станет CA от слов Caps Lock), подписи на клавиатуре при этом сменятся на шифтовые ПКшные аналоги, и выбрать нужный символ.

На клавиатуре есть отдельный ряд функциональных клавиш F1 - F12. Также есть набор вспомогательных клавиш Esc, Tab, Insert, Delete, Home, End, Page Up, Page Down, Print Screen/SysReq, Break/Pause.

Можно зажимать сочетания клавиш, если предварительно нажать в верхнем ряду Ctrl, Shift, Alt, Win клавиши (они будут подсвечены синим). Повторное нажатие на них "отпускает" их, снова превращая в черные.

Таким образом, можно нажать Ctrl+Alt+Del, Ctrl+Break, Alt+Tab, Win+R и любые другие сочетания клавиш. (следует упомянуть, что Ctrl, Shift, Alt на клавиатуре - левые, их правых товарищей на клавиатуре нет).

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

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

BroKB Emulz в DosBox

Есть множество DOS русификаторов, вот, например, один из старых русификаторов от Дмитрия Гуртяка 1993 года выпуска, переключает язык не по обычным сочетаниям Ctrl+Shift или Alt+Shift или Shift+Shift, а по простому нажатию F12: http://blackstrip.ru/keyrus.com

Этот русификатор занимает в памяти всего 11 килобайт.

Запустив этот русификатор можно жать F12 для переключения языка и потом, переключив раскладку по кнопке Рус/Лат, вводить русские символы, например, в Visual Basic for MS-DOS:

Обратное переключение на английский также проводится в два этапа: F12 для переключения языка, Рус/Лат для переключения надписей на кнопках.

Аналогичным образом можно с помощью BroKB Emulz вводить русский текст в любые досовские программы, как текстовые, так и графические:

BroKB Emulz в LBochs

Если установить в LBochs, например, Windows XP, то с помощью BroKB Emulz можно писать в этом эмуляторе русскоязычные тексты в Windows. Язык переключаем прямо с клавиатуры BroKB Emulz по Alt+Shift/Ctrl+Shift, смотря что выбрали в настройках клавиатуры на панели инструментов. Вот как, например, выглядит написание русскоязычных Win32 приложений прямо на телефоне в Delphi 7 (кстати Delphi 7 работает даже на Windows 98, если у вас слабый телефон и XP на нем в эмуляторе тормозит):

Переключаемся на английский и можем продолжать писать код:

Жмем, как обычно, на клавиатуре F9 и у нас есть рабочий EXE-файл. Кстати, LBochs имеет возможность подключения shared-диска, соответствующего выбранной папке на телефоне. Скидываем exe на этот диск и он появляется в указанной папке на телефоне.

(Из опыта работы с LBochs: если файл большой, например, 50 мегабайт - то лучше подождать минуту-другую даже после закрытия окна Windows с индикатором прогресса копирования, т.к. LBochs быстро скопированный файл запоминает, и потом более медленно в фоновом режиме помещает в shared-папку, если завершить работу Windows сразу и выключить эмулятор - то файла в shared-папке может и не появиться).

Вот так можно с помощью клавиатуры BroKB Emulz писать русскоязычные программы под DOS и Windows прямо на телефоне, а также набирать русскоязычные тексты, например, в текстовом редакторе (я набираю, к примеру, статьи системы помощи и потом собираю в CHM-файл в HTML Help Workshop, тоже очень удобно).

Кто желает попробовать подобным образом покодить на телефоне русскоязычные проги - клавиатуру можно взять по адресу http://blackstrip.ru/brokb.apk (ну или на GP, она бесплатная, совсем без рекламы, весом 34 килобайта).

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

Подробнее..

Склонение слов и инициалов в DelphiFreepascal

29.12.2020 18:11:47 | Автор: admin

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

В прошлый раз я объяснял, что не являюсь программистом по роду профессиональной деятельности, но использую любимое хобби для автоматизации всего, что попадается под руку в работе юриста. Я уверен, что 90% всей юридической волокиты может быть успешно автоматизировано: ведение разнообразных баз и карточек, составление документов по шаблонам, контроль сроков выполнения задач, использование любых вспомогательных сервисов, уже имеющих свои api, для прикручивания автоматизации на конкретном рабочем месте и т.д. К этому нужно стремиться, чтобы по заветам Айзека Азимова высвободить время юриста для реализации основной задачи - размышлять над условиями договора и разводить демагогию в суде.


Так вот, много лет назад я сделал очень большой проект для облегчения своей офисной работы. Он собирал все данные по товарным знакам и патентам (а их несколько сотен), контролировал сроки оплаты патентных пошлин, формировал платежные поручения, договоры, заявления, и, разумеется, выдавал разнообразные отчеты. Собственно, почему в прошедшем времени? Проект вполне рабочий. Вот только разработан был по всем возможным антипаттернам, со всеми велосипедами и костылями, какие только обнаружили на Земле. Возвращаться в этот ролтон (или доширак) код, чтобы его отрефакторить, ой, как не хотелось, ведь здесь идеально подходит мем "А давайте всё перепишем на..."

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

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

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

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

IInitialsMorpher = interface  function GetInitials(Initials: string): CasesResponse;  function GetWordsCase(Words: string): CasesResponse;  function GetGenderAndInitials(Initials: string; var Gender: TGender): CasesResponse;end;

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

TWordCase = (Nominative, Gentitive, Dative, Accusative, Instrumental, Prepositional);TGender = (Male, Female, UnrecognizedGender);CasesResponse = array[TWordCase] of string;

Для реализации интерфейса я рассматривал нескольких кандидатов. В итоге остановился на следующих трех, и сразу укажу на достоинства и недостатки:

Сервис

Достоинства

Недостатки

DaData

- Отличная документация и идеальные примеры

- Корректная работа с инициалами с учетом национальных особенностей народов России

- Определение рода

- Склонение ФИО - платный сервис

- не меняет число

Pymorphy

github

- MIT лицензия, в оригинале существует как библиотека python

- Мощнейший морфологический анализатор на словарях Corpora (правильно разберет любой новояз)

- Масса информации на выходе, в т.ч. род, число, разбор на морфемы и лексемы, словарное ядро корня и т.д.

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

Morphos

- MIT лицензия, в оригинале существует как библиотека php

- Корректная работа с инициалами

- Определение рода

- для меня нет, но для некоторых может быть важен беспорядок слов в запросе

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

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

generic function JSONfromRestUri<T>(Uri: string): T;var  HTTPSender: THTTPSend;  JSONStreamer: TJSONDeStreamer;  Json: TJSONObject;begin  HTTPSender := THTTPSend.Create;  JSONStreamer := TJSONDeStreamer.Create(nil);  HTTPSender.Clear;  Result := T.Create;  if not HTTPSender.HTTPMethod('GET', Uri) then   raise EInOutError.Create(RESTOUT_ERROR);  JSON := GetJSON(HTTPSender.Document) as TJSONObject;  JSONStreamer.JSONToObject(JSON, Result);  FreeAndNil(JSON);  FreeAndNil(JSONStreamer);  FreeAndNil(HTTPSender);end;

Функция использует библиотеки Freepascal Synapse и Fpjson, поэтому соответствующие модули (httpsend, fpjson, fpjsonrtti) должны быть установлены и включены в uses. Десериализация ответа сервера из json в объект происходит с использованием rtti, т.е. все свойства такого объекта, во-первых, должны объявляться в секции published, во-вторых, иметь не самые сложные типы (примитивы, массивы, списки), и в-третьих, называться идентично полям в json. Скорее всего, декораторы и аннотация в стиле @SerializedName здесь не завезена, ну или я не нашел.

Строка запроса для сервиса Morphos выглядит следующим образом:

MORPHOS_URL = 'http://morphos.io/api/inflect-name?name=%s&_format=json';

Ответ сервера представляет собой json с массивом из шести строк, внутри - слово/словосочетание из запроса, склоненное по всем падежам русского языка в стандартном порядке ИРВДТП, само слово из запроса в поле name и определенный род в поле gender:

{    "name": "Иванов Иван",    "cases": [        "Иванов Иван",        "Иванова Ивана",        "Иванову Ивану",        "Иванова Ивана",        "Ивановым Иваном",        "об Иванове Иване"    ],    "gender": "m"}

Приступим же к конкретной реализации интерфейса IInitialsMorpher для сервиса Morpher. Сначала объявим класс, в который данные из json будут автоматически десериализоваться библиотеками Fpjson (в документации говорится, что классы должны быть потомками TPersistent):

TMorphosResponse = class(TPersistent)  private    fCases: TStrings;    fGender: string;    fName: string;  public    constructor Create;    destructor Destroy; override;  published    property name: string read fName write fName;    property cases: TStrings read fCases write fCases;    property gender: string read fGender write fGender;  end;

Добавляем класс реализации интерфейса:

TMorphosImpl = class(TInterfacedObject, IInitialsMorpher)  public    function GetInitials (Initials: string): CasesResponse;    function GetWordsCase (Words: string): CasesResponse;    function GetGenderAndInitials(Initials: string; var Gender: TGender): CasesResponse;  end;

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

function TMorphosImpl.GetGenderAndInitials(Initials: string; var Gender: TGender): CasesResponse;var  inf: TWordCase;  i: integer = 0;  response: TMorphosResponse;begin  response := specialize JSONfromRestUri<TMorphosResponse>                (Replacetext(Format(MORPHOS_URL, [Initials]), ' ', '+'));  for inf in TWordCase do begin      Result[inf] := response.cases[i];      inc(i);  end;  case response.gender of       'm': Gender := Male;       'f': Gender := Female;       else Gender := UnrecognizedGender;  end;  FreeAndNil(response);end;

Это функция, возвращающая массив склоненных ФИО и род в отдельном свойстве. Не трудно догадаться, что для реализации первой функции (только ФИО) нужно вызвать её же, но отбросить род, а для реализации другой - результат первой функции привести к нижнему регистру:

function TMorphosImpl.GetInitials(Initials: string): CasesResponse;var  MokeGender: TGender = UnrecognizedGender;begin  Result := GetGenderAndInitials(Initials, MokeGender);end;function TMorphosImpl.GetWordsCase(Words: string): CasesResponse;var  inf: TWordCase;begin  Result := GetInitials(Words);  for inf in TWordCase do    Result[inf] := UTF8LowerString(Result[inf]);end;

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

Для себя я выбрал полу-фабрику полу-фасад, чтобы уже в этом классе добавить еще одну фичу (на самом деле не так уж и нужную, но вдруг, интернет все-таки кончится?) - кеширование результатов на диск для сокращения накладных расходов на http-запросы. И если для Morphos это не особо актуально, то для DaData даже очень, поскольку каждый запрос стоит 10 копеек.

Подробное описание реализации кэша я здесь не стану приводить, поскольку оно выходит за рамки темы. Скажу лишь, что для себя выбрал хранение значений в json файле и обработку их в памяти с помощью НashMap-типа из Generics.Collections - библиотеки, портированной в freepascal из delphi. Ключом при этом являются сами инициалы, и потенциально использование хэш-массива даст оптимальную скорость поиска на больших данных (от которых по идее нужно держаться подальше с помощью разных вариантов авто очистки кэша, ведь иногда и http-запрос может отработать быстрее, чем загрузка огромного словаря в память из файла и дальнейший поиск).

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

Morpher := TMorphFabric.Create(MORPHOS);//...response := Morpher.GetInitials(Text)StringList.AddStrings(response);

Для всех заинтересовавшихся свою получившуюся библиотеку с тестовым оконным приложением для win я буду хранить в открытом репозитории. Дорабатывать, скорее всего, я уже в ней ничего не стану, поскольку сейчас погрузился в открытое море мобильной разработки (kotlin) и python для реализации того самого мема "переписать всё на..."

Всех с наступающим Новым 2020+'1' годом!

Подробнее..

Delphi и SQLite. Альтернатива хранимым процедурам

20.06.2021 14:08:21 | Автор: admin

SQLite во многих случаях является удобным, незаменимым инструментом. Я уже не могу себе представить - как мы все жили без него. Тем не менее, есть некоторые неудобства при его использовании, связанные с тем, что это легкая встраиваемая СУБД.

Самое большое неудобство для меня, как Delphi-разработчика - отсутствие хранимых процедур. Я очень не люблю смешивать Delphi-код и SQL-скрипты. Это делает код намного менее читабильным, и затрудняет его поддержку. Следовательно, нужно как-то разнести код Delphi и тексты SQL-скриптов.

Предлагаю свой вариант решения проблемы

  • Выносим весь SQL-код в отдельный тестовый файл ресурсов, подключенный к проекту.

  • Запросы в SQL-файле разделяем маркерами начала с идентификаторами и маркерами конца. В моём случае синтаксис маркера начала - //SQL ИмяПроцедуры. Маркер конца - GO.

  • Создаем класс - менеджер SQL-запросов. При загрузке приложения он читает SQL-файл из ресурсов и составляет из него список хранимых процедур с уникальными именами-идентификаторами.

  • В процессе работы приложения мендежер извлекает текст SQL-запроса по его идентификатору для последующей его передачи на выполнение.

Главная идея - простота и легкость использования, подобная вызову хранимых процедур и удобство при создании и модификации SQL-запросов

Код юнита менеджера запросов:

unit uSqlList;interfaceuses System.Classes, Winapi.Windows, System.SysUtils,  System.Generics.Collections;type  TSqlList = class(TObjectDictionary<string, TStrings>)  const    SCRIPTS_RCNAME = 'SqlList';  private    function GetScripts(const AName: string): TStrings;    procedure FillList;    function GetItem(const AKey: string): string;  public    constructor Create;  public    property Sql[const Key: string]: string read GetItem; default;  end;var  SqlList: TSqlList;implementationfunction GetStringResource(const AName: string): string;var  LResource: TResourceStream;begin  LResource := TResourceStream.Create(hInstance, AName, RT_RCDATA);  with TStringList.Create do    try      LoadFromStream(LResource);      Result := Text;    finally      Free;      LResource.Free;    end;end;{ TScriptList }constructor TSqlList.Create;begin  inherited Create([doOwnsValues]);  FillList;end;procedure TSqlList.FillList;var  LScripts: TStrings;  I: Integer;  S, LKey: string;  LStarted: Boolean;  LSql: TStrings;begin  LScripts := GetScripts(SCRIPTS_RCNAME);  try    LStarted := False;    LSql := nil;    for I := 0 to LScripts.Count - 1 do    begin      S := LScripts[I];      if LStarted then      begin        if S = 'GO' then        begin          LStarted := False;          Continue;        end        else if not S.StartsWith('//') then          LSql.Add(S);      end      else      begin        LStarted := S.StartsWith('//SQL ');        if LStarted then        begin          LKey := S.Substring(6);          LSql := TStringList.Create;          Add(LKey, LSql);        end;        Continue;      end;    end;  finally    LScripts.Free;  end;end;function TSqlList.GetItem(const AKey: string): string;begin  Result := Items[AKey].Text;end;function TSqlList.GetScripts(const AName: string): TStrings;begin  Result := TStringList.Create;  try    Result.Text := GetStringResource(AName);  except    FreeAndNil(Result);    raise;  end;end;initializationSqlList := TSqlList.Create;finalizationFreeAndNil(SqlList);end.

Пример содержимого файла SQL-скриптов:

//SQL GetOrderSELECT * FROM Orders WHERE ID = :IDGO//SQL DeleteOpenedOrdersDELETE FROM Orders WHERE Closed = 0GO

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

{$R 'SqlList.res' '..\Common\DataBase\SqlList.rc'}

Использование с компонентом TFDConnection:

  Connection.ExecSQL(SqlList['GetOrder'], ['123']);

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

Подробнее..
Категории: Delphi , Sqlite , Sqlite3

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru