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

C

Перевод Пишем загрузчик на Ассемблере и C. Часть 1

07.01.2021 16:12:32 | Автор: admin


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

О чем пойдет речь?


Мы рассмотрим написание кода программы и его копирование в загрузочный сектор образа флоппи-диска, после чего с помощью эмулятора bochs (x86) для Linux научимся проверять работоспособность полученной дискеты с загрузчиком.

О чем речь не пойдет


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

Структура статьи


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

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

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

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

Знакомство с загрузочными устройствами


Что происходит при включении стандартного компьютера?


Обычно при нажатии кнопки включения питания от нее подается сигнал блоку питания о необходимости подачи необходимого напряжения на внутреннее и внешнее оборудование компьютера, такое как процессор, монитор, клавиатура и пр. Процессор при этом инициализирует ПЗУ-чип BIOS (базовую систему ввода/вывода) для загрузки содержащейся в нем исполняемой программы, именуемой также BIOS.

После запуска BIOS выполняет следующие задачи:
Тестирование оборудования при подаче питания (Power On self Test).
Проверка частоты и доступности шин.
Проверка системных часов и аппаратной информации в CMOS RAM.
Проверка настроек системы, предустановок оборудования и т.д.
Тестирование подключенного оборудования, начиная с RAM, дисководов, оптических приводов, HDD и т.д.
В зависимости от определенной в разделе загрузочных устройств информации выполняет поиск загрузочного диска и переходит к его инициализации.

К сведению: все ЦПУ с архитектурой x86 в процессе загрузки запускаются в реальном режиме (Real Mode).

Что такое загрузочное устройство?


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

Что такое сектор?


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

Что такое загрузочный сектор?


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

Как работает загрузочное устройство?


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

Что такое начальный загрузчик?


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

Какие есть виды микропроцессоров?


Я приведу основные:
16 битные
32 битные
64 битные

Чем больше значение бит, тем к большему объему памяти имеют доступ программы, получая большую производительность в плане временного хранилища, обработки и пр. На сегодня микропроцессоры производят две основные компании Intel и AMD. В этой же статьи я буду обращаться только к процессорам семейства Intel (x86).

В чем отличие процессоров Intel и AMD?


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

Знакомство со средой разработки


Что такое реальный режим?


Я уже упоминал, что все процессоры с архитектурой x86 при загрузке с устройства запускаются в реальном режиме. Это очень важно иметь в виду при написании загрузочного кода для любого устройства. Реальный режим поддерживает только 16-битные инструкции. Поэтому создаваемый вами код для загрузки в загрузочную запись или сектор должен компилироваться в 16-битный формат. В реальном режиме инструкции могут работать только с 16 битами одновременно. Например, в 16-битном ЦПУ конкретная инструкция будет способна складывать в одном цикле два 16-битных числа. Если же для процесса будет необходимо сложить два 32-битных числа, то потребуется больше циклов, выполняющих сложение 16-битных чисел.

Что такое набор инструкций?


Это гетерогенная коллекция сущностей, ориентированных на конкретную архитектуру микропроцессора, с помощью которых пользователь может взаимодействовать с ним. Здесь я подразумеваю коллекцию сущностей, состоящую из внутренних типов данных, инструкций, регистров, режимов адресации, архитектуры памяти, обработки прерываний и исключений, а также внешнего I/O. Обычно для семейства микропроцессоров создаются общие наборы инструкций. Процессор Intel-8086 относится к семейству 8086, 80286, 80386, 80486, Pentium, Pentium I, II, III, которое также известно как семейство x86. В этой статье я будут использовать набор инструкций, относящийся именно к этому типу процессоров.

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


Для реализации этой задачи необходимо иметь представление о:
Операционной системе (GNU Linux).
Ассемблере (GNU Assembler).
Наборе инструкций (x86).
Написании инструкций на GNU Assembler для x86 микропроцессоров.
Компиляторе (как вариант язык C).
Компоновщике (GNU linker ld)
Эмуляторе x86, например bochs, используемом для тестирования.

Что такое операционная система?


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

Отдельно отмечу, что все современные ОС работают в защищенном режиме.
Какие виды ОС бывают?
Windows
Linux
MAC


Что значит защищенный режим?


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

Что такое Ассемблер?


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

Разве компилятор делает не то же самое?


На более высоком уровне да, но, фактически, внутри компилятора этим занимается именно ассемблер.

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


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

Зачем нужна ОС для написания кода загрузочного сектора?


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

Какую ОС можно использовать?


Так как я писал загрузочные программы под Ubuntu, то и вам для ознакомления с данным руководством порекомендую именно эту ОС.

Какой следует использовать компилятор?


Я писал загрузчики при помощи GNU GCC и демонстрировать компиляцию кода я буду на нем же. Как протестировать рукописный код для загрузочного сектора? Я представлю вам эмулятор архитектуры x86, который помогает дорабатывать код, не требуя постоянной перезагрузки компьютера при редактировании загрузочного сектора устройства.

Знакомство с микропроцессором


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

Что такое регистры?


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

регистры общего назначения;
сегментные регистры;
индексные регистры;
регистры стека.

Я дам краткое пояснение по каждому типу.

Регистры общего назначения: используются для хранения временных данных, необходимых программе в процессе выполнения. Каждый такой регистр имеет 16 бит в ширину и 2 байта в длину.
AX регистр сумматора;
BX регистр базового адреса;
CX регистр-счетчик;
DX регистр данных.

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

Пример: у нас есть байт, представляющий значение X и расположенный в 10-й позиции от начала блока памяти со стартовым адресом 0x7c00. В данной ситуации мы выразим сегмент как 0x7c00, а смещение как 10.
Абсолютным адресом тогда будет 0x7c00 + 10.

Здесь я хочу выделить четыре категории:
CS сегмент кода;
SS сегмент стека;
DS сегмент данных;
ES расширенный сегмент.

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

movw $0x07c0, %axmovw %ax    , %dsmovw (0x0A) , %ax 


Здесь происходит:

загрузка значения 0x07c0 * 16 в AX;
загрузка содержимого AX в DS;
установка 0x7c00 + 0x0a в ax.

Регистры стека:
BP базовый указатель;
SP указатель стека.

Индексные регистры:
SI: регистр индекса источника.
DI: регистр индекса получателя.
AX: используется ЦПУ для арифметических операций.
BX: может содержать адрес процедуры или переменной (это также могут SI, DI и BP) и использоваться для выполнения арифметических операций и перемещения данных.
CX: выступает в роли счетчика цикла при повторении инструкций.
DX: содержит старшие 16 бит произведения при умножении, а также задействуется при делении.
CS: содержит базовый адрес всех выполняемых инструкций программы.
SS: содержит базовый адрес стека.
DS: содержит предустановленный адрес переменных.
ES: содержит дополнительный базовый адрес переменных памяти.
BP: содержит предполагаемое смещение из регистра SS. Часто используется подпрограммами для обнаружения переменных, переданных в стек вызывающей программой.
SP: содержит смещение вершины стека.
SI: используется в инструкциях перемещения строк. При этом на исходную строку указывает регистр SI.
DI: выступает в роли места назначения для инструкций перемещения строк.

Что такое бит?


В вычислительных средах бит является наименьшей единицей данных, представляющей их в двоичном формате, где 1 = да, а 0 = нет.

Дополнительно о регистрах:

Ниже описано дальнейшее подразделение регистров:
AX: первые 8 бит AX обозначаются как AL, последние 8 бит как AH.
BX: первые 8 бит BX обозначаются как BL, последние 8 как как BH.
CX: первые 8 бит CX обозначаются как CL, последние 8 бит как CH.
DX: первые 8 бит DX обозначаются как DL, последние 8 бит как DH.

Как обращаться к функциям BIOS?


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

Что такое прерывания?


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

Какое прерывание будем использовать мы?


Прерывание INT 0x10.

Написание кода на Ассемблере


Какие типы данных доступны в GNU Assembler?


Типы данных определяют их характеристики и могут быть следующими:

байт;
слово;
int;
ascii;
asciiz.

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

Int: целочисленный тип данных, состоящий из 32 бит, которые могут быть представлены четырьмя байтами или двумя словами.
Прим. Справедливости ради, стоит отметить, что размер Int зависит от архитектуры и может составлять от 16 до 64 бит (а на некоторых системах даже 8 бит). То, очем говорит автор это тип long. Подробнее о типах С можно прочесть по ссылке.

ascii: представляет группу байтов без нулевого символа.

asciiz: выражает группу байтов, завершающуюся нулевым символом.

Как генерировать код для реального режима в Ассемблере?


В процессе запуска ЦПУ в реальном режиме (16 бит) мы можем задействовать только встроенные функции BIOS. Я имею в виду, что с помощью этих функций можно написать собственный код загрузчика, поместить его в загрузочный сектор и выполнить загрузку. Давайте рассмотрим написание на Ассемблере небольшого фрагмента программы для генерации 16-битного кода ЦПУ через GNU Assembler.

Файл-образец: test.S  


.code16                   #генерирует 16-битный код.text                     #расположение исполняемого кода     .globl _start;_start:                   #точка входа     . = _start + 510     #перемещение из позиции 0 к 510-му байту      .byte 0x55           #добавление сигнатуры загрузки     .byte 0xaa           #добавление сигнатуры загрузки


Пояснения:

.code16: это директива, отдаваемая ассемблеру для генерации не 32-, а 16-битного кода. Зачем это нужно? Ассемблер вы будете использовать через операционную систему, а код загрузчика будете писать с помощью компилятора. Но вы также наверняка помните, что ОС работает в защищенном 32-битном режиме. Поэтому, по умолчанию ассемблер в такой ОС будет производить 32-битный код, что не соответствует нашей задаче. Данная же директива исправляет этот нюанс, и мы получаем 16-битный код.
.text: этот раздел содержит фактические машинные инструкции, составляющие вашу программу.
.globl _start: .global <символ> делает символ видимым для компоновщика. При определении символа в подпрограмме его значение становится доступным для других связанных подпрограмм. Иначе говоря, символ получает атрибуты от символа с таким же именем, находящегося в другом файле, связанном с этой программой.
_start: точка входа в основной код, а также предустановленная точка входа для компоновщика.
= _start + 510: переход от начальной позиции к 510-му байту.
.byte 0x55: первый байт, определяемый как часть сигнатуры загрузки (511-й байт).
.byte 0xaa: последний байт, определяемый как часть сигнатуры загрузки (512-й байт).

Как скомпилировать программу ассемблера?


Сохраните код в файле test.S и введите в командной строке:

as test.S -o test.o
ld Ttext 0x7c00 --oformat=binary test.o o test.bin

Что означают эти команды?


as test.S o test.o: преобразует заданный код в промежуточную объектную программу, которая затем преобразуется уже в машинный код.
--oformat=binary сообщает компоновщику, что выходной двоичный файл должен быть простым двоичным образом, т.е. не иметь кода запуска, связывания адресов и пр.
Ttext 0x7c00 сообщает компоновщику, что для вычисления абсолютного адреса нужно загрузить адрес text (сегмент кода) в 0x7c00.

Что такое сигнатура загрузки?


Давайте вспомним о загрузочном секторе, используемом BIOS для запуска системы, и подумаем, как BIOS узнает о наличии такого сектора на устройстве? Тут нужно пояснить, что состоит он из 512 байт, в которых для 510-го байта ожидается символ 0x55, а для 511-го символ 0xaa. Исходя из этого, BIOS проверяет соответствие двух последний байт загрузочного сектора этим значениям и либо продолжает загрузку, либо сообщает о ее невозможности. При помощи hex-редактора можно просматривать содержимое двоичного файла в более читабельном виде, и ниже в качестве примера я привел снимок этого файла.

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


Чтобы создать образ для дискеты размером 1.4Мб, введите в командную строку следующее:
dd if=/dev/zero of=floppy.img bs=512 count=2880

Чтобы скопировать этот код в загрузочный сектор файла образа, введите:
dd if=test.bin of=floppy.img

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

Если bochs не установлен, тогда можно ввести следующее:
sudo apt-get install bochs-x

Файл-образец bochsrc.txt

megs: 32#romimage: file=/usr/local/bochs/1.4.1/BIOS-bochs-latest, address=0xf0000#vgaromimage: /usr/local/bochs/1.4.1/VGABIOS-elpin-2.40floppya: 1_44=floppy.img, status=insertedboot: alog: bochsout.txtmouse: enabled=0 


В результате должно отобразиться стандартное окно эмуляции bochs:



Просмотр:
Если теперь заглянуть в файл test.bin через hex-редактор, то вы увидите, что сигнатура загрузки находится после 510-го байта:



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

Файл-образец: <i>test2.S</i> 

.code16                    #генерирует 16-битный код.text                      #расположение исполняемого кода     .globl _start;_start:                    #точка входа     movb $'X' , %al       #выводимый символ     movb $0x0e, %ah       #выводимый служебный код bios     int  $0x10            #прерывание цпу     . = _start + 510      #перемещение из позиции 0 к 510-му байту     .byte 0x55            #добавление сигнатуры загрузки     .byte 0xaa            #добавление сигнатуры загрузки


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



Поздравляю, ваша первая программа в загрузочном секторе работает!

Просмотр:
В hex-редакторе вы увидите, что символ X находится во второй позиции от начального адреса.



Теперь давайте выведем на экран текст побуквенно.

Образец: test3.S

.code16                  #генерирует 16-битный код.text                    #расположение исполняемого кода     .globl _start;_start:                  #точка входа     #выводит 'H'      movb $'H' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'e'     movb $'e' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'l'     movb $'l' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'l'     movb $'l' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'o'      movb $'o' , %al     movb $0x0e, %ah     int  $0x10     #выводит ','     movb $',' , %al     movb $0x0e, %ah     int  $0x10     #выводит ' '     movb $' ' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'W'     movb $'W' , %al     movb $0x0e, %ah     int  $0x10     #выводит'o'     movb $'o' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'r'     movb $'r' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'l'     movb $'l' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'd'     movb $'d' , %al     movb $0x0e, %ah     int  $0x10     . = _start + 510    #перемещение из позиции 0 к 510-му байту     .byte 0x55            #добавление сигнатуры загрузки     .byte 0xaa            #добавление сигнатуры загрузки


Сохраните файл как test3.S. После компиляции и всех сопутствующих действий перед вами отобразится следующий экран:



Просмотр:



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

Файл-образец: test4.S

#генерирует 16-битный код.code16#расположение исполняемого кода.text.globl _start;#точка входа загрузочного кода_start:      jmp _boot                           #переход к загрузочному коду      welcome: .asciz "Hello, World\n\r"  #здесь мы определяем строку     .macro mWriteString str              #макрос, вызывающий функцию вывода строки          leaw  \str, %si          call .writeStringIn     .endm     #функция вывода строки     .writeStringIn:          lodsb          orb  %al, %al          jz   .writeStringOut          movb $0x0e, %ah          int  $0x10          jmp  .writeStringIn     .writeStringOut:     ret_boot:     mWriteString welcome     #перемещение от начала к 510-му байту и присоединение сигнатуры загрузки     . = _start + 510     .byte 0x55     .byte 0xaa  


Сохраните файл как test4.S. Теперь после компиляции и всего за ней следующего вы увидите:



Отлично! Если вы поняли все проделанные мной действия и успешно создали аналогичную программу, то я вас поздравляю еще раз!

Просмотр:



Что такое функция?


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

Что такое макрос?


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

В чем синтаксическое отличие функции от макроса?


Для вызова функции используется следующий синтаксис:

push call А для макроса такой:

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

Написание кода в компиляторе С



Что такое C?


С это язык программирования общего назначения, разработанный сотрудником Bell Labs Деннисом Ритчи в 1969-1973 годах.

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

Что нужно для написания кода на С?


Мы будем использовать компилятор GNU C под названием GCC и разберем написание в нем программы на примере.

Файл-образец: test.c

__asm__(".code16\n");__asm__("jmpl $0x0000, $main\n");void main() {} 


File: test.ld

ENTRY(main);SECTIONS{    . = 0x7C00;    .text : AT(0x7C00)    {        *(.text);    }    .sig : AT(0x7DFE)    {        SHORT(0xaa55);    }} 


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

gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o
ld -static -Ttest.ld -nostdlib --nmagic -o test.elf test.o
objcopy -O binary test.elf test.bin

Что значат эти команды?


Первая преобразует код C в промежуточную объектную программу, которая в последствии преобразуется в машинный код.
gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o:

Что значат эти команды?


-c: используется для компиляции исходного кода без линковки.
-g: генерирует отладочную информацию для отладчика GDB.
-Os: оптимизация размера кода.
-march: генерирует код для конкретной архитектуры ЦПУ (в нашем случае i686).
-ffreestanding: в среде отдельных программ может отсутствовать стандартная библиотека, а инструкции запуска программы не обязательно располагаются в main.
-Wall: активирует все предупреждающие сообщения компилятора. Рекомендуется всегда использовать эту опцию.
-Werror: активирует трактовку предупреждений как ошибок.
test.c: имя входного исходного файла.
-o: генерация объектного кода.
test.o: имя выходного файла объектного кода.

С помощью всей этой комбинации флагов мы генерируем объектный код, помогающий нам в обнаружении ошибок и предупреждений, а также создаем более эффективный код для данного типа ЦПУ. Если не указать march=i686, будет сгенерирован код для используемой вами машины. В связи с этим нужно указывать, для какого именно типа ЦПУ он создается.

ld -static -Ttest.ld -nostdlib --nmagic test.elf -o test.o:
Эта команда вызывает компоновщик из командной строки, и ниже я поясню, как именно мы его используем.

Что значат эти флаги?


-static: не линковать с общими библиотеками.
-Ttest.ld: разрешить компоновщику следовать командам из его скрипта.
-nostdlib: разрешить компоновщику генерировать код, не линкуя функции запуска стандартной библиотеки C.
--nmagic: разрешить компоновщику генерировать код без фрагментов _start_SECTION и _stop_SECTION.
test.elf: имя входного файла (соответствующий платформе формат хранения исполняемых файлов. Windows: PE, Linux: ELF)
-o: генерация объектного кода.
test.o: имя выходного файла объектного кода.

Что такое компоновщик?


Он выполняет последний этап компиляции. ld (компоновщик) получает один или более объектных файлов либо библиотек и совмещает их в один, как правило, исполняемый файл. В ходе этого процесса он обрабатывает ссылки на внешние символы, присваивает конечные адреса процедурам/функциям и переменным, а также корректирует код и данные для отражения актуальных адресов.
Не забывайте, что мы не используем в коде стандартные библиотеки и сложные функции.

objcopy -O binary test.elf test.bin
Эта команда служит для генерации независимого от платформы кода. Обратите внимание, что в Linux исполняемые файлы хранятся не так, как в Windows. В каждой системе свой способ хранения, но мы создаем всего-навсего небольшой загрузочный код, который на данный момент не зависит от ОС.

Зачем в программе C использовать инструкции ассемблера?


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

Как скопировать код на загрузочное устройство и проверить его?


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

dd if=/dev/zero of=floppy.img bs=512 count=2880
Чтобы скопировать код в загрузочный сектор файла образа, введите:
dd if=test.bin of=floppy.img
Для проверки программы введите:
bochs

Должно отобразиться стандартное окно эмуляции:



Что мы видим: как и в первом нашем примере, пока что здесь отображается только сообщение Booting from Floppy.

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

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

Пишем программу для вывода на экран X


Файл-образец: test2.c

__asm__(".code16\n");__asm__("jmpl $0x0000, $main\n");void main() {     __asm__ __volatile__ ("movb $'X'  , %al\n");     __asm__ __volatile__ ("movb $0x0e, %ah\n");     __asm__ __volatile__ ("int $0x10\n");}


Написав код, сохраните файл как test2.c и скомпилируйте его согласно все тем же инструкциям, изменив исходное имя. После компиляции, копирования кода в загрузочный сектор и выполнения команды bochs вы снова увидите экран, где отображается буква X:



Теперь напишем код для показа фразы Hello, World


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

Файл-образец: test3.c

/* генерирует 16-битный код */__asm__(".code16\n");/* переходит к точке входа загрузочного кода */__asm__("jmpl $0x0000, $main\n");void main() {     /* выводит 'H' */     __asm__ __volatile__("movb $'H' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'e' */     __asm__ __volatile__("movb $'e' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'l' */     __asm__ __volatile__("movb $'l' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'l' */     __asm__ __volatile__("movb $'l' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'o' */     __asm__ __volatile__("movb $'o' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит ',' */     __asm__ __volatile__("movb $',' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит ' ' */     __asm__ __volatile__("movb $' ' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'W' */     __asm__ __volatile__("movb $'W' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'o' */     __asm__ __volatile__("movb $'o' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'r' */     __asm__ __volatile__("movb $'r' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'l' */     __asm__ __volatile__("movb $'l' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'd' */     __asm__ __volatile__("movb $'d' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");}


Сохраните этот код в файле test3.c и следуйте уже знакомым вам инструкциям, изменив имя исходного файла и скопировав скомпилированный код в загрузочный сектор дискеты. Теперь на этапе проверки должна отобразиться надпись Hello, World:



Напишем программу C для вывода строки Hello, World


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

Файл-образец: test4.c

/*генерирует 16-битный код*/__asm__(".code16\n");/*переход к точке входа в загрузочный код*/__asm__("jmpl $0x0000, $main\n");/* пользовательская функция для вывода серии знаков, завершаемых нулевым символом*/void printString(const char* pStr) {     while(*pStr) {          __asm__ __volatile__ (               "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)          );          ++pStr;     }}void main() {     /* вызов функции <i>printString </i>со строкой в качестве аргумента*/     printString("Hello, World");} 


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



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

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

Мини-проект отображения прямоугольников


Файл-образец: test5.c

/* генерирация 16-битного кода                                                 */__asm__(".code16\n");/* переход к главной функции или программному коду                                */__asm__("jmpl $0x0000, $main\n");#define MAX_COLS     320 /* количество столбцов экрана               */#define MAX_ROWS     200 /* количество строк экрана                  *//* функция вывода строки*//* input ah = 0x0e*//* input al = <выводимый символ>*//* прерывание: 0x10*//* мы используем прерывание 0x10 с кодом функции 0x0e для вывода байта из al*//* эта функция получает в качестве аргумента строку и выводит символ за символом, пока не достигнет нуля*/void printString(const char* pStr) {     while(*pStr) {          __asm__ __volatile__ (               "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)          );          ++pStr;     }}/* функция, получающая сигнал о нажатии клавиши на клавиатуре *//* input ah = 0x00*//* input al = 0x00*//* прерывание: 0x10*//* эта функция регистрирует нажатие пользователем клавиши для продолжения выполнения */void getch() {     __asm__ __volatile__ (          "xorw %ax, %ax\n"          "int $0x16\n"     );}/* функция вывода на экран цветного пикселя в заданном столбце и строке *//* входной ah = 0x0c*//* входной al = нужный цвет*//* входной cx = столбец*//* входной dx = строка*//* прерывание: 0x10*/void drawPixel(unsigned char color, int col, int row) {     __asm__ __volatile__ (          "int $0x10" : : "a"(0x0c00 | color), "c"(col), "d"(row)     );}/* функции очистки экрана и установки видео-режима 320x200 пикселей*//* функция для очистки экрана *//* входной ah = 0x00 *//* входной al = 0x03 *//* прерывание = 0x10 *//* функция для установки видео режима *//* входной ah = 0x00 *//* входной al = 0x13 *//* прерывание = 0x10 */void initEnvironment() {     /* очистка экрана */     __asm__ __volatile__ (          "int $0x10" : : "a"(0x03)     );     __asm__ __volatile__ (          "int $0x10" : : "a"(0x0013)     );}/* функция вывода прямоугольников в порядке уменьшения их размера *//* я выбрал следующую последовательность отрисовки: *//* из левого верхнего угла в левый нижний, затем в правый нижний, оттуда в верхний правый и в завершении в верхний левый край */void initGraphics() {     int i = 0, j = 0;     int m = 0;     int cnt1 = 0, cnt2 =0;     unsigned char color = 10;     for(;;) {          if(m < (MAX_ROWS - m)) {               ++cnt1;          }          if(m < (MAX_COLS - m - 3)) {               ++cnt2;          }          if(cnt1 != cnt2) {               cnt1  = 0;               cnt2  = 0;               m     = 0;               if(++color > 255) color= 0;          }          /* верхний левый -> левый нижний */          j = 0;          for(i = m; i < MAX_ROWS - m; ++i) {               drawPixel(color, j+m, i);          }          /* левый нижний -> правый нижний */          for(j = m; j < MAX_COLS - m; ++j) {               drawPixel(color, j, i);          }          /* правый нижний -> правый верхний */          for(i = MAX_ROWS - m - 1 ; i >= m; --i) {               drawPixel(color, MAX_COLS - m - 1, i);          }          /* правый верхний -> левый верхний */          for(j = MAX_COLS - m - 1; j >= m; --j) {               drawPixel(color, j, m);          }          m += 6;          if(++color > 255)  color = 0;     }}/* эта функция является загрузочным кодом и вызывает следующие функции: *//* вывод на экран сообщения, предлагающего пользователю нажать любую клавишу для продолжения. После нажатия клавиши происходит отрисовка прямоугольников в порядке убывания их размера */void main() {     printString("Now in bootloader...hit a key to continue\n\r");     getch();     initEnvironment();     initGraphics();}


Сохраните все это в файле test5.c и следуйте все тем же инструкциям компиляции с последующим копированием кода в загрузочный сектор дискеты.

Теперь в качестве результата вы увидите:



Нажмите любую клавишу.






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



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

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

Подробнее..

Перевод Пишем макет 16-битного ядра на CC

12.01.2021 12:17:05 | Автор: admin


В первой и второй статьях я лишь коротко представил процесс написания загрузчика на ассемблере и C. Для меня это было хоть и непросто, но в то же время интересно, так что я остался доволен. Однако создания загрузчика мне показалось мало, и я увлекся идеей его расширения дополнительной функциональностью. Но так как в итоге размер готовой программы превысил 512 байт, то при попытке запуска системы с несущего ее загрузочного диска я столкнулся с проблемой This is not a bootable disk.

О чем эта статья?


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

Нужен ли для этого опыт?


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

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

План статьи


Ограничения загрузчика
Вызов из загрузчика других файлов диска
Файловая система FAT
Принцип работы FAT
Среда разработки
Написание загрузчика для FAT
Мини-проект: написание 16-битного ядра
Тестирование ядра

Ограничения загрузчика


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

В итоге передо мной стоит две задачи:

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

Как я буду это делать?


Этап 1:

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

Этап 2:

В загрузчике мы можем просто загрузить второй сектор, содержащий kernel.bin, в RAM по адресу, к примеру, 0x1000, а затем перейти к этому адресу из 0x7с00 и запустить kernel.bin.

Вот схема для лучшего понимания идеи:



Запуск из загрузчика других файлов диска


Как мы теперь знаем, у нас есть возможность передачи управления от загрузчика (0x7c00) в другую область памяти, где размещается, например, наш kernel.bin, после чего продолжить выполнение. Но здесь у меня я хочу кое-что уточнить.

Как узнать сколько секторов kernel.bin займет на диске?


Ну это простой вопрос. Для ответа на него нам достаточно выполнить несложную арифметику, а именно разделить размер kernel.bin на размер сектора, который составляет 512 байт. Например, если kernel.bin будет равен 1024 байта, то и займет он 2 сектора.

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

Можно ли добавить помимо kernel.bin другие файлы, например office.bin, entertainment.bin, drivers.bin?


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

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


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

Чего не хватает?


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

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


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

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

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

Как это решается?


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

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

FAT
FAT16
FAT32
NTFS
EXT
EXT2
EXT3
EXT4

FAT


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

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

загрузочный сектор (boot sector);
таблицу размещения файлов (file allocation table);
корневой каталог (root directory);
область данных (data area).

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



Рассмотрим каждую часть подробнее.

Загрузочный сектор


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

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

Блок параметров BIOS


Ниже я привел пример значений из этого блока:



Таблица размещения файлов


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

Это значение кластера служит для:
определения окончания файла. Если оно находится между 0x0ff8 и 0x0fff, значит файл не содержит данных в других секторах, т.е. достигнут его конец.
определения следующего кластера с данными этого файла.

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

Корневой каталог


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

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

Область данных


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

Принцип работы FAT


Продолжим наш пример с kernel.bin, который загрузчик помещает в память для выполнения. Теперь в этом сценарии нужно прописать для загрузчика следующую функциональность:

Сравнить первые 11 байт данных с kernel.bin, начиная со смещения 0 в таблице корневого каталога.
В случае совпадения этой строки извлечь первый кластер kernel.bin из смещения 26 корневого каталога.
Далее преобразовать этот кластер в соответствующий сектор и загрузить его данные в память.
После загрузки первого сектора в память перейти к поиску в FAT следующего кластера файла и определить, является он последним, или есть еще данные в других кластерах.

Ниже я привел очередную схему.



Среда разработки


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

Операционная система (GNU Linux).
Ассемблер (GNU Assembler).
Набор инструкций (x86).
Написание инструкций для микропроцессора x86 на GNU Assembler.
Компилятор (GNU C компилятор GCC).
Компоновщик (GNU linker ld)
Эмулятор, например bochs, используемый для тестирования.

Написание загрузчика FAT


Ниже я привожу фрагмент кода для выполнения файла kernel.bin на FAT-диске.

Вот загрузчик.

Файл: stage0.S



/********************************************************************************* *                                                                               * *                                                                               * *    Name       : stage0.S                                                      * *    Date       : 23-Feb-2014                                                   * *    Version    : 0.0.1                                                         * *    Source     : assembly language                                             * *    Author     : Ashakiran Bhatter                                             * *                                                                               * *    Описание: основная логика подразумевает сканирование файла kernel.bin      * *                 на дискете fat12 и передачу этому файлу права                 * *                 выполнения.                                                   * *    Использование: подробности в файле readme.txt                              * *                                                                               * *                                                                               * *********************************************************************************/.code16.text.globl _start;_start:     jmp _boot     nop     /*блок параметров BIOS                           описание каждой сущности      */     /*--------------------                           --------------------------    */     .byte 0x6b,0x69,0x72,0x55,0x58,0x30,0x2e,0x31    /* метка OEM                  */     .byte 0x00,0x02                                  /* байтов в секторе           */     .byte 0x01                                       /* секторов в кластере        */     .byte 0x01,0x00                                  /* зарезервированных секторов */     .byte 0x02                                       /* таблиц fat                 */     .byte 0xe0,0x00                                  /* записей в каталоге         */     .byte 0x40,0x0b                                  /* всего секторов             */     .byte 0xf0                                       /* описание среды передачи    */     .byte 0x09,0x00                                  /* размер в каждой таблице fat    */     .byte 0x02,0x01                                  /* секторов в дорожке         */     .byte 0x02,0x00                                  /* головок на цилиндр         */     .byte 0x00,0x00, 0x00, 0x00                      /* скрытых секторов           */     .byte 0x00,0x00, 0x00, 0x00                      /* больших секторов           */     .byte 0x00                                       /* идентификатор загрузочного диска*/     .byte 0x00                                       /* неиспользуемых секторов    */     .byte 0x29                                       /* внешняя сигнатура загрузки */     .byte 0x22,0x62,0x79,0x20                        /* серийный номер             */     .byte 0x41,0x53,0x48,0x41,0x4b,0x49              /* метка тома 6 байт из 11    */     .byte 0x52,0x41,0x4e,0x20,0x42                   /* метка тома 5 байт из 11    */     .byte 0x48,0x41,0x54,0x54,0x45,0x52,0x22         /* тип файловой системы       */     /* включение макросов */     #include "macros.S"/* начало основного кода */_boot:     /* инициализация среды */     initEnvironment      /* загрузка stage2 */     loadFile $fileStage2/* бесконечный цикл */_freeze:     jmp _freeze/* непредвиденное завершение программы */_abort:     writeString $msgAbort     jmp _freeze     /* включение функций */     #include "routines.S"     /* пользовательские переменные */     bootDrive : .byte 0x0000     msgAbort  : .asciz "* * * F A T A L  E R R O R * * *"     #fileStage2: .ascii "STAGE2  BIN"     fileStage2: .ascii  "KERNEL  BIN"     clusterID : .word 0x0000     /* перемещение от начала к 510-му байту */     . = _start + 0x01fe     /* добавление сигнатуры загрузки             */     .word BOOT_SIGNATURE

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

Инициализация всех регистров и настройка стека вызовом макроса initEnvironment.
Вызов макроса loadFile для загрузки kernel.bin в память по адресу 0x1000:0000 и последующей передачи ему права выполнения.

Файл: macros.S


Этот файл содержит все предопределенные макросы и функции.

/*********************************************************************************          *                                                                               * *                                                                               * *    Name       : macros.S                                                      * *    Date       : 23-Feb-2014                                                   * *    Version    : 0.0.1                                                         * *    Source     : assembly language                                             * *    Author     : Ashakiran Bhatter                                             * *                                                                               * *                                                                               * *********************************************************************************//* предопределенный макрос: загрузчик                         */#define BOOT_LOADER_CODE_AREA_ADDRESS                 0x7c00#define BOOT_LOADER_CODE_AREA_ADDRESS_OFFSET          0x0000/* предопределенный макрос: сегмент стека                       */#define BOOT_LOADER_STACK_SEGMENT                     0x7c00#define BOOT_LOADER_ROOT_OFFSET                       0x0200#define BOOT_LOADER_FAT_OFFSET                        0x0200#define BOOT_LOADER_STAGE2_ADDRESS                    0x1000#define BOOT_LOADER_STAGE2_OFFSET                     0x0000 /* предопределенный макрос: разметка дискеты                  */#define BOOT_DISK_SECTORS_PER_TRACK                   0x0012#define BOOT_DISK_HEADS_PER_CYLINDER                  0x0002#define BOOT_DISK_BYTES_PER_SECTOR                    0x0200#define BOOT_DISK_SECTORS_PER_CLUSTER                 0x0001/* предопределенный макрос: разметка файловой системы                  */#define FAT12_FAT_POSITION                            0x0001#define FAT12_FAT_SIZE                                0x0009#define FAT12_ROOT_POSITION                           0x0013#define FAT12_ROOT_SIZE                               0x000e#define FAT12_ROOT_ENTRIES                            0x00e0#define FAT12_END_OF_FILE                             0x0ff8/* предопределенный макрос: загрузчик                         */#define BOOT_SIGNATURE                                0xaa55/* пользовательские макросы *//* макрос для установки среды */.macro initEnvironment     call _initEnvironment.endm/* макрос для отображения строки на экране.   *//* Для выполнения этой операции он вызывает функцию _writeString *//* параметр: вводная строка                */.macro writeString message     pushw \message     call  _writeString.endm/* макрос для считывания сектора в памяти  *//* Вызывает функцию _readSector со следующими параметрами   *//* параметры: номер сектора               *//*            адрес загрузки                *//*            смещение адреса          *//*            количество считываемых секторов      */.macro readSector sectorno, address, offset, totalsectors     pushw \sectorno     pushw \address     pushw \offset     pushw \totalsectors     call  _readSector     addw  $0x0008, %sp.endm/* макрос для поиска файла на FAT-диске.   *//* Для этого он вызывает макрос readSector *//* параметры: адрес корневого каталога     *//*               целевой адрес             *//*               целевое смещение          *//*               размер корневого каталога */.macro findFile file     /* считывание таблицы FAT в память */     readSector $FAT12_ROOT_POSITION, $BOOT_LOADER_CODE_AREA_ADDRESS, $BOOT_LOADER_ROOT_OFFSET, $FAT12_ROOT_SIZE     pushw \file     call  _findFile     addw  $0x0002, %sp.endm/* макрос для преобразования заданного кластера в номер сектора *//* Для этого он вызывает _clusterToLinearBlockAddress *//* параметр: номер кластера */.macro clusterToLinearBlockAddress cluster     pushw \cluster     call  _clusterToLinearBlockAddress     addw  $0x0002, %sp.endm/* макрос для загрузки целевого файла в память.  *//* Он вызывает findFile и загружает данные соответствующего файла в память *//* по адресу 0x1000:0x0000 *//* параметр: имя целевого файла */.macro loadFile file     /* проверка наличия файла */     findFile \file     pushw %ax     /* считывание таблицы FAT в память */     readSector $FAT12_FAT_POSITION, $BOOT_LOADER_CODE_AREA_ADDRESS, $BOOT_LOADER_FAT_OFFSET, $FAT12_FAT_SIZE     popw  %ax     movw  $BOOT_LOADER_STAGE2_OFFSET, %bx_loadCluster:     pushw %bx     pushw %ax      clusterToLinearBlockAddress %ax     readSector %ax, $BOOT_LOADER_STAGE2_ADDRESS, %bx, $BOOT_DISK_SECTORS_PER_CLUSTER     popw  %ax     xorw %dx, %dx     movw $0x0003, %bx     mulw %bx     movw $0x0002, %bx     divw %bx     movw $BOOT_LOADER_FAT_OFFSET, %bx     addw %ax, %bx     movw $BOOT_LOADER_CODE_AREA_ADDRESS, %ax     movw %ax, %es     movw %es:(%bx), %ax     orw  %dx, %dx     jz   _even_cluster_odd_cluster:     shrw $0x0004, %ax     jmp  _done _even_cluster:     and $0x0fff, %ax_done:     popw %bx     addw $BOOT_DISK_BYTES_PER_SECTOR, %bx     cmpw $FAT12_END_OF_FILE, %ax     jl  _loadCluster     /* выполнение ядра */     initKernel     .endm/* параметры: имя целевого файла *//* макрос для передачи права выполнения файлу, загруженному *//* в память по адресу 0x1000:0x0000                     *//* параметры: none                       */.macro initKernel     /* инициализация ядра */     movw  $(BOOT_LOADER_STAGE2_ADDRESS), %ax     movw  $(BOOT_LOADER_STAGE2_OFFSET) , %bx     movw  %ax, %es     movw  %ax, %ds     jmp   $(BOOT_LOADER_STAGE2_ADDRESS), $(BOOT_LOADER_STAGE2_OFFSET).endm 

Общая сводка


initEnvironment:
Макрос для установки сегментных регистров.
Аргументов не требует.

Применение: initEnvironment

writeString:
Макрос для отображения на экране строки с завершающим нулем.
В качестве аргумента передается строковая переменная с завершающим нулем.

Применение: writeString <строковая переменная>

readSector:
Макрос для чтения с диска заданного сектора и его загрузки в целевой адрес памяти.
Количество аргументов: 4.

Применение: readSector <номер сектора>, <целевой адрес>, <смещение целевого адреса>, <количество считываемых секторов>

findFile:
Макрос для проверки наличия файла.
Количество аргументов: 1.

Применение: findFile <имя целевого файла>

clusterToLinearBlockAddress:
Макрос для преобразования заданного кластера в номер сектора.
Количество аргументов: 1.

Применение:
clusterToLinearBlockAddress <ID кластера>


loadFile:
Макрос для загрузки целевого файла в память с последующей передачей ему права выполнения.
Количество аргументов: 1.

Применение:
loadFile <имя целевого файла>


initKernel:
Макрос для передачи права выполнения конкретному адресу памяти в RAM.
Аргументов не требует.

Применение: initKernel

Файл: routines.S



/********************************************************************************* *                                                                               * *                                                                               * *    Name       : routines.S                                                    * *    Date       : 23-Feb-2014                                                   * *    Version    : 0.0.1                                                         * *    Source     : assembly language                                             * *    Author     : Ashakiran Bhatter                                             * *                                                                               * *                                                                               * *********************************************************************************//* Пользовательские подпрограммы. *//* функция для настройки регистров и стека *//* параметры: none                  */_initEnvironment:     pushw %bp     movw  %sp, %bp_initEnvironmentIn:     cli     movw  %cs, %ax     movw  %ax, %ds     movw  %ax, %es     movw  %ax, %ss     movw  $BOOT_LOADER_STACK_SEGMENT, %sp     sti_initEnvironmentOut:     movw  %bp, %sp     popw  %bpret/* функция для отображения строки на экране *//* параметр: вводная строка                */_writeString:     pushw %bp     movw  %sp   , %bp     movw 4(%bp) , %si     jmp  _writeStringCheckByte_writeStringIn:     movb $0x000e, %ah     movb $0x0000, %bh     int  $0x0010     incw %si_writeStringCheckByte:     movb (%si)  , %al     orb  %al    , %al     jnz  _writeStringIn_writeStringOut:     movw %bp    , %sp     popw %bpret/* функция для считывания сектора в целевой адрес памяти *//* параметры: номер сектора                              *//*            целевой адрес                              *//*            смещение адреса                            *//*            количество считываемых секторов            */_readSector:     pushw %bp     movw %sp    , %bp     movw 10(%bp), %ax     movw $BOOT_DISK_SECTORS_PER_TRACK, %bx     xorw %dx    , %dx     divw %bx     incw %dx     movb %dl    , %cl     movw $BOOT_DISK_HEADS_PER_CYLINDER, %bx     xorw %dx    , %dx     divw %bx     movb %al    , %ch     xchg %dl    , %dh     movb $0x02  , %ah     movb 4(%bp) , %al     movb bootDrive, %dl     movw 8(%bp) , %bx     movw %bx    , %es     movw 6(%bp) , %bx     int  $0x13     jc   _abort     cmpb 4(%bp) , %al     jc   _abort     movw %bp    , %sp     popw %bpret/* функция поиска файла на дискете         *//* параметры: адрес корневого каталога     *//*               целевой адрес             *//*               целевое смещение          *//*               размер корневого каталога */_findFile:     pushw %bp     movw  %sp   , %bp     movw  $BOOT_LOADER_CODE_AREA_ADDRESS, %ax     movw  %ax   , %es     movw  $BOOT_LOADER_ROOT_OFFSET, %bx     movw  $FAT12_ROOT_ENTRIES, %dx     jmp   _findFileInitValues_findFileIn:     movw  $0x000b  , %cx     movw  4(%bp)   , %si     leaw  (%bx)    , %di     repe  cmpsb     je    _findFileOut_findFileDecrementCount:     decw  %dx     addw  $0x0020, %bx_findFileInitValues:     cmpw  $0x0000, %dx     jne   _findFileIn     je    _abort_findFileOut:     addw  $0x001a  , %bx     movw  %es:(%bx), %ax     movw  %bp, %sp     popw  %bpret/* функция для преобразования заданного кластера в номер сектора *//* параметры: номер кластера                                     */_clusterToLinearBlockAddress:     pushw %bp     movw  %sp    , %bp     movw  4(%bp) , %ax_clusterToLinearBlockAddressIn:     subw  $0x0002, %ax     movw  $BOOT_DISK_SECTORS_PER_CLUSTER, %cx     mulw  %cx     addw  $FAT12_ROOT_POSITION, %ax     addw  $FAT12_ROOT_SIZE, %ax_clusterToLinearBlockAddressOut:     movw  %bp    , %sp     popw  %bpret

Общая сводка


_initEnvironment:
Функция, отвечающая за установку сегментных регистров.
Аргументов не требует.

Применение: call _initEnvironment

_writeString:
Функция для отображения на экране строки с завершающим нулем.
В качестве аргумента получает строковую переменную с завершающим нулем.

Применение:
pushw <строковая переменная>
call _writeString
addw $0x02, %sp


readSector:
Макрос для считывания заданного сектора с диска и его загрузки в целевой адрес памяти.
Количество аргументов: 4.

Применение:
pushw <номер сектора>
pushw <адрес>
pushw <смещение>
pushw <всего секторов>
call _readSector
addw $0x0008, %sp


findFile:
Функция для проверки наличия файла.
Количество аргументов: 1

Применение:
pushw <target file variable>call _findFileaddw $0x02, %sp 


clusterToLinearBlockAddress:
Макрос для преобразования ID заданного кластера в номер сектора.
Количество аргументов: 1

Применение:
pushw <ID кластера>call _clusterToLinearBlockAddressaddw $0x02, %sp


loadFile:
Макрос для загрузки целевого файла в память с последующей передачей ему права выполнения.
Количество аргументов: 1

Применение:
pushw <целевой файл>call _loadFileaddw $0x02, %sp


Файл: stage0.ld


Этот файл служит для линковки файла stage0.object.

/********************************************************************************* *                                                                               * *                                                                               * *    Name       : stage0.ld                                                     * *    Date       : 23-Feb-2014                                                   * *    Version    : 0.0.1                                                         * *    Source     : assembly language                                             * *    Author     : Ashakiran Bhatter                                             * *                                                                               * *                                                                               * *********************************************************************************/SECTIONS{     . = 0x7c00;     .text :     {          _ftext = .;     } = 0}

Файл: bochsrc.txt


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

megs: 32floppya: 1_44=../iso/stage0.img, status=insertedboot: alog: ../log/bochsout.txtmouse: enabled=0 

Мини-проект: написание 16-битного ядра


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

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

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

Файл: kernel.c


/********************************************************************************* *                                                                               * *                                                                               * *    Name       : kernel.c                                                      * *    Date       : 23-Feb-2014                                                   * *    Version    : 0.0.1                                                         * *    Source     : C                                                             * *    Author     : Ashakiran Bhatter                                             * *                                                                               * *    Описание: За загрузку этого файла отвечает stage0.bin, который передает    * *                 ему право выполнения. Его функциональность                    * *                 заключается в отображении экрана-заставки и командной строки. * *    Внимание   : Вводить команды бессмысленно, так как они не запрограммированы*                                                                            *                                                                               * *********************************************************************************//* генерирует 16-битный код                                  */__asm__(".code16\n");/* переход к основной функции                                */__asm__("jmpl $0x1000, $main\n");#define TRUE  0x01#define FALSE 0x00char str[] = "$> ";/* функция установки регистров и стека *//* параметры: none                     */void initEnvironment() {     __asm__ __volatile__(          "cli;"          "movw $0x0000, %ax;"          "movw %ax, %ss;"          "movw $0xffff, %sp;"          "cld;"     );     __asm__ __volatile__(          "movw $0x1000, %ax;"          "movw %ax, %ds;"          "movw %ax, %es;"          "movw %ax, %fs;"          "movw %ax, %gs;"     );}/* VGA-функции. *//* функция для установки режима VGA на 80*24   */void setResolution() {     __asm__ __volatile__(          "int $0x10" : : "a"(0x0003)     );}/* функция очистки буфера экрана разделяющими пробелами */void clearScreen() {     __asm__ __volatile__ (          "int $0x10" : : "a"(0x0200), "b"(0x0000), "d"(0x0000)     );     __asm__ __volatile__ (          "int $0x10" : : "a"(0x0920), "b"(0x0007), "c"(0x2000)     );}/* функция установки позиции курсора на заданный столбец и строку */void setCursor(short col, short row) {     __asm__ __volatile__ (          "int $0x10" : : "a"(0x0200), "d"((row <<= 8) | col)     );}/* функция включения и отключения курсора */void showCursor(short choice) {     if(choice == FALSE) {          __asm__ __volatile__(               "int $0x10" : : "a"(0x0100), "c"(0x3200)          );     } else {          __asm__ __volatile__(               "int $0x10" : : "a"(0x0100), "c"(0x0007)          );     }}/* функция инициализации режима VGA на 80*25,            *//* очистки экрана и установки положения курсора на (0,0) */void initVGA() {     setResolution();     clearScreen();     setCursor(0, 0);}/* I/O-функции. *//* функция для получения символа с клавиатуры без эха*/void getch() {     __asm__ __volatile__ (          "xorw %ax, %ax\n"          "int $0x16\n"     );}/* эта функция аналогична getch(),                                 *//* но возвращает скан-код клавиши и соответствующее значение ascii */short getchar() {     short word;     __asm__ __volatile__(          "int $0x16" : : "a"(0x1000)     );     __asm__ __volatile__(          "movw %%ax, %0" : "=r"(word)     );     return word;}/* функция для отображения нажатых клавиш на экране*/void putchar(short ch) {     __asm__ __volatile__(          "int $0x10" : : "a"(0x0e00 | (char)ch)     );}/* функция вывода на экран строки с завершающим нулем */void printString(const char* pStr) {     while(*pStr) {          __asm__ __volatile__ (               "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0002)          );          ++pStr;     }}/* функция, вызывающая задержку на несколько секунд */void delay(int seconds) {     __asm__ __volatile__(          "int $0x15" : : "a"(0x8600), "c"(0x000f * seconds), "d"(0x4240 * seconds)     );}/* Строковая функция. *//* эта функция вычисляет длину строки и возвращает ее */int strlength(const char* pStr) {     int i = 0;     while(*pStr) {          ++i;     }     return i;}/* Функция UI. *//*эта функция отображает логотип */void splashScreen(const char* pStr) {     showCursor(FALSE);     clearScreen();     setCursor(0, 9);     printString(pStr);     delay(10);}/* Оболочка. *//* функция для отображения фиктивной командной строки.                  *//* При нажатии клавиши Ввод выполняется переход на следующую строку     */void shell() {     clearScreen();     showCursor(TRUE);     while(TRUE) {          printString(str);          short byte;          while((byte = getchar())) {               if((byte >> 8)  == 0x1c) {                    putchar(10);                    putchar(13);                    break;               } else {                    putchar(byte);               }          }     }}/* точка входа в ядро */void main() {     const char msgPicture[] =              "                     ..                                              \n\r"             "                      ++`                                            \n\r"             "                       :ho.        `.-/++/.                          \n\r"             "                        `/hh+.         ``:sds:                       \n\r"             "                          `-odds/-`        .MNd/`                    \n\r"             "                             `.+ydmdyo/:--/yMMMMd/                   \n\r"             "                                `:+hMMMNNNMMMddNMMh:`                \n\r"             "                   `-:/+++/:-:ohmNMMMMMMMMMMMm+-+mMNd`               \n\r"             "                `-+oo+osdMMMNMMMMMMMMMMMMMMMMMMNmNMMM/`              \n\r"             "                ```   .+mMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmho:.`         \n\r"             "                    `omMMMMMMMMMMMMMMMMMMNMdydMMdNMMMMMMMMdo+-       \n\r"             "                .:oymMMMMMMMMMMMMMNdo/hMMd+ds-:h/-yMdydMNdNdNN+      \n\r"             "              -oosdMMMMMMMMMMMMMMd:`  `yMM+.+h+.-  /y `/m.:mmmN      \n\r"             "             -:`  dMMMMMMMMMMMMMd.     `mMNo..+y/`  .   .  -/.s      \n\r"             "             `   -MMMMMMMMMMMMMM-       -mMMmo-./s/.`         `      \n\r"             "                `+MMMMMMMMMMMMMM-        .smMy:.``-+oo+//:-.`        \n\r"             "               .yNMMMMMMMMMMMMMMd.         .+dmh+:.  `-::/+:.        \n\r"             "               y+-mMMMMMMMMMMMMMMm/`          ./o+-`       .         \n\r"             "              :-  :MMMMMMMMMMMMMMMMmy/.`                             \n\r"             "              `   `hMMMMMMMMMMMMMMMMMMNds/.`                         \n\r"             "                  sNhNMMMMMMMMMMMMMMMMMMMMNh+.                       \n\r"             "                 -d. :mMMMMMMMMMMMMMMMMMMMMMMNh:`                    \n\r"             "                 /.   .hMMMMMMMMMMMMMMMMMMMMMMMMh.                   \n\r"             "                 .     `sMMMMMMMMMMMMMMMMMMMMMMMMN.                  \n\r"             "                         hMMMMMMMMMMMMMMMMMMMMMMMMy                  \n\r"             "                         +MMMMMMMMMMMMMMMMMMMMMMMMh                      ";     const char msgWelcome[] =              "              *******************************************************\n\r"             "              *                                                     *\n\r"             "              *        Welcome to kirUX Operating System            *\n\r"             "              *                                                     *\n\r"             "              *******************************************************\n\r"             "              *                                                     *\n\r"              "              *                                                     *\n\r"             "              *        Author : Ashakiran Bhatter                   *\n\r"             "              *        Version: 0.0.1                               *\n\r"             "              *        Date   : 01-Mar-2014                         *\n\r"             "              *                                                     *\n\r"             "              ******************************************************";     initEnvironment();      initVGA();     splashScreen(msgPicture);     splashScreen(msgWelcome);     shell();      while(1);}

Общая сводка


initEnvironment():
  • Устанавливает сегментные регистры и формирует стек.
  • Количество аргументов: none


Применение: initEnvironment();

setResolution():
Устанавливает разрешение экрана 80*25.
Количество аргументов: none.

Применение: setResolution();

clearScreen():
Заполняет буфер экрана пробелами.
Количество аргументов: none

Применение: clearScreen();

setCursor():
Устанавливает курсор в заданное положение на экране.
Количество аргументов: 2.

Применение: setCursor(столбец, строка);

showCursor():
По желанию пользователя активирует или отключает курсор.
Количество аргументов: 1.

Применение: showCursor(1);

initVGA():
Устанавливает разрешение 80*25, очищает экран и устанавливает курсор в позицию (0,0).
Количество аргументов: none

Применение: initVGA();

getch():
Регистрирует нажатия клавиш без эха.
Количество аргументов: none

Применение: getch();

getchar():
Возвращает скан-код нажатой клавиши и соответствующее значение ascii.
Количество аргументов: none.

Применение: getchar();

putchar():
Отображает символы нажатых клавиш на экране.
Количество аргументов: 1.

Применение: putchar(символ);

printString():
Выводит на экран строку с завершающим нулем.
Количество аргументов: 1.

Применение: printString();

delay():
Вызывает задержку на несколько секунд.
Количество аргументов: 1.

Применение: printString(строковая переменная с завершающим нулем);

strlength():
Возвращает значение длины строки с завершающим нулем.
Количество аргументов: 1.

Применение: strlength(строковая переменная с завершающим нулем);

splashScreen():
Отображает заданную картинку определенное время.
Количество аргументов: 1.

Применение: splashScreen(строковая переменная с завершающим нулем);

shell():
Отображает командную строку.
Количество аргументов: none.

Применение: shell();

Тестирование ядра


Использование исходного кода:
В прикрепленном архиве sourcecode.tar.gz находятся все исходные файлы и каталоги, необходимые для генерации исполняемых файлов.
Убедитесь, что вы являетесь супер-пользователем системы, после чего распакуйте архив.
Для перехода к компиляции и тестированию кода установите эмулятор bochs-x64 и GNU bin-utils.
После извлечения файлов вы увидите 5 каталогов:

bin
iso
kernel
log
src

Подготовив среду, откройте терминал и выполните следующие команды:
cd $(DIRECTORY)/src
make -f Makefile test
bochs

Сриншоты


Экран 1:
Это первый экран, отображаемый при выполнении ядра.



Экран 2:
Дальше идет экран приветствия:



Экран 3:
Это командная строка, в которой можно ввести текст.



Экран 4:
Здесь я привожу пример написания команд и перехода строки при нажатии Ввода.



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

Заключение


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

Подробнее..

Как собрать паука в Godot, Unigine или PlayCanvas

04.01.2021 22:21:38 | Автор: admin
С наступившим 21-м годом 21-го века.
В данной статье пробегусь по особенностям работы в трёх игровых движках, на примере написания кода для паукообразного средства передвижения.




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



Godot


Здесь у меня уже был готов небольшой проект с машинками и паука я решил добавить в одну из сцен (префабов), содержащую в себе подкласс машинок, которые не имеют колёс.
Сама сцена specific_base устроена таким образом, что в основе узел-пустышка, который просто висит где-то в мире, без движения, а по миру перемещается kinematic body внутри него. Камера находится внутри сцены, но вне body, просто следуя за ним.



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



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


Код внутри редактора Godot

Пишем код. Я использую GDScript, потому как особого смысла писать именно на C# в Годо не вижу (не настолько фанат фигурных скобочек):

extends Spatialexport var distance = 2.5#максимальная дистанция, после которой случится перерасчётexport var step = 1#переменная для дополнительного смещения лап (вещь необязательная)#ссылки на центр паука и одну из позиций лап, а также элементы для их храненияexport (NodePath) var spidercenter = nullvar trg_centerexport (NodePath) var spiderleg = nullvar trg_leg#переменные для расстояний по осям x и zvar x_dis = 0.0var z_dis = 0.0#переменная-таймер, а также флагvar time_lag = -1.0# инициализацияfunc _ready():self.hide()#скрыть лапуtrg_center = get_node(spidercenter)#запомнить объектыtrg_leg = get_node(spiderleg)LegPlace()#один раз вызвать установку лапки на позицию# основной циклfunc _process(delta):        #развернуть лапу в направлении центра паука. можно ввести таймер, чтобы делать это через малые интервалыself.look_at(trg_center.global_transform.origin, Vector3(0,1,0))        #включить видимость, если лапа была невидимой. это делалось для того, чтобы показывать её снова, после того как она скрывается в момент перестановки (чтобы перестановка выглядела как появление лапы в новой позиции уже развёрнутой в нужную сторону, а не перенесением с последующим разворачиванием). на самом деле можно было вынести внеочередной разворот и последующий показ лапы в LegPlaceif self.visible == false: self.show()if time_lag>=0:#если флаг-таймер запущен, то наращивать его значениеtime_lag +=1*delta if time_lag>0.06:#при истечении задержки сбросить флаг и вызвать перерисовкуtime_lag = -1.0LegPlace()else:#пока флаг неактивен считать дистанции от лапы до позиции лапы по двум осямx_dis = abs(trg_leg.global_transform.origin.x - self.global_transform.origin.x)z_dis = abs(trg_leg.global_transform.origin.z - self.global_transform.origin.z)if (x_dis + z_dis) > distance:#если дистанция больше лимита, запустить флагtime_lag = 0.0passfunc LegPlace():#собственно, сама функция перестановки лапыself.hide()step = step*(-1)self.global_transform.origin = trg_leg.global_transform.origin+Vector3(0,0,0.5*step)



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



Unigine


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



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



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


Unigine для редактирования кода запускает внешнюю среду

Код:

using System;//стандартная "шапка"using System.Collections;using System.Collections.Generic;using Unigine;//уникальный идентификатор компонента, генерируемый при создании скрипта[Component(PropertyGuid = "5a8dd6f85781adf7567432eae578c5414581ddac")]public class theLegBehavior : Component{[ShowInEditor][Parameter(Tooltip = "CenterSpider")]//указатель на центр паукаprivate Node spiderCenter = null;[ShowInEditor][Parameter(Tooltip = "Target Leg Point")]//указатель на точку крепленияprivate Node legPoint = null;//переменные для вычислений дистанций по осямprivate float x_dis= 0.0f;private float z_dis= 0.0f;private float ifps;//переменная для дельтатаймprivate float time_lag = -1.0f;//таймер-флагprivate void Init()//инициализация{node.Enabled = false;//скрыть лапуLegPlace();//вызвать перестановку лапы}private void Update()//основной цикл{ifps = Game.IFps;//сохранить дельтатаймif (time_lag>=0.0f){//далее уже знакомая конструкцияtime_lag += 1.0f*ifps;if (time_lag>=0.6f) {time_lag = -1.0f;LegPlace();}}else{x_dis = MathLib.Abs(legPoint.WorldPosition.x - node.WorldPosition.x);z_dis = MathLib.Abs(legPoint.WorldPosition.z - node.WorldPosition.z);            if (x_dis + z_dis > 0.8f){time_lag = 0.0f;}}}        //функция перерасчёта положения лапы. здесь уже финальный показ лапы встроен внутрь функции. также тут происходит единичный разворот в сторону центра паука. а постоянный разворот считается вне этого скрипта, а в отдельном, наброшенном на лапу скрипте, хотя я по сути уже это вынес оттуда и можно включить в Update этого скрипта.private void LegPlace(){node.Enabled = false;vec3 targetDirection = vec3.ZERO;targetDirection = (legPoint.WorldPosition - node.WorldPosition);quat targetRot = new quat(MathLib.LookAt(vec3.ZERO, targetDirection, vec3.UP, MathLib.AXIS.Y));quat delta = MathLib.Inverse(targetRot);delta.z = 0;delta.Normalize();node.WorldPosition = legPoint.WorldPosition;        targetDirection = (spiderCenter.WorldPosition - node.WorldPosition);node.SetWorldDirection(targetDirection, vec3.UP, MathLib.AXIS.Y);node.Enabled = true;}}



Видеонарезка паучьего теста в Unigine



PlayCanvas


PlayCanvas игровой движок под webGL, использующий javascript. Недавно начал в нём разбираться. Напоминает нечто среднее между Unity и Godot, но с разработкой онлайн редактор открывается в браузере.

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

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



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



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


В playcanvas редактор кода запускается вновой вкладке браузера

Код:
var TheLegBehavior = pc.createScript('theLegBehavior');//ссылка на центр паукаTheLegBehavior.attributes.add('N_spiderCenter', { type: 'entity' });//ссылка на точку постановки этой лапкиTheLegBehavior.attributes.add('N_legPoint', { type: 'entity' });this.x_dis = 0.0;this.z_dis = 0.0;this.time_lag = -1.0;// initialize code called once per entityTheLegBehavior.prototype.initialize = function() {        };// update code called every frameTheLegBehavior.prototype.update = function(dt) {    if (this.N_spiderCenter) {        this.entity.lookAt(this.N_spiderCenter.getPosition());    }};// постапдейтTheLegBehavior.prototype.postUpdate = function(dt) {    //    if (this.N_spiderCenter) {//        this.entity.lookAt(this.N_spiderCenter.getPosition());//    }    if (time_lag>=0.0){        time_lag+=1.0*dt;        if (time_lag>=0.06){            time_lag=-1.0;            this.LegUpdate();        }            } else {                x_dis = Math.abs(this.entity.getPosition().x-this.N_legPoint.getPosition().x);        z_dis = Math.abs(this.entity.getPosition().z-this.N_legPoint.getPosition().z);                if ((x_dis+z_dis)>3.0){         time_lag=0.0;        }                    }};TheLegBehavior.prototype.LegUpdate = function() {        if (this.N_legPoint) {        this.entity.setPosition(this.N_legPoint.getPosition());    }    //    if (this.N_spiderCenter.enabled === false) {//        this.entity.enabled = false;//    }//    if (this.N_spiderCenter.enabled === true) {//        this.entity.enabled = true;//    }    };


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

Потестить получившегося на текущий момент кадавра можно здесь:
https://playcanv.as/p/rOebDLem/

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

На ПК, в отличие от смартфонов в этой демке работает прыжок (по кнопке пробел), заготовка стрейфа (Q и E) и перезагрузка уровня (на R).

Итог



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

Перевод Поиск, устранение и предупреждение утечек памяти в C .NET 8 лучших практик

12.01.2021 16:20:24 | Автор: admin

Для будущих студентов курса Разработчик C# и всех интересующихся подготовили перевод полезного материала.

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


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

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

Что из себя представляют утечки памяти в .NET

В среде со сборкой мусора термин утечка памяти представляется немного контринтуитивным. Как может произойти утечка памяти, когда есть сборщик мусора (GC garbage collector), который берет на себя задачу высвобождения памяти?

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

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

Давайте же перейдем к моему списку лучших практик:

1. Обнаружение утечек памяти с помощью окна средств диагностики

Если вы перейдете в Debug | Windows | Show Diagnostic Tools, вы увидите это окно. Как и я когда-то, вы, вероятно, уже видели это окно после установки Visual Studio, сразу же закрыли его и никогда больше о нем не вспоминали. Окно средств диагностики может быть весьма полезным. Оно может помочь вам легко обнаружить 2 проблемы: утечки памяти и GC Pressure (давление на сборщик мусора).

Когда у вас есть утечки памяти, график использования памяти процессом (Process Memory) выглядит следующим образом:

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

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

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

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

2. Обнаружение утечек памяти с помощью диспетчера задач, Process Explorer или PerfMon

Второй самый простой способ обнаружить серьезные проблемы с утечками памяти с помощью диспетчера задач (Task Manager) или Process Explorer (от SysInternals). Эти инструменты могут показать объем памяти, который использует ваш процесс. Если она постоянно увеличивается со временем, возможно, у вас утечка памяти.

PerfMon немного сложнее в использовании, но у него есть хороший график потребления памяти с течением времени. Вот график моего приложения, которое бесконечно выделяет память, не освобождая ее. Я использую счетчик Process | Private Bytes.

Обратите внимание, что этот метод заведомо ненадежен. Вы можете наблюдать увеличение потребления памяти только потому, что еще не отработал сборщик мусора. Также стоит вопрос об общей и приватной памяти, поэтому вы можете упустить утечки памяти и/или диагностировать утечки, которые не являются вашими собственными (объяснение). Наконец, вы можете принять утечку памяти за GC Pressure. В этом случае у вас нет утечек памяти, но вы создаете и удаляете объекты так быстро, что сборщик мусора не поспевает за вами.

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

3. Использование профилировщика памяти для обнаружения утечек

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

Вот несколько довольно известных профилировщиков для .NET: dotMemory, SciTech Memory Profiler и ANTS Memory Profiler. Также есть бесплатный профилировщик, если у вас стоит Visual Studio Enterprise.

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

Вы можете увидеть, сколько аллоцировано экземпляров каждого типа, сколько памяти они занимают и путь ссылки на GC Root.

GC Root это объект, который сборщик мусора не может освободить, поэтому все, на что ссылается GC Root, также не может быть освобождено. Статические и локальные объекты, текущие активные потоки, являются GC Roots. Подробнее об этом читайте в статье Сборка мусора в .NET.

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

  1. Начните с какого-либо состояния бездействия (Idle state) в вашем приложении. Это может быть Главное меню или что-то в этом роде.

  2. Сделайте снапшот с помощью профилировщика памяти, присоединившись к процессу или сохранив дамп.

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

  4. Сделайте второй снапшот.

  5. Сравните оба снапшота с помощью своего профилировщика.

  6. Изучите New-Created-Instances, возможно, это утечки памяти. Изучите path to GC Root и попытайтесь понять, почему эти объекты не были освобождены.

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

4. Используйте Make Object ID для поиска утечек памяти

В моей последней статье 5 методов, позволяющих избежать утечек памяти из-за событий в C# .NET, которые вы должны знать, я показал способ найти утечку памяти, поместив точку останова в класс Finalizer. Я покажу вам похожий метод, который еще проще в использовании и не требует изменения кода. Здесь используется функция отладчика Make Object ID и окно непосредственной отладки (Immediate Window).

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

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

  2. Наведите курсор на переменную, чтобы открыть всплывающую подсказку отладчика, затем щелкните правой кнопкой мыши и используйте Make Object ID. Вы можете ввести в окне Immediate $1, чтобы убедиться, что Object ID был создан правильно.

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

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

GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();

5. В появившемся окне непосредственной отладки введите $1. Если оно возвращает null, значит, сборщик мусора собрал ваш объект. Если нет, у вас утечка памяти.

Здесь я отлаживаю сценарий с утечкой памяти:

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

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

Важно: этот метод не работает в отладчике .NET Core 2.X (проблема). Принудительная сборка мусора в той же области, что и выделение объекта, не освобождает этот объект. Вы можете сделать это, приложив немного больше усилий, спровоцировав сборку мусора в другом методе вне области видимости.

5. Избегайте известных способов заиметь утечки памяти

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

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

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

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

  • Привязки WPF могут быть опасными. Практическое правило всегда выполнять привязку к DependencyObject или к INotifyPropertyChanged. Если вы этого не сделаете, WPF создаст сильную ссылку на ваш источник привязки (то есть ViewModel) из статической переменной, что приведет к утечке памяти. Дополнительную информацию о WPF утечках можно найти в этом полезном треде StackOverflow.

  • Захваченные члены. Может быть достаточно очевидно, что метод обработчика событий подразумевает, что на объект ссылаются, но когда переменная захвачена анонимным методом на нее также ссылаются. Вот пример такой утечки памяти:

public class MyClass{    private int _wiFiChangesCounter = 0;     public MyClass(WiFiManager wiFiManager)    {        wiFiManager.WiFiSignalChanged += (s, e) => _wiFiChangesCounter++;    }
  • Потоки, которые никогда не завершаются. Live Stack каждого из ваших потоков считается GC Root. Это означает, что до тех пор, пока поток не завершится, любые ссылки из его переменных в стеке не будут собираться сборщиком мусора. Это также включает таймеры. Если обработчик тиков вашего таймера является методом, то объект метода считается ссылочным и не собирается. Вот пример такой утечки памяти:

public class MyClass{    public MyClass(WiFiManager wiFiManager)    {        Timer timer = new Timer(HandleTick);        timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));    }     private void HandleTick(object state)    {        // do something    }

Подробнее об этом читайте в моей статье 8 способов вызвать утечки памяти в .NET.

6. Используйте шаблон Dispose для предотвращения утечек неуправляемой памяти

Ваше приложение .NET постоянно использует неуправляемые ресурсы. Сама платформа .NET в значительной степени полагается на неуправляемый код для внутренних операций, оптимизации и Win32 API. Каждый раз, когда вы используете потоки, графику или файлы, например, вы, вероятно, исполняете неуправляемый код.

Классы .NET Framework, использующие неуправляемый код, обычно реализуют IDisposable. Это связано с тем, что неуправляемые ресурсы должны быть явно освобождены, а это происходит в методе Dispose. Ваша единственная задача не забыть вызвать метод Dispose. Если возможно, используйте для этого оператор using.

public void Foo(){    using (var stream = new FileStream(@"C:\Temp\SomeFile.txt",                                       FileMode.OpenOrCreate))    {        // do stuff     }// stream.Dispose() will be called even if an exception occurs

Оператор using за кулисами преобразует код в оператор try / finally, где метод Dispose вызывается в finally.

Но даже если вы не вызовете метод Dispose, эти ресурсы будут освобождены, поскольку классы .NET используют шаблон Dispose. Это означает, что если Dispose не был вызван раньше, он вызывается из Finalizer, когда объект собирается сборщиком мусора. То есть, если у вас нет утечки памяти и действительно вызывается Finalizer.

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

public class MyClass : IDisposable{    private IntPtr _bufferPtr;    public int BUFFER_SIZE = 1024 * 1024; // 1 MB    private bool _disposed = false;     public MyClass()    {        _bufferPtr =  Marshal.AllocHGlobal(BUFFER_SIZE);    }     protected virtual void Dispose(bool disposing)    {        if (_disposed)            return;         if (disposing)        {            // Free any other managed objects here.        }         // Free any unmanaged objects here.        Marshal.FreeHGlobal(_bufferPtr);        _disposed = true;    }     public void Dispose()    {        Dispose(true);        GC.SuppressFinalize(this);    }     ~MyClass()    {        Dispose(false);    }}

Смысл этого шаблона разрешить явное удаление ресурсов. А также чтобы добавить гарантии того, что ваши ресурсы будут удалены во время сборки мусора (в Finalizer), если Dispose() не был вызван.

GC.SuppressFinalize(this) также имеет важное значение. Она гарантирует, что Finalizer не будет вызван при сборке мусора, если объект уже был удален. Объекты с Finalizer-ами освобождаются иначе и намного дороже. Finalizer добавляется к F-Reachable-Queue, которая позволяет объекту пережить дополнительную генерацию сборщика мусора. Есть и другие сложности.

7. Добавление телеметрии памяти из кода

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

Из самого приложения мы можем получить много информации. Получить текущую используемую память очень просто:

Process currentProc = Process.GetCurrentProcess();var bytesInUse = currentProc.PrivateMemorySize64;

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

PerformanceCounter ctr1 = new PerformanceCounter("Process", "Private Bytes", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr2 = new PerformanceCounter(".NET CLR Memory", "# Gen 0 Collections", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr3 = new PerformanceCounter(".NET CLR Memory", "# Gen 1 Collections", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr4 = new PerformanceCounter(".NET CLR Memory", "# Gen 2 Collections", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr5 = new PerformanceCounter(".NET CLR Memory", "Gen 0 heap size", Process.GetCurrentProcess().ProcessName);//...Debug.WriteLine("ctr1 = " + ctr1 .NextValue());Debug.WriteLine("ctr2 = " + ctr2 .NextValue());Debug.WriteLine("ctr3 = " + ctr3 .NextValue());Debug.WriteLine("ctr4 = " + ctr4 .NextValue());Debug.WriteLine("ctr5 = " + ctr5 .NextValue());

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

Однако вы можете пойти еще дальше. CLR MD (Microsoft.Diagnostics.Runtime) позволяет проверить текущую кучу и получить любую возможную информацию. Например, вы можете вывести все выделенные типы в памяти, включая количество экземпляров, пути к корням и так далее. Вы в значительной степени реализовали профилировщик памяти из кода.

Чтобы получить представление о том, чего можно достичь с помощью CLR MD, ознакомьтесь с DumpMiner Дуди Келети.

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

8. Тестирование на утечки памяти

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

[Test]void MemoryLeakTest(){  var weakRef = new WeakReference(leakyObject)  // Ryn an operation with leakyObject  GC.Collect();  GC.WaitForPendingFinalizers();  GC.Collect();  Assert.IsFalse(weakRef.IsAlive);}

Для более глубокого тестирования профилировщики памяти, такие как .NET Memory Profiler от SciTech и dotMemory, предоставляют тестовый API:

MemAssertion.NoInstances(typeof(MyLeakyClass));MemAssertion.NoNewInstances(typeof(MyLeakyClass), lastSnapshot);MemAssertion.MaxNewInstances(typeof(Bitmap), 10);

Заключение

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

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


- Узнать подробнее о курсе Разработчик C#.

- Зарегистрироваться на открытый вебинар Методы LINQ, которые сделают всё за вас.

Подробнее..

Nuke настраиваем сборку и публикацию .NET-проекта

15.01.2021 12:16:15 | Автор: admin

Введение

В настоящее время существует множество систем CI/CD. У всех есть определенные достоинства и недостатки и каждый выбирает себе наиболее подходящую под проект. Цель данной статьи - познакомить с Nuke на примере web-проекта, использующего уходящий на покой .NET-Framework с прицелом дальнейшего обновления до .NET 5. В проекте уже используется сборщик Fake, но возникла необходимость его обновления и доработки, что в итоге привело переходу на Nuke.

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

  • Web-проект, написанный на C#, в основе которого лежит .NET-Framework 4.8, Razor Pages + frontend скрипты на TypeScript, компилирующиеся в JS-файлы.

  • Сборка и публикация приложения с помощью Fake 4.

  • Хостинг на AWS (Amazon Web Services)

  • Окружения: Production, Staging, Demo

Цель

Необходимо обновить систему сборки, обеспечивая при этом расширяемость и гибкую настройку. Также нужно обеспечить настройку конфигурации в файле Web.config под заданное окружение.
Я рассматривал разные варианты систем сборки и в итоге выбор пал на Nuke, так как он довольно простой и по сути представляет собой консольное приложение расширяемое за счёт пакетов. Кроме того, Nuke довольно динамично развивается и хорошо документирован. Плюсом идёт наличие плагина к IDE (среда разработки - Rider). Я отказался перехода на Fake 5 из-за стремления обеспечить языковое единство проекта и снизить порог входа, вновь пришедшим разработчикам. Кроме того, скрипты сложнее отлаживать. Cake, Psake также отбросил из-за "скриптовости".

Подготовка

Nuke имеет dotnet tool, с помощью которого добавляется build-проект. Для начала установим его.

$ dotnet tool install Nuke.GlobalTool --global

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

В результате добавился проект _build

В каталоге boot лежат shell-скрипты для запуска сборщика.
Класс Build содержит основной код сборщика. Схема работы классическая - запускается цепочка взаимозависимых Target-ов. Вся информация выводится о процессе сборки выводится к консоль с помощью методов класса Logger. Например:

Logger.Info($"Starting build for {ApplicationForBuild} using {BuildEnvironment} environment");


Существует возможность передавать опции сборки через аргументы командной стройки. Для этого к полю класса Build применяется аттрибут [Parameter]. Ниже я приведу пример использования.

Написание кода сборщика

В моем случае сборка и публикация проекта состоит нескольких этапов

  1. Восстановление Nuget-пакетов

  2. Сборка проекта

  3. Публикация приложения

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

[Parameter("Configuration to build - Default is 'Release'")]readonly Configuration Configuration = Configuration.Release;[Parameter(Name="application")]readonly string ApplicationForBuild;[Parameter(Name="environment")]public readonly string BuildEnvironment;

Конфигуратор создается перед запуском сборки. Для этого я переопределяю метод базового класса OnBuildInitialized, который вызывается после того как, приведённые выше, параметры проинициализированы. Существует ещё несколько виртуальных методов в классе NukeBuild с префиксом On, вызываемые после определенных событий (например, старт/окончание сборки).

Код
protected override void OnBuildInitialized(){  ConfigurationProvider = new ConfigurationProvider(ApplicationForBuild, BuildEnvironment, RootDirectory);  string configFilePath = $"./appsettings.json";  if (!File.Exists(configFilePath))  {  throw new FileNotFoundException($"Configuration file {configFilePath} is not found");  }  string configFileContent = File.ReadAllText(configFilePath);  if (string.IsNullOrEmpty(configFileContent))  {  throw new ArgumentNullException($"Config file {configFilePath} content is empty");  }  /* Настойка конфигурации typescript */  ToolsConfiguration = JsonConvert.DeserializeObject<ToolsConfiguration>(configFileContent);  if (ToolsConfiguration == null || string.IsNullOrEmpty(ToolsConfiguration.TypeScriptCompilerFolder))  {  throw new ArgumentNullException($"Typescript compiler path is not defined");  }  base.OnBuildInitialized();}
Код конфигурации
public class ApplicationConfig{  public string ApplicationName { get; set; }  public string DeploymentGroup { get; set; }  /* Опции для замены в файле Web.config */  public Dictionary<string, string> WebConfigReplacingParams { get; set; }  public ApplicationPathsConfig Paths { get; set; }}
Непосредственно конфигуратор
public class ConfigurationProvider{  readonly string Name;  readonly string DeployEnvironment;  readonly AbsolutePath RootDirectory;  ApplicationConfig CurrentConfig;  public ConfigurationProvider(string name,                                string deployEnvironment,                                AbsolutePath rootDirectory)  {    RootDirectory = rootDirectory;    DeployEnvironment = deployEnvironment;    Name = name;  }  public ApplicationConfig GetConfigForApplication()  {    if (CurrentConfig != null) return CurrentConfig;    string configFilePath = $"./BuildConfigs/{Name}/{DeployEnvironment}.json";    if (!File.Exists(configFilePath))    {    throw new FileNotFoundException($"Configuration file {configFilePath} is not found");    }    string configFileContent = File.ReadAllText(configFilePath);    if (string.IsNullOrEmpty(configFileContent))    {    throw new ArgumentNullException($"Config file {configFilePath} content is empty");    }    CurrentConfig = JsonConvert.DeserializeObject<ApplicationConfig>(configFileContent);    CurrentConfig.Paths = new ApplicationPathsConfig(RootDirectory, Name, CurrentConfig.ApplicationName);    return CurrentConfig;  }}

Восстановление Nuget-пакетов

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

Код
Target Restore => _ => _    .DependsOn(Clean)    .Executes(() =>    {    NuGetTasks.NuGetRestore(config =>    {    config = config    .SetProcessToolPath(RootDirectory / ".nuget" / "NuGet.exe")    .SetConfigFile(RootDirectory / ".nuget" / "NuGet.config")    .SetProcessWorkingDirectory(RootDirectory)    .SetOutputDirectory(RootDirectory / "packages");    return config;    });    });

Сборка проекта

Код собирается в два шага. Сначала компилируется .NET-проект, далее TypeScript-файлы компилируются в JavaScript-код.

Код
Target Compile => _ => _  .DependsOn(Restore)  .Executes(() =>  {  AbsolutePath projectFile = ApplicationConfig.Paths.ProjectDirectory.GlobFiles("*.csproj").FirstOrDefault();    if (projectFile == null)    {    throw new ArgumentNullException($"Cannot found any projects in {ApplicationConfig.Paths.ProjectDirectory}");    }    MSBuild(config =>    {      config = config      .SetOutDir(ApplicationConfig.Paths.BinDirectory)      .SetConfiguration(Configuration) //указываем режим сборки: Debug/Release      .SetProperty("WebProjectOutputDir", ApplicationConfig.Paths.ApplicationOutputDirectory)      .SetProjectFile(projectFile)      .DisableRestore(); //так как мы восстановили пакеты на предыдущем этапе, то отключаем восстановление на этапе сборки      return config;    });    /* Запускаем tsc как отдельный процесс. Копируем файлы в каталог для публикации */    IProcess typeScriptProcess = ProcessTasks.StartProcess(@"node",$@"tsc -p {ApplicationConfig.Paths.ProjectDirectory}", ToolsConfiguration.TypeScriptCompilerFolder);    if (!typeScriptProcess.WaitForExit())    {    Logger.Error("Typescript build is failed");    throw new Exception("Typescript build is failed");    }  CopyDirectoryRecursively(ApplicationConfig.Paths.TypeScriptsSourceDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory, DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);  });

Публикация приложения

Проводится также в несколько этапов: подготовка артефактов и собственно публикация.

Сначала идёт трансформация конфигурации в файле Web.config под соответствующее окружение. Она заключается в замене значений определенных опций. Необходимые значения считываются из json-файла конфигурации окружения.

Все файлы архивируются и отправляются через CodeDeploy на сервер. Для работы с AWS я подключил NuGet-пакеты AWSSDK: AWSSDK.Core, AWSSDK.S3, AWSSDK.CodeDeploy. Я написал обертки над вызовами AWS CodeDeploy. Они особого интереса не предоставляют и служат скорее цели сокращения объема кода в классе Build.

Код
Target Publish => _ => _  .DependsOn(Compile).Executes(async () =>    {    PrepareApplicationForPublishing();          await PublishApplicationToAws();    });void PrepareWebConfig(Dictionary<string, string> replaceParams){  if (replaceParams?.Any() != true) return;  Logger.Info($"Setup Web.config for environment {BuildEnvironment}");  AbsolutePath webConfigPath = ApplicationConfig.Paths.ApplicationOutputDirectory / "Web.config";  if (!FileExists(webConfigPath))  {  Logger.Error($"{webConfigPath} is not found");  throw new FileNotFoundException($"{webConfigPath} is not found");  }  XmlDocument webConfig = new XmlDocument();  webConfig.Load(webConfigPath);  XmlNode settings = webConfig.SelectSingleNode("configuration/appSettings");  if (settings == null)  {  Logger.Error("Node configuration/appSettings in the config is not found");  throw new ArgumentNullException(nameof(settings),"Node configuration/appSettings in the config is not found");  }  foreach (var newParam in replaceParams)  {  XmlNode nodeForChange = settings.SelectSingleNode($"add[@key='{newParam.Key}']");  ((XmlElement) nodeForChange)?.SetAttribute("value", newParam.Value);  }  webConfig.Save(webConfigPath);}void PrepareApplicationForPublishing(){AbsolutePath specFilePath = ApplicationConfig.Paths.PublishDirectory / AppSpecFile;AbsolutePath specFileTemplate = ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile;PrepareWebConfig(ApplicationConfig.WebConfigReplacingParams);DeleteFile(ApplicationConfig.Paths.ApplicationOutputDirectory);CopyDirectoryRecursively(ApplicationConfig.Paths.ApplicationOutputDirectory, ApplicationConfig.Paths.PublishDirectory / DeployAppDirectory,DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory,DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);CopyFile(ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile, ApplicationConfig.Paths.PublishDirectory / AppSpecFile, FileExistsPolicy.Overwrite);CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.PublishDirectory / DeployScriptsDirectory,DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);Logger.Info($"Creating archive '{ApplicationConfig.Paths.ArchiveFilePath}'");CompressionTasks.CompressZip(ApplicationConfig.Paths.PublishDirectory, ApplicationConfig.Paths.ArchiveFilePath);}async Task PublishApplicationToAws(){  string s3bucketName = "";  IAwsCredentialsProvider awsCredentialsProvider = new AwsCredentialsProvider(null, null, "");  using S3FileManager fileManager = new S3FileManager(awsCredentialsProvider, RegionEndpoint.EUWest1);  using CodeDeployManager codeDeployManager = new CodeDeployManager(awsCredentialsProvider, RegionEndpoint.EUWest1);  Logger.Info($"AWS S3: upload artifacts to '{s3bucketName}'");  FileMetadata metadata = await fileManager.UploadZipFileToBucket(ApplicationConfig.Paths.ArchiveFilePath, s3bucketName);  Logger.Info(  $"AWS CodeDeploy: create deploy for '{ApplicationConfig.ApplicationName}' in group '{ApplicationConfig.DeploymentGroup}' with config '{DeploymentConfig}'");  CodeDeployResult deployResult =  await codeDeployManager.CreateDeployForRevision(ApplicationConfig.ApplicationName, metadata, ApplicationConfig.DeploymentGroup, DeploymentConfig);  StringBuilder resultBuilder = new StringBuilder(deployResult.Success ? "started successfully\n" : "not started\n");  resultBuilder = ProcessDeloymentResult(deployResult, resultBuilder);  Logger.Info($"AWS CodeDeploy: deployment has been {resultBuilder}");  DeleteFile(ApplicationConfig.Paths.ArchiveFilePath);  Directory.Delete(ApplicationConfig.Paths.ApplicationOutputDirectory, true);  string deploymentId = deployResult.DeploymentId;  DateTime startTime = DateTime.UtcNow;  /* Ожидаем когда деплой завершится и выводим сообщение */  do  {  if(DateTime.UtcNow - startTime > TimeSpan.FromMinutes(30)) break;  Thread.Sleep(3000);  deployResult = await codeDeployManager.GetDeploy(deploymentId);  Logger.Info($"Deployment proceed: {deployResult.DeploymentInfo.Status}");  }  while (deployResult.DeploymentInfo.Status == DeploymentStatus.InProgress  || deployResult?.DeploymentInfo.Status == DeploymentStatus.Created  || deployResult?.DeploymentInfo.Status == DeploymentStatus.Queued);  Logger.Info($"AWS CodeDeploy: deployment has been done");}

Заключение

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


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

Подробнее..

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

15.01.2021 18:06:16 | Автор: admin

Я уже довольно давно хотел написать пост о работе с виртуальной памятью. И когда @jimsagevid в ответ на мой твит написал о ней, я понял, что время пришло.

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

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

Неприлично большой массив

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

Создать массив фиксированного размера очень просто:

objecto *objects[MAXOBJECTS]

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

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

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

#define MAXOBJECTS 1000000000ULLobjecto **objects = virtualalloc(MAXOBJECTS * sizeof(objecto ));

Мы используем 8 ГБ адресного пространства и виртуальной памяти, но физической только столько, сколько нам действительно нужно для наших объектов. Очень простое решение, для которого потребовалась всего одна строчка кода.

Примечание Я использую здесь условный virtualalloc() в качестве системного вызова для выделения виртуальной памяти, не зависящего от ОС. На самом деле в Windows вы бы вызвали VirtualAlloc(), а в Linux mmap().

Еще одно примечание Windows разделяет выделение виртуальной памяти на отдельные вызовы MEMRESERVE и MEMCOMMIT. MEMRESERVE резервирует адресное пространство, а MEMCOMMIT выделяет его в физической памяти. Но это не значит, что физическая память реально выделяется при вызове MEMCOMMIT, физическая память не выделяется, пока вы не обратитесь к страницам. MEMCOMMIT резервирует память в файле подкачки, а если вы попытаетесь выделить больше памяти, чем доступно в файле подкачки, то MEMCOMMIT завершится ошибкой. Поэтому в Windows вы, скорее всего, не будете использовать MEMCOMMIT для всей таблицы из моего примера (потому что у файла подкачки размер ограничен). Вместо этого лучше сначала использовать MEMRESERVE для всей таблицы, а затем MEMCOMMIT только для фактически используемого диапазона.

В отличие от этого, Linux допускает overcommit (чрезмерное выделение памяти). То есть вы можете выделить больше памяти, чем доступно в файле подкачки. По этой причине Linux не нуждается в отдельных операциях резервирования (reserve) и подтверждения (commit), что упрощает использование виртуальной памяти.

Есть ли проблема в резервировании виртуальной памяти для массива в 8 ГБ? Здесь два ограничения. Первое это адресное пространство. В 64-битном приложении адресное пространство составляет 264. Это очень большое число, в котором можно разместить миллиарды массивов гигабайтного размера. Второе ограничение касается виртуальной памяти. Операционная система обычно не позволяет выделять все возможное адресное пространство. Например, в 64-битной Windows мы можем выделить только 256 ТБ виртуальной памяти. Тем не менее в этом объеме можно разместить 32 000 массивов по 8 ГБ каждый, так что пока мы не совсем сходим с ума, все будет в порядке.

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

Вспомните об олдскульном способе писать игры на Си с использованием статических массивов:

uint32t numtanks;tankt tanks[MAXTANKS];uint32C11Cbullets;bulletC12CBULLETS];

Если вы пишете подобный код, то будьте уверены, что найдутся те, кто его будет критиковать, так как здесь есть ограничения на количество объектов. Выглядит забавно, но можно вместо использования std::vector просто избавиться от MAXC13C и выделить 1 ГБ виртуальной памяти для каждого из массивов:

#define GB 1000000000uint32_t num_tanks;tank_t *tanks = virtual_alloc(GB);uint32_t num_bullets;bullet_t *bullets = virtual_alloc(GB);

Уникальные ID в рамках всего приложения

Многим игровым движкам требуются уникальные идентификаторы (ID) для идентификации объектов. Часто код выглядит примерно так:

uint64_t allocate_id(system_t *sys){    return sys->next_free_id++;} 

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

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

system_id_t *allocate_id(system_t *sys){    if (!sys->id_block || sys->id_block_used == PAGE_SIZE) {        sys->id_block = virtual_alloc(PAGE_SIZE);        sys->id_block_used = 0;    }    return (system_id_t *)(sys->id_block + sys->id_block_used++);}

Обратите внимание, что, используя для идентификатора указатель на непрозрачную структуру (opaque struct), мы также получаем некоторую безопасность типа, которой у нас не было с uint64_t.

Обнаружение перезаписи памяти

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

Чтобы понять как, давайте сначала обратим внимание на то, что термин "случайная перезапись памяти" на самом деле неправильный. Адресное пространство в основном пустое. При 64-битном адресном пространстве и размере приложения, скажем, 2 ГБ, адресное пространство пусто на 99,999999988%. Это означает, что если перезапись памяти действительно случайная, то, скорее всего, она попала бы в это пустое пространство, что привело бы к ошибке/нарушению доступа к странице. А это бы привело к падению приложения в момент некорректной записи, а не при невинном чтении, что бы значительно упростило поиск и исправление ошибки.

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

  • Запись в память, которая была освобождена.

  • Запись за пределами выделенной памяти для объекта.

В обоих случаях весьма вероятно, что запись действительно попадет в какой-то другой объект, а не в пустое место. В первом случае память, скорее всего, предназначалась для чего-то другого. А во втором запись, вероятно, попадет в соседний объект или заголовок блока распределения (allocation block header).

Мы можем сделать это более случайным, заменив стандартный системный аллокатор на end-of-page аллокатор (аллокатор в конце страницы). Такой аллокатор размещает каждый объект в виртуальной памяти в собственном множестве страниц и выравнивает объект так, чтобы он располагался в конце блока памяти.

Размещение блока в конце блока страниц.Размещение блока в конце блока страниц.

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

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

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

Написание end-of-page аллокатора совсем несложно. Вот как может выглядеть malloc:

void *eop_malloc(uint64_t size){    uint64_t pages = (size + PAGE_SIZE - 1) / PAGE_SIZE;    char *base = virtual_alloc(pages * PAGE_SIZE);    uint64_t offset = pages * PAGE_SIZE - size;    return base + offset;}

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

Обе эти проблемы можно исправить. Для решения первой проблемы мы можем оставить страницы зарезервированными (reserved), но не подтвержденными (commited). Таким образом, физическая память освобождается и мы получим ошибки страниц, но адреса остаются зарезервированными и не смогут использоваться другими объектами. Для второй проблемы можно зарезервировать дополнительную страницу после наших страниц, но не подтверждать ее. Тогда никакой другой объект не сможет претендовать на эти адреса и запись в них все равно приведет к ошибке доступа (access violation). (Примечание: это работает только в Windows, где reserve и commit являются отдельными операциями.)

Однако на практике мне никогда не приходилось принимать эти дополнительные меры предосторожности. Для меня всегда было достаточно обычного end-of-page аллокатора.

Непрерывное выделение памяти

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

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

Фрагментация памяти.Фрагментация памяти.

Проблема здесь в том, что когда пользователь запрашивает большой объем памяти, то может оказаться, что ни в одной из "дыр" не будет достаточно места. Это означает, что мы должны выделить память из свободного блока в конце, увеличивая использование памяти. Другими словами, появляется много впустую потраченной памяти, которую мы не можем реально использовать. Раньше память была очень дорогим ресурсом. Сегодня это уже не совсем так, но мы по-прежнему не хотим тратить ее впустую.

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

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

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

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

Обратите внимание, что в адресном пространстве у нас все еще остается фрагментация. Т.е. мы не можем выделить большой непрерывный блок памяти в адресном пространстве, где находятся красные страницы. Фрагментация адресного пространства, на самом деле, нас особо не беспокоит, потому что, как было сказано выше, обычно адресное пространство на 99.999999988% пустое. Так что у нас нет проблем с поиском смежных страниц в адресном пространстве. (Но в 32-битных системах это совсем другая история.)

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

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

Размер динамически увеличивающегося буфера можно сделать соответствующим размеру страницы. Это простой, но интересный метод, о котором я очень редко слышу. Допустим, у вас есть массив объектов размером 300 байт. Обычно при необходимости размещения большего количества записей вы увеличиваете размер массива геометрически, например, удваивая. Таким образом, получается увеличение количества элементов с 16 до 32 до 64 и до 128 элементов. Геометрический рост важен, чтобы было меньше затрат на частое увеличение массива.

Однако 16 * 300 = 4800. При выделении виртуальной памяти вам придется округлить это до 8 КБ, тратя впустую почти целую страницу. Но это можно легко исправить. Вместо того чтобы концентрироваться на количестве элементов, мы просто увеличиваем размер буфера кратно размеру страницы: 4 КБ, 8 КБ, 16 КБ, 32 КБ, , а затем помещаем туда столько элементов, сколько поместится в него (13, 27, 54, 109,). Это по-прежнему геометрический рост, но теперь внутренняя фрагментация составляет в среднем всего 150 байт вместо 2 КБ.

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

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

Возможно, появятся некоторые дополнительные затраты памяти ОС для отслеживания большого количества отдельных выделений памяти. Также время тратится на системные вызовы выделения и освобождения страниц. Может быть, в этом и есть причина. Или просто дело в том, что аллокаторы написаны для работы в различных средах и в 32-битных системах и в системах с большими страницами поэтому они не могут использовать преимуществ 64-битных систем и 4KБ страниц.

Кольцевой буфер

Об этом трюке я узнал из блога Фабиана Гизена (Fabian Giesen). Но, кажется, что это довольно давняя идея.

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

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

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

enum {BUFFER_SIZE = 8*1024};struct ring_buffer_t {    uint8_t data[BUFFER_SIZE];    uint64_t read;    uint64_t written;};

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

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

void write(ring_buffer_t *rb, uint8_t *p, uint64_t n){    uint64_t offset = rb->written % BUFFER_SIZE;    uint64_t space = BUFFER_SIZE - offset;    uint64_t first_write = n < space ? n : space;    memcpy(rb->data + offset, p, first_write);    memcpy(rb->data, p + first_write, n - first_write);    rb->written += n;}

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

Как здесь может помочь виртуальная память? Мы можем использовать технику "огромного массива" и просто зарезервировать большой массив вместо кольцевого буфера и фиксировать (commit) страницы по мере продвижения указателя на запись, а по мере продвижения читателя отменять фиксацию (decommit). При этом нам даже не нужно будет задавать фиксированный размер массива он просто может использовать столько памяти, сколько потребуется. Довольно красивое решение. Но учтите, что вам может понадобиться очень большой массив. Для буферизации сетевого потока 1 Гбит/с с аптаймом в течение года вам потребуется зарезервировать 4 ПБ (петабайта) памяти. К сожалению, как мы видели выше, 64-разрядная Windows ограничивает объем виртуальной памяти 256 ТБ. Кроме того, вызовы commit и decommit не бесплатны.

Но мы можем поступить и другим способом, воспользовавшись тем, что в одну физическую страницу может отображаться несколько виртуальных. Обычно это используется для совместного использования памяти разными процессами. Но можно использовать и в рамках одного процесса. Чтобы использовать этот подход для кольцевого буфера, создадим несколько страниц сразу после кольцевого буфера (ring buffer), указывающих на одну и ту же физическую память.

Кольцевой буфер (ring buffer) с маппингом страниц.Кольцевой буфер (ring buffer) с маппингом страниц.

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

void write(ring_buffer_t *rb, uint8_t *p, uint64_t n){    memcpy(rb->data + (rb->written % BUFFER_SIZE), p, n);    rb->written += n;}uint8_t *read(ring_buffer_t *rb, uint64_t n){    uint8_t *p = rb->data + (rb->read % BUFFER_SIZE);    rb->read += n;    return p;}

Это намного лучше, но мы по-прежнему используем тот же объем физической памяти.

Обратите внимание, что настройка такой схемы размещения в памяти может быть немного запутанной. В Windows нужно создать отображение файлов в виртуальную память с помощью CreateFileMapping(). Да, даже если никакие файлы на диске не задействованы, все равно нужно использовать "отображение файла", потому что совместно виртуальная память используется именно так. Но поскольку файл на диске нам не нужен, то для дескриптора файла используется INVALID_HANDLE_VALUE, создающий отображение в файл подкачки. Затем мы используем MapViewOfFileEx(), чтобы настроить отображение на две области памяти. К сожалению, нет никакого способа гарантировать, что переданные области памяти будут доступны. Мы можем зарезервировать их, а затем освободить непосредственно перед вызовом MapViewOfFileEx(), но все равно остается промежуток времени, когда, если нам очень не повезет, кто-то другой может прийти и выделить что-то в этом пространстве адресов. Возможно, нам придется повторить попытку отображения несколько раз, прежде чем оно будет успешным. Но после этого мы можем использовать буфер, не беспокоясь ни о чем.

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

Дополнения про Linux

Выше я написал, что "Linux допускает overcommit (чрезмерное выделение памяти)". Но, как я недавно обнаружил, на самом деле это не совсем так, или, по крайней мере, это очень сильное упрощение.

По умолчанию Linux допускает "некоторый" избыточный commit (определяется эвристически). Потому что если вы отключите overcommit, то будете тратить много памяти зря, поскольку процессы выделяют фактически не используемую память. С другой стороны, если вы разрешите слишком большой overcommit, вы можете столкнуться с ситуацией, когда процесс успешно выделил память, но при попытке получить к ней доступ, он не сможет этого сделать, потому что у системы не будет физической памяти. Для предотвращения таких ситуаций приходит OOM killer и убивает некоторые процессы.

Вы можете настроить систему, чтобы разрешить неограниченный overcommit (vm.overcommit_memory = 1) или указать ограничение (vm.overcommit_memory = 2). См. https://www.kernel.org/doc/Documentation/vm/overcommit-accounting

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

Это можно реализовать так же, как в Windows: разделить операции резервирования (reserve) и подтверждения (commit). Резервирование памяти не зависит от параметра overcommit_memory.

По документации mmap() это не совсем очевидно, но виртуальную память в Linux можно зарезервировать через вызов mmap() с PROT_NONE. После этого commit зарезервированной памяти можно сделать, используя системный вызов mprotect().

Примечание Использование MAP_NORESERVE вместо PROT_NONE не работает, когда overcommit_memory = 2, поскольку в этом случае флаг MAP_NORESERVE игнорируется. См. https://lwn.net/Articles/627557/


Перевод статьи подготовлен специально для будущих студентов курса "Программист С".

Также приглашаем всех желающих зарегистрироваться на открытый онлайн-вебинар: "ООП на C: пишем видеоплеер".

Подробнее..

Создание Dockers в Corel Draw

15.01.2021 18:06:16 | Автор: admin

В Corel Draw начиная с 17 версии появилась удобная возможность создавать дополнения не только на VBA, но и на C# VSTA. Так давайте воспользуемся этим и приблизим мечту о кнопке "Сделать красиво".

Дисклеймер

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

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

Что понадобится, чтобы магия заработала:

Corel Draw

Visual Studio

Visual Studio Tools for Applications (если хотите писать простые макросы, не обязательно)

Начальные знания C#

Начальные знания WPF

Для удобства написания воспользуемся дополнениями для Visual Studio от bonus360:

CorelDraw Addons Templates

CorelDraw Addons Packer


Запустим студию от имени администратора, чтобы при компилировании копировать файлы в системные папки. Создаем новый проект, выбрав в качестве шаблона CorelDRAW Docker Addon. Присваиваем имя, например MagicUtilites.

В появившемся окошке присваиваем имя докеру, например также MagicUtilites, и выбираем те версии CorelDraw, под которые будем разрабатывать. Жмем Done и наблюдаем как рутина выполняется сама.

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

На этом этапе можно нажать F5 и найти в меню CorelDraw - Window - Dockers ваш докер. Сейчас он пустой и ничего не делает, но мы это исправим.


Открываем файл DockerUI.xaml в конструкторе XAML и добавляем кнопку на докер.

<Grid Margin="0,0,0,0"><StackPanel><Button Content="Text Convert to Curves" Height="25" Margin="4" Click="Button_Click"/></StackPanel></Grid>

В этот раз сделаем не следуя шаблону MVVM (именно им надо пользоваться разрабатывая на платформе WPF), а разместим код в обработчике события нажатия на кнопку. Но больше такого не повторится, обещаю.


Открываем файл DockerUI.xaml.cs

private corel.Application corelApp;

Класс corel.Application представляет приложение, в котором выполняется код докера. Значение полю присваивается в конструкторе.

Отредактируем метод Button_Click.

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

private void Button_Click(object sender, RoutedEventArgs e){  if (corelApp.ActiveDocument == null)return;}

Свойство ActiveDocument типа corel.Application возвращает ссылку на активный документ.

Добавим быстродействия программе.

private void Button_Click(object sender, RoutedEventArgs e){if (corelApp.ActiveDocument == null)return;corelApp.BeginDraw();corelApp.EndDraw();}

Остальной код должен быть между этими строками. Метод расширения BeginDraw() отключает перерисовку экрана, вызов событий и выделение corel объектов во время выполнения. Метод расширения EndDraw() восстанавливает настройки.

В активном документе выполним перебор всех страниц. На каждой странице выполним перебор всех corel объектов для поиска текстовых объектов.

private void Button_Click(object sender, RoutedEventArgs e){  if (corelApp.ActiveDocument == null)    return;  corelApp.BeginDraw();  foreach (corel.Page page in corelApp.ActiveDocument.Pages)  {    foreach (corel.Shape shape in page.Shapes.All())    {      if (shape.Type == corel.cdrShapeType.cdrTextShape)        shape.ConvertToCurves();    }  }  corelApp.EndDraw();}

При нахождении текстовых объектов, вызывается метод ConvertToCurves(), который переводит этот corel объект в кривые.

Запустим выполнение. Для проверки кнопки Text Convert to Curves нужен открытый документ и текст в нем. Реализованный код работает с любыми текстовыми corel объектами, но если этот объект находится в группе объектов или в PowerClip преобразования в кривые не произойдет.

Для решения этой проблемы, разберем как Corel Draw представляет объекты в коде.

Класс corel.Shape содержит свойства и методы для взаимодействия с corel объектами. Свойство Type возвращает именованную константу которая определяет тип corel объекта. Если corel.Shape представляет группу corel объектов, свойство Type вернёт константу cdrGroupShape. Тогда обратившись к свойству Shapes, получим коллекцию corel объектов из группы.

Также с другими типами corel объектов. Если свойство Type возвращает cdrBitmapShape, то свойство Bitmap возвращает ссылку на картинку. Если свойство Type возвращает cdrGuidelineShape, свойство Guide возвращает ссылку на направляющую.

Узнать что corel объект PowerClip так не получится. Чтобы проверить является ли corel объект PowerClip-ом, проверьте свойство PowerClip на null.

Вернемся к коду.

Выделим перебор corel объектов в два отдельных метода.

private void MakeToAllPages(){  if (corelApp.ActiveDocument == null)    return;  corelApp.BeginDraw();  foreach (corel.Page page in corelApp.ActiveDocument.Pages)  {    MakeToShapeRange(page.Shapes.All());  }  corelApp.EndDraw();}

Метод MakeToAllPages перебирает все страницы документа.

private void MakeToShapeRange(corel.ShapeRange sr){  foreach (corel.Shape shape in sr)  {    if (shape.Type == corel.cdrShapeType.cdrGroupShape)      MakeToShapeRange(shape.Shapes.All());    if (shape.PowerClip != null)      MakeToShapeRange(shape.PowerClip.Shapes.All());    if (shape.Type == corel.cdrShapeType.cdrTextShape)      shape.ConvertToCurves();  }}

Метод MakeToShapeRange рекурсивно перебирает переданную коллекцию corel объектов.

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

private void Button_Click(object sender, RoutedEventArgs e){  MakeToAllPages();}

В методе Button_Click остается только вызов метода MakeToAllPages.

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


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

<StackPanel>  <Button Content="Text convert to curves" Height="25" Margin="4" Click="ConvertToCurves"/>  <Separator Margin="4"/>  <Button Content="Uniform fill to CMYK" Height="25" Margin="4" Click="UniformFillToCMYK"/>  <Button Content="Outline fill to CMYK" Height="25" Margin="4" Click="OutlineFillToCMYK"/>  <Button Content="Fountain fill to CMYK" Height="25" Margin="4" Click="FountainFillToCMYK"/>  <Separator Margin="4"/>  <Button Content="Bitmap to CMYK" Height="25" Margin="4" Click="BitmapToCMYK"/>  <Button Content="Resample Bitmap to 300 dpi" Height="25" Margin="4" Click="ResampleBitmap"/></StackPanel>

Добавим обработчики нажатия для этих кнопок.

private void ConvertToCurves(object sender, RoutedEventArgs e){}private void BitmapToCMYK(object sender, RoutedEventArgs e){}private void UniformFillToCMYK(object sender, RoutedEventArgs e){}private void OutlineFillToCMYK(object sender, RoutedEventArgs e){}private void FountainFillToCMYK(object sender, RoutedEventArgs e){}private void ResampleBitmap(object sender, RoutedEventArgs e){}

Сейчас вся работа с corel объектом происходит в методе MakeToShapeRange. Но теперь нам надо находить не только текст, но и картинки и определять есть ли заливка или обводка у corel объекта. Чтобы много раз не копировать код метода MakeToShapeRange, воспользуемся делегатами.

Изменим сигнатуру метода MakeToAllPages() на MakeToAllPages(Action<corel.Shape> action). Так как вся работа происходит в методе MakeToShapeRange изменим и его сигнатуру. А в методе MakeToAllPages изменим его вызов.

private void MakeToAllPages(Action<corel.Shape> action){  if (corelApp.ActiveDocument == null)    return;  corelApp.BeginDraw();  foreach (corel.Page page in corelApp.ActiveDocument.Pages)  {    MakeToShapeRange(page.Shapes.All(), action);  }  corelApp.EndDraw();}private void MakeToShapeRange(corel.ShapeRange sr, Action<corel.Shape> action){  foreach (corel.Shape shape in sr)  {    if (shape.Type == corel.cdrShapeType.cdrGroupShape)      MakeToShapeRange(shape.Shapes.All(), action);    if (shape.PowerClip != null)      MakeToShapeRange(shape.PowerClip.Shapes.All(), action);    action(shape);  }}

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

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

Далее уточнения будут в комментариях кода.

private void ConvertToCurves(object sender, RoutedEventArgs e){  MakeToAllPages((s) =>  {    if (s.Type == corel.cdrShapeType.cdrTextShape) // если текст      s.ConvertToCurves(); // перевести в кривые  });}private void BitmapToCMYK(object sender, RoutedEventArgs e){  MakeToAllPages((s) =>  {    if (s.Type == corel.cdrShapeType.cdrBitmapShape) // если картинка      if (s.Bitmap.Mode != corel.cdrImageType.cdrCMYKColorImage) // цветовая модель не CMYK        s.Bitmap.ConvertTo(corel.cdrImageType.cdrCMYKColorImage); // конвертировать в CMYK  });}private void UniformFillToCMYK(object sender, RoutedEventArgs e){  MakeToAllPages((s) =>  {    if (s.CanHaveFill) // у объекта может быть заливка      if (s.Fill.Type == corel.cdrFillType.cdrUniformFill) // заливка сплошная        if (s.Fill.UniformColor.Type != corel.cdrColorType.cdrColorCMYK) // цветовая модель не CMYK          s.Fill.UniformColor.ConvertToCMYK(); // конвертировать в CMYK  });}private void OutlineFillToCMYK(object sender, RoutedEventArgs e){  MakeToAllPages((s) =>  {    if (s.CanHaveOutline) // у объекта может быть обводка      if (s.Outline.Type == corel.cdrOutlineType.cdrOutline) // обводка есть        if (s.Outline.Color.Type != corel.cdrColorType.cdrColorCMYK) // цветовая модель не CMYK          s.Outline.Color.ConvertToCMYK(); // конвертировать в CMYK  });}private void FountainFillToCMYK(object sender, RoutedEventArgs e){  MakeToAllPages((s) =>  {    if (s.CanHaveFill) // у объекта может быть заливка      if (s.Fill.Type == corel.cdrFillType.cdrFountainFill) // заливка градиент      {        foreach (corel.FountainColor c in s.Fill.Fountain.Colors) // перебор всех ключей в градиенте        {          if (c.Color.Type != corel.cdrColorType.cdrColorCMYK) // цветовая модель не CMYK            c.Color.ConvertToCMYK(); // конвертировать в CMYK        }      }  });}private void ResampleBitmap(object sender, RoutedEventArgs e){  MakeToAllPages((s) =>  {    int resolution = 300;    if (s.Type == corel.cdrShapeType.cdrBitmapShape) // если картинка      if (s.Bitmap.ResolutionX != resolution || s.Bitmap.ResolutionY != resolution) // разрешение не совпадает с заданным        s.Bitmap.Resample(0, 0, true, resolution, resolution); // изменяем разрешение на заданное  });}

Запустим выполнение.


На этом сеанс практической магии на сегодня закончен.

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


Пользуясь случаем, рекомендую очень классные и бесплатные интерактивные онлайн-курсы по программированию от фирмы Контур. Так же рекомендую канал Павла Шмачилина по WPF, это лучшее что я видел на YouTube по этой теме.

Подробнее..

Duck typing и C

05.01.2021 10:07:35 | Автор: admin

Доброго времени суток. В последнее время я много эксперементрировал с .Net 5 и его Source Generator-ами. И мне внезапно пришла идея как можно использовать Source Generator-ы для реализации "duck typing"-а в C#. Я не мог просто оставить эту идею. В итоге вышла, я бы сказал, чисто акамическая штука(никто не будет использовать это на проде, я надеюсь), но результат получился довольно интересен. Всем кому интересно прошу под кат!


Спойлер

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


Как этим пользоваться


Представим что у нас есть следующий пример:


public interface ICalculator{  float Calculate(float a, float b);}public class AddCalculator{  float Calculate(float a, float b);}

Важно отметить что AddCalculator никаким образом не реализует ICalculator.
Они лишь имеют идентичные сигнатуры. Если мы попытаемся использовать их следующим образом, то у нас ничего не получится:


var addCalculator = new AddCalculator();var result = Do(addCalculator, 10, 20);float Do(ICalculator calculator, float a, float b){  return calculator.Calculate(a, b);}

Компилятор С# скажет следующее:


Argument type 'AddCalculator' is not assignable to parameter type 'ICalculator'


И он будет прав. Но поскольку сигнатура AddCalculator полностью совпадает с ICalculator и нам очень хочеться это сделать, то решением может быть duck typing который не работает в С#. Иммено тут и пригодится nuget пакет DuckInterface. Все что нужно будет сделать, это установить его и немножечко подправить наши сигнатуры. Начнем с интерфейса добавив к нему аттрибут Duckable:


[Duckable]public interface ICalculator{  float Calculate(float a, float b);}

Дальше обновим метод Do. Нужно заменить ICalculator на DICalculator. DICalculator это класс который был сгенерен нашим DuckInterface.
Сигнатура DICalculator полностью совпадает с ICalculator и может содержать неявные преобразования для нужных типов. Все эти неявные преобразования будут генериться в тот момент когда мы пишем код в нашей IDE. Генерится они будуть в зависимости от того как мы используем наш DICalculator.


Итоговый пример:


var addCalculator = new AddCalculator();var result = Do(addCalculator, 10, 20);float Do(DICalculator calculator, float a, float b){  return calculator.Calculate(a, b);}

И это всё. Ошибок компиляции больше нет и все работает как часы.


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


Здесь используются два независимых генератора. Первый ищет аттрибут Duckable и генерит "базовый" класс для интерфейса. Например, для ICalculator он будет иметь следующий вид:


public partial class DICalculator : ICalculator {  [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]   private readonly Func<float, float, float> _Calculate;          [System.Diagnostics.DebuggerStepThrough]  public float Calculate(float a, float b)  {      return _Calculate(a, b);  }}

Второй генератор ищет вызовы методов и присваивания чтобы понять как duckable интерфейс используется. Расмотрим следующий пример:


var result = Do(addCalculator, 10, 20);

Анализатор увидит что метод Do имеет первый аргумент типа DICalculator, а потом проверит переменную addCalculator. Если её тип имеет все необходимые поля и методы, то генератор расширит DICalculator следующим образом:


public partial class DICalculator{  private DICalculator(global::AddCalculator value)   {       _Calculate = value.Calculate;  }  public static implicit operator DICalculator(global::AddCalculator value)  {      return new DICalculator(value);  }}

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


Пример:


[Duckable]public interface ICalculator{    float Zero { get; }    float Value { get; set; }    float Calculate(float a, float b);}// ....public partial class DICalculator : ICalculator {    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]     private readonly Func<float> _ZeroGetter;    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]     private readonly Func<float> _ValueGetter;    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]     private readonly Action<float> _ValueSetter;    [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]     private readonly Func<float, float, float> _Calculate;            public float Zero    {         [System.Diagnostics.DebuggerStepThrough] get { return _ZeroGetter(); }    }    public float Value    {         [System.Diagnostics.DebuggerStepThrough] get { return _ValueGetter(); }         [System.Diagnostics.DebuggerStepThrough] set { _ValueSetter(value); }    }    [System.Diagnostics.DebuggerStepThrough]    public float Calculate(float a, float b)    {        return _Calculate(a, b);    }}

Что не работает


На этом хорошие новости закончились. Всетаки реализовать прямо вездесущий duck typing не получится. Поскольку мы скованы самим компилятором. А именно будут проблемы с дженериками и ref struct-урами. В теории часть проблем с дженериками можно починить, но не все. Например, было бы прикольно чтобы мы могли использовать наши интерфейсы вместе с where как-то вот так:


float Do<TCalcualtor>(TCalcualtor calculator, float a, float b)    where TCalcualtor: DICalculator{  return calculator.Calculate(a, b);}

В таком случаи мы могли бы получили прямо zero cost duct typing(и щепотку метапрограмирования, если копнуть глубже), поскольку, мы легко можем заменить partial class на partial struct в реализации нашего duck интерфейса. В результате, было бы сгенерено множестао Do методов для каждого уникального TCalcualtorкак это происходит со структурами. Но увы, компилятор нам скажет, что нечего такого он не умеет.
На этом все. Спасибо за внимание!


Nuget тут: https://www.nuget.org/packages/DuckInterface/
Github тут: https://github.com/byme8/DuckInterface

Подробнее..

Оркестратор бесконечных задач

11.01.2021 20:23:30 | Автор: admin

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

В большинстве случаев всяenterpriseразработка сводится к выполнению одних и тех же требований: создается заявка, в зависимости от типа заявки у нее есть какой-то жизненный цикл, по завершению жизни заявки мы получаем (или не получаем) желаемое. Под заявкой мы можем подразумевать все что угодно, начиная с покупки в интернет-магазине товара, денежного перевода или расчета траектории баллистической ракеты. У каждой заявки есть свой жизненный путь и что важно отметить -время жизни, и чем меньше это время, тем лучше. Иными словами, чем быстрее мой банковский перевод осуществится, тем лучше. Требования тоже схожи, побольшеRPCoperationspersecond, поменьшеLatency, система должна быть отказоустойчивой, масштабируемой и должна быть готовавчера. Есть миллион инструментов, сотни баз данных, различные подходы и паттерны. И все уже давно написано, нам остается лишь правильно использовать готовые технологии в наших проектах.

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


Давайте сначала поймем, что значит бесконечная задача и где в природе такое вообще может встречаться. Бесконечная задача это некий процесс (Job), который выполняет работу до тех пор, пока ему не скажут прекратить это. Аналогию можно провести с бесконечными циклами. В природе же подобное встречается, когда нам нужны наблюдатели, которые следят и реагируют на определённые события. Например: нам необходимо следить за изменениями цен на бирже, повышением или понижением цены актива. Представим нам нужно следить за всеми валютами, всеми активами, на разных биржах, тогда количество наблюдателей может превышать десятки тысяч единиц. Что же из себя может представлять наблюдатель- это может быть отдельноеWebSocketсоединение, которое должно быть постоянноconnected. Этот наблюдатель, может получать данные,денормализовывать, производить расчеты, сохранять и много чего еще. Для удобства, наблюдателем я буду называть неObserverиз известного паттерна, а модуль, который постоянно в работе и бесконечно долго выполняет полезную работу.

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

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

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

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

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

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

Статья описана в 3 актах. Все листинги с кодом на С#, но в процессе написания старался уделять меньше внимания примерам с кодом и больше самой идеи. Поэтому листинги должны быть понятны даже тем людям, которые вообще не писали на C# и не знакомы с .Net.

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

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

  3. Очередь, которая думает, что она планировщик. Финальная часть статьи, где мы реализуем системуоркестрациизадач через очереди сообщений. Я использовалRabbitMq, и какFramework-MassTransit, поэтому все примеры будут тесно связаны с данными инструментами. Но принцип будет оставаться тот же.

Всё естьTask

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

Рассмотрим на простом примере. Возьмём метод, которыйпишет HelloWord в консольотправляет письмо:

public async Task SendEmailAsync(Email email, CancellationToken token) {    // отправляем письмо }

Чтобы отправить письмо, не дожидаясь получения результата, нам достаточно просто забыть поставитьawaitперед вызовомSendEmailAsync.

foreach (var email in emails {    if(token.IsCancellationRequested)         break;     _emailSender.SendEmailAsync(email, token); //нет await } 

Минусов у данного подхода много:

  • Мы никак не гарантируем выполнение отправки письма.

  • FireAndForgetи как следствие о возникновенииExceptionмы не узнаем.

  • Так же не узнаем и о выполнении.

  • Многие считают, что это грех большой, вообщеантипаттерни я с ними согласен.

Более детально о том почему желательно рано или поздноawait-ить таску, можно почитатьпро async/await антипаттерны.

Наша задача во многом похожа на отправкуemail, только внутри у нас будет подобие бесконечного цикла и метод закончит работу естественным путем только тогда, когда будет вызванCancellationToken. Мы можем, конечно, написать свои костыли, которые позволят нам отслеживать состояние задачи и уведомлять, когда она завершилась. У нее будутRetryPolicyи много чего ещё, но зачем?! Когда есть уже готовые планировщики задач, которые заточены под данные требования.

Schedulers

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

Тутесть неплохое сравнение планировщиков. Больше всего нас интересует возможность иметь неограниченное количествосерверов, (тут может быть недопонимание, сервер это не физическая машина, где выполняется наше приложение, этоinstanceпланировщика)где будут исполнятся наши задачи/Tasks. Лично я отдал предпочтениеHangfire, по большей части из-за хорошо описанной документации и встроенного UI, который позволяет не только отображать метрики по задачам, но и вручную запускать их. Всё это весьма приятные бонусы.

А теперь посмотрим на то, как отправить наше письмо с использованиемHangfire. В этом нам поможет статический методBackgroundJob.Enqueue(Expression<Action>methodCall).

var jobIds = new List<string>(); foreach (var email in emails) {    if(token.IsCancellationRequested)       break;    jobIds.Add(BackgroundJob.Enqueue(       async () => await _emailSender.SendEmailAsync(email, token))); } 

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

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

_observer.DoWork(observerArg,newCancellationToken())

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

var client = new BackgroundJobClient(JobStorage.Current);//задаем имя очереди, где будет хоститься задача.var state = new EnqueuedState(unique-queue-name); client.Create(() =>_observer.DoWork(observerArg,newCancellationToken()), state);

И конечно же мы должны иметь сервис, который займется обработкой данной очереди. В настройках которого будет указано имя той самой очереди-unique-queue-name.

// Настраиваем instance hangfire сервера. _server = new BackgroundJobServer(new BackgroundJobServerOptions() {       WorkerCount = 10,     Queues = new[] { unique-queue-name },     ServerName = _serverOptions.ServerName }); 

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

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

_monitoringApi = JobStorage.Current.GetMonitoringApi(); 

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

Observer-service - сервис, который может выполнять одновременно несколько задач, количество задается через конфиг или рассчитывается с учетом количества ядер и мощности сервера (ВHangFilreэтоWorkerCount).

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

Schedulercommondbпсевдо-очередьи хранилище всей информации по задачам,Hangfireподдерживает какMsSql, такPostgreSqlи дажеRedis.

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

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

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

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

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

3)Добавление задачи в очередь возвращает числовой идентификатор и идентифицировать задачу можно только по нему. Нельзя задать свойcustom-id, например поиск по названию. Поэтому получив идентификатор задачи нужно сохранить связку идентификатор-название в собственное хранилище.

4)В случае ошибки во время исполнения задачи, она автоматически будет перемещена в default очередь. Крайне неприятный момент, о котором узнал уже на этапе тестирования, так как в документации о таком не рассказали. Решается черезjob-filtersили черезатрибуты. Второй вариант делает код более связанным и не подходит, так как значение атрибута не может задаваться динамически.

5)В случае если сервер откажет, задачи, которые на нем исполнялись, не будут перераспределены между работающими. Можно, конечно, реализовать данную логику в менеджере и сделать его ответственным за это, но хотелось бы чтобыframeworkумел это из коробки.

6)Отсутствие транзакционности, ВедьHangfireуниверсален как дляMsSql, так и дляRedis, а в нем транзакции не предусмотрены.

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

Очередь, которая думает, что она планировщик

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

Как же сделать из очереди сообщений планировщик задач? Хотя тут корректней был бы термин оркестратор.Весь вышеописанный функционал решается использованием только одной настройки-PrefetchCountи особенностью обработки сообщений.

  • Когда сообщение попадает в очередь оно имеет состояниеReady.

  • КогдаConumerобрабатывает сообщение, оно переходит в состояниеUnacked. И другойConsumerможет взять следующие сообщение из очереди.

  • Если в момент обработки сообщения происходит ошибка, оно помещается в _Errorочередь.

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

И теперь главное -PrefetchCountэто количество одновременно обрабатываемых сообщений в очереди, а если сообщение никогда не будет дочитано (бесконечно обрабатывается), то его можно воспринимать какWorkerCount, прям как уHangfire.

Разберем на пальцах:

На данной схеме у нас есть триObserver-services, каждый из них слушает очередь в ожидании поступления сообщения.PrefetchCountу каждого стоит 1. Это значит, что за раз каждый сервис будет обрабатывать одно сообщение. А так как мы знаем, что сообщение это запуск бесконечной задачи, то оно никогда не прочитается и всегда будет в состоянииUnacked.

Дадим команду на создание двух "наблюдателей, таким образом в очереди у нас окажется два сообщения:

Так какObserver-servicesслушают одну и ту же очередь, то сообщения между ними будут распределятся равномерно, черезRound-robin.

  • msg1поступает в очередь. Его начинает обрабатывать один из свободныхконсьюмеров, допустим Observer1. Сообщение переходит в состояниеUnackedи теперь новые сообщения, которые поступят в очередь будут доступны для другихконсьюмеров.

  • msg2поступает в очередь. Observer1у нас уже занят, и поэтому сообщение на обработку достанется всем свободнымконсьюмерам, в данном случае оно достаетсяObserver2.

Давайте теперь представим, что Observer-service1у нас сломался, например он находится на отдельном сервере и сервер вышел из строя (самый популярный контраргумент - а что... если свет вырубили?).

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

Резонным будет замечание - что будет если ошибка произойдет в самомконсьюмере, в процессе обработки сообщения. Оно в таком случае прейдёт в очередь с пометкой _Error, чтобы этого избежать можем настроитьRetryPolicy. И тогда в случае ошибки сообщение не попадет обратно в очередь, аконсьюмерпопытается заново обработать это сообщение.
Правила дляRetryPolicyмогут быть гибкими:

  • Попробовать 1000 раза и положить в очередь ошибок.

  • Попробовать 5 раз с интервалом 1,4,10...минут и потом положить в очередь ошибок.

  • Вообще попробоватьint.MaxValueраз.

Что же мы имеем в итоге? Мы можем иметь абсолютно любое количество наблюдателей, каждый из которых смотрит на одну очередь и каждый обрабатывает свою задачу/сообщение. Мы можем увеличитьPrefetchCount, допустим до 10, и тогда у сервиса будет 10 свободных консьюмеров, которые будут ждать команды на работу. Сервисы можно распределять по разным серверамиесли мы допускаем что кой-то сервер может выйти из строя, нужно просто иметь свободный сервис, который в случае поломки возьмет задачибольного.Например, если у нас есть 10 серверов, мощностей каждого из которых достаточно для обработки 5 наблюдателей, и шанс того, что один процент из них может выйти из строя, нужнозадеплоитьодин 11-ый сервер с той же мощностью, который будет на подстраховку.

А как жеконсистентность? Да и как вообще всем этим управлять? Да, мы можем добавить сообщения в очередь, но как убрать их оттуда... не очищать же очередь вручную?! Тем более, в идеале, наши "наблюдатели" должны закончить свою жизнь естественным образом, то есть через вызовCancellationToken.

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

  • Id(Идентификатор) -Guidгенерируемый при рождении.

  • Name(Имя), которое мы сами дали ему, когда сервис деплоили, оно уникальное для каждого сервиса.

  • CreatedAt/ModifyAt(Дата создания/Дата изменения).

  • WorkersCount, это будетPrefetchCount- его мощность, сколько он может обрабатывать задач одновременно.

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

Id

Name

WorkerCount

CreatedAt

ModifyAt

IsDeleted

{Uniqueid}

Observerservice1

10

{somedate}

null

false

{Uniqueid}

Observerservice2

10

{somedate}

null

false

{Uniqueid}

Observerservice3

10

{somedate}

null

false

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

Перед отключением сервисы так же отправят сообщения о том, что они закончили работать и большенедоступны, менеджербудет знать, что теперь на N сервисов у него меньше в строю. Тем самым он сделает пометку в базе данных и проставит каждому удаленному значениеIsDeleted=true.

Есть вероятность с тем, что сервис может не успеть отправить свое последнее сообщение о прекращении работы (Kill9, все тот же свет вырубили). За работоспособность компонентов у нас должна отвечать инфраструктура, напримерDocker. Мы должны быть уверены, если сервис непредвиденно прекратил работу, контейнер пере поднимется и сервис заново начнет работу. В таком случае при рождении, он заново отправит сообщение, но уже с новым идентификатором, но старым именем. Менеджеру достаточно будет данной информации чтобы привести данные в консистентное состояние и понять, что со старым сервисом случилось что-то страшное.

А теперь попробуем создать нового наблюдателя через API. Отправляем команду на создание (Мы должны позаботиться о том, что менеджер в процессе инициализации прочитает все сообщения из Statequeue и будет содержать последние актуальные данные о состоянии сервисов). Менеджер проверяет есть ли наблюдатель с таким именем уже, если нет, он проверяет наличие свободных сервисов, а они пока все свободны, далее он дает команду на создание - кладет сообщение в очередь.

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

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

Id

Name

CreatedAt

ModifyAt

ServiceId

Status

{Observerid}

My_new_observer

{created date}

null

null

Created

Менеджер, дождавшись ответа от сервиса, которому досталась задача, изменяет статус наProcessingи связывает задачу с сервисом.

Id

Name

CreatedAt

ModifyAt

ServiceId

Status

{Observerid}

My_new_observer

{created date}

{modifydate}

{Observerservice1id}

Processing

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

Перечень статусов:

  • Created

  • Processing

  • OnDeleting

  • Deleted

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

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

2) Направить сообщение всем доступным сервисам, черезFanOut.Сервис,у которого есть наблюдатель с нужным идентификатором будет удален, а все остальные сервисы просто проигнорируют это сообщение.

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

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

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

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

  • ЕслиOnDeletingилиDeleted, то возвращается ответ - запрос на удаление уже был отправлен или наблюдатель удален, соответственно.

  • ЕслиProcessing, то менеджер переводит наблюдателя в статусOnDeletingи отправляет сообщения на удаление в очередь. Сообщениеброадкаститсявсем сервисам. Сервис, у которого был нужный наблюдатель, вызываетCancellationTokenи оправляет сообщение в statequeue. Менеджер же, получив данное сообщение актуализирует данные и делает пометку переводя изOnDeletingвDeleted.

Id

Name

CreatedAt

ModifyAt

ServiceId

Status

{Observerid}

My_new_observer

{created date}

{modifydate}

{Observerservice1id}

Deleted

Рассмотрим критичные сценарии:

1) Отказала шина данных.

Вся инфраструктура, будь то шина данных или база данных должна находится опосредованно от нашей системы и бытькластеризированной. От себя добавлю следующий тезис, который как бритва Оккама отсечет ряд критичных сценариев -MsSql,RabbitMq,Kafka, дажеKubernetesсюда можно добавить, все это надежные системы, и при соблюдении SLA будут работать без отказа. За спиной у них огромные компании или комьюнити, сотни разработчиков. А вот собственную систему нужно воспринимать как что-то ненадежное, где любой компонент в любой момент времени может выйти из строя.

2) Полныйblackout, везде нет света.

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

3) Вылетел один сервер.

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

4) Отказал менеджер. В процессе того пока он был неактивен, ломались сервера с "наблюдателями.

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

5) Попытка удалить конкретного наблюдателя, в том момент, пока он перераспределяется.

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

Итог

Мы реализовали оркестратор задач, на базе механизма отправки сообщений. Где сообщение это задача, с двумя статусами, в работе -Unacked, и в ожидании работы -Ready. Очередь сама распределяет задачи между исполнителями, делая это событийно, а не черезpollingсостояния, как это делают планировщики. Система масштабируемая - мы можем иметь неограниченно количество "наблюдателей, которые могут быть распределены на разных серверах. Более того мы можем масштабировать как горизонтально, так и вертикально, увеличивая количество одновременно обрабатываемых сервисом задач, просто увеличиваяPrefetchCount. И последнее, время на разработку оказалось меньше, чем время на изучение и внедрение планировщика.

Подробнее..

GTK Как выглядит первый запуск анализатора в цифрах

04.01.2021 10:13:21 | Автор: admin

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

Введение

GTK кроссплатформенная библиотека элементов интерфейса. Недавно состоялся релиз GTK 4, что стало хорошим инфоповодом изучить качество кода проекта с помощью статического анализатора кода PVS-Studio. Такая деятельность для нас является регулярной, и нам часто приходится настраивать анализатор с нуля на многих проектах перед исследованием качества кода. В этой заметке я поделюсь опытом быстрой настройки PVS-Studio на C++ проекте.

Анализ GTK

Первые результаты

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

4 (Fails) + 1102 (High) + 1159 (Medium) + 3093 (Low) = 5358 предупреждений.

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

Исключаем директории

Рассмотрим такое предупреждение:

V530 [CWE-252] The return value of function 'g_strrstr_len' is required to be utilized. strfuncs.c 1803

/* Testing functions bounds */static voidtest_bounds (void){  ....  g_strrstr_len (string, 10000, "BUGS");  g_strrstr_len (string, 10000, "B");  g_strrstr_len (string, 10000, ".");  g_strrstr_len (string, 10000, "");  ....}

Это код тестов, причём не относящихся непосредственно к GTK, поэтому составляем список директорий для исключения из анализа и перезапускаем PVS-Studio.

При следующем запуске из анализа будут исключены следующие директории:

gtk/_build/gtk/subprojects/gtk/tests/gtk/testsuite/

Открываем отчёт и получаем следующий результат:

2 (Fails) + 819 (High) + 461 (Medium) + 1725 (Low) = 3007 предупреждений.

Ещё один положительный эффект, который мы получили после такой настройки, это ускорение анализа.

Исключаем макросы

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

V501 There are identical sub-expressions '* (& pipe->ref_count)' to the left and to the right of the '^' operator. gdkpipeiostream.c 65

static GdkIOPipe *gdk_io_pipe_ref (GdkIOPipe *pipe){  g_atomic_int_inc (&amp;pipe->ref_count);  return pipe;}

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

#V501//-V:g_atomic_int_:501#V547//-V:GTK_IS_:547//-V:GDK_IS_:547//-V:G_IS_:547//-V:G_VALUE_HOLDS:547#V568//-V:g_set_object:568

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

Смотрим результат:

2 (Fails) + 773 (High) + 417 (Medium) + 1725 (Low) = 2917 предупреждений.

Отключаем диагностики

Некоторые диагностики изначально выдают неподходящие предупреждения для конкретного проекта. Рассмотрим предупреждение V1042:

V1042 [CWE-1177] This file is marked with copyleft license, which requires you to open the derived source code. main.c 12

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

2 (Fails) + 164 (High) + 417 (Medium) + 1725 (Low) = 2308 предупреждений.

Изучаем фейлы

В проекте имеются 2 предупреждения типа Fails:

  • V002 Some diagnostic messages may contain incorrect line number in this file. gdkrectangle.c 1

  • V002 Some diagnostic messages may contain incorrect line number in this file. gdktoplevelsize.c 1

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

Эти предупреждения можно просто проигнорировать.

Выводы

Итоговый результат такой:

164 (High) + 417 (Medium) + 1725 (Low) = 2306 предупреждений.

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

V501 There are identical sub-expressions 'G_PARAM_EXPLICIT_NOTIFY' to the left and to the right of the '|' operator. gtklistbase.c 1151

static voidgtk_list_base_class_init (GtkListBaseClass *klass){  ....  properties[PROP_ORIENTATION] =    g_param_spec_enum ("orientation",                       P_("Orientation"),                       P_("The orientation of the orientable"),                       GTK_TYPE_ORIENTATION,                       GTK_ORIENTATION_VERTICAL,                       G_PARAM_READWRITE |                       G_PARAM_EXPLICIT_NOTIFY |  // &lt;=                       G_PARAM_EXPLICIT_NOTIFY);  // &lt;=  ....}

Это отличный результат! И показатели других диагностик тоже значительно выросли. Мизерными настройками удалось уменьшить отчёт анализатора на целых 57%. Соответственно, показатель верных предупреждений к ложным тоже значительно вырос.

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

А теперь время передать эстафету моему коллеге Андрею Карпову.

Примечание Андрея Карпова

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

Конечно, моя задача проще и сильно отличается от процесса настройки и внедрения анализатора в реальный проект. Мне достаточно поверхностно пробежаться по списку предупреждений и выписать явные ошибки, игнорируя ложные срабатывания или непонятные предупреждения в сложных участках кода. При реальном использовании понадобится больше времени, чтобы настроить анализатор, точечно подавить ложные срабатывания, улучшить макросы и так далее. Но на самом деле, это не страшно. Например, в статье про проверку проекта EFL Core Libraries я показал, что можно достаточно легко настроить анализатор, чтобы он выдавал всего 10-15% ложных предупреждений. Согласитесь, неплохо, когда на каждые 1-2 ложных срабатывания вы исправляете 8-9 ошибок.

Ну и не забывайте, что вам всегда доступен механизм массового подавления срабатываний анализатора. Это позволяет начать быстро использовать анализатор даже в большом проекте. Все предупреждения объявляются техническим долгом и пока не показываются. И команда начинает работать только с предупреждениями, относящимися к новому или изменённому коду. Подробнее про это предлагаю почитать в статье "Как внедрить статический анализатор кода в legacy проект и не демотивировать команду".

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Svyatoslav Razmyslov. GTK: The First Analyzer Run in Figures.

Подробнее..

С каких книг можно начать изучать программирование (Python, C, C, Java, Lua,)

05.01.2021 14:11:03 | Автор: admin

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

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

Для начинания есть несколько путей:

  • запись в кружок или на курс

  • обучаться по книгам и документации

  • обучаться по видеороликам

Выбираем кружки и курсы.

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

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

Я часто записываюсь на дистанционные курсы. Для начинающих лучше попробовать бесплатные. Например на Stepik есть огромное количество курсов.
Например, есть пару моих:
По основам программирования на python

По основам робототехники на базе Lego EV3

Курсов там огромное количество выбирай на свой вкус.

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

Выбираем книги для обучения программированию и робототехники

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

Почему энциклопедии? Это связано с тем, что энциклопедии содержат достаточно полную информацию о всех направления науки и неплохое разъяснение по той или иной теме кратко, но доступно. Например, я пользуюсь энциклопедиями по математике и физике для детей Аванта+

Энциклопедия Аванта по математике

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

Из книг по программированию рекомендую начать с основ. Например, Джейсона Бриггса Python для детей.

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

В качестве продолжения, могу рекомендовать данные книги по программированию. Все они связаны с математикой, 3D координатами, списками, функциями и классами1 из 2

Как уже и писал ранее python универсален и подойдёт для изучения в робототехники.

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

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

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

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

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

Если же вам нравятся языки со статической типизацией, то можно взять что по C++

Данная книга для студентов

Также есть хорошие книги по Delphi

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

Если ваша мечта касается создания игр, то можно изучить C#на базе Unity.

Всё в ваших руках. Если вы горите этой идей, то вы обязательно достигните своей цели.

И напоследок, моё видео о выборе книг для программирования.

Подробнее..

Перевод Интригующие возможности С 20 для разработчиков встраиваемых систем

07.01.2021 08:23:49 | Автор: admin

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

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

С++20 это седьмая итерация С++, которой предшествовали, например, С++17, С++14 и С++11. Каждая итерация добавляла новые функциональные возможности и при этом влияла на пару функций, добавленных ранее. Например, ключевое слово auto С++14.

Прим. перев.:

В С++14 были введены новые правила для ключевого слова auto. Ранее выражения auto a{1, 2, 3}, b{1};были разрешены и обе переменные имели тип initializer_list<int>. В С++14 auto a{1, 2, 3}; приводит к ошибке компиляции, а auto b{1};успешно компилируется, но тип переменной будет int а не initializer_list<int>. Эти правила не распространяются на выражения auto a = {1, 2, 3}, b = {1};, в которых переменные по-прежнему имеют тип initializer_list<int>.

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

Так уж получилось, что в С++20 было добавлено довольно много новых функциональных возможностей. Новые итераторы и поддержка форматирования строк будут полезны с новой библиотекой синхронизации. У всех на слуху оператор трехстороннего сравнения, он же оператор космический корабль. Как и большинство функциональных возможностей, описание этого оператора выходит за рамки данной статьи, но если кратко, то сравнение типа x < 20, сначала будет преобразовано в x.operator<=>(20) < 0. Таким образом, поддержка сравнения, для обработки операторов типа <, <=, >= и >, может быть реализована с помощью одной или двух операторных функций, а не дюжины. Выражение типа x == yпреобразуется в operator<=>(x, y) == 0.

Прим. перев.:

Более подробную информацию об операторе космический корабль смотрите в статье @Viistomin Новый оператор spaceship (космический корабль) в C++20

Но прейдём к более интересным вещам и рассмотрим функциональные возможности С++20 которые будут интересны разработчикам встраиваемых систем, а именно:

Константы этапа компиляции

Разработчикам встраиваемых систем нравится возможность делать что-то на этапе компиляции программы, а не на этапе её выполнения. С++11 добавил ключевое слово constexpr позволяющее определять функции, которые вычисляются на этапе компиляции. С++20 расширил эту функциональность, теперь она распространяется и на виртуальные функции. Более того, её можно применять с конструкциями try/catch. Конечно есть ряд исключений.

Новое ключевое слово consteval тесно связано с constexpr что, по сути, делает его альтернативой макросам, которые наряду с константами, определенными через директиву #define, являются проклятием Си и С++.

Сопрограммы

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

Прим. перев.:

Более подробную информацию о сопрограммах и о том для чего они нужны, смотрите в статье @PkXwmpgN C++20. Coroutines и в ответе на вопрос, заданный на stackoverflow.

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

Концепты и ограничения

Концепты и ограничения были экспериментальной функциональной возможностью С++17, а теперь являются стандартной. Поэтому можно предположить, что эксперимент прошел успешно. Если вы надеялись на контракты Ada и SPARK, то это не тот случай, но концепты и ограничения C++20 являются ценными дополнениями.

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

#include <string>#include <cstddef>#include <concepts>using namespace std::literals; // Объявляем концепт "Hashable", которому удовлетворяет// любой тип 'T' такой, что для значения 'a' типа 'T',// компилируется выражение std::hash{}(a) и его результат преобразуется в std::size_ttemplate <typename T>concept Hashable = requires(T a) {    { std::hash{}(a) } -> std::convertible_to<std::size_t>;}; struct meow {}; template <Hashable T>void f(T); // Ограниченный шаблон функции С++20 // Другие способы применения того же самого ограничение:// template<typename T>//    requires Hashable<T>// void f(T); // // template <typename T>// void f(T) requires Hashable<T>;  int main() {  f("abc"s); // OK, std::string удовлетворяет Hashable  f(meow{}); // Ошибка: meow не удовлетворяет Hashable}

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

Модули

Повсеместный #include заменяется модулями. Ключевые слова import и export находятся там, где когда-то находился #include.

Директива #include упростила написание компиляторов. Когда компилятор её встречает он просто выполняет чтение файла, указанного в ней. По сути получается, что включаемый файл как бы является частью оригинального файла. Такой подход зависит от порядка, в котором объединяются файлы. В целом это работает, однако такое решение плохо масштабируется при работе с большими и сложными системами, особенно с учетом всех взаимосвязей, которые могут принести шаблоны и классы С++.

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

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

Я по-прежнему склонен программировать на Ada и SPARK, но новые изменения в C++20 делают его лучшей платформой C++ для разработки безопасного и надежного программного обеспечения. На самом деле мне нужно поработать с новыми функциями C++20, чтобы увидеть, как они влияют на мой стиль программирования. Я старался избегать сопрограмм, поскольку они были нестандартными. Хотя концепты и ограничения будут немного сложнее, они будут более полезны в долгосрочной перспективе.

Положительным моментом является то, что компиляторы с открытым исходным кодом поддерживают C++20, а также то, что в сети интернет появляется всё больше коммерческих версий компиляторов, поддерживающих С++20.

Подробнее..

Перевод Сюрпризы в коде на Си

16.01.2021 06:18:31 | Автор: admin

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

x = x + 1;x += 1;x++;++x;

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

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

y = x++; // переменной y присваивается значение x, до его инкрементаy = ++x; // переменной y присваивается значение х, после его инкремента

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

Если бы х был указателем на переменную типа int, то увеличение на 1 фактически означало бы увеличение на 4 (на 32-х битной машине). Если это большой сюрприз, то стоит освежить в памяти арифметические операции с указателями.

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

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

alpha = 99;beta = 99;gamma = 99;

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

alpha = beta = gamma = 99;

Они обе на 100 % эквивалентны. Или нет?

В большинстве случаев обе конструкции совершенно эквивалентны, но есть (по крайней мере) четыре ситуации при которых выбор той или иной конструкции имеет значение.

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

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

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

mov r0, 99mov alpha, r0mov r0, 99mov beta, r0mov r0, 99mov gamma, r0

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

Наконец, остается вопрос о порядке выполнения. В первой конструкции совершенно ясно, что alpha будет проинициализирована первой, а gamma последней. Вторую конструкцию компилятор интерпретирует следующим образом:

alpha = (beta = (gamma = 99));

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

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

Подробнее..

Домино на Unity

05.01.2021 18:18:32 | Автор: admin

Учебные материалы для школы программирования. Часть 2

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

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

Spoiler

Хочется напомнить, что занятия разрабатывались для детей 10-16 лет, поэтому многие этапы упрощены.

Первое занятие вы можете найти по ссылке http://personeltest.ru/aways/habr.com/ru/post/535916/

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

Домино

Цель данного занятия - научиться создавать и редактировать игровые объекты, компоненты и материалы. Итак, начнем!

Импортируем приложенный ассет. Скачать его можно по ссылке

Создаём новую сцену. На сцене создаём плоскость с помощью quad или plane, выставляем размер 200х200.

Создаём новый куб и придаём ему форму домино. На этом этапе нужно показать несколько способов, как это можно сделать инструментами или ручным вводом.

Spoiler

Unity - отлично интегрируется с уроками по математике и физике. Смело вплетайте сложные или "скучные" темы по этим предметам в ваше занятие по Unity. Такой прием поможет не только преподнести тему интересно, но и ответит на частый вопрос "а зачем мне это учить, если в жизни не пригодится?" - пригодится, отвечаем мы, и рассказываем на проекте Домино, например, о понятии линейного размера.


Далее, на наше домино надо закинуть Rigidbody, чтобы домино могли падать.

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

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Reset : MonoBehaviour {  Rigidbody rig;  Vector3 startPos;  Quaternion startRot;    // Use this for initialization  void Start() {    rig = GetComponent<Rigidbody>();    startPos = transform.position;    startRot = transform.rotation;  }    // Update is called once per frame  void  Update() {    if(Input.GetKeyDown(KeyCode.Space)) {      if(rig) {        transform.position = startPos;        transform.rotation = startRot;        rig.velocity = Vector3.zero;        rig.angularVelocity = Vector3.zero;      }     }  }}

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

Этот этап урока отлично подойдет, чтобы рассказать о глобальной и локальной системе координат, о центре объекта и его позиции (center / pivot).

Несмотря на малый, с первого взгляда, объём занятия, у ребят уходит 1,5-2 часа. Время затрачивается не только на выставление домино в желаемой последовательности (рекомендуем каждому воплотить свою задумку по "узору" расположения костей), но и на разбор важных, для дальнейшей работы, тем: система координат, точки отсчёта, понимание углов Эйлера (pitch, yaw, roll), и масштабирования объектов.

Если занятие пройдёт быстрее, чем ожидалось, добавьте скрипт GameLogic, создав препятствие для шарика. Закиньте скрипт на пустой или статичный объект (пол или трамплин).

Этот скрипт по нажатию на пробел выставляет шарик в изначальное положение и включает-выключает объект-препятствие.

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

Подробнее..

Экраны отсутствующего контента в мобильном приложении на примере Xamarin

08.01.2021 14:08:13 | Автор: admin

С чего все началось

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

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

Первые шаги к улучшению ситуации

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

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

public class EmptyStateViewModel : ViewModel{    public EmptyStateViewModel(string image, string title, string description)    {        Image = image;        Title = title;        Description = description;    }    public string Image { get; }    public string Title { get; }    public string Description { get; }}

Следующим шагом на платформах(или в xaml в случае для Xamarin Forms) нужно прописать Bindings на проперти вью модели, в зависимости от того, какой mvvm-фреймворк используется.

А что дальше?

А далее возник вопрос - что делать, если по каким-то причинам наш запрос на бекенд фейлится. И тут пришла мысль - переиспользовать уже готовый EmptyStateView, но добавить внизу кнопку Retry, для возможности повторной отправки запроса. Потому мы просто наследовались от EmptyStateViewModel, но добавили поля с текстом кнопки, и командой на клик.

public class ErrorStateViewModel : EmptyStateViewModel{    public ErrorStateViewModel(string image, string title, string description, string actionTitle, Command actionCommand)        : base(image, title, description)    {        ActionTitle = actionTitle;        ActionCommand = actionCommand;    }    public string ActionTitle { get; }    public Command ActionCommand { get; }}

И как все это использовать?

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

public static class OverlayFactory{    public static T None<T>()        where T : EmptyStateViewModel    {        return null;    }    public static EmptyStateViewModel CreateCustom(string image, string title, string description)    {        return new EmptyStateViewModel(image, title, description);    }    public static ErrorStateViewModel CreateCustom(string image, string title, string description, string actionTitle, Command actionCommand)    {        return new ErrorStateViewModel(image, title, description, actionTitle, actionCommand);    }}

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

public class SomeViewModel : BaseViewModel{    private IItemsLoadingService _itemsLoadingService;        public SomeViewModel(IItemsLoadingService itemsLoadingService)    {        _itemsLoadingService = itemsLoadingService;    }    public ObservableCollection<ItemViewModel> Items { get; } = new ObservableCollection<ItemViewModel>();    public EmptyStateViewModel EmptyState { get; protected set; }    public ErrorStateViewModel ErrorState { get; protected set; }    public override async Task InitializeAsync()    {        await base.InitializeAsync();        await LoadItemsAsync();    }    private async Task LoadItemsAsync()    {        try        {            var result = await _itemsLoadingService.GetItemsAsync();            var items = result.ToList();            ErrorState = OverlayFactory.None<ErrorStateViewModel>();            if (items.Count == 0)            {                EmptyState = OverlayFactory.CreateCustom("img_empty_state", "Title", "Description");            }            else            {                EmptyState = OverlayFactory.None<ErrorStateViewModel>();                // Add items to list            }        }        catch        {            ErrorState = OverlayFactory.CreateCustom("img_error_state", "Title", "Description", "Retry", new Command(() => LoadItemsAsync));        }    }}

На платформах же нам необходимо прописать кастомный Binding для EmptyState/ErrorState вьюх на соответствующие вью модели, в зависимости от используемого mvvm-фреймворка, и проверять, если у нас EmptyStateViewModel/ErrorStateViewModel null, то скрывать соответствующую вьюху. Для этого в нашем случае использовался простой метод SetViewModel.

Для андроида тут все просто, при задании для View ее ViewModel мы установим View уже существующий ViewState из коробки. Если ViewModel null - тогда попросту задаем ViewState Gone, если существует - то Visible:

public void SetViewModel(EmptyStateViewModel viewModel){    ViewModel = viewModel;    View.Visibility = viewModel != null ? ViewStates.Visible : ViewStates.Gone;}

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

public void SetViewModel(EmptyStateViewModel viewModel){    ViewModel = viewModel;    View.SetVisibility(viewModel != null ? ViewStates.Visible : ViewStates.Gone);}

Нам потребуется несколько extension методов

public static void SetVisibility(this UIView view, ViewVisibility visibility){    var constraints = GetViewConstraints(view) ?? new NSLayoutConstraint[] {};    if (visibility == ViewVisibility.Gone)    {        SaveViewConstraints(view, constraints);        NSLayoutConstraint.DeactivateConstraints(constraints);        view.Hidden = true;        return;    }      if (visibility == ViewVisibility.Visible)    {        SaveViewConstraints(view, null);        NSLayoutConstraint.ActivateConstraints(constraints);        view.Hidden = false;        return;    }}

Тут мы в случае установки ViewVisibility.Gone предварительно сохраняем constraints нашей view и деактивируем их, а при включении видимости - наоборот достаем предварительно сохраненные constraints, обнуляем сохранение, а затем активируем их.

private static NSLayoutConstraint[] GetViewConstraints(UIView view){    return view.GetAssociatedObject<NSMutableArray<NSLayoutConstraint>>(Key)?.ToArray() ??           view.Superview?.Constraints               .Where(constraint => (constraint.FirstItem?.Equals(view) == true) || constraint.SecondItem.Equals(view))               .ToArray();}private static void SaveViewConstraints(UIView view, NSLayoutConstraint[] constraints){    NSMutableArray<NSLayoutConstraint> viewConstraints = null;    if (constraints.Length > 0)    {        viewConstraints = new NSMutableArray<NSLayoutConstraint>();        viewConstraints.AddObjects(constraints);    }    view.SetAssociatedObject(Key, viewConstraints, AssociationPolicy.RetainNonAtomic);}

Первый метод позволяет достать предварительно сохраненные constraints, если такие есть, либо же, если нет, получить текущие. Если же родительский view отсутствует, то вернется null.

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

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

PS - первая статья на Хабре, потому не судите строго. Но нужно же с чего-то начать.

Подробнее..

Flappy Bird на Unity 3D

10.01.2021 16:05:45 | Автор: admin

Учебные материалы для школы программирования. Часть 3

Spoiler

Часть 1 вы можете найти здесь

Часть 2 вы можете найти здесь

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

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

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

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

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

Цель занятия: научиться работать с 2д-физикой и Canvas при использовании последнего на разных разрешениях целевой платформы.

Рассматриваем с учащимися темы:

- включение AudioSource посредством Event-системы UI;
- детектирование тапа по экрану посредством Event-системы UI - Спрайты;
- система анимаций в разрезе 2D-игр;
- коллайдеры в 2D и их редактирование;
- сборка проекта под Android.

Поехали!

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

Для начала научимся пользоваться спрайтами. В ресурсах игры лежит файл sprites.png, который импортирован как спрайт с пометкой Multiple, это позволяет посредством кнопки SpriteEditor разрезать его на несколько одиночных спрайтов.

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

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

Анимация спрайтов.

Следующим шагом будет создание движущихся труб. Трубы устроены следующим образом: пара труб с установленным между ними триггером со скриптом, убивающим игрока, расположены в одном объекте. Передвигать нужно именно этот объект. Записываем на него анимацию, выходим из режима записи и проверяем.

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

Для компиляции под Android важно не забыть про удобное управление. Для мобильных игр, в большинстве своём, оно должно осуществляться одним пальцем. Делается это довольно просто (решение не является оптимальным, но оно самое простое).

В Canvas создаём панель и делаем её абсолютно прозрачной.

Все элементы управления закидываем внутрь этой панели. Это обязательно. Если этого не сделать, панель и кнопки могут некорректно отвечать на нажатия.
На панель необходимо закинуть компонент EventTrigger, и создать новое поле OnPointerEnter. В это поле закидываем птицу и выбираем внутри метод Jump.

После этого птица корректно начнет реагировать на нажатия по экрану.

Компиляция проекта под андроид.
Зайдём в File->Preferences->External Tools. Внизу страницы находятся три поля - SDK, JDK и NDK. Рядом кнопки Browse и Download. Первая - для выставления нужной папки, в которую установлены sdk, jdk, ndk, вторая - перекидывает на страницу загрузки того или иного инструмента.

Сперва необходимо установить Android studio и необходимые пакеты инструментов и SDK. На момент написания этих строк, устанавливалось API уровня 16 и 25. Также для компиляции необходим JDK.
NDK для данного проекта устанавливать нет необходимости.

Далее необходимо зайти в File-> Build Settings , выбрать платформу Android и нажать Switch Platform.

Затем переходим в Edit -> Project Settings -> Player

В свитке Resolution and presentation выбираем расположение Landscape left, это заставляет экран всегда быть в одном положении и не поворачиваться, если включён автоповорот.

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

По желанию можно выставить иконку и компилировать. На выходе мы получим APK файл игры.
Если подключить телефон на андроиде в режиме отладки и нажать Build and Run, то игра автоматически установится на телефон и запустится.

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

Подробнее..

Гибриды побеждают или холивары дорого

11.01.2021 02:05:19 | Автор: admin

Мотивом для написания данной статьи послужил тот факт, что на habr.com участилось появление материалов маркетингового характера про Apache Kafka. А также тот факт, что из статей складывается впечатление что пишут их немного далекие от реального использования люди это конечно же только впечателение, но почему-то в большинстве своем статьи обязательно содержат сравнение Apache Kafka с RabbitMQ, причем не в пользу последнего. Что самое интересное читая подобные статьи управленцы без технического бэкграунда начинают тратить деньги на внутренние исследования, чтобы ведущие разработчики и технические директора выбрали одно из решений. Так как я очень жадный/домовитый, а также так как я сторонник тезиса "В споре НЕ рождается истина" предлагаю вам ознакомится с другим подходом почти без сравнения разных брокеров.


Без сравнения никуда


Вообще, по правильному, я должен был сделать статью в формате Kafka+RabbitMQ+Nats+ActiveMQ+Mosquito+etc, но мне кажется, что для Вас дорогие читатели это будет перебор, хотя обычно в моих архитектурных решениях присутствуют все вышеуказанные сервисы (и не только). И это я еще не рассказываю про AzureServiceBus/AmazonServiceBus которые также участвуют в "гибридах" при крупных программах проектов. Поэтому пока остановимся на связке Kafka+RabbitMQ и далее вы поймете почему: по аналогии можно подключить любой сервис с его протоколом. Потому что:


сравнивая Apache Kafka и RabbitMQ вы сравниваете 2 (два) бренда, а точнее 2 коммерческие компании Confluent и vmWare, и немножко Apache Software Foundation (но это не компания)

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


  • RabbitMQ мультипротокольный и расширяемый брокер сообщений
  • Apache Kafka платформа для распределенной потоковой передачи событий
  • Confluent Platform платформа потоковой передачи событий с возможностью создания высокопроизводительных конвейеров обработки данных для целей аналитики и интеграции в бизнес-сценариях

Я не зря третьим пунктом выделяю наработки компании Confluent те кто собирается использовать Apache Kafka в продуктиве должны хотя бы видеть какую функциональность дополнительно добавляет Confluent к Apache Kafka. А это SchemeRegistry, RestProxy, kSQL и еще несколько интересных штук, о одной из которых мы поговорим ниже, она называется Kafka-Connect.


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


Итак чтобы было совсем понятно, куда я веду.


  • ключевая особенность RabbitMQ мультипротокольность и расширяемость. (основной язык якобы Erlang)
  • ключевая особенность экосистемы Kafka потоковая передача с обработкой (основной язык якобы Scala/Java)

Отсюда и возникают минусы каждого из решений


  • для RabbitMQ мы не сможем построить нормального решения для потоковой обработки. Точнее сможем, но НЕ штатно.
  • а для Kafka мы не сможем сделать мультипротокольность, точнее сможем но НЕ штатно.

Сократ не говорил, что в споре рождается истина


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


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


  • ODBC
  • AMQP
  • MSMQ
  • XMPP
  • IP over Avian Carriers

так как тогда наша задача была интегрировать всякое (python, C#, java) и 1С был придуман проект One-S-Connectors (https://code.google.com/archive/p/one-c-connectors/source/default/source). Сейчас он имеет сугубо академический интерес (так как в 1С мире моя персона достаточно известна и на Хабре много 1С специалистов из сообщества "воинствующих 1С-ников" эта ссылка специально для них).
Однако уже тогда (в 2006 году) стало понятно, что по большому счету конечному разработчику придется менять/выбирать протокол под бизнес-задачу. А инфраструктурщикам придется обеспечить максимально широкий спектр интеграционных протоколов. От ODBC до Kafka/NATs/ModBus.


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



маленькое примечание для менеджеров про Kombu как то так получилось, что имплементация протокола Apache Kafka до сих пор открыта https://github.com/celery/kombu/issues/301 и почему-то перешла в разряд "Дайте денег", поэтому для Python проектов приходится использовать дополнительно https://github.com/confluentinc/confluent-kafka-python

Когда вы дочитаете до этого момента предполагаю, что вы зададите вопрос про остальные языки: Java, GoLang, RUST, etc. Но во первых я не зря выше указал что по серьезному в наш обсуждаемый сегодня гибрид нужно добавить историю про NATs и ActiveMQ и внезапно JMS поэтому просьба дочитать до конца: Java будет, а во вторых мы переходим к еще трем полезным ссылкам



Прокоментируем их? Дело в том, что как бы вы не хотели, а для полноценного использования "в длинную" вам придется подписаться на историю релизов как сервера RabbitMQ и самое главное на те самые расширения (лежат в каталоге /deps) которые постоянно добавляются в ядро RabbitMQ, так и на портал компании Confluent где она публикует приложения полезные для конечного бизнеса использующего Apache Kafka в продуктиве.


подход к расширяемости за счет активируемых расширений также используется в экосистеме PostgreSQL тот который CREATE EXTENSION hypopg, так что подход реализованный компанией Pivotal/vmWare далеко не новый в нашем чудесном мире архитектуры программного обеспечения

Дополнительно на чудесном рынке облачных услуг в формате "Серьезная штука как сервис" есть еще один игрок это компания 84Codes https://github.com/84codes. Когда в рамках проектов внедрения нет нормальных инженеров по инфраструктуре именно 84Codes спасает пилотные проекты, потому как у них можно легко арендовать бесплатные/сильнодешевые контура CloudAMQP и CloudKarafka.


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


  • компания vmWare зарабатывает известно на чем, поэтому RabbitMQ ей развивается как часть своей платформы то есть они инвестируют в открытый проект не особо занимаясь его монетизацией. Возврат их инвестиций происходит в других местах, ну и также за счет контрибьторов на GitHub.
  • а вот компания Confuent собирается монетизировать свою платформу через Enterprise лицензию в которую включает те самые коннекторы Enterprise-Kafka-Connect, а также GUI для управления платформой.

Когда-то давно существовал https://github.com/jcustenborder/kafka-connect-rabbitmq, примечателен тот факт что товарищ Джереми его скрыл, оставив только свои наработки для Java разработчиков в виде Maven Archetype https://github.com/jcustenborder/kafka-connect-archtype еще раз обращаю Ваше внимание, что компания Confluent будет и дальше пытаться монетизировать свою деятельность, так что переводить всю интеграцию только на Kafka я бы на вашем месте поостерегся.

Поэтому когда вам топят за Kafka учитывайте, что вы либо изучаете Java, либо платите за Enterprise лицензию. А когда вам топят за RabbitMQ учитывайте, что либо вы изучаете системное администрирование (Erlang накладывает особенности системного администрирования), либо покупаете сервис у провайдеров типа 84Codes. Кодить на Erlang никогда не придется там это не нужно, если только вы не контрибьюторы OpenStack.


Поставил и забыл уже не работает


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


  • использование только одного протокола интеграции приводит к появлению ProtocolLock и как следствие к VendorLock я же не зря выше написал, что за каждым открытым продуктом, стоит какой-то ключевой комплект вендоров как они себя поведут: мы не знаем.
  • в мире ИТ больше нет серьезных продуктов, которые бы представляли собой монолитную службу все приложения давно стали композитными.
  • все нормальные вендоры сокращают свои релизные циклы по ключевым продуктам нормальной практикой стало выпускать редакции раз в 3 месяца TDD, BDD, CICD, ScallableAgile и DevOps (DocOps, DevSecOps) эти инженерные практики и методики управления не просто так развиваются. Всем очень хочется сокращать себестоимость и TimeToMarket.

Абзац выше важен, как финальный аккорд, прежде чем мы перейдем к Docker-Compose. А именно к нему я вел чтобы и разработчики и инфраструктурщики понимали что такое гибридная инфраструктура в режиме мультипротокольности (с) нужно сделать так, чтобы каждый мог поэкспериментировать с предлагаемым контуром. Как я уже указал выше первично подобное применительно к Kafka+RabbitMQ было подсмотрено именно у коллег из 84Codes (хорошие ребята всем советую https://www.84codes.com/).


Чтобы вы смогли поэкспериментировать сами


Итак подходим к примерам, так как обоснования и вводных уже хватит. Предположим вы уже поняли, что вам также нужна мультипротокольность, однако мы же помним, что все рекламные материалы про Apache Kafka нам рассказывают что это единственное решение с реализацией exactly-ones доставки сообщений от отправителя получателю. Собственно на самом деле нам и нужен гибрид, чтобы сделать из связки ТочкаОбмена->Очередь журнал Kafka (это тот который Topic) чтобы возникла сущность под называнием Offsets у нашей очереди событий.


exactly-ones

проверка на внимательность читающего exactly-ones это шутка в формате "Хотя бы один раз из 1С", а имеется в виду концепт Exactly once строго однократная доставка сообщений получателю, без необходимости повторной отправки от отправителя.


Предлагаю попробовать. Концепт для проверки Вашими руками будет состоять из:


  • Zookeper
  • KafkaBroker
  • RabbitMQ
  • KafkaConnect

и трех приложений приложений


  • отправитель на Python по протоколу AMQP 0.9
  • получатель на С# по протоколу AMQP 1.0
  • получатель на C# по протоколу Kafka

Еще интересное замечание: когда вы смотрите на всякие обучающие видео по Apache Kafka авторы часто (но не всегда) старательно пишут примеры на Java, это они делают скорее всего для того, чтобы скрыть от вас особенности использования librdkafka C++ библиотеки на основе которой сделаны многие не-джава адаптеры,. Я же наоборот предлагаю вам начинать исследование интеграции с Kafka именно с неё, чтобы четко оценивать риски "куда вы ввязываетесь": очень примечательно что там работает фактически один разработчик, формально в одиночку https://github.com/edenhill/librdkafka/pulse/monthly, а допустим wmWare старается поддерживать свою линейку клиентов под своим брендом https://github.com/rabbitmq

ну и самое главное и тяжелое:


контур содержит открытый форк старого RabbitMQ-Kafka-Sinc-Connector того самого который товарищи из Confluent в своё время скрыли с Github.


Докер контура для экспериментов


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


Развертываем RabbitMQ и Kafka


контур инфраструктуры который нам понадобится запускается достаточно просто


docker-compose -f dockers/infra.yml up -d

Если вам интересно что же там внутри, нашего композитного приложения, то в конце статьи дается ссылка на полный комплект исходников, наиболее интересен в нем Kafka-UI и непосредственно RabbitMQ-Sinc, все остальное обычно и штатно для всех известных примеров по Kafka или RabbitMQ


    image: provectuslabs/kafka-ui:latest    ports:      - 8080:8080    depends_on:      - kafka-broker      - zookeeper    environment:      KAFKA_CLUSTERS_0_NAME: local      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: broker:29092      KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181      KAFKA_CLUSTERS_0_JMXPORT: 9101

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


    <parent>        <groupId>com.github.jcustenborder.kafka.connect</groupId>        <artifactId>kafka-connect-parent</artifactId>        <version>1.0.0</version>    </parent>

Если подробно изучить pom.xml то выяснится, что существует заглавный проект для всех конекторов к Кафка https://github.com/jcustenborder/kafka-connect-parent, в котором используется Java-Kafka-Adapter


И непосредственно синхронизацией c RMQ занимается штатный Java клиент https://www.rabbitmq.com/java-client.html


            <groupId>com.rabbitmq</groupId>            <artifactId>amqp-client</artifactId>            <version>${rabbitmq.version}</version>

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


  • собрать из исходников java синхронизатор -1-build-connect-jar.bat
  • собрать контейнер с синхрозатором 00-build-connect-image.sh

и уже потом запустить полный инфраструктурный контур


  • стартуем полный инфраструктурный контур 01-start-infra.sh

обратите внимание так как Docker использует разное поведение при работе с PWD для Windows и Linux приходится делать дубликаты скриптов. В остальных случаях под обоими операционными системами используется интерпретатор sh

В итоге вы получите следующий комплект сервисов



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


Назначение портов:


  • 9092 будет использоваться для Kafka протокола
  • 8080 используется для отображения красивой картинки состояния Apache Kafka UI
  • 5672 будет использоваться для протокола AMQP 0.9 и он же будет работать и как AMQP 1.0
  • 15672 используется для красивой картинки управления RabbitMQ
  • 28082 отладочный порт для управления через curl трансформатором протоколов

В этот момент нужно остановится и прокомментировать особенность развертывания RabbitMQ в Docker:


  • хорошей практикой является версионирование включенных плагинов расширений enabled-rmq-plugins

[    rabbitmq_management,     rabbitmq_amqp1_0,     rabbitmq_mqtt,     rabbitmq_federation,     rabbitmq_federation_management,    rabbitmq_shovel,    rabbitmq_shovel_management,    rabbitmq_prometheus].

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

     "bindings":[        {           "source":"orders-send",           "vhost":"/",           "destination":"orders-amqp-10-consumer",           "destination_type":"queue",           "routing_key":"",           "arguments":{

Запускаем наши приложения


Остается только запустить наши приложения эмулирующие подключения


docker-compose -f dockers/infra.yml restart protocol-connect-syncdocker-compose -f applications.yml builddocker-compose -f applications.yml up

Топология наших тестовых приложений достаточно простая



Исходный код также максимально упрощён:


  • отправляется как-будто бы заказ Васи с периодичностью в 2 секунды

        producer = conn.Producer(serializer='json')        producer.publish({'client': 'Вася', 'count': 10, 'good': 'АйФончик'},                      exchange=order_exchange,                      declare=[kafka_queue, amqp10_queue])        time.sleep(2)

RUN python -m pip install \    kombu \    librabbitmq

причем используется для этого максимально производительная библиотека на Си для AMQP 0.9 librabbitmq наследуется именно от неё https://github.com/alanxz/rabbitmq-c


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

            Attach recvAttach = new Attach()            {                Source = new Source()                {                    Address = "orders-amqp-10-consumer",                    Durable = 1,                },

            ReceiverLink receiver =                 new ReceiverLink(session,"netcore_amqp_10_consumer", recvAttach, null);            Console.WriteLine("Receiver connected to broker.");            while (true) {                Message message = receiver.Receive();                if (message == null)                {                    Console.WriteLine("Client exiting.");                    break;                }                Console.WriteLine("Received "                   + System.Text.Encoding.UTF8.GetString((byte[])message.Body)

Причем в качестве драйвера выбран


  <ItemGroup>    <PackageReference Include="AMQPNetLite.Core" Version="2.4.1" />  </ItemGroup>

именно его https://github.com/Azure/amqpnetlite Microsoft использует для маркетинга своей реализации сервисной шины. Собственно именно AMQP 1.0 как протокол они и рекламируют https://docs.microsoft.com/ru-ru/azure/service-bus-messaging/service-bus-amqp-overview


Ну и финально


  • создан подписчик по протоколу Kafka который при каждом старте перечитывает с нуля журнал отправленных заказов Васи. Тот самый Exactly once.

                AutoOffsetReset = AutoOffsetReset.Earliest

                c.Subscribe("orders-from-amqp");

                    while (true)                    {                        try                        {                            var cr = c.Consume(cts.Token);

Выглядит наш контур в итоге следующим образом:


  • 5 инфраструктурных контейнеров


  • 3 контейнера с приложениями


  • готовый журнал транзакций заказов который можно посмотреть через Kafka-Ui


  • и готовый контур связей для RabbitMQ


А где же Java ?


Не волнуйтесь при таком гибридном подходе, без неё никуда, для того чтобы всё вышеуказанное заработало пришлось сделать форк и актуализировать версии Kafka-Connect-Base


[submodule "dockers/rabbitmq-kafka-sink"]    path = dockers/rabbitmq-kafka-sink    url = https://github.com/aliczin/kafka-connect-rabbitmq

Но самое интересное не это, самое интересное что в этом самом Kafka-Connect нет по сути никакой магии только код трансформации.


По сути нам предлагают:


  • создать наследника абстрактной задачи Источника

public class RabbitMQSourceTask extends SourceTask {

  • выполнить подписку на очередь сообщений

        this.channel.basicConsume(queue, this.consumer);        log.info("Setting channel.basicQos({}, {});", this.config.prefetchCount, this.config.prefetchGlobal);        this.channel.basicQos(this.config.prefetchCount, this.config.prefetchGlobal);

  • трасформировать полученные сообщения в абстрактные записи причем с буфером.

  @Override  public List<SourceRecord> poll() throws InterruptedException {    List<SourceRecord> batch = new ArrayList<>(4096);    while (!this.records.drain(batch)) {

Отдельно можно выделить чудесный трансформатор сообщений из AMQP 0.9 в Кафка. У несведующего в Java глаз может задергаться. У автора чувствуется многолетный опыт работы в J2EE.


  private static final Logger log = LoggerFactory.getLogger(MessageConverter.class);  static final String FIELD_ENVELOPE_DELIVERYTAG = "deliveryTag";  static final String FIELD_ENVELOPE_ISREDELIVER = "isRedeliver";  static final String FIELD_ENVELOPE_EXCHANGE = "exchange";  static final String FIELD_ENVELOPE_ROUTINGKEY = "routingKey";  static final Schema SCHEMA_ENVELOPE = SchemaBuilder.struct()      .name("com.github.jcustenborder.kafka.connect.rabbitmq.Envelope")      .doc("Encapsulates a group of parameters used for AMQP's Basic methods. See " +          "`Envelope <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html>`_")      .field(FIELD_ENVELOPE_DELIVERYTAG, SchemaBuilder.int64().doc("The delivery tag included in this parameter envelope. See `Envelope.getDeliveryTag() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#getDeliveryTag-->`_").build())      .field(FIELD_ENVELOPE_ISREDELIVER, SchemaBuilder.bool().doc("The redelivery flag included in this parameter envelope. See `Envelope.isRedeliver() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#isRedeliver-->`_").build())      .field(FIELD_ENVELOPE_EXCHANGE, SchemaBuilder.string().optional().doc("The name of the exchange included in this parameter envelope. See `Envelope.getExchange() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#getExchange-->`_"))      .field(FIELD_ENVELOPE_ROUTINGKEY, SchemaBuilder.string().optional().doc("The routing key included in this parameter envelope. See `Envelope.getRoutingKey() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#getRoutingKey-->`_").build())      .build();

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


Итоги


Все что здесь продемонстрировано естественно лежит на Github.


В репозитории https://github.com/aliczin/hybrid-eventing. Лицензия выставленна простая до невозможности Creative Commons Attribution 4.0 International.


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


Схема коммуникации в итоге для "разработчика интеграционных потоков" (с) выглядит следующим образом для источника и брокеров


orderEventsApp->Amqp09: send orderAmqp09->Amqp10: fanout\n copy eventAmqp09->KafkaQ: fanout\n copy eventKafkaQ->KafkaConnect: consume\n on messageKafkaConnect->KafkaConnect: transform\n messageKafkaConnect->Kafka: publish to topic


а для приемников все упрощается


Amqp10->orderEventSubApp: subcribe\n for eventorderJournalApp->Kafka: read kafka journal


Приемники берут нужные им данные только по нужному им протоколу

Ключевые посылы


Ключевые моменты которые я хотел расскрыть данной статьей


  • стройте эксперименты и продуктивы с Apache Kafka не со штатным Java клиентом, а librdkafka и базирующихся на ней адаптерах это позволит вам отладить сценарии разных версий протоколов KafkaAPI. Java вам пригодится в другом месте.


  • не ввязывайтесь с священные войны, что лучше RabbitMQ/Kafka/Nats/ActiveMQ просто развертывайте сервисы и публикуйте протоколы и пробуйте свои бизнес-сценарии.


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


  • реальный ИТ ландшафт почти всегда будет мультипротокольным



Примечание для понимающих


чтобы гибриды развивались дальше:


  • Mosquito очень удобен как встраиваемый брокер на уровне контролера SCADA для преобразования из ModBus/OPC-UA. Хотя как вы уже поняли из статьи интересны реализации "мостов из протокола в протокол" пример https://github.com/mainflux/mainflux


  • ActiveMQ удобен для Java разработчиков, потому что у них есть боязнь Erlang, но как мы выше уже сказали мост RabbitMQ AMQP 1.0 -> ActiveMQ легко организуется средствами RabbitMQ, кстати также как и JMS.


  • NATs интересен как часть OpenFaaS платформы, при внедрении "своего маленького" Amazon Lambda с преферансом. И опять же подход будет всё тот же мультипротокольные мосты с трансформацией: https://github.com/nats-io/nats-kafka если Вам не страшно посмотрите эксперименты с OpenFaaS веселых 1С-ников 2.5 часа примеров https://youtu.be/8sF-oGGVa9M



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


Функциональность: Мультипротокольный адаптер    Как разработчик я хочу иметь абстракцию Produser/Consumer    С возможность изменения протокола интеграции    Чтобы под каждую задачу выбирать разные протоколы     и единый интерфейс вызова для обеспечения независимости от вендора предоставляющего транспортСценарий: vmWare реализует протокол Stream средствами RabbitMQ     Когда vmWare закончит свой плагин для потоков    Тогда я активирую новый протокол     И быстро воткну его в приложение    И так как у меня есть продуктивный кластер RabbitMQ    И мне нужно будет просто поменять канал для отдельных бизнес сценариевСценарий: Завтра придут 1С-ники со своим ActiveMQ из Шины для 1С    Когда мне нужно быстро включить очереди 1С в общий контур    И чтобы на Питоне использовать старые наработки с Kafka API    Тогда я добавляю трансформацию ActivemeMQ2Kafka    и живу по старому а события ходят уже и из 1Сetc

А чтобы вы не думали, что данный подход это нечто уникальное вот Вам еще интересная ссылка: https://github.com/fclairamb/ftpserver/pull/34 это когда нужен FTP сервер, а хочется S3.


Ну и в качестве финального момента обратите внимание: есть и риски данного подхода: но они я думаю Вам и так понятны.


  • Придется оркестрировать такой комплект сервисов и вручную это почти невозможно. Придется использовать DevOps штуки типа k8s, OpenShift, etc но если вы уже решились на интеграцию в режимах слабой связаности приложений в режиме онлайн, у вас что-то на эту тему уже скорее всего есть.
  • Трансформаторы между протоколами приходится дорабатывать ничего готового открытого и PRODUCTION-READY на данный момент найти почти невозможно.

Финальное примечение для любителей писать ТЗ по ГОСТу


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


комплект программ для интеграции должен реализовывать коммуникацию конечных приложений по открытым протоколам HTTP, AMQP 0.9, AMQP 1.0, Apache Kafka не ниже версии 23, MQTT, WebSockets, <ЛюбойДругойХотьSOAPХотяЭтоЖуть> с возможность преобразования между протоколами дополнительными средствами администрирования

Надеюсь моя публикация после долгого перерыва Вам будет полезна в ваших интеграционных проектах. Предполагаю что будет вопрос про 1С и тут у меня совет только один. Используйте Google по ключевым словам 1С+RabbitMQ или 1С+Kafka или 1С+OpenFaas и RabbitMQ и Kafka "в 1С" давно и непринужденно используются. Потому что 1С это не только язык, но и несколько сообществ где уже давно сделаны все возможные адаптеры и платные и бесплатные. Собственно как и в Java/C#/Python/C++/Rust/etc.


Данная статья написана с применением расширения https://shd101wyy.github.io/markdown-preview-enhanced для Visual Studio Code за что автору летят дополнительные лучи добра.


Ну и в качестве финального момента хотел бы заметить, что выбор Cunfluent Inc в качестве платформы разработки Kafka-Connect экосистемы JDK выглядит все таки странно. Не удивлюсь если их конкуренты сделают такое же, но на GoLang, NodeJS (что-нибудь типа Kafka-Beats-Hub)



Красивые картинки в формате GraphViz я делаю при помощи хитрого проекта Docker2GraphViz помогает поддерживать актуальный контур и техническую документацию в формате Markdown


set CURPATH=%~dp0set DOCKER_DIR=%CURPATH%\dockersdocker run --rm -it --name dcv -v %DOCKER_DIR%\:/input pmsipilot/docker-compose-viz render -m image --force --output-file=infra-topology.png infra.ymldocker run --rm -it --name dcv -v %CURPATH%\:/input pmsipilot/docker-compose-viz render -m image --force --output-file=apps-topology.png applications.ymlcopy /b/v/y dockers\infra-topology.png content\assets\infra-topology.pngcopy /b/v/y apps-topology.png content\assets\apps-topology.png
Подробнее..

Свободный мини AUTOSAR редактор для микроконтроллеров

11.01.2021 20:23:30 | Автор: admin

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

В 2018 году меня позвали работать в EPAM Беларусь на аутсорсный проект по разработке ПО для рулевых реек. Я с большим удовольствием согласился и уехал жить и трудиться в Беларусь.

На этом проекте я познакомился со стандартом разработки ПО в автомобильной промышленности - AUTOSAR.

Пару слов об AUTOSAR

Согласно стандарту, архитектура приложения разделяется на 3 уровня:

  • Application;

  • Run Time Environment (RTE);

  • Basic software.

Главная идея стандарта в том, чтобы разделить систему на компоненты, а также определить уровни ответственности каждого из уровней. При этом application в основном стараются делать независимым от железа, чтобы можно было безболезненно переносить ПО с одного МК на другой. Система разделяется на независимые компоненты, и они взаимодействуют друг с другом с помощью RTE. Это позволяет иметь возможность протестировать каждый компонент в отдельности и добиться практически 100% покрытия кода тестами.

Подробнее о стандарте можно почитать здесь.

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

  • Порты для взаимодействия друг с другом;

  • Функции, вызываемые по событиям из RTE (runnables);

  • Калибровочные параметры Calibration Data (CData);

  • Память принадлежащая отдельному экземпляру компонента Per Instance Memory (PIM);

  • Переменные взаимодействия между runnables одного компонента Inter Runnable Variable (IRV).

Взаимодействие runnables компонента с его портами CData, PIM, IRV происходит с помощью функций RTE.

Взаимодействие компонентов проекта между собой происходит с помощью портов. Существует несколько видов портов, и они образуют между собой пары, которые могут быть соединены друг с другом. Выделим главные из них: sender-receiver, client-server.

  • Sender порт порт для записи в порт;

  • Receiver порт порт для чтения из порта;

  • Server порт предоставляет клиентам определенную функциональность;

  • Client порт позволяет вызывать функционал другого компонента.

На картинке ниже представлен пример взаимодействия компонентов SWC1 и SWC2, а также внутренняя структура SWC1.

Картинка взята отсюда.

AUTOSAR GUI Editor

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

  • Создание компонентов и композиций;

  • Создание client-server и sender-receiver интерфейсов;

  • Создание простых типов данных с указанием разрешенного диапазона. Этот диапазон может применяться на этапе тестирования компонента;

  • Создание комплексных типов данных(struct), массивов и перечислений;

  • Создание портов в компонентах и композициях и задание им их интерфейсов;

  • Соединение портов, имеющих одинаковый интерфейс, при этом один write порт может быть подключен к нескольким read портам, также как и server порт к нескольким клиентским портам;

  • Возможно размножение компонентов одного типа (Multiple instantiation);

  • Возможно создание PIM и CData для компонентов, при этом для каждого образца компонента можно задать своё индивидуальное начальное значение для PIM и CData;

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

  • Создание задач планировщика с приоритетами и их собственными периодами и распределение runnables компонентов по этим задачам;

  • Автоматическая генерация RTE файлов, которые объединяют компоненты друг с другом согласно проекту;

  • Автоматическая генерация шаблонов компонентов для первоначального использования этих файлов в проекте;

  • Генерация test environment для отдельно взятого определения компонента;

  • Генерация планировщика вызова runnables;

  • Проверка проекта на ошибки.

Есть и определенные ограничения на данный момент:

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

  • Отсутствуют асинхронные клиент-серверные операции;

  • Первоначально я сделал генерацию тасок ОС как тасок FreeRTOS, но ввиду того, что у меня периодически отказывала одна из тасок и не хотелось отлавливать баг в программе, я решил переписал планировщик таким образом, чтобы ОС таски вызывались одна за другой в соответствии с их приоритетом и отойти от использования FreeRTOS в моём домашнем проекте. Генерация фриртосных тасок осталось в анналах git истории редактора.

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

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

Создание элементов проекта

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

Далее, как только необходимый элемент был создан, откроется окно для его настроек.

Чтобы поместить компонент в композицию, необходимо создать его definition, затем выбрать в дереве объектов композицию, в которую необходимо добавить образец компонента, и после перетащить definition в композицию(drag and drop). Если всё выполнено верно, то в композиции появится новый компонент. Для того, чтобы сделать возможность создавать несколько образцов компонента, необходимо поставить галочку Multiple instantiation. В меню компонента также можно задать ему ранаблы, порты, PIM и CData.

Пример простого проекта из 4х компонентов.Пример простого проекта из 4х компонентов.

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

Для того чтобы задать init values для PIM и CData необходимо выбрать компонент в композиции и во вкладке Component properties вписать нужные данные в поля.

Чтобы создавать соединения между портами компонентов, необходимо чтобы порты соединяемых компонентов имели один и тот же интерфейс. Нажав Insert->Connection зажимаем ЛКМ над одним портом компонента, а затем отжимаем её над другим. Если всё хорошо, то появится прямая линия, соединяющая порты компонентов. Можно добавить несколько точек, в соединительную линию, чтобы она обходила элементы.

Перемещение по композиции

Зажав колесико, можно перемещать композицию

Покрутив колесико - изменять масштаб

Двойной клик по колесику - растягивает текущую композицию в отображаемое окно

Перемещение компонентов по композиции осуществляется путем зажатия ЛКМ на компоненте, перемещения, а затем отпускания ЛКМ.

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

Планировщик

Настройка планировщика осуществляется с помощью окна Runnables management, которое может быть запущено с помощью System-> Runnables order. Для того, чтобы планировщик был корректно создан, необходимо сперва создать таски ОС и затем распределить ранаблы компонентов по этим таскам.

Создание и редактирование задач планировщика.Создание и редактирование задач планировщика.Распределение runnables по задача планировщика.Распределение runnables по задача планировщика.

На данный момент поле Stack size in bytes не используется. Но при необходимости, можно создать(откопать в git истории) генератор планировщика, который создает таски RTOS с указанным значением размера стэка.

Генерация RTE

Перед генерацией RTE очень благоприятно убедиться в отсутствии ошибок в проекте, например не указанных интерфейсах в портах или пропущенных типов данных в структурах. Делается это с помощью команды System->Check errors.

Далее необходимо указать место, куда будут сохраняться сгенерированные RTE файлы и шаблоны компонентов в окне Project Settings. (Project->Settings). Здесь же необходимо будет указать частоту системного таймера, на котором будет основываться расчет вызовов задач планировщика.

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

После того, как все ошибки устранены и указаны пути сохранения, жмем Project->Generate RTE и получаем папку RTE и папки с шаблонами компонентов. Важно знать, что файлы в папках RTE и RteSkeleton всегда будут перезаписываться при перегенерировании RTE.

Настройка проекта в STM32CubeIDE

Пример выполнен на STM32CubeIDE, чтобы показать, как можно использовать порты, runnables, таски и СData с помощью моего редактора.

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

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

  2. Написать код для компонентов или просто оставить как есть, чтобы убедиться для начала, что проект собирается.

  3. Добавить в Includes проектакаждую include папку компонента, а также папку RTE.

4. Добавить в Source Location папки Components и RTE

5.В main.c добавить:


#include "Rte_Task_Scheduler.h"
а также вызывать работу планировщика в бесконечном цикле.

6.В stm32f4xx_it.c между /* USER CODE BEGIN Includes */ и /* USER CODE END Includes */ добавить:
#include "Rte_Task_Scheduler.h"

и также обновить обработчик прерывания SysTick_Handler.

7. После этого настройка проекта в кубе завершена и можно собирать проект и запускать его на железке.

Пример также выложил на github.

Конечно, AGE может содержать ошибки и ему есть ещё куда расти, но своей основной задачей генерацией RTE, он справляется на ура.

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

Заключение

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

Ссылка на AUTOSAR Gui Editor.

Ссылка на проект.

Подробнее..

Давайте напишем Linux терминал

13.01.2021 14:06:06 | Автор: admin

Приветствие

Всем привет! Хочу поделиться своим опытом написания собственного терминала Linux используя Posix API, усаживайтесь поудобнее.

Итоговый результатИтоговый результат

Что должен уметь наш терминал

  1. Запуск процессов в foreground и background режиме

  2. Завершение background процессов из терминала

  3. Поддержка перемещения по директориям

Как устроена работа терминала

  1. Считывание строки из стандартного потока ввода

  2. Разбиение строки на токены

  3. Создание дочернего процесса с помощью системного вызова fork

  4. Замена дочернего процесса на необходимый с помощью системного вызова exec

  5. Ожидание завершения дочернего процесса (в случае foreground процесса)

Немного про системный вызов fork()

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

Рассмотрим пример:

#include <stdio.h>#include <unistd.h>#include <wait.h>int main() {    pid_t pid = fork();        if (pid == 0) {        printf("I'm child process!\n");        } else {        printf("I'm parent process!\n");        wait(NULL);    }        return 0;}
Что выведет данная программа:

I'mparentprocess!
I'mchildprocess!

Что же произошло? Системный вызов fork создал клон процесса, т. е. теперь мы имеем родительский и дочерний процесс.

Чтобы отличить дочерний процесс от родительского в коде достаточно сделать проверку. Если результат функции fork равен 0 - мы имеем дело с дочерним процессом, если нет - с родительским. Это не означает, что в операционной системе id дочернего процесса равен 0.

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

Подробнее про exec()

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

Системный вызов exec заменяет текущий процесс сторонним. Естественно, сторонний процесс задается через параметры функции.

Рассмотрим пример:

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <wait.h>int main() {    pid_t pid = fork();        if (pid == 0) {        execlp("ls", "ls", "-l", NULL);        exit(1);    } else {        waitpid(pid, NULL, 0);    }        return 0;}
Что выведет данная программа

total 16
-rwxr-xr-x 1 runner runner 8456 Jan 13 07:33 main
-rw-r--r-- 1 runner runner 267 Jan 13 07:33 main.c

Что произошло?

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

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

Перейдем к полноценной реализации

Часть 1. Чтение строки с консоли.

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

char* readline() {    char*   line = NULL;    size_t  size = 0;    ssize_t str_len;    // Reading line from stdin    if ((str_len = getline(&line, &size, stdin)) == -1) {        // Logging all errors except Ctrl-D - terminal shutdown        if (errno != 0) {            printf("[ERROR] Couldn't read from stdin\n");        }        free(line);        printf("\n");        return NULL;    }    // Remove useless \n symbol if exists    if (line[str_len - 1] == '\n') {        line[str_len - 1] = '\0';    }    return line;}

В данной функции происходит чтение строки с применением функции getline. После чего, если в конце строки имеется символ переноса строки, удаляем его.

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

Часть 2. Разбиение строки на токены.

В данной части будет представлена реализация разбиения строки на массив токенов.

#define DEFAULT_BUFF_SIZE 16#define TOKENS_DELIMITERS " \t"

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

char** split(char* line) {    size_t position  = 0;    size_t buff_size = DEFAULT_BUFF_SIZE;    char* token;    // Allocate memory for tokens array    char** tokens = (char**)malloc(sizeof(char*) * buff_size);    if (tokens == NULL) {        printf("[ERROR] Couldn't allocate buffer for splitting!\n");        return NULL;    }    // Tokenize process    token = strtok(line, TOKENS_DELIMITERS);    while (token != NULL) {        // Emplace token to array        tokens[position++] = token;        // If array free space ended - increase array        if (position >= buff_size) {            buff_size *= 2;            tokens = (char**)realloc(tokens, buff_size * sizeof(char*));            if (tokens == NULL) {                printf("[ERROR] Couldn't reallocate buffer for tokens!\n");                return NULL;            }        }        // Getting next token        token = strtok(NULL, TOKENS_DELIMITERS);    }    // Place NULL to the end of tokens array    tokens[position] = NULL;    return tokens;}

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

Очередной токен получается с использованием функции strtok. После чего данный токен копируется в массив токенов. Если в массиве токенов не достаточно места, массив увеличивается в 2 раза.

Завершается всё добавлением завершающего токена равного NULL, т. к. функция exec() ожидает наличие данного завершающего токена.

Часть 3. Выполнение процессов.

Структура хранения списка запущенных процессов.

Напишем определения структур для foreground и background процессов, fg_task и bg_task. А также определение структуры для хранения всех процессов tasks.

// Struct of background taskstruct bg_task_t {    pid_t  pid;           // Process id    bool   finished;      // Process state    char*  timestamp;     // Process state    char*  cmd;           // Command cmd};typedef struct bg_task_t bg_task;// Struct of foreground taskstruct fg_task_t {    pid_t pid;     // Process id    bool finished; // Process state};typedef struct fg_task_t fg_task;// Struct of all tasksstruct tasks_t {    fg_task  foreground; // Process id of foreground bg_task    bg_task* background; // Background task list    size_t   cursor;     // Cursor of background tasks    size_t   capacity;   // Background array capacity};typedef struct tasks_t tasks;

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

// Global variable for storing active taskstasks t = {    .foreground = {        .pid = -1,        .finished = true    },    .background = NULL,    .cursor = 0,    .capacity = 0};

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

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

void set_foreground(pid_t pid) {    t.foreground.pid = pid;    t.foreground.finished = 0;}

Добавление background процесса выглядит посложнее.

int add_background(pid_t pid, char* name) {    // Temp background task variable    bg_task* bt;    // If end of free space in background array - increase size    if (t.cursor >= t.capacity) {        t.capacity = t.capacity * 2 + 1;        t.background = (bg_task*)realloc(t.background, sizeof(bg_task) * t.capacity);        if (t.background == NULL) {            printf("[ERROR] Couldn't reallocate buffer for background tasks!\n");            return -1;        }    }    // Print info about process start    printf("[%zu] started.\n", t.cursor);    // Save task in temp variable    bt = &t.background[t.cursor];    // Save process info in array    bt->pid = pid;    bt->finished = false;    time_t timestamp = time(NULL);    bt->timestamp = ctime(&timestamp);    bt->cmd = strdup(name);    // Move cursor right    t.cursor += 1;    return 0;}

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

Данная функция возвращает -1 в случае неудачи.

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

Добавим функцию экстренного завершения foreground процесса. Данная функция с помощью системного вызова kill с параметром SIGTERM завершает процесс по id процесса.

void kill_foreground() {    if (t.foreground.pid != -1) {        // Kill process        kill(t.foreground.pid, SIGTERM);        // Set finished flag        t.foreground.finished = true;        printf("\n");    }}

Также добавим функцию для завершения background процесса.

int term(char** args) {    char* idx_str;      // Cursor in index arg    int   proc_idx = 0; // Converted to int index arg    if (args[1] == NULL) {        printf("[ERROR] No process index to stop!\n");    } else {        // Set cursor in index arg        idx_str = args[1];        // Convert string index arg to int        while (*idx_str >= '0' && *idx_str <= '9') {            proc_idx = (proc_idx * 10) + ((*idx_str) - '0');            // Move cursor to right            idx_str += 1;        }        // Kill process if process index not bad        // and target process not finished        if (*idx_str != '\0' || proc_idx >= t.cursor) {            printf("[ERROR] Incorrect background process index!\n");        } else if (!t.background[proc_idx].finished) {            kill(t.background[proc_idx].pid, SIGTERM);        }    }    return CONTINUE;}

Данная функция принимает в себя массив токенов вида {"term", "<bg task index>", NULL}. После чего преобразует токен индекса background задачи в число. Убивает background задачу посредством системного вызова kill.

Непосредственно запуск процессов.

Для удобства введем функцию is_background, определяющую является ли задача фоновым процессом. Данная функция просто проверяет наличие & в конце.

int is_background(char** args) {    // Current position in array    int last_arg = 0;    // Finding last arg in array    while (args[last_arg + 1] != NULL) {        last_arg += 1;    }    // Checking if task is background`    if (strcmp(args[last_arg], "&") == 0) {        // Remove '&' token for future executing        args[last_arg] = NULL;        // Return true        return 1;    }    // Return false if: '&' wasn't founded    return 0;}

Введем функцию launch которая будет запускать background процесс если в конце присутствует токен &, иначе будет запускаться foreground процесс.

int launch(char** args) {    pid_t pid;        // Fork process id    int   background; // Is background task    // Checking if task is background    background = is_background(args);    // Create child process    pid = fork();    // If created failure log error    if (pid < 0) {        printf("[ERROR] Couldn't create child process!\n");    }    // Child process    else if (pid == 0) {        // Try launch task        if (execvp(args[0], args) == -1) {            printf("[ERROR] Couldn't execute unknown command!\n");        }        exit(1);    }    // Parent process    else {        if (background) {            // Try add background task to array            if (add_background(pid, args[0]) == -1) {                // Kill all processes and free                // memory before exit                quit();            }        } else {            // Set foreground task to store            set_foreground(pid);            // Wait while process not ended            if (waitpid(pid, NULL, 0) == -1) {                // Logging error if process tracked with error                // Except when interrupted by a signal                if (errno != EINTR) {                    printf("[ERROR] Couldn't track the completion of the process!\n");                }            }        }    }    return CONTINUE;}

То, что происходит в этой функции уже должно быть все понятно.

  1. Создается дубликат процесса с помощью системного вызова fork

  2. Заменяем дочерний процесс на требуемый с помощью системного вызова exec

  3. Определяем является ли процесс фоновым

  4. Если процесс фоновый - просто добавляем его в список bacground задач

  5. Если процесс не фоновый - дожидаемся окончания выполнения процесса

В функции присутствует неизвестная функция quit. Ее мы разберем в следующем блоке.

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

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

int execute(char** args) {    if (args[0] == NULL) {        return CONTINUE;    } else if (strcmp(args[0], "cd") == 0) {        return cd(args);    } else if (strcmp(args[0], "help") == 0) {        return help();    } else if (strcmp(args[0], "quit") == 0) {        return quit();    } else if (strcmp(args[0], "bg") == 0) {        return bg();    } else if (strcmp(args[0], "term") == 0) {        return term(args);    } else {       return launch(args);    }}

Данная функция пропускает действие, если первый токен NULL. Переходит в директорию, если первый токен cd. Выводит справку о пользовании, если первый токен help. Завершает работу терминала, если первый токен quit. Выводит список background задач, если первый токен bg. Убивает процесс по индексу, если первый токен term.

Во всех других случаях запускается процесс.

Реализация вспомогательных функций.

#define CONTINUE 1#define EXIT     0

Значение CONTINUEозначает дальнейшее исполнение главного цикла терминала. Значение EXITпрерывает выполнение главного цикла программы.

int cd(char** args) {    if (args[1] == NULL) {        printf("[ERROR] Expected argument for \"cd\" command!\n");    } else if (chdir(args[1]) != 0) {        printf("[ERROR] Couldn't change directory to \"%s\"!\n", args[1]);    }    return CONTINUE;}
int help() {    printf(        "Simple shell by Denis Glazkov.                               \n\n"        "Just type program names and arguments, and hit enter.          \n"        "Run tasks in background using '&' in the end of command.     \n\n"        "Built in functions:                                           \n"        "  cd   <path>        - Changes current working directory      \n"        "  term <bg_task_idx> - Prints list of background tasks        \n"        "  help               - Prints info about shell                \n"        "  bg                 - Prints list of background tasks        \n"        "  quit               - Terminates shell and all active tasks\n\n"        "Use the man command for information on other programs.         \n"    );    return CONTINUE;}
int quit() {    // Temp background task variable    bg_task* bt;    // Disable logging on child killed    signal(SIGCHLD, SIG_IGN);    // Kill foreground process    if (!t.foreground.finished) {        kill_foreground();    }    // Kill all active background tasks    for (size_t i = 0; i < t.cursor; i++) {        // Place background task to temp variable        bt = &t.background[i];        // Kill process if active        if (!bt->finished) {            kill(bt->pid, SIGTERM);        }        // Free memory for command name        free(bt->cmd);    }    return EXIT;}

Функция quit отключает все callback функции по событию SIGCHLD - т. е. функции, выполняющиеся когда дочерний элемент был завершен. После этого завершает все активные процессы.

#define PRIMARY_COLOR   "\033[92m"#define SECONDARY_COLOR "\033[90m"#define RESET_COLOR     "\033[0m"

Основные цвета терминала.

int bg() {    // Temp background task variable    bg_task* bt;    for (size_t i = 0; i < t.cursor; i++) {        // Store background task in temp variable        bt = &t.background[i];        // Print info about task        printf(            "[%zu]%s cmd: %s%s;%s pid: %s%d; %s"            "state: %s%s;%s timestamp: %s%s", i,            SECONDARY_COLOR, RESET_COLOR, bt->cmd,            SECONDARY_COLOR, RESET_COLOR, bt->pid,            SECONDARY_COLOR, RESET_COLOR, bt->finished ? "finished" : "active",            SECONDARY_COLOR, RESET_COLOR, bt->timestamp        );    }    return CONTINUE;}

Часть 4. Главный цикл терминала.

#include <stdlib.h>#include <signal.h>#include "include/shell.h"int main() {    char*  line;   // User input    char** args;   // Tokens in user input    int    status; // Status of execution    // Add signal for killing foreground child on ctrl-c    signal(SIGINT, kill_foreground);    // Add signal for handling end of child processes    signal(SIGCHLD, mark_ended_task);    // Shell is running while    // status == CONTINUE    do {        // Printing left shell info        display();        // Reading user input        line = readline();        if (line == NULL) {            exit(1);        }        // Parse line to tokens        args = split(line);        if (args == NULL) {            free(line);            exit(2);        }        // Try execute command        status = execute(args);        // Free allocated memory        free(line);        free(args);    } while (status);    return 0;}

Здесь и происходит вся магия. Взгляните на следующие строки. С помощью функции signal задаются callback функции на заданные события.

// Add signal for killing foreground child on ctrl-csignal(SIGINT, kill_foreground);// Add signal for handling end of child processessignal(SIGCHLD, mark_ended_task);

Событие SIGINT- срабатывает при нажатии комбинации ctrl-C, которое в дефолтном поведении завершает работу программы. В нашем же случае мы переназначаем его на завершение foreground процесса.

Событие SIGCHLD - срабатывает при завершении дочернего процесса созданyого с помощью системного вызова fork. В нашем случае мы переопределяем его на пометку фоновой задачи как выполненной с помощью функции mark_ended_task.

void mark_ended_task() {    // Temp background task variable    bg_task* bt;    // Get process id of ended process    pid_t pid = waitpid(-1, NULL, 0);    // Handle foreground process    if (pid == t.foreground.pid) {        t.foreground.finished = true;    }    // Handle background process    else {        // Search and remove process form background tasks array        for (size_t i = 0; i < t.cursor; i++) {            // Place task to temp variable            bt = &t.background[i];            if (bt->pid == pid) {                // Print info about process end                printf("[%zu] finished.\n", i);                // Set new state for background process                bt->finished = 1;                break;            }        }    }}

Все что описано в главном цикле терминала можно описать словами:

  1. Вывод информации о пользователе и текущей директории с помощью функции display

  2. Чтение строки из стандартного потока ввода

  3. Разбиение строки на токены

  4. Выполнение ранее описанной функции execute, которая в зависимости от массива токенов выполняет нужное нам действие.

Нам осталось реализовать одну оставшуюся функцию display. Которая получает информацию о текущей директории с помощью функции getcwd и имя пользователя с помощью функции getpwuid.

void display() {    // Try get and print username with color    uid_t uid = geteuid();    struct passwd *pw = getpwuid(uid);    if (pw != NULL) {        printf("%s%s%s:", PRIMARY_COLOR, pw->pw_name, RESET_COLOR);    }    // Try get and print current directory with color    char cwd[MAX_DIRECTORY_PATH];    if (getcwd(cwd, MAX_DIRECTORY_PATH) != NULL) {        printf("%s%s%s", SECONDARY_COLOR, cwd, RESET_COLOR);    }    // Print end of shell info    printf("# ");}

Часть 5. Итоговый результат.

Итоговый результатИтоговый результат

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

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

Подробнее..

Гравитационная комната в Unity 3D

16.01.2021 12:10:38 | Автор: admin

Учебные материалы для школы программирования. Часть4

Spoiler

Предыдущие уроки можно найти здесь:

  1. Spaceship

  2. Домино

  3. Flappy Bird

В первых публикациях я немного рассказывала про школу программирования, в рамках которой создавались проекты по Unity 3D. Знакомясь с нашими наработками, у многих возникали вопросы, которые нельзя оставить без внимания:

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

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

Во-вторых, как основатель проекта, я преследовала свою идею и мечту. Мне не хотелось учить ребят тому, что им нигде не пригодится, давать навыки, которые они не смогут перенести на другую деятельность. Поэтому, в основу проекта лег лозунг "Профессиональные инструменты доступные детям".

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

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

Построение образовательных траекторий требует дружелюбности, бесконечной эмпатии, гибкости и вариативности. К нам приходили разные ученики, с разным опытом, с разной мотивацией. Из разрозненной группы подростков, нам надо было сделать сообщество. А еще, нам надо было создать такое пространство, куда бы ребята хотели сами придти, а не потому что "мама сказала - надо!". Поэтому занятия облегчены, но для тех, кому дается легко - ты просто "вынимаешь" элемент из проекта, добавляя в него больше самостоятельности. Для тех, кому самый облегченный вариант не по зубам - рядом преподаватель (группы не более 7 человек), или товарищ, который всегда придет на помощь.

- Почему такая простая графика?

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

Перейдем от вопросов к делу!

Гравитационная комната

На уроке учимся:

- Работать с вертекснои привязкои при перемещении объектов (см. функцию клавиши V в Unity);

- Работать с привязкои к мировому пространству через Configurable Joint;

- Знакомим с акселерометром и гравитацией в разрезе игровых движков;

- Повторяем работы с rigidbody и коллаидерами.

Порядок выполнения

Создается новыи проект, импортируется приложенныи ассет, создается новая сцена. На сцене создается куб размером примерно 7х7х7, затем его размер, с помощью инструмента scale, меняют примерно до 7х0.3х7.

Данную фигуру размножают, поворачивают на 90 градусов и привязывают по углам с зажатои клавишеи V так, чтобы получился закрытыи куб. У стенки, которая находится с положительнои стороны оси Z, выключаем компонент MeshRenderer, в итоге, одна из стенок становится прозрачнои. Переименовываем ее в "Комната" и закидываем остальные стены, пол и потолок. Затем, размер всеи комнаты можно менять с помощью scale.

Из сферы и цилиндров создаем подобие лампочки, на сферу накладываем материал со стандартным шеидером и эмиссиеи. Внутри сферы создаем point light, а стандартныи directional light со сцены удаляем. Чтобы задать нашеи лампочке ось вращения, создаем пустои объект на самои верхушке провода лампочки, называем его "ось лампочки", закидываем в него все объекты, относящиеся к лампочке.

На ось накладываем Configurable joint, при этом, автоматически добавится rigidbody. Необходимо зафиксировать положение оси в мире, установив transform X, Y и Z в положение Locked.

Устанавливаем камеру так, чтобы она нормально покрывала всю комнату, при этом в Transform камеры будет установлен угол 180 градусов по Y.

Также, на сцену добавляются объекты, к которым применяется MeshCollider -> Convex и Rigidbody с массои в районе 4-5 кг.

Пишем скрипт, которыи в Update() содержит всего одну строку:

и закидываем в любои объект на сцене, допустим, в камеру. Для того, чтобы игра работала на слабых устроиствах, закидываем скрипт Resolution туда же. Этот скрипт принудительно занижает разрешение рендера до 800х450 на устроиствах с экранами большего размера.

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

Подробнее..

Категории

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

© 2006-2021, personeltest.ru