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

C++

Перевод Эмуляция компьютера интерпретатор CHIP-8

22.12.2020 16:08:30 | Автор: admin


Меня, по ряду причин, всегда завораживала эмуляция. Программа, которая выполняет другую программу Мне эта идея кажется невероятно привлекательной. И у меня такое ощущение, что тот, кто напишет подобную программу, не пожалеет ни об одной минуте потраченного на это времени. Кроме того, написание эмулятора это очень похоже на создание настоящего компьютера программными средствами. Мне было очень интересно разбираться в устройстве компьютерной архитектуры, писать простой HDL-код, но эмуляция это гораздо более простой способ ощутить себя тем, кто своими руками создаёт компьютер. А ещё, в детстве, когда я впервые увидел игру Super Mario World, я поставил себе цель, которая до сих пор не потеряла для меня ценности. Она заключается в том, чтобы полностью понять то, как именно работает эта игра. Именно поэтому я уже некоторое время подумываю о написании эмулятора SNES/SNS. Недавно я решил, что пришло время сделать первый шаг к этой цели.

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



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

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

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

// main.cppvoid Run() {CpuChip8 cpu;cpu.Initialize("/path/to/program/file");bool quit = false;while (!quit) {cpu.RunCycle();}}int main(int argc, char** argv) {try {Run();} catch (const std::exception& e) {std::cerr << "ERROR: " << e.what();return 1;}}

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

Нашей CHIP-8-системой будет Telmac 1800. В нашем распоряжении окажется 4 Кб памяти, монохромный дисплей с разрешением 64x32 пикселя, а также возможность воспроизводить звуки. Это очень хорошо. Сам интерпретатор CHIP-8 будет реализован посредством виртуальной машины. Нам понадобится обеспечить функционирование шестнадцати 8-битных регистров общего назначения (V0 VF), 12-битного индексного регистра (I), счётчика команд, двух 8-битных таймеров и стека на 16 кадров.

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

0x000 |-----------------------|| Память интерпретатора ||            |0x050 |  Встроенные шрифты  |0x200 |-----------------------||            ||            || Память программы   || и динамически     || выделяемая память   ||            |0xFFF |-----------------------|

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

// cpu_chip8.hclass CpuChip8 {public:public Initialize(const std::string& rom);void RunCycle();private:// Заполняет набор инструкций (instructions_).void BuildInstructionSet();using Instruction = std::function<void(void)>;std::unordered_map<uint16_t, Instruction>> instructions_;uint16_t current_opcode_;uint8_t memory_[4096]; // 4Kuint8_t v_register_[16];uint16_t index_register_;// Указывает на следующую инструкцию в памяти, которую нужно выполнить.uint16_t program_counter_;// Таймеры на 60 Гц.uint8_t delay_timer_;uint8_t sound_timer_;uint16_t stack_[16];// Указывает на следующую пустую ячейку стека.uint16_t stack_pointer_;// 0 если ни одна клавиша не нажата.uint8_t keypad_state_[16];};

Мы специально используем целочисленные типы. Это позволяет обеспечить правильность обработки ситуаций, связанных с исчезновением значащих разрядов и переполнением. Для 12-битных значений нам нужно использовать 16-битные типы. У нас, кроме того, имеется 16 клавиш, состояние которых (нажата клавиша или нет) тоже хранится в этом классе. Когда мы подключим подсистему обработки ввода, мы найдём способ передачи соответствующих данных в класс между циклами. Работать с кодами операций несложно благодаря тому, что все инструкции CHIP-8 имеют длину, равную 2 байта.

Это даёт нам возможность обрабатывать 0xFFFF (65535) инструкций (хотя многие из них не используются). Мы, на самом деле, можем сохранить все возможные инструкции в контейнере map. И, когда получаем код операции, можем просто тут же выполнить инструкцию, обращаясь к связанной с кодом операции сущности Instruction из instructions_. Мы не привязываем особенно много данных к функциям, в результате весь контейнер map с инструкциями должен поместиться в кеш-памяти.

Функция Initialize это то место, где осуществляется настройка описанной выше схемы распределения памяти:

// cpu_chip8.cppCpuChip8::Initialize(const std::string& rom) {current_opcode_ = 0;std::memset(memory_, 0, 4096);std::memset(v_registers_, 0, 16);index_register_ = 0;// Память, предназначенная для программ, начинается с адреса 0x200.program_counter_ = 0x200;delay_timer_ = 0;sound_timer_ = 0;std::memset(stack_, 0, 16);stack_pointer_ = 0;std::memset(keypad_state_, 0, 16);uint8_t chip8_fontset[80] ={0xF0, 0x90, 0x90, 0x90, 0xF0, // 00x20, 0x60, 0x20, 0x20, 0x70, // 10xF0, 0x10, 0xF0, 0x80, 0xF0, // 20xF0, 0x10, 0xF0, 0x10, 0xF0, // 30x90, 0x90, 0xF0, 0x10, 0x10, // 40xF0, 0x80, 0xF0, 0x10, 0xF0, // 50xF0, 0x80, 0xF0, 0x90, 0xF0, // 60xF0, 0x10, 0x20, 0x40, 0x40, // 70xF0, 0x90, 0xF0, 0x90, 0xF0, // 80xF0, 0x90, 0xF0, 0x10, 0xF0, // 90xF0, 0x90, 0xF0, 0x90, 0x90, // A0xE0, 0x90, 0xE0, 0x90, 0xE0, // B0xF0, 0x80, 0x80, 0x80, 0xF0, // C0xE0, 0x90, 0x90, 0x90, 0xE0, // D0xF0, 0x80, 0xF0, 0x80, 0xF0, // E0xF0, 0x80, 0xF0, 0x80, 0x80 // F};// Загрузка встроенного набора шрифтов в адреса 0x050-0x0A0std::memcpy(memory_ + 0x50, chip8_fontset, 80);// Загрузка ROM в память, предназначенную для программы.std::ifstream input(filename, std::ios::in | std::ios::binary);std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(input)),(std::istreambuf_iterator<char>()));if (bytes.size() > kMaxROMSize) {throw std::runtime_error("File size is bigger than max rom size.");} else if (bytes.size() <= 0) {throw std::runtime_error("No file or empty file.");}std::memcpy(memory_ + 0x200, bytes.data(), bytes.size());BuildInstructionSet();}

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

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

// cpu_chip8.cppvoid CpuChip8::RunCycle() {// Прочитать слово кода операции в формате big-endian.current_opcode_ = memory_[program_counter_] << 8 |memory_[program_counter_ + 1];auto instr = instructions_.find(current_opcode_);if (instr != instructions_.end()) {instr->second();} else {throw std::runtime_error("Couldn't find instruction for opcode " +std::to_string(current_opcode_));}// TODO: Обновить таймеры, отвечающие за звук и задержку.}

Тут, в общем-то, всё устроено очень просто: мы ищем инструкцию, которую надо выполнить. Единственное, что тут может показаться необычным, это то, как выполняется чтение следующего кода операции. CHIP-8 использует формат big-endian. Это означает, что наиболее значимая часть слова идёт первой, а за ней идёт наименее значимая часть слова. В современных системах, основанных на архитектуре x86, используется обратный порядок представления данных (little-endian).

Memory location 0x000: 0xFFMemory location 0x001: 0xABBig endian interpretation:  0xFFABLittle endian interpretation: 0xABFF

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

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

// cpu_chip8.cpp#define NEXT program_counter_ += 2#define SKIP program_counter_ += 4void CpuChip8::BuildInstructionSet() {instructions_.clear();instructions_.reserve(0xFFFF);instructions_[0x00E0] = [this]() { frame_.SetAll(0); NEXT; }; // CLSinstructions_[0x00EE] = [this]() {program_counter_ = stack_[--stack_pointer_] + 2; // RET};for (int opcode = 0x1000; opcode < 0xFFFF; opcode++) {uint16_t nnn = opcode & 0x0FFF;uint8_t kk =  opcode & 0x00FF;uint8_t x =   (opcode & 0x0F00) >> 8;uint8_t y =   (opcode & 0x00F0) >> 4;uint8_t n =   opcode & 0x000F;if ((opcode & 0xF000) == 0x1000) {instructions_[opcode] = GenJP(nnn);} else if ((opcode & 0xF000) == 0x2000)) {instructions_[opcode] = GenCALL(nnn);}// ...}

В каждой инструкции могут быть закодированы какие-то параметры, которые мы декодируем и, по мере возникновения необходимости в них, используем. Тут мы, для генерирования функций std::function, можем воспользоваться std::bind, но я, в данном случае, решил объявить функции в виде Gen[INSTRUCTION_NAME], что позволяет возвращать функции в виде лямбда-выражений с привязанными к ним данными.

Рассмотрим ещё некоторые интересные функции.

// cpu_chip8.cppCpuChip8::Instruction CpuChip8::GenJP(uint16_t addr) {return [this, addr]() { program_counter_ = addr; };}

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

// cpu_chip8.cppCpuChip8::Instruction CpuChip8::GenCALL(uint16_t addr) {return [this, addr]() {stack_[stack_pointer_++] = program_counter_;program_counter_ = addr;};}

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

// cpu_chip8.cppCpuChip8::Instruction CpuChip8::GenSE(uint8_t reg, uint8_t val) {return [this, reg, val]() {v_registers_[reg] == val ? SKIP : NEXT;};}

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

// cpu_chip8.cppCpuChip8::Instruction CpuChip8::GenADD(uint8_t reg_x, uint8_t reg_y) {return [this, reg_x, reg_y]() {uint16_t res = v_registers_[reg_x] += v_registers_[reg_y];v_registers_[0xF] = res > 0xFF; // set carryv_registers_[reg_x] = res;NEXT;};}CpuChip8::Instruction CpuChip8::GenSUB(uint8_t reg_x, uint8_t reg_y) {return [this, reg_x, reg_y]() {v_registers_[0xF] = v_registers_[reg_x] > v_registers_[reg_y]; // set not borrowv_registers_[reg_x] -= v_registers_[reg_y];NEXT;};}

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

// cpu_chip8.cppCpuChip8::Instruction CpuChip8::GenLDSPRITE(uint8_t reg) {return [this, reg]() {uint8_t digit = v_registers_[reg];index_register_ = 0x50 + (5 * digit);NEXT;};}

Наша функция загрузки спрайтов достаточно проста. Она используется программой для выяснения того, где именно во встроенном наборе шрифтов находится определённый символ. Тут стоит помнить о том, что встроенный набор шрифтов мы сохранили по адресу 0x50, и то, что каждый символ описывается последовательностью из 5 байтов. Поэтому мы и устанавливаем I, пользуясь конструкцией 0x50 + 5 * digit.

// cpu_chip8.cppCpuChip8::Instruction CpuChip8::GenSTREG(uint8_t reg) {return [this, reg]() {for (uint8_t v = 0; v <= reg; v++) {memory_[index_register_ + v] = v_registers_[v];}NEXT;};}CpuChip8::Instruction CpuChip8::GenLDREG(uint8_t reg) {return [this, reg]() {for (uint8_t v = 0; v <= reg; v++) {v_registers_[v] = memory_[index_register_ + v];}NEXT;};}

Когда мы напрямую работаем с памятью, пользователь предоставляет максимальный регистр из последовательности регистров, в которые нужно загрузить данные. Например, если надо загрузить данные, последовательно хранящиеся в MEM[I], в регистры V0, V1 и V2, то, после установки I, передаётся регистр V2.

Итоги


Только что мы создали интерпретатор CHIP-8! Конечно, к нему пока не подключены звуковая и графическая подсистемы, но на нём уже можно запустить простые тестовые ROM, в которых соответствующие возможности не используются. Следующая часть этой серии статей посвящена разработке графической подсистемы эмулятора. Вывод графики это самая сложная из задач, решаемых нашей системой.



Подробнее..

Перевод Эмуляция компьютера интерпретатор CHIP-8 и формирование изображений

01.01.2021 18:11:05 | Автор: admin
Недавно мы опубликовали перевод первого материала из серии статей, посвящённой эмуляции компьютера. Автор этих статей подробно рассказывает о написании интерпретатора CHIP-8 на C++. В той публикации мы устроили опрос о целесообразности перевода продолжения цикла. Почти 94% тех, кто принял участие в опросе, продолжение перевода поддержали. Поэтому сегодня мы представляем вашему вниманию второй материал о CHIP-8.



Подготовка к выводу изображений


В прошлый раз мы написали интерпретатор CHIP-8, который способен выполнять все операции за исключением одной Dxyn (DRW Vx, Vy, nibble). Ради упрощения реализации этой инструкции мы инкапсулируем графическую память и код в классе Image. Кадр размером 64x32 пикселя будет представлен в виде единого фрагмента данных в памяти. Каждому пикселю будет соответствовать один байт:

0x000:|--------------------------------------------------------------|0x040:|                               |0x080:|                               |0x0C0:|                               |...0x7C0:|--------------------------------------------------------------|

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

img[col=0, row=0] = img[0]img[col=0, row=1] = img[width]img[col=1, row=3] = img[3*width+1]

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

// image.hclass Image {public:// Выделение и освобождение памяти в ctor и dtor.Image(int cols, int rows);~Image();uint8_t* Row(int r);// Возвращает пиксель, который может быть изменён.uint8_t& At(int c, int r);void SetAll(uint8_t value);private:int cols_;int rows_;uint8_t* data_;};

Тут надо обратить внимание на то, что мы динамически выделяем память, владельцем которой будет этот класс. В более крупной системе мы могли бы решить воспользоваться std::unique_ptr вместе с особой функцией для выделения памяти. Но тут мы просто используем malloc в конструкторе и free в деструкторе класса.

// image.cppImage::Image(int cols, int rows) {data_ = static_cast<uint8_t*>(malloc(cols * rows * sizeof(uint8_t)));cols_ = cols;rows_ = rows;}Image::~Image() {free(data_);}uint8_t* Image::Row(int r) {return &data_[r * cols_];}uint8_t& Image::At(int c, int r) {return Row(r)[c];}void Image::SetAll(uint8_t value) {std::memset(data_, value, rows_ * cols_);}void Image::DrawToStdout() {for (int r = 0; r < rows_; r++) {for (int c = 0; c < cols_; c++) {if (At(c,r) > 0) {std::cout << "X";} else {std::cout << " ";}}std::cout << std::endl;}std::cout << std::endl;}

Здесь мне удобнее пользоваться такими именами переменных, как rows_ (строки) и cols_ (столбцы), а не x и y. Дело в том, что имя переменной rows чётко ассоциируется у меня со строками, а вот о том, что такое x, я вполне могу забыть. Функция At возвращает uint8_t&, что даёт нам возможность и получать значения отдельных пикселей, и устанавливать эти значения. Это плохо с точки зрения инкапсуляции, но такой приём часто используется в графических API. Мы, кроме того, предусмотрели тут удобную функцию DrawToStdout, которая позволяет выводить в консоль то, что должно быть отображено на экране, делая это даже тогда, когда подсистема графического вывода эмулятора ещё не реализована. Сейчас мы можем добавить в класс CpuChip8 поле frame_ типа Image и поработать над реализацией соответствующих механизмов.

// cpu_chip8.hclass CpuChip8 {public:constexpr innt kFrameWidth = 64;constexpr innt kFrameHeight = 32;CpuChip8() : frame_(kFrameWidth, kFrameHeight) {}...private:...Image frame_;};

Теперь давайте поговорим о том, как CHIP-8 выполняет вывод графических данных. А именно, рисование спрайта в текущем (и единственном) кадровом буфере выполняется по принципам, используемым в операции XOR. Все спрайты описываются в виде изображений с глубиной цвета в 1 бит (каждый пиксель может быть либо включен, либо выключен). Ширина спрайта равняется 8 битам, высота может меняться. Ограничение на ширину спрайта применяется из-за того, что каждый пиксель спрайта представлен единственным битом. Посмотрим на описание набора шрифтов, присутствующее в предыдущем материале, и попробуем расшифровать одну из цифр.

0xF0, 0x90, 0x90, 0x90, 0xF0, // 00xF0 это 1111 0000 -> XXXX0x90 это 1001 0000 -> X X0x90 это 1001 0000 -> X X0x90 это 1001 0000 -> X X0xF0 это 1111 0000 -> XXXX

Замечательно! Помните, я говорил о том, что вывод графики основан на операции XOR? Так вот, это значит, что единственный способ убрать спрайт с экрана заключается в том, чтобы вывести ещё один спрайт поверх него (фактически тот же самый спрайт), так как 1 1 даёт 0. Именно поэтому при работе с CHIP-8-программами часто заметно мерцание, так как спрайты постоянно выводятся на экран и стираются с него для вывода движущихся объектов.

Итак, мы готовы к тому, чтобы создать функцию для вывода спрайтов. Нам понадобится начальная точка и сам спрайт (область памяти). Так как спрайты могут иметь переменную высоту, мы получаем и соответствующий параметр, описывающий её. Отмечу, что одна особенность интерпретатора CHIP-8 потребовала некоторого времени на её отладку. Она заключается в том, что интерпретатор поддерживает вывод графики за пределами экрана. Когда спрайт выходит за границы экрана, его рисование продолжается на другой стороне экрана. Это поведение проявляется и при указании стартовых координат спрайта (то есть вывод 15 строк в координате 255,255 это совершенно нормально). Кроме того, интерпретатору нужно сообщать о том, был ли при выводе спрайта стёрт какой-нибудь пиксель (это часто используется для обнаружения столкновений объектов, выводимых на экран).

// image.cpp// Возвращает true в том случае, если новое значение стирает пиксель.bool Image::XOR(int c, int r, uint8_t val) {uint8_t& current_val = At(c, r);uint8_t prev_val = current_val;current_val ^= val;return current_val == 0 && prev_val > 0;}bool Image::XORSprite(int c, int r, int height, uint8_t* sprite) {// Переход на другую сторону экрана при выводе спрайта.bool pixel_was_disabled = false;for (int y = 0; y < height; y++) {int current_r = r + y;while (current_r >= rows_) { current_r -= rows_; }uint8_t sprite_byte = sprite[y];for (int x = 0; x < 8; x++) {int current_c = c + x;while (current_c >= cols_) { current_c -= cols_; }// Обратите внимание: Сканирование выполняется от MSbit до LSbituint8_t sprite_val = (sprite_byte & (0x80 >> x)) >> (7-x);pixel_was_disabled |= XOR(current_c, current_r, sprite_val);}}return pixel_was_disabled;}

Нам нужно позаботиться о том, чтобы извлекать биты, представляя их значениями 1 или 0. Так как класс Image поддерживает [0..255], операции XOR, без этого ограничения, могут наделать много беспорядка. Когда же применяется это ограничение, соответствующая инструкция нашего CPU получается очень простой нужно всего лишь извлечь параметры, необходимые для вызова XORSprite.

CpuChip8::Instruction CpuChip8::GenDRAW(uint8_t reg_x, uint8_t reg_y, uint8_t n_rows) {return [this, reg_x, reg_y, n_rows]() {uint8_t x_coord = v_registers_[reg_x];uint8_t y_coord = v_registers_[reg_y];bool pixels_unset = frame_.XORSprite(x_coord, y_coord, n_rows,memory_ + index_register_);v_registers_[0xF] = pixels_unset;NEXT;};}

Итоги


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

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

Если бы вы писали собственный интерпретатор CHIP-8 каким языком программирования вы бы пользовались?

Подробнее..

Перевод Эмуляция компьютера интерпретатор CHIP-8, таймеры и обработка ввода

03.01.2021 20:22:24 | Автор: admin
Мы уже создали вполне рабочий эмулятор CHIP-8, но он, к сожалению, получился очень медленным. Почему? Если заглянуть в его главный цикл можно увидеть, что данные на экран выводятся после выполнения каждого шага цикла. При включённом vsync SDL пытается привязать скорость рендеринга к частоте обновления кадров дисплея (возможно это 60 Гц). Для нас это означает то, что метод SDLViewer::Update, почти при каждом его вызове, долго будет находиться в заблокированном состоянии, ожидая сигнала vsync от монитора.



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

Правда, мы можем поступить иначе. Известно, что в течение секунды должно быть выполнено, в среднем, 540 операций. Конечно, это будет не так в том случае, если каждая из инструкций будет представлять собой что-то сложное, вроде вывода графики, но в реальных программах такой подход жизнеспособен. Мы, кроме того, знаем о том, что CHIP-8-компьютеры рассчитаны на частоту обновления экрана в 60 Гц. Это означает, что наш эмулятор должен ждать до следующего vsync столько времени, сколько нужно на выполнение 9 (540/60) инструкций.

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

// main.cppvoid Run() {...while (!quit) {for (int i = 0; i < 9; i++) {cpu.RunCycle();}cpu.GetFrame()->CopyToRGB24(rgb24, /*r=*/255, /*g=*/0, /*b=*/0);viewer.SetFrameRGB24(rgb24, height);auto events = viewer.Update();...}}

Благодаря использованию вышеприведённого кода весьма велики шансы того, что эмулятор теперь выполняет 540 инструкций в секунду и выводит графику с частотой 60 Гц!

Правда, если попытаться запустить эмулятор на компьютере, частота обновления экрана которого больше 60 Гц, характеристики аппаратного обеспечения уже не будут соответствовать параметрам эмулятора. Это приведёт к тому, что эмулятор будет работать слишком быстро. Если вы столкнулись с этой проблемой вы можете сэмулировать и vsync. Известно, что выполнение 9 инструкций и рендеринг кадра должны занимать 16,67 мс (1000/60). Поэтому можно выполнить данные операции, измерить время, необходимое на их выполнение, а после этого усыпить программу на столько, сколько понадобится. Так как это время не удастся вычислить достаточно точно, можно измерить и время выполнения 540 операций (60 сэмулированных циклов обновления экрана) и усыпить программу до наступления новой секунды для того чтобы внести необходимые коррективы в длительность vsync-сна. Именно это я и сделал в исходной версии эмулятора, на которой основана эта серия статей. В том проекте, кроме того, для эмуляции CPU использовался отдельный поток. В этом, вероятно, необходимости нет, но получилось, всё равно, очень интересно.

Теперь, когда эмулятор выполняет 540 инструкций в секунду, поддержка CPU-таймеров реализуется весьма просто. В CHIP-8 имеется два таймера: таймер задержки и звуковой таймер. Значения обоих таймеров 60 раз в секунду уменьшаются на 1. Компьютер издаёт звуки до тех пор, пока звуковой таймер не равен 0. Теперь можно просто, каждый раз, когда выполнено 9 инструкций, уменьшать значения, хранящиеся в таймерах, на 1. Это даст нам нужную скорость работы таймеров:

// cpu_chip8.cppvoid CpuChip8::RunCycle() {...// Обновление значений, хранящихся в таймерахnum_cycles_++;if (num_cycles_ % 9 == 0) {if (delay_timer_ > 0) delay_timer_--;if (sound_timer_ > 0) {std::cout << "BEEPING" << std::endl;sound_timer_--;}}}

Нам осталось поговорить лишь о вводе данных в систему. Это простая задача, которая сводится к обработке событий, возвращённых из SDLViewer::Update. Как уже было сказано, в CHIP-8 используется 16-клавишная клавиатура. Эти клавиши можно привязать к любым кнопкам обычной клавиатуры. Вот фрагмент кода, отвечающий за обработку ввода:

// main.cpp...auto events = viewer.Update();uint8_t cpu_keypad[16];for (const auto& e : events) {if (e.type == SDL_KEYDOWN || e.type == SDL_KEYUP) {if (e.key.keysym.sym == SDLK_1) {cpu_keypad[0] = e.type == SDL_KEYDOWN;} else if (e.key.keysym.sym == SDLK_2) {cpu_keypad[1] = e.type == SDL_KEYDOWN;} else if (e.key.keysym.sym == SDLK_3) {cpu_keypad[2] = e.type == SDL_KEYDOWN;} else if (e.key.keysym.sym == SDLK_4) {cpu_keypad[3] = e.type == SDL_KEYDOWN;} else if (e.key.keysym.sym == SDLK_q) {cpu_keypad[4] = e.type == SDL_KEYDOWN;}...}}cpu.SetKeypad(cpu_keypad);// cpu_chip8.cppvoid CpuChip8::SetKeypad(uint8_t* keys) {std::memcpy(keypad_state_, keys, 16);}

Итоги


Наш эмулятор готов. Мы создали полноценный интерпретатор CHIP-8. Он поддерживает монохромный кадровый буфер и систему, преобразующую данные, хранящиеся в этом буфере, в RGB-изображения, которые передаются на GPU в виде текстур. А только что мы настроили временные параметры работы эмулятора и подключили к нему подсистему для обработки ввода.

Не знаю, как вы, а о себе могу сказать, что я, работая над этим проектом, узнал много нового. Мне очень понравились ощущения, которые испытываешь, создавая что-то с нуля и доводя до завершения. А этот проект особенно хорош тем, что, окончив работу над эмулятором, я смог запускать на нём настоящие программы для CHIP-8. Завершив работу над этим проектом я ещё на шаг приблизился к пониманию того, что происходит в недрах Super Mario World.

Планируете ли вы написать собственный интерпретатор CHIP-8?

Подробнее..

Перевод Эмуляция компьютера интерпретатор CHIP-8, графика и стриминг текстур

10.01.2021 16:05:45 | Автор: admin
В прошлый раз мы остановились на том, что создали интерпретатор CHIP-8 и оснастили его системой для формирования кадров. Видеть то, что должно попасть на экран, можно в консоли. Теперь же мы собираемся взять то, что формирует интерпретатор, вынести это за пределы консоли и показать на экране.

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



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

Изображение, формируемое средствами класса Image, представлено в некоем графическом формате. Но SDL понимает лишь определённый набор пиксельных форматов. Если взглянуть на эти форматы, то окажется, что нам вполне может подойти SDL_PIXELFORMAT_RGB24. Настроим класс SDLViewer, который будет стримить изображения в этом формате, а чуть позже поразмыслим о том, как преобразовать данные нашего кадрового буфера в RGB24.

// sdl_viewer.h// SDL-окно RAII с поддержкой аппаратного ускорения.// Оптимизировано для стриминга RGB24-текстур.class SDLViewer {public:// Ширина и высота должны быть равны параметрам изображения, загружаемого// через SetFrameRGB24.SDLViewer(const std::string& title, int width, int height, int window_scale = 1);~SDLViewer();// Рендеринг текущего кадра, возврат списка событий.std::vector<SDL_Event> Update();// Предполагается, что это - 8-битное RGB-изображение, ширина которого в байтах равна его ширине в пикселях (без необходимости использовать заполнители).void SetFrameRGB24(uint8_t* rgb24, int height);private:SDL_Window* window_ = nullptr;SDL_Renderer* renderer_ = nullptr;SDL_Texture* window_tex_ = nullptr;};

Мы планируем использовать этот класс как SDL-окно RAII, которое получает актуальные сведения о текстурах и выполняет рендеринг. Конструктор принимает показатель масштабирования окна, так как если попытаться вывести на экран изображение размером 64x32 пикселя без масштабирования, оно окажется очень маленьким.

SDLViewer::SDLViewer(const std::string& title, int width, int height, int window_scale) :title_(title) {if(SDL_Init(SDL_INIT_VIDEO) < 0) {throw std::runtime_error(SDL_GetError());}// Создание SDL-окна с учётом коэффициента масштабирования.window_ = SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, width * window_scale, height * window_scale, SDL_WINDOW_SHOWN);// Настройка аппаратной системы рендеринга и текстуры, которую мы будем стримить.renderer_ = SDL_CreateRenderer(window_, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);window_tex_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGB24,SDL_TEXTUREACCESS_STREAMING, width, height);}SDLViewer::~SDLViewer() {SDL_DestroyTexture(window_tex_);SDL_DestroyRenderer(renderer_);SDL_DestroyWindow(window_);SDL_Quit();}std::vector<SDL_Event> SDLViewer::Update() {std::vector<SDL_Event> events;SDL_Event e;while (SDL_PollEvent(&e)) { events.push_back(e); }// Рендеринг текстуры.SDL_RenderCopy(renderer_, window_tex_, NULL, NULL );SDL_RenderPresent(renderer_);return events;}void SDLViewer::SetFrameRGB24(uint8_t* rgb24, int height) {void* pixeldata;int pitch;// Блокировка текстуры и загрузка изображения в GPU.SDL_LockTexture(window_tex_, nullptr, &pixeldata, &pitch);std::memcpy(pixeldata, rgb24, pitch * height);SDL_UnlockTexture(window_tex_);}

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

Загрузка текстуры в GPU выполняется в SetFrameRGB24. Функция принимает сведения о фрагменте памяти, в котором хранится изображение в нужном формате, а так же сведения о высоте изображения. SDL_LockTexture возвращает CPU-память для копирования графических данных. Ещё эта функция возвращает длину строки изображения в байтах. После того, как изображение скопировано в выделенный участок памяти, мы вызываем функцию SDL_UnlockTexture, которая выгружает изображение в GPU в виде новой текстуры.

Теперь нам надо отредактировать код главного цикла, сделав так, чтобы в нём использовалось бы новое окно.

// main.cppvoid Run() {int width = 64;int height = 32;SDLViewer viewer("CHIP-8 Emulator", width, height, /*window_scale=*/8);uint8_t* rgb24 = static_cast<uint8_t*>(std::calloc(width * height * 3, sizeof(uint8_t)));viewer.SetFrameRGB24(rgb24, height);CpuChip8 cpu;cpu.Initialize("/path/to/program/file");bool quit = false;while (!quit) {cpu.RunCycle();cpu.GetFrame()->CopyToRGB24(rgb24, /*r=*/255, /*g=*/0, /*b=*/0);viewer.SetFrameRGB24(rgb24, height);auto events = viewer.Update();for (const auto& e : events) {if (e.type == SDL_QUIT) {quit = true;}}}}

Мы инициализируем RGB24-картинку пустым изображением (нулями, чёрным цветом). Обратите внимание на то, что размер этого изображения вычисляется не как width * height (ширина * высота), а как width * height * 3 (ширина * высота * 3). Мы ведь работаем с RGB-изображением, имеющим 3 цветовых канала. Загрузка текстуры и вывод её на экран выполняются в каждом цикле. Из-за использования vsync оказывается, что эмулятор работает очень медленно. Но мы это исправим, добравшись до настройки временных параметров работы эмулятора. Теперь нам осталось лишь разобраться в том, что собой представляет графический формат RGB24, и реализовать Image::CopyToRGB24.

При создании RGB-изображений данные красного (red), зелёного (green) и синего (blue) цветовых каналов каждого пикселя часто идут в памяти друг за другом. Поэтому простое добавление 1 к адресу памяти уже необязательно позволит нам получить значение, соответствующее следующему пикселю.

0x000 :|RGBRGBRGB...----------------------------------------|0x040*3:|RGBRGBRGB...                    |0x080*3:|RGBRGBRGB...                    |..0x7C0*3:|RGBRGBRGB...----------------------------------------|

Нам, прежде чем мы сможем это обсудить, понадобится ввести некоторые новые термины. То, что называется stride или pitch, представляет собой ширину строки изображения в байтах. В данном случае это 3 * width_px (3 * ширина в пикселях). Мы можем говорить о байтовой ширине строки изображения и в смысле её отношения к цветовым каналам. Для того чтобы перейти от одного значения красного цвета (канала) в некоем пикселе к такому же значению для следующего пикселя, мы должны прибавить к адресу этого первого значения 3 (это называется 0-dimension stride). То же самое справедливо и для синего, и для зелёного каналов. При этом каждое отдельное значение, как и прежде, представлено 8 битами (значение может находиться в диапазоне от 0 до 255), но для описания каждого пикселя теперь нужно 3 значения (число 24 в названии RGB24, в результате, означает результат умножения 3 каналов на 8 битов). Собственно говоря, теперь у нас, похоже, есть всё необходимое для того чтобы сгенерировать изображение нужного формата на основе нашего монохромного изображения.

// image.cppvoid Image::CopyToRGB24(uint8_t* dst, int red_scale, int green_scale, int blue_scale) {int cols = Cols();for (int row = 0; row < Rows(); row++) {for (int col = 0; col < cols; col++) {dst[(row * cols + col) * 3] = At(col, row) * red_scale;dst[(row * cols + col) * 3 + 1] = At(col, row) * green_scale;dst[(row * cols + col) * 3 + 2] = At(col, row) * blue_scale;}}}

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

Итоги


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

Как вы думаете, почему эмулятор CHIP-8 столь популярен?
Подробнее..

Перевод Пишем макет 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:
Здесь я привожу пример написания команд и перехода строки при нажатии Ввода.



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

Заключение


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

Подробнее..

Конструктор Lego и объектно-ориентированное программирование в Tcl. Разбор сертификата x509.v3

21.12.2020 18:22:45 | Автор: admin
imageЧасто приходится слышать, что скриптовому языку Tcl не хватает поддержки объектно-ориентированного стиля программирования. Сам я до последнего времени мало прибегал к объектно-ориентированному программированию, тем более в среде Tcl. Но за Tcl стало обидно. Я решил разобраться. И оказалось, что практически с момента своего появления появилась возможность объектно-ориентированного программирования (ООП) в среде Tcl. Все неудобство заключалось в необходимости подключить пакет с поддержкой ООП. А таких пакетом было и есть несколько, как говорится на любой вкус. Это и Incr Tcl, Snit и XoTcl.
Программисты, привыкшие к языку C++, чувствуют себя как дома, программируя в среде Incr Tcl. Это было одним из первых широко используемых расширений для OOП на основе Tcl.
Пакет Snit в основном используется при построении Tk-виджетов, а XoTcl и его преемник nx предназначались для исследования динамического объектно-ориентированного программирования.
Обобщение опыта, полученного при использовании этих систем, позволило внедрить ООП в ядро Tcl начиная с версии 8.6. Так появился TclOO Tcl Object Oriented.
Сразу отметим, что Tcl не просто поддерживает объектно-ориентированное программирование, а в полном смысле динамическое объектно-ориентированное программирование.
Разрабатывая приложения на Tcl/Tk, например удостоверяющий центр CAFL63, я не прибегал к ООП. И, как сейчас понимаю, зря. Где, где, а в УЦ объектов хватает. Это и запросы на сертификаты, это и сами сертификаты, списки отозванных сертификатов и много чего другого:



Начать было решено с рассмотрения сертификата x509.v3 с учетом российской специфики как объекта при ООП. Тем более, что имеется опыт разбора квалифицированного сертификата на Python. Именно на примере разбора и работы с сертификатом мы и покажем объектно-ориентированный стиль программирования в TclOO.

О DER и BER кодировках

Для доступа к сертификату будет создан класс certificate, в конструкторе которого при создании объекта конкретного сертификата будет проводится разбор его на составные части. Для этого нам потребуется в первую очередь пакет asn (package require asn), который поможет с разбором asn-структуры сертификата. К сожалению, этот пакет (кстати, в других скриптовых языках встречается аналогичная проблема) заточен на разбор asn-структур в DER-кодировке. Но сегодня еще встречаются сертификаты (и электронные подписи и много чего другого) в BER-кодировке. Но оказалось решить эту проблему можно достаточно просто, заменив процедуру ::asn::asnLength из пакета ASN на новую, которая будет подсчитывать длины тега как в DER, так и BER-кодировках:
package require asn#Переименовываем оригинальную процедуру подсчета длины rename ::asn::asnGetLength ::asn::asnGetLength.orig#Новая процедура подсчета длиныproc ::asn::asnGetLength {data_var length_var} {    upvar 1 $data_var data  $length_var length    asnGetByte data length    if {$length == 0x080} {#Поддержка BER-кодировкиset lendata [string length $data]set tvl 1set length 0set data1 $datawhile {$tvl != 0} {    ::asn::asnGetByte data1 peek_tag     ::asn::asnPeekByte data1 peek_tag1    if {$peek_tag == 0x00 && $peek_tag1 == 0x00} {incr tvl -1::asn::asnGetByte data1 tag incr length 2continue    }    if {$peek_tag1 == 0x80} {incr tvlif {$tvl > 0} {    incr length 2}::asn::asnGetByte data1 tag     } else {set l1 [string length $data1]::asn::asnGetLength data1 llset l2 [string length $data1]set l3 [expr $l1 - $l2]incr length $l3incr length $llincr length::asn::asnGetBytes data1 $ll strt    }}return    }    if {$length > 0x080} {        set len_length [expr {$length & 0x7f}]          if {[string length $data] < $len_length} {            return -code error \"length information invalid, not enough octets left"         }        asnGetBytes data $len_length lengthBytes        switch $len_length {            1 { binary scan $lengthBytes     cu length }            2 { binary scan $lengthBytes     Su length }            3 { binary scan \x00$lengthBytes Iu length }            4 { binary scan $lengthBytes     Iu length }            default {                                binary scan $lengthBytes H* hexstrscan $hexstr %llx length            }        }    }    return}

Что нам еще потребуется? Любая ASN-структура, особенно такая как сертификат X509.v3 содержит большое количество OID-ов, для которых могут существовать достаточно общепризнанные символьные обозначения. Значительная часть OID-ов, которые используются в сертификатах, присутствует в пакете pki. Мы его тоже будем использовать (package require pki). Естественно, что в этом пакете ничего не известно об OID-ах, которые используются в квалифицированных сертификатах и об OID-ах для российской криптографии. Их тоже целесообразно добавить в массив ::pki::oids:
set ::pki::oids(1.2.643.100.1)  "OGRN"set ::pki::oids(1.2.643.100.5)  "OGRNIP"set ::pki::oids(1.2.643.3.131.1.1) "INN"set ::pki::oids(1.2.643.100.3) "SNILS"#Для КПП ЕГАИСset ::pki::oids(1.2.840.113549.1.9.2) "UN"#set ::pki::oids(1.2.840.113549.1.9.2) "unstructuredName"#Алгоритмы подписиset ::pki::oids(1.2.643.2.2.3) "GOST R 34.10-2001 with GOST R 34.11-94"set ::pki::oids(1.2.643.2.2.19) "GOST R 34.10-2001"set ::pki::oids(1.2.643.7.1.1.1.1) "GOST R 34.10-2012-256"set ::pki::oids(1.2.643.7.1.1.1.2) "GOST R 34.10-2012-512"set ::pki::oids(1.2.643.7.1.1.3.2) "GOST R 34.10-2012-256 with GOSTR 34.11-2012-256"set ::pki::oids(1.2.643.7.1.1.3.3) "GOST R 34.10-2012-512 with GOSTR 34.11-2012-512"set ::pki::oids(1.2.643.100.113.1) "KC1 Class Sign Tool"set ::pki::oids(1.2.643.100.113.2) "KC2 Class Sign Tool"set ::pki::oids(2.5.4.42)  "givenName"

Для полноты не мешает также добавить символьное представление параметров подписи:
#Параметры подписи
#Параметры подписиset ::pki::oids((1.2.643.2.2.35.1)"id-GostR3410-2001-CryptoPro-A-ParamSet"set ::pki::oids(1.2.643.2.2.35.2)"id-GostR3410-2001-CryptoPro-B-ParamSet"set ::pki::oids(1.2.643.2.2.35.3)"id-GostR3410-2001-CryptoPro-C-ParamSet"set ::pki::oids(1.2.643.2.2.36.0)"id-GostR3410-2001-CryptoPro-XchA-ParamSet"set ::pki::oids(1.2.643.2.2.36.1)"id-GostR3410-2001-CryptoPro-XchB-ParamSet"set ::pki::oids(1.2.643.7.1.2.1.1.1)"id-tc26-gost-3410-2012-256-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.1.2)"id-tc26-gost-3410-2012-256-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.1.3)"id-tc26-gost-3410-2012-256-paramSetC"set ::pki::oids(1.2.643.7.1.2.1.1.4)"id-tc26-gost-3410-2012-256-paramSetD"set ::pki::oids(1.2.643.7.1.2.1.2.1)"id-tc26-gost-3410-2012-512-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.2.2)"id-tc26-gost-3410-2012-512-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.2.3)"id-tc26-gost-3410-2012-512-paramSetC"


Создание класса

Объявление класса в TclOO мало чем отличается от объявления класса в других языках. Класс в TclOO также содержит область данных, конструктор, область объектно-ориентированных методов и деструктор. При этом область данных, конструктор и деструктор могут опускаться. Напомним, что конструктор вызывается при создание объекта (экземпляра объекта) заданного класса, а деструктор при его уничтожении. Конструктор (в отличии от деструктора), также как и методы, может иметь параметры. В нашем случае параметром для конструктора выступает сертификат в DER или PEM кодировке.
Области данных может предшествовать область наследуемых классов (superclass). Её будем рассматривать ниже. Но, для написания универсального класса certificate, эта область будет нами задействована.
В TclOO можно узнать какие классы в данный момент доступны в программе. Для этих целей служит команда следующего вида:
info class instances oo::class

В последующем, мы будем задействовать для наследования класс pubkey. Поэтому в нашем определении класса certificate присутствует проверка наличия класса pubkey и, если он присутствует, он объявляется как наследуемый (superclass pubkey).
Итак, ниже представлен класс для сертификата пока что с одним методом parse_cert, который возвращает список элементов сертификата:
Объявление класса certificate
oo::class create certificate {#Список доступных классов    foreach cl  "[info class instances oo::class]" {if {$cl == "::pubkey" } {#Если класс pubkey есть, то наследуем его. Это будет использовано в примере 3    superclass pubkey    break}    }#Переменные класса#Доступны только в пределах класса#Переменная для хранения разобранного сертификата.     variable ret#Переменная для хранения расширений сертификата    variable extcert#Конструктор    constructor {cert} {array set parsed_cert [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]set cert_seq $parsed_cert(data)array set ret [list]#Полный сертификат der в hexbinary scan $cert_seq H* ret(cert_full)  # Decode X.509 certificate, which is an ASN.1 sequence::asn::asnGetSequence cert_seq wholething::asn::asnGetSequence wholething cert#tbs - сертификатset ret(tbsCert) [::asn::asnSequence $cert]binary scan $ret(tbsCert) H* ret(tbsCert)::asn::asnPeekByte cert peek_tagif {$peek_tag != 0x02} {    # Version number is optional, if missing assumed to be value of 0    ::asn::asnGetContext cert - asn_version    ::asn::asnGetInteger asn_version ret(version)    incr ret(version)} else {    set ret(version) 1}::asn::asnGetBigInteger cert ret(serial_number)::asn::asnGetSequence cert data_signature_algo_seq::asn::asnGetObjectIdentifier data_signature_algo_seq ret(data_signature_algo)::asn::asnGetSequence cert issuer    set ret(issuer) $issuer::asn::asnGetSequence cert validity::asn::asnGetUTCTime validity ret(notBefore)::asn::asnGetUTCTime validity ret(notAfter)::asn::asnGetSequence cert subject    set ret(subject) $subject::asn::asnGetSequence cert pubkeyinfobinary scan $pubkeyinfo H* ret(pubkeyinfo_hex)::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid ret(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset extensions_list [list]while {$cert != ""} {    ::asn::asnPeekByte cert peek_tag    switch -- [format {0x%02x} $peek_tag] {    "0x81" {    ::asn::asnGetContext cert - issuerUniqueID        }    "0x82" {    ::asn::asnGetContext cert - subjectUniqueID        }    "0xa1" {    ::asn::asnGetContext cert - issuerUniqID        }    "0xa2" {    ::asn::asnGetContext cert - subjectUniqID        }    "0xa3" {    ::asn::asnGetContext cert - extensions_ctx    ::asn::asnGetSequence extensions_ctx extensions#Убираем перевод oid в текстset ::pki::oids1 [array get ::pki::oids]array unset ::pki::oids     while {$extensions != ""} {            ::asn::asnGetSequence extensions extension            ::asn::asnGetObjectIdentifier extension ext_oid        ::asn::asnPeekByte extension peek_tag        if {$peek_tag == 0x1} {        ::asn::asnGetBoolean extension ext_critical            } else {        set ext_critical false            }        ::asn::asnGetOctetString extension ext_value_seq        set ext_oid [::pki::_oid_number_to_name $ext_oid]        set ext_value [list $ext_critical]        switch -- $ext_oid {                        id-ce-basicConstraints {                ::asn::asnGetSequence ext_value_seq ext_value_bin                if {$ext_value_bin != ""} {            ::asn::asnGetBoolean ext_value_bin allowCA                } else {            set allowCA "false"                }            if {$ext_value_bin != ""} {            ::asn::asnGetInteger ext_value_bin caDepth                } else {            set caDepth -1                }                lappend ext_value $allowCA $caDepth                        }                        default {                binary scan $ext_value_seq H* ext_value_seq_hex                lappend ext_value $ext_value_seq_hex                        }                    }        lappend extensions_list $ext_oid $ext_value    }#Возвращаем перевод oid-ов в текстarray set ::pki::oids $::pki::oids1        }    }}set ret(extensions) $extensions_listarray set extcert $extensions_list::asn::asnGetSequence wholething signature_algo_seq::asn::asnGetObjectIdentifier signature_algo_seq ret(signature_algo)::asn::asnGetBitString wholething ret(signature)set ret(serial_number) [::math::bignum::tostr $ret(serial_number)]set ret(signature) [binary format B* $ret(signature)]binary scan $ret(signature) H* ret(signature)#Инициируем класс pubkeyinfo при наследовании - superclassif {[llength [self next]]} {#Если есть наследуемый класс, то вызываем его конструкторnext $ret(pubkeyinfo_hex)}    }    method parse_cert {} {        return [array get ret]    }}


В области данных командой variable определяются данные/переменные объекта через, которые доступны во всех методах класса.
Метод method определяется точно так же, как процедура proc Tcl. Методы могут иметь произвольное количество параметров. Внутри метода можно определять свои данные командой
my variable <идентификатор переменной>
. Методы могут быть публичными (экспортируемыми) и приватными.
Экспортируемые методы методы видимы за пределами класса. По умолчанию экспортируются методы начинаются со строчной буквы. По умолчанию методы, чьи имена начинаются с прописной буквы считаются неэкспортируемыми (приватными) методами. Область видимости независимо от первого символа можно задать явно. Для указания того, что метод является публичным служит следующая команда:
export <идентификатор метода>
.
Для запрета экспорта метода используется следующая команда:
unexport <идентификатор метода>
.
Для вызова одного метода из другого метода внутри класса используется команда my:
my <идентификатор метода>
.
Для этой же цели можно использовать внутреннюю команда класса self, которая возвращает идентификатор текущего объекта:
[self] <идентификатор метода>

Ниже мы увидим всё это.
Для дальнейшей работы соберем весь рассмотренный код в файле classparsecert.tcl.
Содержимое файла classparsecert.tcl
package require asn#Переименовываем оригинальную процедуру подсчета длины rename ::asn::asnGetLength ::asn::asnGetLength.orig#Новая процедура подсчета длиныproc ::asn::asnGetLength {data_var length_var} {    upvar 1 $data_var data  $length_var length    asnGetByte data length    if {$length == 0x080} {#Поддержка BER-кодировкиset lendata [string length $data]set tvl 1set length 0set data1 $datawhile {$tvl != 0} {    ::asn::asnGetByte data1 peek_tag     ::asn::asnPeekByte data1 peek_tag1    if {$peek_tag == 0x00 && $peek_tag1 == 0x00} {incr tvl -1::asn::asnGetByte data1 tag incr length 2continue    }    if {$peek_tag1 == 0x80} {incr tvlif {$tvl > 0} {    incr length 2}::asn::asnGetByte data1 tag     } else {set l1 [string length $data1]::asn::asnGetLength data1 llset l2 [string length $data1]set l3 [expr $l1 - $l2]incr length $l3incr length $llincr length::asn::asnGetBytes data1 $ll strt    }}return    }    if {$length > 0x080} {        set len_length [expr {$length & 0x7f}]        if {[string length $data] < $len_length} {            return -code error \"length information invalid, not enough octets left"         }        asnGetBytes data $len_length lengthBytes        switch $len_length {            1 { binary scan $lengthBytes     cu length }            2 { binary scan $lengthBytes     Su length }            3 { binary scan \x00$lengthBytes Iu length }            4 { binary scan $lengthBytes     Iu length }            default {                                binary scan $lengthBytes H* hexstrscan $hexstr %llx length            }        }    }    return}package require pkiset ::pki::oids(1.2.643.100.1)  "OGRN"set ::pki::oids(1.2.643.100.5)  "OGRNIP"set ::pki::oids(1.2.643.3.131.1.1) "INN"set ::pki::oids(1.2.643.100.3) "SNILS"#Для КПП ЕГАИСset ::pki::oids(1.2.840.113549.1.9.2) "UN"#set ::pki::oids(1.2.840.113549.1.9.2) "unstructuredName"#Алгоритмы подписиset ::pki::oids(1.2.643.2.2.3) "GOST R 34.10-2001 with GOST R 34.11-94"set ::pki::oids(1.2.643.2.2.19) "GOST R 34.10-2001"set ::pki::oids(1.2.643.7.1.1.1.1) "GOST R 34.10-2012-256"set ::pki::oids(1.2.643.7.1.1.1.2) "GOST R 34.10-2012-512"set ::pki::oids(1.2.643.7.1.1.3.2) "GOST R 34.10-2012-256 with GOSTR 34.11-2012-256"set ::pki::oids(1.2.643.7.1.1.3.3) "GOST R 34.10-2012-512 with GOSTR 34.11-2012-512"set ::pki::oids(1.2.643.100.113.1) "KC1 Class Sign Tool"set ::pki::oids(1.2.643.100.113.2) "KC2 Class Sign Tool"set ::pki::oids(2.5.4.42)  "givenName"#Параметры подписиset ::pki::oids((1.2.643.2.2.35.1)"id-GostR3410-2001-CryptoPro-A-ParamSet"set ::pki::oids(1.2.643.2.2.35.2)"id-GostR3410-2001-CryptoPro-B-ParamSet"set ::pki::oids(1.2.643.2.2.35.3)"id-GostR3410-2001-CryptoPro-C-ParamSet"set ::pki::oids(1.2.643.2.2.36.0)"id-GostR3410-2001-CryptoPro-XchA-ParamSet"set ::pki::oids(1.2.643.2.2.36.1)"id-GostR3410-2001-CryptoPro-XchB-ParamSet"set ::pki::oids(1.2.643.7.1.2.1.1.1)"id-tc26-gost-3410-2012-256-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.1.2)"id-tc26-gost-3410-2012-256-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.1.3)"id-tc26-gost-3410-2012-256-paramSetC"set ::pki::oids(1.2.643.7.1.2.1.1.4)"id-tc26-gost-3410-2012-256-paramSetD"set ::pki::oids(1.2.643.7.1.2.1.2.1)"id-tc26-gost-3410-2012-512-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.2.2)"id-tc26-gost-3410-2012-512-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.2.3)"id-tc26-gost-3410-2012-512-paramSetC"#Класс certificateoo::class create certificate {#Наследуем класс pubkey#    superclass pubkey#Переменные класса#Доступны только в пределах класса#Переменная для хранения разобранного сертификата.     variable ret#Переменная для хранения расширений сертификатаvariable extcert#Конструктор    constructor {cert} {array set parsed_cert [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]set cert_seq $parsed_cert(data)array set ret [list]#Полный сертификат der в hexbinary scan $cert_seq H* ret(cert_full)  # Decode X.509 certificate, which is an ASN.1 sequence::asn::asnGetSequence cert_seq wholething::asn::asnGetSequence wholething cert#tbs - сертификатset ret(tbsCert) [::asn::asnSequence $cert]binary scan $ret(tbsCert) H* ret(tbsCert)::asn::asnPeekByte cert peek_tagif {$peek_tag != 0x02} {    # Version number is optional, if missing assumed to be value of 0    ::asn::asnGetContext cert - asn_version    ::asn::asnGetInteger asn_version ret(version)    incr ret(version)} else {    set ret(version) 1}::asn::asnGetBigInteger cert ret(serial_number)::asn::asnGetSequence cert data_signature_algo_seq::asn::asnGetObjectIdentifier data_signature_algo_seq ret(data_signature_algo)::asn::asnGetSequence cert issuer    set ret(issuer) $issuer::asn::asnGetSequence cert validity::asn::asnGetUTCTime validity ret(notBefore)::asn::asnGetUTCTime validity ret(notAfter)::asn::asnGetSequence cert subject    set ret(subject) $subject::asn::asnGetSequence cert pubkeyinfobinary scan $pubkeyinfo H* ret(pubkeyinfo_hex)::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid ret(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset extensions_list [list]while {$cert != ""} {    ::asn::asnPeekByte cert peek_tag    switch -- [format {0x%02x} $peek_tag] {    "0x81" {    ::asn::asnGetContext cert - issuerUniqueID        }    "0x82" {    ::asn::asnGetContext cert - subjectUniqueID        }    "0xa1" {    ::asn::asnGetContext cert - issuerUniqID        }    "0xa2" {    ::asn::asnGetContext cert - subjectUniqID        }    "0xa3" {    ::asn::asnGetContext cert - extensions_ctx    ::asn::asnGetSequence extensions_ctx extensions#Убираем перевод oid в текстset ::pki::oids1 [array get ::pki::oids]array unset ::pki::oids     while {$extensions != ""} {            ::asn::asnGetSequence extensions extension            ::asn::asnGetObjectIdentifier extension ext_oid        ::asn::asnPeekByte extension peek_tag        if {$peek_tag == 0x1} {        ::asn::asnGetBoolean extension ext_critical            } else {        set ext_critical false            }        ::asn::asnGetOctetString extension ext_value_seq        set ext_oid [::pki::_oid_number_to_name $ext_oid]        set ext_value [list $ext_critical]        switch -- $ext_oid {                        id-ce-basicConstraints {                ::asn::asnGetSequence ext_value_seq ext_value_bin                if {$ext_value_bin != ""} {            ::asn::asnGetBoolean ext_value_bin allowCA                } else {            set allowCA "false"                }            if {$ext_value_bin != ""} {            ::asn::asnGetInteger ext_value_bin caDepth                } else {            set caDepth -1                }                           lappend ext_value $allowCA $caDepth                        }                        default {                binary scan $ext_value_seq H* ext_value_seq_hex                lappend ext_value $ext_value_seq_hex                        }                    }        lappend extensions_list $ext_oid $ext_value    }#Возвращаем перевод oid-ов в текстarray set ::pki::oids $::pki::oids1        }    }}set ret(extensions) $extensions_listarray set extcert $extensions_list::asn::asnGetSequence wholething signature_algo_seq::asn::asnGetObjectIdentifier signature_algo_seq ret(signature_algo)::asn::asnGetBitString wholething ret(signature)set ret(serial_number) [::math::bignum::tostr $ret(serial_number)]set ret(signature) [binary format B* $ret(signature)]binary scan $ret(signature) H* ret(signature)#Инициируем класс pubkeyinfo при наследовании - superclass#next $ret(pubkeyinfo_hex)    }    method parse_cert {} {        return [array get ret]    }}


После того как был определен класс можно создавать конкретный объект (экземпляр объекта). Для этого может быть использована одна из следующих команд:
<имя класса> create <идентификатор экземпляра класса> [параметры для констуктура]

или
set <переменная для идентификатора экземпляра класса > <имя класса> new [параметры для констуктура]

В первом случае программист сам назначает идентификатор для создаваемого экземпляра объекта. Этот идентификатор фактически будет командой, через которую осуществляется доступ к объекту и его методам:
<идентификатор объекта>  <идентификатор метода> [<параметры>]

Во втором случае идентификатор создаваемого объекта назначается интерпретатором и возвращается как результат выполнения команды new для указанного класса. В этом случае идентификатор объекта будет браться из этой переменной.
Интересно сравнить с созданием объекта в Python. И что мы видим? Несущественную синтаксическую разницу.

Напишем небольшой пример example1.tcl использования этого класса:
#Загружаем описание классаsource ./classparsecert.tcl#Загружаем сертификатset file [lindex $argv 0]if {$argc != 1 || ![file exists $file]} {    puts "Usage: tclsh example1 <файл с сертификатом>"    exit}puts "Loading file: $file"set fd [open $file]chan configure $fd -translation binaryset data [read $fd]close $fdif {[catch {certificate create cert1 $data} er1]} {puts "Файл не содержит СЕРТИФИКАТ"exit}array set cert_parse [cert1 parse_cert]#parray cert_parseputs "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}

Выполним пример:
$tclsh ./example1.tclLoading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        cert_parse(pubkeyinfo_hex)        cert_parse(extensions)        cert_parse(issuer)        cert_parse(data_signature_algo)        cert_parse(cert_full)        cert_parse(serial_number)        cert_parse(signature)        cert_parse(pubkey_algo)        cert_parse(notAfter)        cert_parse(signature_algo)        cert_parse(notBefore)        cert_parse(version)        cert_parse(tbsCert)$ 

О конструкторе Lego

У читателя, наверное, так и хочет сорваться с языка вопрос:- А причем здесь конструктор Lego? А вот при чем. Если, скажем в C++ класс объекта должен быть определен сразу, то в TclOO класс может собираться постепенно как модель в конструкторе. Более того одни части класса могут удаляться и заменяться другими и т.д. Более того, такой метод конструирования класса распространяется и на объекты, да на конкретные объекты.
Предположим, что необходимо вывести информацию и о владельце и об издателе сертификата. Для этого нам потребуется два публичных issur и subject и один приватный метод parse_dn для разбора отличительного имени (DN) издателя и владельца. Традиционно нам пришлось бы переписать класс certificate, добавив в него указанные методы. В TclOO можно поступить по другому. Можно просто в нужном месте программы выполнить оператор добавления в существующий класс новых членов.
Для добавления в класс новых членов в область данных используется команда (модуль конструктора) вида:
oo::define <идентификатор класса>  {#Область данных классаvariable <идентификатор переменной>  [<идентификатор переменной>][ variable <идентификатор переменной> ]}

Может быть несколькокоманд variable, каждая из которых определяет один или несколько элементов данных.
Аналогично добавляются методы:
oo::define <идентификатор класса>  {#методыmethod <идентификатор метода 1>  {<параметры>} {<тело метода>}[method <идентификатор метода N>  {<параметры>} {<тело метода>}]}

Любой метод можно удалить в любое время с помощьюкоманды deletemethod внутри сценария определения класса. Эта команды будет рассмотрена ниже при рассмотрении примера с отзывом сертификата.
Про видимость методов (публичные, приватные методы) мы уже говорили выше.
Отметим, что первоначально класс может создаваться абсолютно пустым:
oo::class create <Идентификатор класса>

с последующим наполнением его через команду:
oo::define <идентификатор класса>  {}

Итак, добавляем новые методы в класс Certificate:
oo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}

Теперь дополним наш пример кодом для распечатки информации об издателе и владельце:
...puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}...

Таким образом мы получим второй пример.
Тестовый пример example2.tcl
source ./classparsecert.tcl
#Загружаем сертификат
set file [lindex $argv 0]
if {$argc != 1 || ![file exists $file]} {
puts Usage: tclsh example1 <файл с сертификатом>
exit
}
puts Loading file: $file
set fd [open $file]
chan configure $fd -translation binary
set data [read $fd]
close $fd
if {[catch {certificate create cert1 $data} er1]} {
puts Файл не содержит СЕРТИФИКАТ
exit
}
array set cert_parse [cert1 parse_cert]
#parray cert_parse
puts Распарсенный сертификат
foreach ind [array names cert_parse] {
puts "\tcert_parse($ind)"
}
#Добавляем новые методы
oo::define certificate {
method issuer {} {
return [ my parse_dn $ret(issuer)]
}
method subject {} {
return [ my parse_dn $ret(subject)]
}
method parse_dn {asnblock} {
set lret {}
while {[string length $asnblock]} {
asn::asnGetSet asnblock AttributeValueAssertion
asn::asnGetSequence AttributeValueAssertion valblock
asn::asnGetObjectIdentifier valblock oid
set name [::pki::_oid_number_to_name $oid]
::asn::asnGetString valblock value
lappend lret [string toupper $name]
lappend lret $value
}
return $lret
}
#Приватный метод
unexport parse_dn
}
#Применяем методы
puts Сведения о владельце:
foreach {oid value} [cert1 subject] {
puts "\t$oid=$value"
}
puts Сведения об издателе:
foreach {oid value} [cert1 issuer] {
puts "\t$oid=$value"
}

Попробуем выполнить этот пример:
$tclsh example2.tcl minenergo.cer  Loading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        . . .         cert_parse(tbsCert)Сведения о владельце:        EMAIL=xxxxxxxxxxx        INN=xxxxxxxxxxx        OGRN=............. . .        ST=77 г. Москва        C=RU        CN=Мин РоссииСведения об издателе: . . .        C=RU        ST=77 Москва        L=Москва        CN=Тестовый удостоверяющий центр$

О наследовании


Определяющей характеристикой объектно-ориентированных систем является поддержка наследования.Наследование относится к способностипроизводногокласса (также называемогоподклассом) наследовать область данных и методы из наследуемого класса (из супер класса).
При разборе сертификата, естественно, требуется получить и полную информацию о его публичном ключе. Предположим у нас уже есть класс pubkey, который на основе asn-структуры pubkeyinfo выдает полную информацию о публичном ключе, включая RSA, EC, GOST:
oo::class create pubkey {#Внутренняя переменная класса для хранения asn-структуры pubkeyinfo    variable infopk    constructor {pubkinfo} {set infopk $pubkinfo    }    method infopubkey {} {array set retpk [list]set pubkeyinfo [binary format H* $infopk]::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid retpk(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset pubkey [binary format B* $pubkey]binary scan $pubkey H* retpk(pubkey)set retpk(pkcs11id_hex) [::sha1::sha1  $pubkey]if {"1 2 643" == [string range $retpk(pubkey_algo) 0 6]} {#ГОСТ-ключ        set retpk(type) gost    ::asn::asnGetSequence pubkey_algoid pubalgost  #OID - параметра    ::asn::asnGetObjectIdentifier pubalgost retpk(paramkey)    set retpk(paramkey) [::pki::_oid_number_to_name $retpk(paramkey)]    if {$pubalgost != ""} {  #OID - Функция хэша::asn::asnGetObjectIdentifier pubalgost retpk(hashkey)    } else {set retpk(hashkey) ""    }} elseif {"1 2 840 10045 2 1" == $retpk(pubkey_algo) } {#EC-key        set retpk(type) ec    ::asn::asnGetObjectIdentifier pubkey_algoid retpk(pubkey_algo_par)} elseif {"1 2 840 113549 1 1 1" == $retpk(pubkey_algo) }  {#RSA- key        set retpk(type) rsa    binary scan $pubkey H* retpk(pubkey)    ::asn::asnGetSequence pubkey pubkey_parts    ::asn::asnGetBigInteger pubkey_parts retpk(n)    ::asn::asnGetBigInteger pubkey_parts retpk(e)    set retpk(n) [::math::bignum::tostr $retpk(n)]    set retpk(e) [::math::bignum::tostr $retpk(e)]    set retpk(l) [expr {int([::pki::_bits $retpk(n)] / 8.0000 + 0.5) * 8}]} else {        set retpk(type) unknown}return [array get retpk]    }}

Сохраним этот класс в файле classpubkeyinfo.tcl.
Для того, чтобы наследовать метод infopubkey для объектов класса certificate, в определение класса certificate добавляется определение суперкласса, методы которого будут наследоваться:
superclass pubkey

Также добавляем в конструктор класса certificate вызов конструктора класса pubkey с передачей ему в качестве параметра asn-структуры pubkeyinfo:
next $ret(pubkeyinfo_hex)

Команда next вызывает одноименный метод (в данном случае constructor) из суперкласса, т.е. из класса pubkey. Конструктор в классе pubkey просто сохранит в переменной класса infopk asn-структуру публичного ключа. Этот код с соответствующей проверкой наличия в теле программы класса pubkey и его конструктора был включен при определении класса certificate.
Полный техт example3.tcl здесь.
source ./classpubkeyinfo.tclsource ./classparsecert_and_pk.tclset file [lindex $argv 0]if {$argc != 1 || ![file exists $file]} {    puts "Usage: tclsh example1 <файл с сертификатом>"    exit}puts "Loading file: $file"set fd [open $file]chan configure $fd -translation binaryset data [read $fd]close $fdif {[catch {certificate create cert1 $data} er1]} {puts "Файл не содержит сертификата"exit}array set cert_parse [cert1 parse_cert]puts "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}#Добавляем новые методыoo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}puts "INFO PUB KEY"foreach {oid value} [cert1 infopubkey] {    puts "\t$oid=$value"}#Создаем объект pubkeyputs "КЛАСС INFO PUB KEY"if {[catch {pubkey create pk1 $cert_parse(pubkeyinfo_hex)} er1]} {puts "НЕ PUBKEYINFO"exit}foreach {oid value} [pk1 infopubkey] {    puts "\t$oid=$value"}puts "Публичные методы класса certificate"puts "\t[info class methods certificate]"puts "Все методы класса certificate, включая приватные"puts "\t[info class methods certificate -private]"

Выполним пример example3.tcl:
$ tclsh example3.tcl minenergo.cer Loading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        . . .        cert_parse(tbsCert)Сведения о владельце:        . . .        ST=77 г. Москва        C=RU        CN=Мин РоссииСведения об издателе:        C=RU        ST=77 Москва        . . .        CN=Тестовый удостоверяющий центрINFO PUB KEY        pkcs11id_hex=842205ac57465fd853a158544f1ea1ba1de58569        pubkey=04401dc81447918c7694a74dbe6bb4e4c10a63ca21d6b95a41ae20837deda4700f2404a0c1141d9b535b95707bb751791eb684bd09ce8f0c98d912dea947e4b8bbdb        hashkey=1 2 643 7 1 1 2 2        paramkey=id-GostR3410-2001-CryptoPro-XchA-ParamSet        type=gost        pubkey_algo=1 2 643 7 1 1 1 1Публичные методы класса certificate        subject parse_cert issuerВсе методы класса certificate, включая приватные        parse_dn subject parse_cert issuer . . .$

Отметим также, что TclOO допускает и множественное наследование, но это тема для отдельной публикации.

Информационная поддержка


В результатах выполнения примера мы видим перечень методов доступных в классе certificate.
Для получения списка методов используется следующая команда:
info class methods <идентификатор класса> [-private]

Если флаг "-private" не задан, то выдается список публичных методов. В противном случае, выдается весь перечень методов, включая приватные.
Проверить принадлежность объекта тому или иному классу можно командой:
info object clacc <идентификатор объекта>
.
В нашем примере объект cert1 принадлежит двум классам: certuficate и pubkey.
Если требуется узнать какие классы наследует тот или иной класс, достаточно выполнить коиманду:
info class superclasses <идентификатор класса>

А если требуется получить информацию о том, какими классами наследуется тот или иной класс, то достаточно выполнить следующую команду:
info class subclasses <идентификатор класса>
.
В нашем примере мы имеем:
$ . . .Публичные методы класса certificate        subject parse_cert issuerВсе методы класса certificate, включая приватные        parse_dn subject parse_cert issuerПринадлежность объекта cert1 классу certificate        1Принадлежность объекта cert1 классу pubkey        1Супер классы класса certificate        ::pubkeyСупер классы класса pubkey        ::oo::objectПодклассы класса certificateПодклассы класса pubkey        ::certificate$ 

Подмешивание (mix in) методов в класс


Для расширения возможность класса, прежде всего с точки зрения его функциональности, помимо наследования можно использовать так называемый метод подмешивания (mix in).
Если мы хотим распечатать сертификат в текстовом виде, то нам потребуется разбор asn-структур расширений сертификата. Это и начначение ключа сертификата, это свойства квалифицированного сертификата и многое другое. Оформим разбор расширений сертификата в отдельный класс parseexts, в котором отсутствует констуктор и деструктор:
#Класс разбора расширений сертификатаoo::class create parseexts {#Переменные с распарсенным сертификатом и его расширениями#Область данных берется их класса, к которому будем плдмешивать    variable ret    variable extcert#Подмешиваемые методы    method issuerSignTool {} {set member {"Наименование СКЗИ УЦ" "Наименование УЦ" "Сертификат СКЗИ УЦ" "Сертификат УЦ"}#Проверка наличия расширенияif {![info exists extcert(1.2.643.100.112)]} {    return [list ]}set rr [list]set iss [binary format H* [lindex $extcert(1.2.643.100.112) 1]]::asn::asnGetSequence iss iss_polfor {set i 0} {[string length $iss_pol] > 0}  {incr i} {    ::asn::asnGetUTF8String iss_pol retist    lappend rr [lindex $member $i]    lappend rr $retist}return $rr  }    method subjectSignTool {} {#Проверка наличия расширенияif {![info exists extcert(1.2.643.100.111)]} {    return [list ]}set iss [binary format H* [lindex $extcert(1.2.643.100.111) 1]]lappend rr "User CKZI"::asn::asnGetUTF8String iss retsstlappend rr $retsstreturn $rr    }    method keyUsage {} {    #keyUsageset critcert "No"array set ist [list]#Проверка наличия расширенияif {![info exists extcert(2.5.29.15)]} {    return [array get ist]}    set ku_hex [lindex $extcert(2.5.29.15) 1]if {[lindex $extcert(2.5.29.15) 0] == 1} {    set critcert "Yes"}set ku_options {"Digital signature" "Non-Repudiation" "Key encipherment" "Data encipherment" "Key agreement" "Certificate signature" "CRL signature" "Encipher Only" "Decipher Only" "Revocation list signature"}set ku [binary format H* $ku_hex]::asn::asnGetBitString ku ku_binset retku {}for {set i 0} {$i < [string length $ku_bin]}  {incr i} {    if {[string range $ku_bin $i $i] > 0 } {    lappend retku [lindex $ku_options $i]    }}array set aku [list]set aku(keyUsage) $retkuset aku(critcert) $critcertreturn [array get aku]    }}

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

Для нашего примера это будет выглядеть следующим образом:
oo::define certificate {mixin parseexts}

Полный пример использования подмешивания example4.tcl находится здесь.

source ./classpubkeyinfo.tcl
source ./classparsecert.tcl
set file [lindex $argv 0]
if {$argc != 1 || ![file exists $file]} {
puts Usage: tclsh example1 <файл с сертификатом>
exit
}
puts Loading file: $file
set fd [open $file]
chan configure $fd -translation binary
set data [read $fd]
close $fd
if {[catch {certificate create cert1 $data} er1]} {
puts Файл не содержит сертификата
exit
}
array set cert_parse [cert1 parse_cert]
if {0} {
puts Распарсенный сертификат
foreach ind [array names cert_parse] {
puts "\tcert_parse($ind)"
}
}
#Добавляем новые методы
oo::define certificate {
method issuer {} {
return [ my parse_dn $ret(issuer)]
}
method subject {} {
return [ my parse_dn $ret(subject)]
}
method parse_dn {asnblock} {
set lret {}
while {[string length $asnblock]} {
asn::asnGetSet asnblock AttributeValueAssertion
asn::asnGetSequence AttributeValueAssertion valblock
asn::asnGetObjectIdentifier valblock oid
set name [::pki::_oid_number_to_name $oid]
::asn::asnGetString valblock value
lappend lret [string toupper $name]
lappend lret $value
}
return $lret
}
unexport parse_dn
}
puts Сведения о владельце:
foreach {oid value} [cert1 subject] {
puts "\t$oid=$value"
}
puts Сведения об издателе:
foreach {oid value} [cert1 issuer] {
puts "\t$oid=$value"
}
puts INFO PUB KEY
foreach {oid value} [cert1 infopubkey] {
puts "\t$oid=$value"
}
#Класс разбора расширений сертификата
oo::class create parseexts {
#Переменная с распарсенным сертификатом
variable ret
variable extcert
method issuerSignTool {} {
set member {Наименование СКЗИ УЦ Наименование УЦ Сертификат СКЗИ УЦ Сертификат УЦ}
#Проверка наличия расширения
if {![info exists extcert(1.2.643.100.112)]} {
return [list ]
}
set rr [list]
set iss [binary format H* [lindex $extcert(1.2.643.100.112) 1]]
::asn::asnGetSequence iss iss_pol
for {set i 0} {[string length $iss_pol] > 0} {incr i} {
::asn::asnGetUTF8String iss_pol retist
lappend rr [lindex $member $i]
lappend rr $retist
}
# unset extcert(1.2.643.100.112)
return $rr
}
method subjectSignTool {} {
#Проверка наличия расширения
if {![info exists extcert(1.2.643.100.111)]} {
return [list ]
}
set iss [binary format H* [lindex $extcert(1.2.643.100.111) 1]]
lappend rr User CKZI
::asn::asnGetUTF8String iss retsst
lappend rr $retsst
# unset extcert(1.2.643.100.111)
return $rr
}
method keyUsage {} {
#keyUsage
set critcert No
array set ist [list]
#Проверка наличия расширения
if {![info exists extcert(2.5.29.15)]} {
return [array get ist]
}
set ku_hex [lindex $extcert(2.5.29.15) 1]
if {[lindex $extcert(2.5.29.15) 0] == 1} {
set critcert Yes
}
set ku_options {Digital signature Non-Repudiation Key encipherment Data encipherment Key agreement Certificate signature CRL signature Encipher Only Decipher Only Revocation list signature}
set ku [binary format H* $ku_hex]
::asn::asnGetBitString ku ku_bin
set retku {}
for {set i 0} {$i < [string length $ku_bin]} {incr i} {
if {[string range $ku_bin $i $i] > 0 } {
lappend retku [lindex $ku_options $i]
}
}
array set aku [list]
set aku(keyUsage) $retku
set aku(critcert) $critcert
return [array get aku]
}
}
oo::define certificate {
mixin parseexts
}
puts keyUsage
foreach {oid value} [cert1 keyUsage] {
puts "\t$oid=$value"
}
puts issuerSignTool
foreach {oid value} [cert1 issuerSignTool] {
puts "\t$oid=$value"
}
puts subjectSignTool
foreach {oid value} [cert1 subjectSignTool] {
puts "\t$oid=$value"
}
puts Публичные методы класса certificate
puts "\t[info class methods certificate]"
puts Все методы класса certificate, включая приватные
puts "\t[info class methods certificate -private]"
puts Принадлежность объекта cert1 классу certificate
puts "\t[info object class cert1 certificate]"
puts Принадлежность объекта cert1 классу pubkey
puts "\t[info object class cert1 pubkey]"
puts Супер классы класса certificate
puts "\t[info class superclasses certificate]"
puts Супер классы класса pubkey
puts "\t[info class superclasses pubkey]"
puts Подклассы класса certificate
puts "\t[info class subclasses certificate]"
puts Подклассы класса pubkey
puts "\t[info class subclasses pubkey]"
puts Mixin-ы класса certificate
puts "\t[info class mixins certificate]"

Результат выполнения примера:
$tclsh example4.tcl cert.cer. . .Сведения об издателе:. . .        C=RU        ST=77 Москва        L=Москва. . .        CN=Тестовый удостоверяющий центрINFO PUB KEY        pkcs11id_hex=842205ac57465fd853a158544f1ea1ba1de58569        pubkey=04401dc81447918c7694a74dbe6bb4e4c10a63ca21d6b95a41ae20837deda4700f2404a0c1141d9b535b95707bb751791eb684bd09ce8f0c98d912dea947e4b8bbdb        hashkey=1 2 643 7 1 1 2 2        paramkey=id-GostR3410-2001-CryptoPro-XchA-ParamSet        type=gost        pubkey_algo=1 2 643 7 1 1 1 1keyUsage        critcert=Yes        keyUsage={Digital signature} Non-Repudiation {Key encipherment} {Data encipherment}issuerSignTool        Наименование СКЗИ УЦ="CSP"         Наименование УЦ="Удостоверяющий центр" версии         Сертификат СКЗИ УЦ=Сертификат соответствия         Сертификат УЦ=Сертификат соответствия  subjectSignTool        User CKZI=CSP Публичные методы класса certificate        subject parse_cert issuerВсе методы класса certificate, включая приватные        parse_dn subject parse_cert issuerПринадлежность объекта cert1 классу certificate        1Принадлежность объекта cert1 классу pubkey        1Супер классы класса certificate        ::pubkeyСупер классы класса pubkey        ::oo::objectПодклассы класса certificateПодклассы класса pubkey        ::certificateMixin-ы класса certificate        ::parseexts

Добавление/переопределение методов у объектов


В принципе этого материала достаточно, чтобы начать использовать ООП в Tcl. Но мы упомянули и то, что в TcllOO можно динамически конструировать не только сам класс, то и экземпляры класса, т.е. объекты. На одной из таких возможностей хотелось бы остановится.
Для этого добавим в класс certificate еще один метод для подписания этим сертификатом некоторого документа:
#Метод для Подписания документаoo::define certificate {method signDoc {doc} {set sign "Здесь должна находиться подпись документа  $doc"#Счетчик подписанных документовmy variable signedDoc#Количество подписанных документовincr signedDocreturn [list $signedDoc $sign]}}

При вызове этого метода должно происходить подписание документа и увеличение счетчика подписанных документов на единицу. В качестве результата работы этого метода возвращается общее число подписанных на данный момент документов и сама подпись:
. . . set doc "Подпись1"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}. . .

Результат будет выглядеть так:
. . .Подписание документа Подпись1        Подписано документов на данный момент=1        Подпись документа="Здесь должна находиться подпись документа  Подпись1". . .

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

image

А теперь представим, что владелец сертификата убывает в отпуск. Он знает, что в отпуске он будет отдыхать и не в коем случае не будет работать с документами и тем более что-либо подписывать. Он хочет отозвать сертификат, а когда вернется восстановить его действие. Для этих целей служит следующая функция:
#Процедура отзыва сертификатаproc revoke {cert_obj} {    oo::objdefine $cert_obj {#Переопределяем метод подписи для конкретного объекта        method signDoc {args} {#Переменная accessCert хранит число несанкционированных попыток подписания            my variable accessCert             set sign "Сертификат временно отозван. Не пытайтесь им подписывать!"#Число попыток несанкционированного использования возрастает на 1            incr accessCert            return [list $accessCert $sign]        }        method unrevoke {} {            my variable accessCert#Вызов метод  unrevoke удалит метод подписи для конкретного объекта,#восстанавливая тем самым действие  метода signDoc из класса и #удалит сам метод unrevoke            oo::objdefine [self] { deletemethod signDoc unrevoke }            if {![info exist accessCert]} {                return 0            }            return $accessCert        }    }}

Вызов этой функции определяет новый функционал методв signDoc для конкретного объекта. Для остальных объектов, как существующих и так и новых, сохраняется действие метода, определенного для класса. Также определяется новый метод unrevoke, вызов которого сотрудником по возвращению из отпуска приведет к восстановлению метода signDoc из класса certificate, путем удаления метода signDoc для объекта, а также удалит и сам метод unrevoke.
Полный текст примера example5.tcl находится здесь
source ./classpubkeyinfo.tclsource ./classparsecert.tcl#Примерset file [lindex $argv 0]if {$argc != 1 || ![file exists $file]} {    puts "File $file not exist"    puts "Usage: tclsh example1 <файл с сертификатом>"    exit}puts "Loading file: $file"set fd [open $file]chan configure $fd -translation binaryset data [read $fd]close $fdif {$data == "" } {    puts "Bad file with certificate=$file"    usage 1    exit}if {[catch {certificate create cert1 $data} er1]} {puts "НЕ СЕРТИФИКАТ"exit}array set cert_parse [cert1 parse_cert]#parray cert_parseif {0} {puts "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}}#Добавляем новые методыoo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}puts "INFO PUB KEY"foreach {oid value} [cert1 infopubkey] {    puts "\t$oid=$value"}#Метод для Подписания документаoo::define certificate {method signDoc {doc} {set sign "Здесь должна находиться подпись документа  $doc"#Счетчик подписанных документовmy variable signedDoc#Количество подписанных документовincr signedDocreturn [list $signedDoc $sign]}}set doc "Подпись1"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}set doc "Подпись2"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}#Процедура отзыва сертификатаproc revoke {cert_obj} {    oo::objdefine $cert_obj {#Переопределяем метод подписи для конкретного объекта        method signDoc {args} {#Переменная accessCert хранит число несанкционированных попыток подписания            my variable accessCert             set sign "Сертификат временно отозван. Не пытайтесь им подписывать!"#Число попыток несанкционированного использования возрастает на 1            incr accessCert            return [list $accessCert $sign]        }        method unrevoke {} {            my variable accessCert#Вызов метод  unrevoke удалит метод подписи для конкретного объекта,#восстанавливая тем самым действие  метода signDoc из класса и #удалит сам метод unrevoke            oo::objdefine [self] { deletemethod signDoc unrevoke }            if {![info exist accessCert]} {                return 0            }            return $accessCert        }    }}#Клонируем объектoo::copy cert1 cert11#Отзыв сертификатаputs "Отзыв сертификата"revoke cert1foreach doc "Подпись3 подпись4" {    puts "Попытка подписать документ $doc"    foreach {count sign} [cert1 signDoc $doc] {puts "\tПопыток несанкционированного доступа=$count"puts "\tПодпись документа=\"$sign\""    }}#Для клонированного объекта отзыв не действуетforeach doc "Подпись3к подпись4к" {    puts "Попытка подписать документ $doc клонированным объектом"    foreach {count sign} [cert11 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""    }}#Восстанавливаем действие сертификатаforeach {count info} [cert1 unrevoke] {    puts "Действие сертификата восстанвлено"    puts "\tЗа время его отзыва было $count попытки несанкционированного досьупа"}foreach doc "\"Подпись после восстановления\"" {    puts "Попытка подписать документ $doc"    foreach {count sign} [cert1 signDoc $doc] {puts "\tПодписано документов на данный момент=$count"puts "\tПодпись документа=\"$sign\""    }}

Ниже приведен фрагмент выполнения примера example5.tcl:
. . . Подписание документа Подпись1        Подписано документов на данный момент=1        Подпись документа="Здесь должна находиться подпись документа  Подпись1"Подписание документа Подпись2        Подписано документов на данный момент=2        Подпись документа="Здесь должна находиться подпись документа  Подпись2"Отзыв сертификатаПопытка подписать документ Подпись3        Попыток несанкционированного доступа=1        Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"Попытка подписать документ подпись4        Попыток несанкционированного доступа=2        Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"Действие сертификата восстанвлено        За время его отзыва было 2 попытки несанкционированного досьупаПопытка подписать документ Подпись после восстановления        Подписано документов на данный момент=3        Подпись документа="Здесь должна находиться подпись документа  Подпись после восстановления". . .

Упомянем еще один оператор. Это оператор клонирования объекта:
oo::copy <идентификатор исходного объекта> <идентификатор клона>
Говорить и писать об ООП на TclOO можно долго и долго.
Еще интересней его исследовать.
Подробнее..

Перчатка Mark gauntlet v4.2

06.01.2021 00:04:36 | Автор: admin

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

Начало

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

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

Починить эту горку термоклея гвоздей и изоленты я так и не смог. В своё оправдание могу сказать, что в тот момент я не умел паять, из электроники понимал только что нельзя замыкать + и -, а о существовании 3D печати и не слышал.

В конце лета заказал себе первый принтер - Anet A8.

Обычный принтер для ознакомления с технологией: рама из акрила, кинематика с "дрыгостолом" и шумные моторы (скорее их драйвера)

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

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

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

С последней и предпоследней версией я победил на 2 мероприятиях и решил расширять серию Mark. Именно так на скорую руку я записал относительно нереальные планы на роботов, включая большие металлические базы для роботов. Но затем я всё же переосмыслил идею серии - можно же сделать реально интересную систему марсоходов, которая может себя показать и на Земле.

Собственно вот как я пока что это позиционирую:

Система роботовMark- это исследовательский комплекс дляавтономногоисследования местности, в частности - поверхностиМарса.

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

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

Mark4 -шнекоход, также выполняет роль спасательного аппарата.

Mark5 -инсектоидс крыльями и 6 ногами. Может использоваться дляизученияочень узких проходов.

Mark7 -робозмея, также как иMark5 может исследовать узкиепроходы иотверстия.

Markgauntlet перчатка для ручного управления всеми роботами.

Из представленных роботов у меня есть почти готовые Mark 6, Mark 4, ну и собственно Mark 3 и Mark gauntlet.

Из интересного по ним пока есть только основа Mark 6 и его шасси, которые пока печатаются

Разработка перчатки: версия 1

Первая версия перчатки была сделана весной 2020 года и сразу заработала с тестовым стендом, но там мало что могло не сработать: я использовал обычный радиомодуль на 433 МГц с антенной из куска провода. Более подробно там есть в видео (моё первое видео, так что там всё очень посредственно) https://youtu.be/eEAHhr9Suug?t=194

Разработка перчатки: версия 2

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

Тут уже был радиомодуль nrf24l01, несколько режимов работы и выбор канала передачи. На работу перчатки можно глянуть в видео https://youtu.be/P_fq7KkfJrI

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

Разработка перчатки: версии 3 и 4

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

3 версия:

Функционал:

  • WiFi модуль esp8266

  • Радиомодуль NRF24l01+

  • Мини радиомодуль на 433 МГц

  • Bluetooth модуль

  • Акселерометр + гироскоп на перчатке

  • Панель управления с OLED дисплеем

В целом получилась нормальная версия, но её было бы сложно повторять из-за пайки навесом прямо на корпусе. Вот подобие описания этой версии https://youtu.be/52WvejA6dyk .

4 версия:

Тут уже я взял всё что подходило под концепцию и добавил к этому контроллер Atmega2560

Видео с процессом её создания:

Функционал:

  • WiFi модуль

  • Радиомодуль NRF24L01+

  • Радиомодуль LoRa

  • MP3 плеер и динамик к нему

  • ИК- светодиод (для простейшей связи)

  • Мощные адресные светодиоды сбоку

  • Акселерометр+гироскоп

  • Датчик цвета + жестов

  • Панель управления с OLED дисплеем

На этом можно было бы и остановиться, но я решил пойти дальше и сделать версию 4.2

Версия 4.2 или завершающий штрих перчатки

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

Основа возвращается с первой версии из-за подходящей геометрии

Перчатка скорее всего останется с версии 4

Для питания будут использоваться 3 аккумулятора 18650 на 3.4 А*ч каждый, что обеспечит достаточно большую автономность. Крепиться это будет на плечо.

Почти вся электроника будет распаяна на 2 печатные платы, которые соединятся вместе

Ну и первоначальный код, который будет использоваться для теста на работоспособность. В нём я не использовал пока только LoRa модуль. Ссылка на гитхаб: https://github.com/Madjogger1202/Mark_GauntletV4.2/blob/main/src/main.cpp

Дальнейшее создание этой версии будет уже в ближайшее время.

/*  Hi stranger, this is main code file for this project  I'm not a 100% programmer, but i can make electronics work,  so i will be grateful if you add any features  it is fully opensource project, so anyone can build stuff based on this code   have a great time reading this badly written working code (^_^) */#include <Arduino.h>      // why not...#include <Wire.h>#include <SPI.h>// i have to make all modules work, so i will use some libraris to make life easier//1) Display.      im using 0.96 oled from china, it is not standart at dimentions, bt i like how it looks in final designs :)#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> // Adafruit librari works 50/50, it depends on display driver (yes, they can hava same names, bt diffrent drivers)//2) RGB Led panel.       LEDs 2812 (8-bit panel) #include <microLED.h>//3) NRF24L01+ #include <nRF24L01.h>#include <RF24.h>//4)APDC9960 usefull sensor#include "Adafruit_APDS9960.h"//5) LoRa radio sx1278#include <RH_RF95.h>//6) MPU6050 gyro + acsel#include <Adafruit_Sensor.h>#include <Adafruit_MPU6050.h>//7) MP3 module#include <DFPlayer_Mini_Mp3.h>// first switches connectionint8_t first_sw[8] = { A14, A13, A12, A11, A10, A9, A8, A7 };// second switches connectionint8_t second_sw[8] = { 38, 37, 36, 35, 34, A6, 32, A15 };// buttons connectionint8_t buttons[4] = { A3, A1, A0, A2 };#define LED1 10#define LED2 11#define JOY_X A6#define JOY_Y A5#define POT A4#define LORA_D0 42#define LORA_NSS 43#define LORA_RST 44#define NRF_CSN 40#define NRF_CE 41#define IR_LED 7#define R_LED 4#define G_LED 5#define B_LED 6#define WS_LED 45LEDdata leds[8];microLED strip(leds, 8, WS_LED); #define ORDER_GRB RF24 radio(NRF_CE, NRF_CSN);Adafruit_MPU6050 mpu;Adafruit_SSD1306 display(128, 32, &Wire, -1);Adafruit_APDS9960 apds;volatile bool irqMPU;volatile bool irqAPDC;struct allData{  volatile boolean irqMPU;  volatile boolean irqAPDC;  bool stable;  int8_t x_acs;  int8_t y_acs;  int8_t z_acs;  uint8_t mode;  uint8_t channel;  uint16_t button;    uint16_t potData;  uint16_t joyX;  uint16_t joyY;  uint8_t led1Mode;  uint8_t led2Mode;  uint8_t redLedMode;  uint8_t blueLedMode;  uint8_t greenLedMode;  uint8_t wsLedMode;  }mainData;struct radioData{  bool stable;  int8_t x_acs;  int8_t y_acs;  int8_t z_acs;  uint8_t mode;  uint8_t channel;  uint16_t button;    uint16_t potData;  uint16_t joyX;  uint16_t joyY;} telemetriData;void readMode();void readCh();void readAcs();void readJoy();void readPot();void readButtons();void sendNRF();void sendBL();void sendLoRa();   // will reliase it soonvoid displayInfo();// at all it is possible to create up to 256 diffrent modes,// but if you need more - connect mode counter with channel counter (maybe partly)void n1Mode();void n2Mode();void n3Mode();void n4Mode();void n5Mode();void n6Mode();void n7Mode();void n8Mode();void n9Mode();void n10Mode();void n11Mode();void n12Mode();void acsel(){  mainData.irqMPU=true;}void gesture(){  mainData.irqAPDC=true;}void setup() {  for(int i=0;i<8;i++)    pinMode(first_sw[i], INPUT_PULLUP);  for(int i=0;i<8;i++)    pinMode(second_sw[i], INPUT_PULLUP);  for(int i=0;i<4;i++)    pinMode(buttons[i], INPUT_PULLUP);  pinMode(LED1, OUTPUT);  pinMode(LED2, OUTPUT);  analogWrite(LED1, 10);  analogWrite(LED2, 100);    pinMode(JOY_X, INPUT);  pinMode(JOY_Y, INPUT);  pinMode(POT, INPUT_PULLUP);    pinMode(LORA_D0, OUTPUT);  pinMode(LORA_NSS, OUTPUT);  pinMode(LORA_RST, OUTPUT);    pinMode(NRF_CSN, OUTPUT);  pinMode(NRF_CE, OUTPUT);    pinMode(IR_LED, OUTPUT);  pinMode(R_LED, OUTPUT);  pinMode(G_LED, OUTPUT);  pinMode(B_LED, OUTPUT);    pinMode(WS_LED, OUTPUT);  strip.setBrightness(130);    strip.clear();  strip.show();   strip.fill(mCOLOR(YELLOW));  strip.show();  Serial.begin(115200);  Serial2.begin(9600);  mp3_set_serial(Serial2);  mp3_set_volume(30);  mp3_play (1);  if (!mpu.begin())    Serial.println("Sensor init failed");  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32    Serial.println(F("SSD1306 allocation failed"));    for(;;); // Don't proceed, loop forever  }  display.display();  display.clearDisplay();    display.display();  if(!apds.begin())    Serial.println("failed to initialize device! Please check your wiring.");  apds.enableProximity(true);  apds.enableGesture(true);  radio.begin();                                        radio.setChannel(100);                                 radio.setDataRate     (RF24_1MBPS);                     radio.setPALevel      (RF24_PA_HIGH);                   radio.openWritingPipe (0x1234567899LL);                 radio.setAutoAck(false);  attachInterrupt(0, acsel, RISING);  attachInterrupt(1, gesture, RISING);  Serial1.begin(9600);         // bluetooth module connected to Serial1   delay(5000);  mp3_stop ();    }void loop(){ readMode(); readCh(); readAcs(); readJoy(); readPot(); readButtons(); Serial.println(digitalRead(A14)); Serial.println(digitalRead(A13)); Serial.println(digitalRead(A12)); Serial.println(digitalRead(A11)); Serial.println(digitalRead(A10)); Serial.println(digitalRead(A9)); Serial.println(digitalRead(A8)); Serial.println(digitalRead(A7)); Serial.println(); Serial.println();   displayInfo();  switch (mainData.mode)  {  case 0:    n1Mode();    break;  case 2:    n2Mode();    break;  case 3:    n3Mode();    break;  case 4:    n4Mode();    break;    }}void readAcs()      // reading acseleration values from sensor directly to main struct{  sensors_event_t a, g, temp;  mpu.getEvent(&a, &g, &temp);  mainData.x_acs = a.acceleration.x;  mainData.y_acs = a.acceleration.y;  mainData.z_acs = a.acceleration.z;  return;}void readJoy()     // i am filering analog values for better perfomance {  mainData.joyX = (analogRead(JOY_X)+analogRead(JOY_X)+analogRead(JOY_X)+analogRead(JOY_X))/4;  mainData.joyY = (analogRead(JOY_Y)+analogRead(JOY_Y)+analogRead(JOY_Y)+analogRead(JOY_Y))/4;  return;}void readPot(){  mainData.potData = analogRead(POT);  return;}void readButtons()   // buttons : 1) 1; 2)0; 3)1; 4)1;   and mainData.button == 1011 {  mainData.button = !digitalRead(A1)*1000+!digitalRead(A2)*100+!digitalRead(A3)*10+!digitalRead(A0);  return;}void sendNRF(){  // i am writing telemetri struct only when sending data  // in this case i can track how relevant telemetri data is  telemetriData.stable = mainData.stable;  telemetriData.x_acs = mainData.x_acs;  telemetriData.y_acs = mainData.y_acs;  telemetriData.z_acs = mainData.z_acs;  telemetriData.mode = mainData.mode;  telemetriData.channel = mainData.channel;  telemetriData.button = mainData.button;    telemetriData.potData = mainData.potData;  telemetriData.joyX = mainData.joyX;  telemetriData.joyY = mainData.joyY;  radio.write(&telemetriData, sizeof(telemetriData));}void sendBL(String inp){  Serial1.print(inp);  return;}// void sendLoRa();void displayInfo(){  display.clearDisplay();  display.setTextSize(1);               display.setTextColor(WHITE);         display.setCursor(0, 0);              display.print(mainData.channel);      display.print("  ");            display.print(mainData.mode);  display.print("  ");  display.println(mainData.z_acs);        display.print(mainData.button);  display.print("  ");  display.print(mainData.joyX);  display.print("  ");  display.print(mainData.joyX);  display.print("  ");  display.println(mainData.potData);  display.display();}void readMode(){  bitWrite(mainData.mode, 0, (!digitalRead(A14)));  bitWrite(mainData.mode, 1, (!digitalRead(A13)));  bitWrite(mainData.mode, 2, (!digitalRead(A12)));  bitWrite(mainData.mode, 3, (!digitalRead(A11)));  bitWrite(mainData.mode, 4, (!digitalRead(A10)));  bitWrite(mainData.mode, 5, (!digitalRead(A9)));  bitWrite(mainData.mode, 6, (!digitalRead(A8)));  bitWrite(mainData.mode, 7, (!digitalRead(A7)));  return;}void readCh(){  bitWrite(mainData.channel, 0, digitalRead(second_sw[0]));  bitWrite(mainData.channel, 1, digitalRead(second_sw[1]));  bitWrite(mainData.channel, 2, digitalRead(second_sw[2]));  bitWrite(mainData.channel, 3, digitalRead(second_sw[3]));  bitWrite(mainData.channel, 4, digitalRead(second_sw[4]));  bitWrite(mainData.channel, 5, digitalRead(second_sw[5]));  bitWrite(mainData.channel, 6, digitalRead(second_sw[6]));  bitWrite(mainData.channel, 7, digitalRead(second_sw[7]));  return;}void n1Mode(){  sendNRF();  digitalWrite(LED1, !digitalRead(LED1)); // just blink to understand, that it is working}void n2Mode(){}void n3Mode(){}void n4Mode(){}void n5Mode(){}void n6Mode(){}void n7Mode(){}void n8Mode(){}void n9Mode(){}void n10Mode(){}void n11Mode(){}void n12Mode(){}
Подробнее..

Как не держать лишнее железо и справляться с ростом нагрузки внедрение graceful degradation в Яндекс.Маркете

14.01.2021 12:05:51 | Автор: admin

Привет, меня зовут Евгений. Я разрабатываю инфраструктуру поиска Яндекс.Маркета. Хочу рассказать, как graceful degradation помогает нам обрабатывать больше запросов, чем физически могут выдержать наши сервера, и что происходит с поиском в Маркете, если один из дата-центров отключается.

Проблема

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

Обычное состояниеОбычное состояние

Но когда все ДЦ работают, каждый из них оказывается задействован лишь на 60%. Ещё 10% резервируются для экспериментов и непредвиденного роста, а оставшиеся 30% используются только в случае, если один из ДЦ отключается, и нужно перераспределить запросы.

ДЦ 2 отключёнДЦ 2 отключён

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

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

ДЦ 1 и ДЦ 3 не справляются с нагрузкойДЦ 1 и ДЦ 3 не справляются с нагрузкой

Применяем graceful degradation

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

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

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

Чтобы запустить graceful degradation, нам надо было решить две задачи:

  1. Разработать механизм уменьшения нагрузки.

  2. Сделать автоматизацию включения механизма.

Механизм уменьшения нагрузки

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

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

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

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

Бэкенд получает запрос и раcпределяет его на 8 серверов с шардами. В шардах хранятся предложения от магазинов.

Общая схема обработки поискового запросаОбщая схема обработки поискового запроса

На каждом шарде поиск проходит несколько стадий. На стадии фильтрации ищется примерно 50000 предложений, это число зависит от категории. На этапе ранжирования для каждого предложения вычисляется релевантность, учитывается цена, рейтинг товара, рейтинг магазина и ещё более 2000 факторов. ML по факторам вычисляет вес каждого предложения. Затем берётся только 48 лучших. Meta Search получает эти 48 предложений с каждого шарда, то есть всего 48*8=384 предложения. Предложения снова ранжируются, опять берётся 48 лучших. Последние 48 уже показываются пользователю. То есть чтобы показать нашим пользователям 48 телефонов, мы обрабатываем 400 000 предложений.

Количество обрабатываемых документов без graceful degradationКоличество обрабатываемых документов без graceful degradation

В случае с graceful degradation, когда надо уменьшить нагрузку, мы можем скомандовать: теперь обрабатывай 95% документов, а теперь 90% или 80%. Если обрабатывать 95%, то есть 400000*0.95=380 000 документов, то из них всё равно выбираются 48 лучших предложений для выдачи. И в среднем только 2 предложения будут отличаться от изначальной выдачи без снижения качества. При таком маленьком изменении большинство пользователей даже не заметят разницы.

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

Автоматизация включения механизма

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

Общий алгоритм выглядит так:

При выключении ДЦ: балансеры перераспределяют запросы в оставшиеся ДЦ => нагрузка на CPU повышается => при превышении порогового значения происходит снижение качества по заданной формуле.

При включении ДЦ: балансеры перераспределяют запросы на все ДЦ => нагрузка на CPU снижается => понижение качества прекращается.

Повышение нагрузки при выключении ДЦ. Линии на верхнем графике показывают загрузку CPU в отдельных ДЦ. Нагрузка выросла с 82% до 98%. Нижний график показывает процент срезанных документов. Повышение нагрузки при выключении ДЦ. Линии на верхнем графике показывают загрузку CPU в отдельных ДЦ. Нагрузка выросла с 82% до 98%. Нижний график показывает процент срезанных документов.

Внедрение

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

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

Выводы

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

Подробнее..

Интерпретатор скрипта на С

24.12.2020 20:04:36 | Автор: admin
Всем привет.

Написал простой интерпретатор, конечно не конкурент lua, но тоже может пригодиться.
Кому интересно прошу.

Сразу пример, что получилось:

stringstream ss;ss << "$a = 5;"      "$b = 2;"      "while($a > 1){"      "  $a -= 1;"      "  $b = summ($b, $a);"      "  if($a < 4){"      "    break;"      "  }"      "}"      "$b";string res = ir.cmd(ss.str()); // 9

Что хотелось


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

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


Скриптовый язык вышел простой и ограниченный конечно.

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

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

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


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

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

Все ошибки написания скрипта находятся на этом этапе.

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

Внутри все построено на рекурсивном вызове функций и проверках условий вызова.

Основные компоненты скрипта:

  • Переменная. Любая последовательность символов в коде скрипта начинающаяся с '$', считается переменной.

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

    Объявляются и используются только в коде скрипта, сразу использовать без объявления можно (значение по умолчанию пустая строка):

    $c = 5 + 6;summ($c, 6);
    

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

    Intrerpreter ir;ir.addFunction("summScriptVars", [&ir](const vector<string>& args) ->string {    int res = 0;    for (auto& v : ir.allVariables()) {      if (isNumber(v.second)) res += stoi(v.second);    }    return to_string(res);  });
    
  • Выражение. Состоит из переменных, операторов и вызовов функций.

    Обязательно должно заканчиваться символом ';'.

    Может быть параметром функции, в этом случае его не нужно закрывать символом ';'.

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

    $b = 4; $c = 5 + $b + 3 - 7; $a = $b * (3 + $c) + summ($a, $b, $c + 1);
    
  • Функция. Любые функции создаются на уровне основного кода, в скрипте только используются. Функция принимает массив параметров, возвращает строку как результат работы.
    Сначала функцию нужно определить и добавить в основном коде:

    Interpreter ir;ir.addFunction("summ", [](const vector<string>& args) ->string {    int res = 0;    for (auto& v : args) {      if (isNumber(v)) res += stoi(v);    }    return to_string(res);  });
    

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

    $b = summ($b, $a);
    

    Функция может принимать другие функции и выражения:

    $b = 1;$c = summ($b, summ($b + 5, $b + $b - 1), 4);$a = $c - summ($b, 3);
    
  • Оператор. Любая последовательность символов в коде скрипта, заранее определенная в основном коде, считается оператором.

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

     Interpreter ir; ir.addOperator("+", [](string& leftOpd, string& rightOpd) ->string {    if (isNumber(leftOpd) && isNumber(rightOpd))      return to_string(stoi(leftOpd) + stoi(rightOpd));    else      return leftOpd + rightOpd;  }, 1);   ir.addOperator("==", [](string& leftOpd, string& rightOpd) ->string {    return leftOpd == rightOpd? "1" : "0";  }, 2);  ir.addOperator("=", [](string& leftOpd, string& rightOpd) ->string {    leftOpd = rightOpd;    return leftOpd;  }, 17);
    

    При создании оператора помимо определения нужно задать приоритет.

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

    Операторы используются в выражениях.

    $c = 5 + 6;$b = 2;$a = $c + 5; $c = summ($a + 5 / $b);
    


Теперь опишу остальные ключевые слова языка скрипта, в основном это управляющие конструкции.

  • while(condition){body}. Выполняет циклически последовательность выражений (далее, тело цикла) в зависимости от результата выполнения условия.

    Условие заключается в скобочки '()' и, как и в любом языке, рассчитывается на каждой итерации цикла.

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

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

    $c = 1;$b = 4; while($b > 0){  $c *= $b;   $b -= 1;}
    

  • if(condition){body}. Выполняет однократно последовательность выражений в зависимости от результата выполнения условия.

    $c = 1;$b = 4; if(($b - 4) == 0){  $c = $b;}
    

  • else{body}. Выполняет однократно последовательность выражений, если не было выполнено предыдущее условие.

    $c = 1;$b = 4; if(($b - 3) == 0){  $c = $b;}else{  $b = $c;}
    

  • elseif(condition){body}. Выполняет однократно последовательность выражений, если не было выполнено предыдущее условие и выполняется текущее условие.

    $c = 1;$b = 4; if(($b = $b - 3) == 0){  $c = $b;}elseif($c == summ($b)){  $b = $c;}
    

  • break;. Выполняет прерывание текущего цикла.
    continue;. Начинает заново текущий цикл.

    $b = 4; while($b > 0){  $b = rand(10);  if ($b == 3){    continue;  }  if ($b == 2){    break;   }}
    

  • #macro name{body}. Объявление макроса.

    #name;. Вставка тела макроса далее в коде.

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

    #macro myMc{ $c = 1; $b = 4; };$d = 5;#myMc;
    

  • goto l_name;. Перемещение на метку вверх или вниз по скрипту. Должен быть единственным оператором в выражении.

    l_name:. Метка, на которую можно переместиться.

    Метка обязательно должна начинаться с 'l_' (элл и нижнее подчеркивание) и заканчиваться ':'.

    $a = 5; while($a > 0){  $a -= 1;  if ($a == 2){    goto l_myLabel;  }  }l_myLabel: $a;
    

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

    Interpreter ir;ir.addFunction("myJump", [&ir](const vector<string>& args) ->string {    if (!args.empty())      ir.gotoOnLabel(args[0]);    }    return "";  });
    


Как использовать и где может быть полезен


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

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

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

Еще можно попробовать построить RPC на его основе.

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


Если коротко, то ничего.

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

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

Распространяется свободно, лицензия MIT

Спасибо.

P.S.:

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

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

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

Томограф для нефтегазовых месторождений, или Пересечение трёхмерной расчётной сетки и плоскости на CUDA

28.12.2020 08:09:56 | Автор: admin
В данной статье приведены описание и алгоритм решения задачи построения рисунка внутренностей месторождения, являющегося результатом пересечения расчётной сетки с плоскостью. А также приведены тайминги построения решения, которые получаются на типичном компьютере геолога-модельера или гидродинамика.
image
Визуализация расчётной сетки и куба


Моделирование месторождений уже упоминалось ранее в предыдущих наших обзорах (http://personeltest.ru/aways/habr.com/ru/company/bashnipineft/blog/512052/). При построении 3D-моделей нефтегазовых месторождений в различных областях в гидродинамике, в геологии, в механике часто используются объёмные расчётные сетки. Также сетки нужны, например, при использовании метода конечных объёмов.

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

Часто пользователю, который работает с моделью, нужно визуально оценить распределение физических свойств внутри модели месторождения. Это можно делать разными способами, например, созданием сечений или наложением масок-фильтров на ячейки сетки.
Реализовать в программе визуализацию слоев (i, j, k) сетки довольно просто и быстро.

image
Визуализация слоев сетки i, j (слайсов)

Однако хотелось бы получать рисунок вдоль произвольной плоскости, при этом иметь возможность двигать и крутить эту плоскость в произвольном направлении с хорошим FPS. Таким образом, модельеру нужен этакий быстрый томограф модели месторождения.
Геометрия угловой точки (corner-point grid) способ задания геометрии расчётных сеток из шестигранников, наиболее часто использующийся в гидродинамическом и геологическом моделировании. Такой формат подразумевает:

Наличие пилларов отрезков, каждый из которых задан двумя точками, а каждая точка тремя координатами (x, y, z). Пиллары могут быть вертикальными, наклонными, но никак не горизонтальными. Пиллары образуют множество размером M x N, но не обязательно с постоянным шагом. Множество пилларов задаётся массивом COORD.

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

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

image
Геометрия угловой точки

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

  1. размерность сетки;
  2. массив COORD;
  3. массив ZCORN;
  4. массив ACTNUM;

2) коэффициенты общего уравнения секущей плоскости:
$Ax+By+Cz+D=0$

Решение не обязательно должно быть точным, ведь решение будет оцениваться пользователем только визуально. На выходе должны получиться массивы:
  • Упорядоченный массив троек индексов (i, j, k) пересечённых ячеек;
  • Упорядоченный массив координат точек полигонов пересечения ячеек;
  • Упорядоченный массив индексов точек начала нового полигона в массиве координат точек пересечения. В конце массива для удобства должно быть добавлено общее количество точек пересечения.

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

Хорошо бы, чтобы алгоритм построения пересечения был нетребователен к памяти, так как сами сетки довольно объёмны. Например, большая сетка на 1000х1000х1000 ячеек, построенная на типе данных одинарной точности (float) занимает примерно 30 гигабайт памяти.

Нами был разработан довольно экономный к памяти алгоритм построения пересечения геометрии угловой точки и плоскости:
1. Проход по всем ячейкам и определение факта пересечение плоскости с ячейкой:
a. Подстановка каждой вершины в левую часть уравнения плоскости.
b. Если среди полученных чисел есть хотя бы два с противоположными знаками, то пересечение есть.
c. Если среди полученных чисел есть хотя бы три нуля, то пересечение есть.
d. Запись в массив флагов факта пересечения.

image
Геометрия угловой точки, секущая плоскость, пересечённые ячейки

2. Stream Compaction: из массива флагов собираем индексы ячеек, которые пересекаются с плоскостью. Располагаем их в новом массиве компактно.

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

4. Prefix sum: префиксная сумма по массиву количества точек пересечения в ячейках. Это позволит определить смещение от начала массива координат точек для каждой ячейки.

5. Поиск и запись точек пересечения в массив координат точек пересечения.

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

image
Упорядочивание точек пересечения для одной ячейки в полигон

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

image
Результат пересечения геометрии угловой точки и плоскости

Видно, что каждый шаг алгоритма можно распараллелить.
Была создана реализация алгоритма, распараллеленная на потоки с помощью OpenMP. В такой реализации примерно 87% времени выполнения занимает первый пункт алгоритма проход по всем ячейкам и определение факта пересечения. Время выполнения реализации на OpenMP оказалось слишком большим для комфортной работы с пересечениями в окнах визуализации.
Чтобы ускорить построение пересечения, алгоритм был реализован на CUDA. Сетка должна быть постоянно в видеопамяти, т. к. пересылка её с оперативной памяти занимает слишком много времени, даже если использовать CUDA Streams. В такой реализации примерно 43% времени выполнения занимает первый пункт алгоритма, 20% времени пересылка результатов с видеопамяти в оперативную память. Kernel-функция, используемая в реализации для определения факта пересечения, довольно проста:

Заголовок спойлера
__global__ void checkIntersectCell (const TCOORD* coord,const TZCORN* zcorn,const TACTNUM* actnum,TFLAGS* flags,TIJK I, TIJK J, TIJK K,TPLANE A, TPLANE B, TPLANE C, TPLANE D,const TCOEF* coefX, // предрасчитанные коэффициенты для поиска координат x узловconst TCOEF* coefY // предрасчитанные коэффициенты для поиска координат y узлов){const auto globalIndex = blockDim.x * blockIdx.x + threadIdx.x;if (globalIndex < (I * J * K)){if (actnum[globalIndex]){// globalIndex = K * J * i + K * j + k = K * (J * i + j) + kconst auto k = globalIndex % K;const auto j = (globalIndex / K) % J;const auto i = (globalIndex / K) / J;int_fast8_t signPlus = 0;int_fast8_t signMinus = 0;int_fast8_t signZero = 0;for (size_t p = 0; p < vertexCount; ++p){const auto indexPillar = (J + 1) * (i + p / 4) + (j + (p / 2) % 2);const auto zPillarTop = coord[6 * indexPillar + 2];const auto z = zcorn[(2 * i + p / 4) * 2 * 2 * J * K + (2 * j + (p / 2) % 2) * 2 * K + (2 * k + p % 2)];const auto x = (z - zPillarTop) * coefX[indexPillar] + coord[6 * indexPillar + 0];const auto y = (z - zPillarTop) * coefY[indexPillar] + coord[6 * indexPillar + 1];switch (sign<int_fast8_t>(A * x + B * y + C * z + D)){case -1: ++signMinus; break;case 1: ++signPlus; break;case 0: ++signZero; break;}}flags[globalIndex] = static_cast<TFLAGS>((signMinus > 0 && signPlus > 0) || signZero >= 3);}else{flags[globalIndex] = static_cast<TFLAGS>(0);}}}


Для тестирования был взят случай, который даёт большое количество полигонов пересечения. Сетка в тестах состояла из прямоугольных параллелепипедов, по каждому направлению (x, y и z) сетка располагалась от 0 м до 100 м. Плоскость проходила через центр сетки и имела нормаль (1, 1, 1).

image
Геометрия угловой точки и секущая плоскость для тестирования

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

image
Таблица с результатами тестирования и замерами времени выполнения

image
Синяя диаграмма показывает ускорение построения пересечения на OpenMP на 8 потоках относительно OpenMP на 1 потоке. Зелёная диаграмма показывает ускорение построения пересечения на CUDA относительно OpenMP на 8 потоках

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

Также видно, что количество вершин в результате пересечения незначительно отличается. Это происходит в случае, когда плоскость касается угла ячейки. Из-за точности расчётов в результате может засчитаться в пересечение очень маленький треугольник, а может и быть пропущен. Результат различается не только между построением на CPU и GPU, но также и между построением на различных CPU и на различных GPU.
Пересечение геометрии угловой точки и плоскости обычно просматривают в 3D окне.

image
Модель месторождения и секущая плоскость

image
Результат пересечения модели месторождения и плоскости

Заключение: в статье показан пример решения одной из многих задач, возникающих при создании программного обеспечения для процессов моделирования месторождений нефти и газа. Несмотря на огромный парк open-source кодов, решение подобных задач требует мультидисциплинарной подготовки разработчиков, и поэтому представляется интересными развивающими вызовами в области computer science, в которых доля рутинного программирования минимальна, а мозги надо включать на полную. Этим и интересны задачи нашей предметной области, к которым хотелось бы привлечь внимание аудитории и потенциально заинтересованных энтузиастов и разработчиков. В частности, создание построителя пересечения геометрии угловой точки и плоскости являлось задачей на одном из хакатонов, проведённых в 2019 году РН-БашНИПИнефть при участии Уфимского Государственного Авиационного Технического Университета (УГАТУ).

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

P. S. Возможно, поиск пересекаемых ячеек был бы быстрее, если бы применялось K-d дерево. Но реализовать такое решение гораздо сложнее, учитывая, что нужно придумать, как удачно построить само дерево поиска и равномерно распараллелить поиск на потоки. К тому же, если даже теоретически поиск ускорить так, чтобы он вообще не занимал времени (сейчас занимает 87% времени), то производительность на OpenMP реализации вырастет в 7.7 раз, что всё равно медленнее, чем реализация на CUDA.
Подробнее..

RESTinio-0.6.13 последний большой релиз RESTinio в 2020 и, вероятно, последний в ветке 0.6

29.12.2020 12:11:44 | Автор: admin


RESTinio это относительно небольшая C++14 библиотека для внедрения HTTP/WebSocket сервера в C++ приложения. Мы старались сделать RESTinio простой в использовании, с высокой степенью кастомизации, с приличной производительностью. И, вроде бы, пока что это получается.


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


Кому интересно, милости прошу под кат.


Главная фича версии 0.6.13: цепочки из синхронных обработчиков


Главной целью, которую мы преследовали начиная в 2017-ом году проект RESTinio, было упрощение написания HTTP-точек входа в C++ приложения. И одним из способов такого упрощения было заимствование лучшего из того, что нас окружало. В частности, в RESTinio мы сделали аналог роутера запросов из ExpressJS. В итоге express_router стал чуть ли не наиболее востребованной из возможностей RESTinio.


Но в ExpressJS кроме роутера есть еще важная штука: middleware. И вот её-то мы изначально в RESTinio и не стали переносить.


Сперва эта функциональность нам была не нужна. Но по мере взросления RESTinio мы стали сталкиваться с ситуациями, в которых что-то похожее на middleware из ExpressJS было бы полезным. А раз так, то захотелось эти самые middleware поиметь и в RESTinio.


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


Цепочки из синхронных обработчиков


Итак, начиная с версии 0.6.13 обработчики запросов в RESTinio можно выстраивать в цепочки. И такие обработчики будут последовательно вызываться для обработки очередного запроса. Движение по цепочке от обработчика до обработчика происходит пока все они возвращают специальное значение not_handled. Если же какой-то из обработчиков возвращает accepted или rejected, то обработка запроса прекращается и оставшиеся в цепочке обработчики не вызываются.


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


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

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


auto incoming_req_logger(const restinio::request_handle_t & req){  ... // Логируем запрос.  // Разрешаем запустить следующий обработчик в цепочке.  return restinio::request_not_handled();  }auto mandatory_fields_checker(const restinio::request_handle_t & req){  ... // Выполняем нужные проверки.  if(!ok) {    // Отсылаем отрицательный ответ и прерываем цепочку.    return req->create_response(restinio::status_bad_request())      ...      .done(); // Здесь возвращается accepted.  }  // Разрешаем запустить следующий обработчик в цепочке.  return restinio::request_not_handled();  }auto permissions_checker(const restinio::request_handle_t & req){  ... // Проверяем пользователя и его права.  if(!ok) {    // Отсылаем отрицательный ответ и прерываем цепочку.    return req->create_response(restinio::status_unauthorized())      ...      .done(); // Здесь возвращается accepted.  }  // Разрешаем запустить следующий обработчик в цепочке.  return restinio::request_not_handled();}auto actual_processor(const restinio::request_handle_t & req){  ... // Основная обработка запроса.  return restinio::request_accepted();}

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


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


Итак, поскольку количество элементов в цепочке у нас строго фиксировано, то используем fixed_size_chain_t:


// Этот заголовочный файл нужно подключать явным образом.#include <restinio/sync_chain/fixed_size.hpp>...struct my_traits : public restinio::default_traits_t {  using request_handler_t = restinio::sync_chain::fixed_size_chain_t<4>;};

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


restinio::run(restinio::on_this_thread<my_traits>()  .port(...)  .address(...)  .request_handler(    // Перечисляем обработчики в порядке их вызова.    incoming_req_logger,    mandatory_fields_checker,    permissions_checker,    actual_processor)  ...);

Вот, собственно, и все.


Почему цепочка из синхронных обработчиков?


RESTinio строился с прицелом именно на асинхронную обработку запросов. Но добавленные в версию 0.6.13 цепочки обработчиков отрабатывают только синхронно. Почему так?


Тут надо зайти издалека.


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


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


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


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


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


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


Теперь вернемся к цепочкам.


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


Предположим, что один из обработчиков вернул не not_handled, не rejected, а accepted. В каком состоянии находится запрос?


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


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


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


Значит ли это, что внутри цепочки нельзя делегировать обработку кому-то еще?


Нет. Обработчик может делегировать обработку запроса на какую-то другую нить. Но после этого обработчик должен возвратить accepted и цепочка будет прервана.


Можно ли сделать цепочку из асинхронных обработчиков?


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


Обмен данными между обработчиками в цепочке


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


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


  • authentification_handler, который проверяет наличие параметров аутентификации клиента и выполняет аутентификацию;
  • permissions_checker, который проверяет, есть ли у пользователя права на доступ к запрашиваемому ресурсу;
  • admin_access_logger, который логирует доступ пользователя к административным ресурсам;
  • actual_processor, который выполняет обработку запроса.

Первый обработчик должен породить некий объект user_permissions, в котором будет содержаться идентификатор пользователя и информация о его правах. Далее этот объект должен использоваться в permissions_checker-е (для проверки возможности доступа к ресурсу) и в admin_access_logger (для фиксации в журнале).


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


При поиске ответа на этот вопрос было рассмотрено несколько вариантов. В работу пошел вариант, который позволяет встроить пользовательские данные внутрь RESTinio-вского объекта request_t.


Выглядит это следующим образом.


Сперва пользователь определяет некую структуру/класс, экземпляры которой и должны встраиваться в объект-запрос:


struct user_permissions {...};...// Вот эта структура должна быть добавлена в каждый запрос.struct per_request_data {  user_permissions user_info_;  ... // Возможно, что-то еще.};

Далее нужно создать тип т.н. extra-data-factory, т.е. фабрики для этой самой дополнительной информации:


struct my_extra_data_factory {  // Внутри extra-data-factory должен быть тип с именем data_t.  using data_t = per_request_data;  // А также вот такой фабричный метод.  void make_within(restinio::extra_data_buffer_t<data_t> buf) {    new(buf.get()) data_t{};  }};

Затем нужно указать тип нашей фабрики в свойствах сервера:


struct my_traits : public restinio::default_traits_t {  using extra_data_factory_t = my_extra_data_factory;};

Ну и, самое, важное: теперь у обработчиков запросов поменяется формат. Вместо аргумента типа restinio::request_handle_t они будут получать restinio::generic_request_handle_t<per_request_data>:


restinio::request_handling_status_t authentification_handler(  const restinio::generic_request_handle_t<per_request_data> & req);restinio::request_handling_status_t permissions_checker(  const restinio::generic_request_handle_t<per_request_data> & req);restinio::request_handling_status_t admin_access_logger(  const restinio::generic_request_handle_t<per_request_data> & req);restinio::request_handling_status_t actual_processor(  const restinio::generic_request_handle_t<per_request_data> & req);

Собственно, это все.


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


// Пусть у каждого запроса будет собственный поток для журналирования.struct per_request_data {   std::shared_ptr<log_stream> log_;   per_request_data(std::shared_ptr<log_stream> log)      : log_{std::move(log)}   {}};// За создание этих потоков будет отвечать фабрика.class my_extra_data_factory {   std::shared_ptr<logger> logger_;public:   using data_t = per_request_data;   my_extra_data_factory(std::shared_ptr<logger> logger)      : logger_{std::move(logger)}   {}   void make_within(restinio::extra_data_buffer_t<data_t> buf) {      new(buf.get()) data_t{         std::make_shared<log_stream>(logger_)      };   }};struct my_traits : public restinio::default_traits_t {   using extra_data_factory_t = my_user_data_factory;};auto logger = std::make_shared<logger>(...);// Фабрику нужно будет вручную создать перед запуском сервера.restinio::run(restinio::on_thread_pool<my_traits>(16)   .port(...)   .address(...)   // Вот мы создаем фабрику и передаем её RESTinio.   .extra_data_factory(std::make_shared<my_user_data_factory>(logger))   .request_handler(...));

Внутри обработчика запросов доступ к дополнительным данным можно получить посредством метода extra_data у объекта generic_request_t:


restinio::request_handling_status_t authentification_handler(  const restinio::generic_request_handle_t<per_request_data> & req){  ... // Производим аутентификацию.  if(!ok) {    // Шлем отрицательный ответ.    return req->create_response(...)...done();  }  else {    // Сохраняем информацию о пользователе внутри запроса.    req->extra_data().user_info_ = user_permissions{...};    return restinio::request_not_handled();  }}restinio::request_handling_status_t permissions_checker(  const restinio::generic_request_handle_t<per_request_data> & req){  // Запрашиваем информацию о пользователе с предыдущего шага.  const auto & user_info = req->extra_data().user_info_;  ... // Работа с информацией о пользователе.}

Дополнительная информация, generic_request_t<Extra_Data> и совместимость со старым кодом


По сути, начиная с версии 0.6.13, RESTinio работает уже с двумя новыми типами: шаблонным классом generic_request_t<Extra_Data> и шаблонным псевдонимом generic_request_handle_t<Extra_Data> (который есть std::shared_ptr<generic_request_t<Extra_Data>>).


А для того, чтобы такое кардинальное нововведение не поломало ранее написанный код, старые имена request_t и request_handle_t теперь являются всего лишь псевдонимами для generic_request_t<no_extra_data_factory_t::data_t> и generic_request_handle_t<no_extra_data_factory_t::data_t>, где no_extra_data_factory_t это новый тип для фабрики по умолчанию.


В restinio::traits_t, restinio::default_traits_t и restinio::default_single_thread_traits_t именно no_extra_data_factory_t используется в качестве extra_data_factory_t. Поэтому старый код, который использует имена request_t и request_handle_t, сохраняет свою работоспособность и требует только лишь перекомпиляции.


extra-data и express-/easy_parser_router


Выше уже говорилось, что express_router, сделанный по мотивам ExpressJS, является одной из наиболее востребованных возможностей RESTinio. При этом express_router вводит собственный формат для обработчиков запросов. Соответственно, появление extra-data для запроса сказалось и на express_router-е.


Если программист хочет использовать extra-data с запросами, которые обрабатываются посредством express_router-а, то ему нужно явно указать express_router-у тип фабрики extra-data. Например:


struct my_extra_data_factory { ... };struct my_traits : public restinio::default_traits_t {  using extra_data_factory_t = my_extra_data_factory;  using request_handler_t = restinio::router::express_router_t<    restinio::router::std_regex_engine_t,    extra_data_factory_t>;};

Вот после этого первым аргументом в обработчик запроса для express_router вместо request_handle_t будет generic_request_handle_t<my_traits::extra_data_factory_t::data_t>.


Тоже самое относится и к easy_parser_router:


struct my_traits : public restinio::default_traits_t {  using extra_data_factory_t = my_extra_data_factory;  using request_handler_t = restinio::router::easy_parser_router_t<    extra_data_factory_t>;};

Зачем делать RESTinio-0.7?


Теперь можно сказать несколько слов о том, почему же время жизни ветки 0.6 подходит к концу и зачем начинать ветку 0.7.


Причин несколько.


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


Во-вторых, в коде RESTinio уже обнаружилось несколько просчетов, которые было бы желательно исправить. Но исправления поломали бы совместимость, поэтому пока что эти исправления не вносились. Однако, рано или поздно с этим нужно было бы что-то делать. Так почему бы не сейчас?


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


  • поддержка не только http/1.1, но и http/2, а затем и http/3;
  • дополнительный режим работы, в котором RESTinio не загружает весь запрос в память перед вызовом обработчика, а вызывает обработчик по мере загрузки отдельных частей запроса;
  • поддержка цепочки асинхронных обработчиков.

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


А есть ли у вас какие-то пожелания к RESTinio?


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


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


Сразу скажу, что клиента на базе RESTinio мы за свой счет не потянем. Об этом не просите :( Если только просьба не будет подкреплена материально ;)


В общем, приглашаю всех желающих высказать свои соображения о функциональности, которую хотелось бы видеть в RESTinio, в Issues или Discussions на GitHub. Или в Google-группу. Ну или можно прямо сюда, в комментарии.


Вместо заключения


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


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


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


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


Ну и с наступающим Новым Годом!

Подробнее..

Micro Property минималистичный сериализатор двоичных данных для embedded систем. Часть 2

06.01.2021 16:04:08 | Автор: admin
Некоторое время назад я опубликовал свою статью о разработке велосипедного велосипеда, в которой описал причины, побудившие меня этим заняться.

Если вкратце, то мне была нужна миниатюрная библиотека для микроконтроллеров с сериализатором двоичных данных для последующей передачи этих сообщений по низко скоростным линиям связи, тогда как обычные форматы xml, json, bson, yaml, protobuf, Thrift, ASN.1 и др. мне по разным причинам не подходили.

Как и ожидалось, решение оказалось более чем велосипедом, и тем не менее, сама публикация статьи на Хабре мне очень сильно помогла. Дело в том, что при первоначальном анализе возможных библиотек, я почему то упустил из вида сериализаторы MessagePack, CBOR и UBJSON.

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

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




CBOR vs. MessagePack


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

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

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

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

А вот в MessagePack пошли еще дальше! В этом формате минимальные накладные расходы на хранение значения составляют всего 1 (ОДИН!) бит информации. Соответственно и диапазон возможных значений для хранения с помощью одного байта значительно больше (0 до 127). А для указания дополнительной информации о типе поля используются значения с установленным старшим битом.

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

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

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

Первоисточники:


Спецификация CBOR. Есть хорошая статья с описанием на Хабре.

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

Перевод Введение в Data Parallel C. Пишем первую программу

13.01.2021 10:12:44 | Автор: admin


Перед вами введение в программирование на языке Data Parallel C++ или, коротко, DPC++. DPC++ основан на Khronos SYCL это означает, что перед нами модель современного параллельного программирования. Новейшим текущим стандартом Khronos является SYCL 1.2.1, хотя предварительная спецификация SYCL 2020 уже доступна для изучения. Intel и другие участники рабочей группы SYCL в настоящее время занимаются финализацией следующей версии спецификации. DPC++ содержит расширения, которые облегчают использование SYCL, при этом многие из них, как ожидается, войдут в состав SYCL 2020. Внедрение таких расширений в компилятор DPC++ помогает сообществу оценить их эффективность заранее перед стандартизацией.

Для кого предназначена эта статья


Эта статья для программистов, уже имеющих достаточное представление о С++ и параллельности. Изучение С++ и параллельности непростая задача, и написано об этом немало. О SYCL информации намного меньше, а о DPC++ еще меньше, поэтому на них мы и сосредоточимся.

SYCL имеет свои корни в OpenCL, их модели исполнения довольно схожи. Если вам нужна помощь в понимании модели исполнения SYCL/OpenCL, обратитесь к этому обзору-презентации(на английском, но кратко, ёмко и с картинками).

Для кого НЕ предназначена эта статья


Когда я рассказываю о SYCL, то часто говорю: Если вам нравится современный С++, то понравится и SYCL, потому что это определенно современный С++. И наоборот, если вам не по душе С++, то не понравятся и SYCL с DPC++. Поэтому, если вы не хотите писать на современном С++, эта учебная статья не для вас.

OpenMP 5.0 имеет почти тот же функционал, что и SYCL/DPC++, но поддерживает большую тройку ISO-стандартизированных языков: С, С++ и Fortran. Если вы хотите программировать на CPU и GPU с использованием Fortran, C или C++ до 11 версии с применением открытого промышленного стандарта, выберите OpenMP.

Другой альтернативой SYCL/DPC++ без C++ является OpenCL. OpenCL намного более многословен, чем SYCL, но если вы программист на С, то, скорее всего, предпочтете явный контроль за эффективностью синтаксиса.

Сложение векторов в SYCL


Мы начнем со сложения векторов, которое в некотором смысле является аналогом Hello, world! в мире HPC и численных методов. Очевидно, вывод символьной строки не может считаться хорошей задачей для языка параллельного программирования.

Операцией, которую мы пытаемся реализовать, является SAXPY A умножить на X плюс Y, одинарной точности. На C (или C++ в простейшем случае) это делается так:



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

Вот как выглядит тот же цикл в SYCL. Здесь надо много объяснять, поэтому разобьем код на маленькие кусочки.



Как вы, наверное, догадались, parallel_for это параллельный цикл for. Тело цикла представляет собой лямбда-выражение. Лямбда выражения это безымянные локальные функции, которые можно создавать прямо внутри какого-либо выражения. Их код выглядит как [..]{..}.

Оператор цикла выражен в терминах sycl::range и sycl::id. В нашем простом примере оба они одномерны, что указывается с помощью <1>. В SYCL диапазоны и, соответственно, идентификаторы могут быть одно-, двух-, или трехмерными. OpenCL и CUDA имеют то же ограничение.

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

Возможно, вас удивит аргумент <class saxpy> в цикле parallel_for. Это просто способ дать наименование ядру, что необходимо, поскольку вы можете захотеть использовать разные С++ компиляторы SYCL на хосте и устройстве. В этом случае два компилятора должны договориться об имени ядра. Во многих компиляторах SYCL, таких как Intel DPC++, этого не требуется. И мы можем попросить компилятор не беспокоиться о поиске имен с помощью опции -fsycl-unnamed-lambda.

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

Очереди в SYCL


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

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



По умолчанию селектор указывает на GPU при его наличии, и на CPU в противном случае. Мы можем создать очереди, ассоциированные с конкретным типом устройства, таким образом:



Выбор селектора host_selector() или cpu_selector() может существенно влиять на результат, даже если они показывают на одно и то же железо, поскольку host_selector() может использовать последовательное исполнение для целей отладки, при этом cpu_selector() использует рантайм OpenCL и задействует все имеющиеся процессорные ядра. Кроме того, JIT компилятор OpenCL в этом случае может генерировать другой код, поскольку он использует совершенно другой компилятор. Не стоит думать, что из-за того, что хостом всегда является CPU, хост и CPU означают одно и то же в SYCL.

Управление данными в SYCL с использованием буферов


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



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

Контроль исполнения устройства


Поскольку код устройства может потребовать другой компилятор или механизм генерации кода, важно ясно обозначить секции кода устройства. Ниже мы видим, как это выглядит в SYCL 1.2.1. Мы используем метод submit чтобы добавить задачу в очередь устройства q. Данный метод возвращает opaque handler, согласно которому мы исполняем ядра, в данном случае посредством parallel_for.



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

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

Вычислительные ядра и буферы


Аксессоры это финальный фрагмент нашей первой SYCL программы. Аксессоры, возможно, незнакомы программистам под GPU, но у них есть несколько преимуществ по сравнению с другими методами. В то время как SYCL позволяет программисту перемещать данные принудительно, с использованием, например, метода copy(), методы аксессора не требуют этого, поскольку они генерируют граф потоков данных, который может использоваться компилятором или рантаймом для передвижения данных в нужное время. Это особенно эффективно, когда множество ядер задействуются последовательно. В данном случае SYCL-реализация заключит, что данные используются повторно и не вернет их без нужды на хост. Также мы можем запланировать асинхронное перемещение данных (т.е. накладывающееся на исполнение устройства). Опытные GPU-программисты могут сделать это вручную, но, зачастую аксессоры SYCL показывают лучшую производительность по сравнению с программами на OpenCL, где данные приходится перемещать принудительно.



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

Наша первая SYCL программа


Приведем только что описанную программу SYCL SAXPY целиком:



Полный исходный код примера имеется на GitHub.

Unified Shared Memory (USM)


Хотя приведенная выше программа полностью функциональна и может быть использована на множестве платформ, некоторые пользователи сочтут ее довольно громоздкой. Кроме того, она несовместима с библиотеками и фреймворками, управляющими памятью с помощью указателей. Чтобы решить эту проблему в SYCL 1.2.1, Intel разработал расширение в DPC++ под названием Unified Shared Memory (USM), поддерживающее управление памятью с помощью указателей.

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

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



Если мы используем device allocation, данные должны быть перемещены явно, то есть с использованием SYCL метода memcpy, который работает так же, как и std::memcpy (то есть, цель находится слева):



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



Вы найдете полностью рабочие примеры обеих версий USM в репозитории под названиями saxpy-usm.cc и saxpy-usm2.cc соответственно.

Сжатый синтаксис SYCL 2020


И последнее на случай, если вам до сих пор интересно, зачем в каждой из программ был нужен opaque handler h, вроде как, совсем не нужный. Ниже приведена эквивалентная реализация, добавленная в предварительную спецификацию SYCL 2020. Более того, мы можем также воспользоваться лямбда-именами, они включены как опция в предварительную спецификацию SYCL 2020. Вместе эти два небольших изменения делают код ядра SYCL такого же размера, как и исходный цикл С++, показанный в самом начале:



Мы начали с трех строк кода, который работал последовательно на CPU и закончили тремя строками кода, который работает параллельно на CPU, GPU, FPGA и прочих устройствах. Очевидно, что не все будет таким простым, как SAXPY, но, по крайней мере, вы теперь знаете, что SYCL не усложняет простые вещи, и что он построен на основе современных фишек С++ и универсальных концепций типа parallel for, а не на чем-то принципиально новом, требующем дополнительного изучения.

Дополнительная литература


Подробнее..

Перевод Vulkan. Руководство разработчика. Устройства и очереди

14.01.2021 18:11:53 | Автор: admin
Я переводчик из ижевской компании CG Tribe, и я продолжаю выкладывать перевод руководства к Vulkan API. Ссылка на источник vulkan-tutorial.com.

В этой публикации представлен перевод последних двух глав раздела Drawing a triangle, подраздела Setup, которые называются Physical devices and queue families и Logical device and queues.

Содержание
1. Вступление

2. Краткий обзор

3. Настройка окружения

4. Рисуем треугольник

  1. Подготовка к работе
  2. Отображение на экране
  3. Основы графического конвейера (pipeline)
  4. Отрисовка
  5. Повторное создание цепочки показа

5. Буферы вершин

  1. Описание
  2. Создание буфера вершин
  3. Staging буфер
  4. Буфер индексов

6. Uniform-буферы

  1. Дескриптор layout и буфера
  2. Дескриптор пула и sets

7. Текстурирование

  1. Изображения
  2. Image view и image sampler
  3. Комбинированный image sampler

8. Буфер глубины

9. Загрузка моделей

10. Создание мип-карт

11. Multisampling

FAQ

Политика конфиденциальности


Физические устройства и семейства очередей




Выбор физического устройства


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

Добавим функцию pickPhysicalDevice и добавим ее вызов в функцию initVulkan.

void initVulkan() {    createInstance();    setupDebugMessenger();    pickPhysicalDevice();}void pickPhysicalDevice() {}

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

VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

Составление списка видеокарт похоже на составление списка расширений и начинается с запроса их количества.

uint32_t deviceCount = 0;vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

Если устройства, поддерживающие Vulkan, не найдены, нет смысла выполнять дальнейшие действия.

if (deviceCount == 0) {    throw std::runtime_error("failed to find GPUs with Vulkan support!");}

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

std::vector<VkPhysicalDevice> devices(deviceCount);vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

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

bool isDeviceSuitable(VkPhysicalDevice device) {    return true;}

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

for (const auto& device : devices) {    if (isDeviceSuitable(device)) {        physicalDevice = device;        break;    }}if (physicalDevice == VK_NULL_HANDLE) {    throw std::runtime_error("failed to find a suitable GPU!");}

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


Проверка соответствия устройства


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

VkPhysicalDeviceProperties deviceProperties;vkGetPhysicalDeviceProperties(device, &deviceProperties);

Информация о поддержке опциональных возможностей, таких как, сжатие текстур, 64-битные числа с плавающей точкой и рендеринг в несколько viewport-ов (multi viewport rendering) запрашивается с помощью vkGetPhysicalDeviceFeatures:

VkPhysicalDeviceFeatures deviceFeatures;vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

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

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

bool isDeviceSuitable(VkPhysicalDevice device) {    VkPhysicalDeviceProperties deviceProperties;    VkPhysicalDeviceFeatures deviceFeatures;    vkGetPhysicalDeviceProperties(device, &deviceProperties);    vkGetPhysicalDeviceFeatures(device, &deviceFeatures);    return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&           deviceFeatures.geometryShader;}

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

Вы можете реализовать что-то подобное:

#include <map>...void pickPhysicalDevice() {    ...    // Use an ordered map to automatically sort candidates by increasing score    std::multimap<int, VkPhysicalDevice> candidates;    for (const auto& device : devices) {        int score = rateDeviceSuitability(device);        candidates.insert(std::make_pair(score, device));    }    // Check if the best candidate is suitable at all    if (candidates.rbegin()->first > 0) {        physicalDevice = candidates.rbegin()->second;    } else {        throw std::runtime_error("failed to find a suitable GPU!");    }}int rateDeviceSuitability(VkPhysicalDevice device) {    ...    int score = 0;    // Discrete GPUs have a significant performance advantage    if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {        score += 1000;    }    // Maximum possible size of textures affects graphics quality    score += deviceProperties.limits.maxImageDimension2D;    // Application can't function without geometry shaders    if (!deviceFeatures.geometryShader) {        return 0;    }    return score;}

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

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

bool isDeviceSuitable(VkPhysicalDevice device) {    return true;}


Семейства очередей


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

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

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

uint32_t findQueueFamilies(VkPhysicalDevice device) {    // Logic to find graphics queue family}

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

struct QueueFamilyIndices {    uint32_t graphicsFamily;};QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {    QueueFamilyIndices indices;    // Logic to find queue family indices to populate struct with    return indices;}

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

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

#include <optional>...std::optional<uint32_t> graphicsFamily;std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // falsegraphicsFamily = 0;std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true

std: :optional этот объект-обертка не содержит значения, пока вы не присвоите ему что-либо. В любой момент вы можете узнать, существует ли значение, с помощью метода has_value(). Это значит, что мы можем изменить логику:

#include <optional>...struct QueueFamilyIndices {    std::optional<uint32_t> graphicsFamily;};QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {    QueueFamilyIndices indices;    // Assign index to queue families that could be found    return indices;}

Начнем реализацию findQueueFamilies:

QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {    QueueFamilyIndices indices;    ...    return indices;}

Мы используем vkGetPhysicalDeviceQueueFamilyProperties уже известным вам способом:

uint32_t queueFamilyCount = 0;vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());

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

int i = 0;for (const auto& queueFamily : queueFamilies) {    if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {        indices.graphicsFamily = i;    }    i++;}

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

bool isDeviceSuitable(VkPhysicalDevice device) {    QueueFamilyIndices indices = findQueueFamilies(device);    return indices.graphicsFamily.has_value();}

Для удобства мы также добавим общую проверку в саму структуру:

struct QueueFamilyIndices {    std::optional<uint32_t> graphicsFamily;    bool isComplete() {        return graphicsFamily.has_value();    }};...bool isDeviceSuitable(VkPhysicalDevice device) {    QueueFamilyIndices indices = findQueueFamilies(device);    return indices.isComplete();}

Теперь мы можем использовать ее для раннего выхода из findQueueFamilies:

for (const auto& queueFamily : queueFamilies) {    ...    if (indices.isComplete()) {        break;    }    i++;}

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

Код C++



Логическое устройство и семейства очередей




Вступление


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

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

VkDevice device;

Добавим функцию createLogicalDevice и ее вызов из функции initVulkan.

void initVulkan() {    createInstance();    setupDebugMessenger();    pickPhysicalDevice();    createLogicalDevice();}void createLogicalDevice() {}


Указание очередей, которые нужно создать


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

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);VkDeviceQueueCreateInfo queueCreateInfo{};queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();queueCreateInfo.queueCount = 1;

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

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

float queuePriority = 1.0f;queueCreateInfo.pQueuePriorities = &queuePriority;


Указание используемых возможностей устройства


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

VkPhysicalDeviceFeatures deviceFeatures{};


Создание логического устройства


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

VkDeviceCreateInfo createInfo{};createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

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

createInfo.pQueueCreateInfos = &queueCreateInfo;createInfo.queueCreateInfoCount = 1;createInfo.pEnabledFeatures = &deviceFeatures;

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

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

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

createInfo.enabledExtensionCount = 0;if (enableValidationLayers) {    createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());    createInfo.ppEnabledLayerNames = validationLayers.data();} else {    createInfo.enabledLayerCount = 0;}

Расширения для конкретного устройства нам пока не понадобятся.

Теперь мы можем создать логическое устройство с помощью вызова в функции vkCreateDevice.

if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {    throw std::runtime_error("failed to create logical device!");}

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

Устройство нужно будет уничтожить в cleanup с помощью функции vkDestroyDevice:

void cleanup() {    vkDestroyDevice(device, nullptr);    ...}

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


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


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

VkQueue graphicsQueue;

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

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

vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);

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

Код C++
Подробнее..

Стоило ли столько ждать, чтобы найти баг?

21.12.2020 08:23:20 | Автор: admin
image1.png

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

Хронология событий


Minetest это открытый кроссплатформенный игровой движок, содержащий около 200 тысяч строк кода на C, C++ и Lua. Он позволяет создавать разные игровые режимы в воксельном пространстве. Поддерживает мультиплеер, и множество модов от сообщества.

10 ноября 2018 года в багтрекере проекта отрыли Issue #7852 item_image_button[]: button too small.

Описание следующее:
The button is too small resulting in the image exceeding its borders. Button should be the same size as inventory slots. See example below (using width and height of 1).
И скриншот:

image2.png

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

Следующим событием в этой замечательной истории стала публикация технической статьи "PVS-Studio: Анализ pull request-ов в Azure DevOps при помощи self-hosted агентов" в июле 2020 года. Чтобы привести пример интеграции анализатора в Azure DevOps, был выбрана та самая игра minetest. В статье приведено несколько найденных ошибок, но нам интересна одна конкретная из них:

V636 The 'rect.getHeight() / 16' expression was implicitly cast from 'int' type to 'float' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. hud.cpp 771

void drawItemStack(....){  float barheight = rect.getHeight() / 16;  float barpad_x = rect.getWidth() / 16;  float barpad_y = rect.getHeight() / 16;  core::rect<s32> progressrect(    rect.UpperLeftCorner.X + barpad_x,    rect.LowerRightCorner.Y - barpad_y - barheight,    rect.LowerRightCorner.X - barpad_x,    rect.LowerRightCorner.Y - barpad_y);}

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

И вот, спустя полгода результаты анализа были замечены разработчиками игры, и создана Issue 10726 Fix errors found by professional static code analyzer, где установили связь этого бага с Issue #7852. Это округление и искажало размеры кнопок.

Выводы


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

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

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


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Svyatoslav Razmyslov. Did It Have to Take So Long to Find a Bug?.
Подробнее..

Dynamic_cast на этапе компиляции

24.12.2020 18:19:54 | Автор: admin

Приветствую все читающих :)


О чём статья (или задача статьи): практический ответ на вопрос "возможно ли создать большой проект так, чтобы полностью отказаться от dynamic_cast на этапе выполнения?", где под большим проектом подразумевает такой в котором уже нет человека, что бы держал в голове всю кодовую базу проекта целиком.
Предварительный ответ: ДА, это возможно возможно создать механизм, позволяющий решить задачу dynamic_cast на этапе компиляции, но едва ли подобное будет применяться на практике по причинам как: (1) с самого начала целевой проект должен строиться согласно наперёд заданным правилам, в следствии чего взять и применить методику к существующему проекту, очень трудоёмко (2) существенное повышение сложности кода с точки зрения удобства его читаемости в определённых местах, где, собственно, происходит замена логики dynamic_cast на предложенную ниже (3) использование шаблонов, что может быть неприемлемым в некоторых проектах по идеологическим соображениям ответственных за него (4) интерес автора исключительно в том, чтобы дать ответ на поставленный вопрос, а не в том, чтобы создать универсальный и удобный механизм решения поставленной задачи (в конце-концов, не нужно на практике решать проблемы, которые не являются насущными).


Идея реализации


За основу была взята идея списка типов, описанная Андреем Александреску (https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B5%D0%BA%D1%81%D0%B0%D0%BD%D0%B4%D1%80%D0%B5%D1%81%D0%BA%D1%83,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9) и реализованная им же в библиотеке Loki (http://loki-lib.sourceforge.net/html/a00681.html). Данная идея была доработана по следующим пунктам (пункты помеченные * означают, что по данному пункту автор статьи не согласен с видением Александреску по поводу списков типов):


  • добавлена возможность генерации произвольного по длине списка типов без использования макросов и/или шаблонных структур, с количеством шаблонных параметров равных длине создаваемого списка;
  • добавлена возможность генерации списка типов на основе типа(ов) и/или существующего списка(ов) типов в их произвольной комбинации;
  • *удалена возможность создавать списки типов элементы которых могут являться списками типов;
  • *удалены функции MostDerived и DerivedToFront как и любая логика завязанная на наследовании классов, т.к. (1) логика её работы сильно зависит от порядка типов в списке типов, а потому требует бдительности от программиста при создании соответствующих списков и, что более важно, полного знания проекта программистом, который будет применять эту логику, что противоречит условиям задачи (2) распознавание наследования вниз по иерархии наследования, вообще говоря, простая задача не требующая какой-либо дополнительной логики этапа компиляции помимо уже имеющейся, тогда как автора статьи интересует в первую очередь логика распознавания наследования вверх на этапе компиляции, в чём выше обозначенные функции помочь не в силах;
  • добавлены проверки через static_assert, что позволяет получать осмысленные сообщения об ошибках при компиляции списка типов, в случае возникновения таковых;
  • добавлены функции как RemoveFromSize, CutFromSize позволяющие получать подсписки списков типов.
    Итоговый код библиотеки, для работы со списками типов, в полном виде вы можете посмотреть здесь (https://github.com/AlexeyPerestoronin/Cpp_TypesList), тогда как в статье будет присутствовать код, непосредственно использующий функционал данной библиотеки, необходимый для решения задачи.

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


#include <gtest/gtest.h>#include <TypesList.hpp>#include <memory>class A {    public:    using BASE_t = TL::Refine_R<TL::CreateTypesList_R<void>>;    A() {}    A(int a) {        buffer << ' ' << a;    }    virtual void F1() = 0;    protected:    std::stringstream buffer;};class B : public A {    public:    using BASE_t = TL::Refine_R<TL::CreateTypesList_R<A, A::BASE_t>>;    B() {}    B(int a, int b)        : A(a) {        buffer << ' ' << b;    }    virtual void F1() override {        std::cout << "class::B" << buffer.str() << std::endl;    }};class C : public B {    public:    using BASE_t = TL::Refine_R<TL::CreateTypesList_R<B, B::BASE_t>>;    C() {}    C(int a, int b, int c)        : B(a, b) {        buffer << ' ' << c;    }    virtual void F1() override {        std::cout << "class::C" << buffer.str() << std::endl;    }};class D : public C {    public:    using BASE_t = TL::Refine_R<TL::CreateTypesList_R<C, C::BASE_t>>;    D() {}    D(int a, int b, int c, int d)        : C(a, b, c) {        buffer << ' ' << d;    }    virtual void F1() override {        std::cout << "class::D" << buffer.str() << std::endl;    }};TEST(Check_class_bases, test) {    {        using TClass = A;        EXPECT_EQ(TClass::BASE_t::size, 1);        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));    }    {        using TClass = B;        EXPECT_EQ(TClass::BASE_t::size, 2);        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));    }    {        using TClass = C;        EXPECT_EQ(TClass::BASE_t::size, 3);        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>));    }    {        using TClass = D;        EXPECT_EQ(TClass::BASE_t::size, 4);        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>));        EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, C>));    }}// TT - Type to Typetemplate<class Type, class BASE_t>struct T2T {    std::shared_ptr<Type> value;    using PossibleTo_t = BASE_t;};template<class To, class From, class... Arguments>auto T2TMake(Arguments&&... arguments) {    T2T<To, TL::Refine_R<TL::CreateTypesList_R<From, From::BASE_t>>> result{};    result.value = std::make_shared<From>(arguments...);    return result;}template<class BASE_t>void AttemptUse(T2T<A, BASE_t> tb) {    static_assert(TL::IsInList_R<BASE_t, C>, "this function can to use only with C-derivative params");    tb.value->F1();}TEST(T2TMake, test) {    {        auto value = T2TMake<A, B>();        using TClass = decltype(value)::PossibleTo_t;        EXPECT_EQ(TClass::size, 3);        EXPECT_TRUE((TL::IsInList_R<TClass, void>));        EXPECT_TRUE((TL::IsInList_R<TClass, A>));        EXPECT_TRUE((TL::IsInList_R<TClass, B>));        // AttemptUse(value); // compilation error    }    {        auto value = T2TMake<A, B>(1, 2);        using TClass = decltype(value)::PossibleTo_t;        EXPECT_EQ(TClass::size, 3);        EXPECT_TRUE((TL::IsInList_R<TClass, void>));        EXPECT_TRUE((TL::IsInList_R<TClass, A>));        EXPECT_TRUE((TL::IsInList_R<TClass, B>));        // AttemptUse(value); // compilation error    }    {        auto value = T2TMake<A, C>();        using TClass = decltype(value)::PossibleTo_t;        EXPECT_EQ(TClass::size, 4);        EXPECT_TRUE((TL::IsInList_R<TClass, void>));        EXPECT_TRUE((TL::IsInList_R<TClass, A>));        EXPECT_TRUE((TL::IsInList_R<TClass, B>));        EXPECT_TRUE((TL::IsInList_R<TClass, C>));        AttemptUse(value);    }    {        auto value = T2TMake<A, C>(1, 2, 3);        using TClass = decltype(value)::PossibleTo_t;        EXPECT_EQ(TClass::size, 4);        EXPECT_TRUE((TL::IsInList_R<TClass, void>));        EXPECT_TRUE((TL::IsInList_R<TClass, A>));        EXPECT_TRUE((TL::IsInList_R<TClass, B>));        EXPECT_TRUE((TL::IsInList_R<TClass, C>));        AttemptUse(value);    }    {        auto value = T2TMake<A, D>();        using TClass = decltype(value)::PossibleTo_t;        EXPECT_EQ(TClass::size, 5);        EXPECT_TRUE((TL::IsInList_R<TClass, void>));        EXPECT_TRUE((TL::IsInList_R<TClass, A>));        EXPECT_TRUE((TL::IsInList_R<TClass, B>));        EXPECT_TRUE((TL::IsInList_R<TClass, C>));        EXPECT_TRUE((TL::IsInList_R<TClass, D>));        AttemptUse(value);    }    {        auto value = T2TMake<A, D>(1, 2, 3, 4);        using TClass = decltype(value)::PossibleTo_t;        EXPECT_EQ(TClass::size, 5);        EXPECT_TRUE((TL::IsInList_R<TClass, void>));        EXPECT_TRUE((TL::IsInList_R<TClass, A>));        EXPECT_TRUE((TL::IsInList_R<TClass, B>));        EXPECT_TRUE((TL::IsInList_R<TClass, C>));        EXPECT_TRUE((TL::IsInList_R<TClass, D>));        AttemptUse(value);    }}int main(int argc, char* argv[]) {    ::testing::InitGoogleTest(&argc, argv);    return RUN_ALL_TESTS();}

  1. Создание первого класса в иерархии class A


    class A {public:using BASE_t = TL::Refine_R<TL::CreateTypesList_R<void>>;A() {}A(int a) {    buffer << ' ' << a;}virtual void F1() = 0;protected:std::stringstream buffer;};
    

    class A это простой чисто-вирутуальный класс, главной особенностью которого является строка: using BASE_t = TL::Refine_R<TL::CreateTypesList_R<void>>;, которая определяет для класса список типов от которых наследуется класс.
    Здесь и далее:


    • TL::CreateTypesList_R структура, создающая список типов произвольной длинны.
    • TL::Refine_R структура, очищающая список типов от дубликатов, в случае наличия таковых.
      Т.к. класс А ни от кого не наследуется, то единственным типом к которому он может быть напрямую приведён является void.

  2. Создание втрого класса в иерархии class B


    class B : public A {public:using BASE_t = TL::Refine_R<TL::CreateTypesList_R<A, A::BASE_t>>;B() {}B(int a, int b)    : A(a) {    buffer << ' ' << b;}virtual void F1() override {    std::cout << "class::B" << buffer.str() << std::endl;}};
    

    Как видим, класс В наследуется от класса А, а потому в его BASE_t списке типов к которым может быть приведёт класс В, содержится класс А и все базовые типы класса А.


  3. Создание третьего класса в иерархии class C


    class C : public B {public:using BASE_t = TL::Refine_R<TL::CreateTypesList_R<B, B::BASE_t>>;C() {}C(int a, int b, int c)    : B(a, b) {    buffer << ' ' << c;}virtual void F1() override {    std::cout << "class::C" << buffer.str() << std::endl;}};
    

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


  4. Создание четвёртого класса в иерархии class D


    class D : public C {public:using BASE_t = TL::Refine_R<TL::CreateTypesList_R<C, C::BASE_t>>;D() {}D(int a, int b, int c, int d)    : C(a, b, c) {    buffer << ' ' << d;}virtual void F1() override {    std::cout << "class::D" << buffer.str() << std::endl;}};
    

    Аналогично предыдущему классу, класс D наследуется от класса С и его BASE_t содержит класс С и все его базовые типы.


  5. Проверка базовых типов классов
    Здесь и далее, структура TL::IsInList_R<TypesList, Type> возвращает true когда и только тогда, когда тип Type входит в список типов TypesList, и false в противном случае.
    TEST(Check_class_bases, test) {{    using TClass = A;    EXPECT_EQ(TClass::BASE_t::size, 1);    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));}{    using TClass = B;    EXPECT_EQ(TClass::BASE_t::size, 2);    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));}{    using TClass = C;    EXPECT_EQ(TClass::BASE_t::size, 3);    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>));}{    using TClass = D;    EXPECT_EQ(TClass::BASE_t::size, 4);    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, void>));    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, A>));    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, B>));    EXPECT_TRUE((TL::IsInList_R<TClass::BASE_t, C>));}}
    

    Как видно из фрагмента кода, каждый из созданных классов: class A, class B, class C и class D, содержит в своём BASE_t типы к которым этот класс может быть приведён вниз по иерархии наследования.


    Создание структуры с информацией о наследовании вверх по иерархии
    // T2T - Type to Typetemplate<class Type, class BASE_t>struct T2T {std::shared_ptr<Type> value;using PossibleTo_t = BASE_t;};
    

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


    Создание функции для создания структуры T2T
    template<class To, class From, class... Arguments>auto T2TMake(Arguments&&... arguments) {T2T<To, TL::Refine_R<TL::CreateTypesList_R<From, From::BASE_t>>> result{};result.value = std::make_shared<From>(arguments...);return result;}
    

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

    • From истинный тип объекта, создаваемого для хранения в структуре T2T;
    • To тип под которым созданный объект хранится в структуре T2T;
    • Arguments типы аргументов для создания целевого объекта.
      Как видно, данная фукнция будет компилироваться только в случае, если тип From является наследником типа To, а запись TL::Refine_R<TL::CreateTypesList_R<From, From::BASE_t>> создаёт список типов для структуры T2T по которому в дальнейшем можно будeт однозначно определить всё множество типов к которым может быть приведён указатель на объект value.

    Создание функции для проверки корректности работы структуры T2T
    template<class BASE_t>void AttemptUse(T2T<A, BASE_t> tb) {static_assert(TL::IsInList_R<BASE_t, C>, "this function can to use only with C-derivative params");tb.value->F1();}
    

    По придуманным условиям, данная функция может работать с объектами класса А, являющимися приведёнными от объектов не ниже класса С по иерархии типов, причём, и это самое важное, данное условие проверяется ещё на этапе компиляции.


    Итоговое тестирование созданной логики
    TEST(T2TMake, test) {{    auto value = T2TMake<A, B>();    using TClass = decltype(value)::PossibleTo_t;    EXPECT_EQ(TClass::size, 3);    EXPECT_TRUE((TL::IsInList_R<TClass, void>));    EXPECT_TRUE((TL::IsInList_R<TClass, A>));    EXPECT_TRUE((TL::IsInList_R<TClass, B>));    // AttemptUse(value); // compilation error}{    auto value = T2TMake<A, B>(1, 2);    using TClass = decltype(value)::PossibleTo_t;    EXPECT_EQ(TClass::size, 3);    EXPECT_TRUE((TL::IsInList_R<TClass, void>));    EXPECT_TRUE((TL::IsInList_R<TClass, A>));    EXPECT_TRUE((TL::IsInList_R<TClass, B>));    // AttemptUse(value); // compilation error}{    auto value = T2TMake<A, C>();    using TClass = decltype(value)::PossibleTo_t;    EXPECT_EQ(TClass::size, 4);    EXPECT_TRUE((TL::IsInList_R<TClass, void>));    EXPECT_TRUE((TL::IsInList_R<TClass, A>));    EXPECT_TRUE((TL::IsInList_R<TClass, B>));    EXPECT_TRUE((TL::IsInList_R<TClass, C>));    AttemptUse(value);}{    auto value = T2TMake<A, C>(1, 2, 3);    using TClass = decltype(value)::PossibleTo_t;    EXPECT_EQ(TClass::size, 4);    EXPECT_TRUE((TL::IsInList_R<TClass, void>));    EXPECT_TRUE((TL::IsInList_R<TClass, A>));    EXPECT_TRUE((TL::IsInList_R<TClass, B>));    EXPECT_TRUE((TL::IsInList_R<TClass, C>));    AttemptUse(value);}{    auto value = T2TMake<A, D>();    using TClass = decltype(value)::PossibleTo_t;    EXPECT_EQ(TClass::size, 5);    EXPECT_TRUE((TL::IsInList_R<TClass, void>));    EXPECT_TRUE((TL::IsInList_R<TClass, A>));    EXPECT_TRUE((TL::IsInList_R<TClass, B>));    EXPECT_TRUE((TL::IsInList_R<TClass, C>));    EXPECT_TRUE((TL::IsInList_R<TClass, D>));    AttemptUse(value);}{    auto value = T2TMake<A, D>(1, 2, 3, 4);    using TClass = decltype(value)::PossibleTo_t;    EXPECT_EQ(TClass::size, 5);    EXPECT_TRUE((TL::IsInList_R<TClass, void>));    EXPECT_TRUE((TL::IsInList_R<TClass, A>));    EXPECT_TRUE((TL::IsInList_R<TClass, B>));    EXPECT_TRUE((TL::IsInList_R<TClass, C>));    EXPECT_TRUE((TL::IsInList_R<TClass, D>));    AttemptUse(value);}}
    


    Выводы


    dynamic_cast на этапе компиляции это реально.
    Однако, вопрос о целесообразности остаётся насущным.


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

Подробнее..
Категории: C++ , C++11 , Templates

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.

Подробнее..

Создаем Swift Package на основе Cбиблиотеки

13.01.2021 02:07:50 | Автор: admin
Фото Kira auf der HeideнаUnsplashФото Kira auf der HeideнаUnsplash

Данная статья поможет вам создать свой первый Swift Package. Мы воспользуемся популярной C++ библиотекой для линейной алгебры Eigen, чтобы продемонстрировать, как можно обращаться к ней из Swift. Для простоты, мы портируем только часть возможностей Eigen.


Трудности взаимодействия C++ и Swift

Использование C++ кода из Swift в общем случае достаточно трудная задача. Все сильно зависит от того, какой именно код вы хотите портировать. Данные 2 языка не имеют соответствия API один-к-одному. Для подмножества языка C++ существуют автоматические генераторы Swift интерфейса (например,Scapix,Gluecodium). Они могут помочь вам, если разрабатывая библиотеку, вы готовы следовать некоторым ограничениям, чтобы ваш код просто конвертировался в другие языки. Тем не менее, если вы хотите портировать чужую библиотеку, то, как правило, это будет не просто. В таких ситуациях зачастую ваш единственный выбор: написать обертку вручную.

Команда Swift уже предоставляет interop дляCиObjective-Cв их инструментарии. В то же время,C++ interopтолько запланирован и не имеет четких временных рамок по реализации. Одна из сложно портируемых возможностей C++ шаблоны. Может показаться, что темплейты в C++ и дженерики в Swift схожи. Тем не менее, у них есть важные отличия. На момент написания данной статьи, Swift не поддерживает параметры шаблона не являющиеся типом, template template параметры и variadic параметры. Также, дженерики в Swift определяются для типов параметров, которые соблюдают объявленные ограничения (похоже на C++20 concepts). Также, в C++ шаблоны подставляют конкретный тип в месте вызова шаблона и проверяют поддерживает ли тип используемый синтаксис внутри шаблона.

Итого, если вам нужно портировать C++ библиотеку с обилием шаблонов, то ожидайте сложностей!


Постановка задачи

Давайте попробуем портировать вручную С++ библиотеку Eigen, в которой активно используются шаблоны. Эта популярная библиотека для линейной алгебры содержит определения для матриц, векторов и численных алгоритмов над ними. Базовой стратегией нашей обертки будет: выбрать конкретный тип, обернуть его в Objective-C класс, который будет импортироваться в Swift.

Один из способов импортировать Objective-C API в Swift это добавить C++ библиотеку напрямую в Xcode проект и написатьbridging header. Тем не менее, обычно удобнее, когда обертка компилируется в качестве отдельного модуля. В этом случае, вам понадобится помощь менеджера пакетов. Команда Swift активно продвигаетSwift Package Manager (SPM). Исторически, в SPM отсутствовали некоторые важные возможности, из-за чего многие разработчики не могли перейти на него. Однако, SPM активно улучшался с момента его создания. В Xcode 12, вы можете добавлять в пакет произвольные ресурсы и даже попробовать пакет в Swift playground.

В данной статье мы создадим SPM пакетSwiftyEigen. В качестве конкретного типа мы возьмем вещественную float матрицу с произвольным числом строк и колонок. КлассMatrixбудет иметь конструктор, индексатор и метод вычисляющий обратную матрицу. Полный проект можно найти наGitHub.


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

SPM имеет удобный шаблон для создания новой библиотеки:

foo@bar:~$ mkdir SwiftyEigen && cd SwiftyEigenfoo@bar:~/SwiftyEigen$ swift package initfoo@bar:~/SwiftyEigen$ git init && git add . && git commit -m 'Initial commit'

Далее, мы добавляем стороннюю библиотеку (Eigen) в качестве сабмодуля:

foo@bar:~/SwiftyEigen$ git submodule add https://gitlab.com/libeigen/eigen Sources/CPPfoo@bar:~/SwiftyEigen$ cd Sources/CPP && git checkout 3.3.9

Отредактируем манифест нашего пакета,Package.swift:

// swift-tools-version:5.3import PackageDescriptionlet package = Package(    name: "SwiftyEigen",    products: [        .library(            name: "SwiftyEigen",            targets: ["ObjCEigen", "SwiftyEigen"]        )    ],    dependencies: [],    targets: [        .target(            name: "ObjCEigen",            path: "Sources/ObjC",            cxxSettings: [                .headerSearchPath("../CPP/"),                .define("EIGEN_MPL2_ONLY")            ]        ),        .target(            name: "SwiftyEigen",            dependencies: ["ObjCEigen"],            path: "Sources/Swift"        )    ])

Манифест является рецептом для компиляции пакета. Сборочная система Swift соберет два отдельных таргета для Objective-C и Swift кода. SPM не позволяет смешивать несколько языков в одном таргете. Таргет ObjCEigen использует файлы из папкиSources/ObjC, добавляет папкуSources/CPP в header search paths, и опеделяетEIGEN_MPL2_ONLY, чтобы гарантировать лицензию MPL2 при использовании Eigen. ТаргетSwiftyEigen зависит отObjCEigen и использует файлы из папкиSources/Swift.


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

Теперь напишем заголовочный файл для Objective-C класса в папкеSources/ObjCEigen/include:

#pragma once#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface EIGMatrix: NSObject@property (readonly) ptrdiff_t rows;@property (readonly) ptrdiff_t cols;- (instancetype)init NS_UNAVAILABLE;+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(zeros(rows:cols:));+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(identity(rows:cols:));- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(value(row:col:));- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(setValue(_:row:col:));- (EIGMatrix*)inverse;@endNS_ASSUME_NONNULL_END

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

Дальше напишем файл реализации вSources/ObjCEigen:

#import "EIGMatrix.h"#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdocumentation"#import <Eigen/Dense>#pragma clang diagnostic pop#import <iostream>using Matrix = Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic>;using Map = Eigen::Map<Matrix>;@interface EIGMatrix ()@property (readonly) Matrix matrix;- (instancetype)initWithMatrix:(Matrix)matrix;@end@implementation EIGMatrix- (instancetype)initWithMatrix:(Matrix)matrix {    self = [super init];    _matrix = matrix;    return self;}- (ptrdiff_t)rows {    return _matrix.rows();}- (ptrdiff_t)cols {    return _matrix.cols();}+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Zero(rows, cols)];}+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Identity(rows, cols)];}- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)col {    return _matrix(row, col);}- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)col {    _matrix(row, col) = value;}- (instancetype)inverse {    const Matrix result = _matrix.inverse();    return [[EIGMatrix alloc] initWithMatrix:result];}- (NSString*)description {    std::stringstream buffer;    buffer << _matrix;    const std::string string = buffer.str();    return [NSString stringWithUTF8String:string.c_str()];}@end

Теперь сделаем Objective-C код видимым из Swift с помощью файла вSources/Swift(смотритеSwift Forums):

@_exported import ObjCEigen

И добавим индексирование для более чистого API:

extension EIGMatrix {    public subscript(row: Int, col: Int) -> Float {        get { return value(row: row, col: col) }        set { setValue(newValue, row: row, col: col) }    }}

Пример использования

Теперь мы можем воспользоваться классом вот так:

import SwiftyEigen// Create a new 3x3 identity matrixlet matrix = EIGMatrix.identity(rows: 3, cols: 3)// Change a specific valuelet row = 0let col = 1matrix[row, col] = -2// Calculate the inverse of a matrixlet inverseMatrix = matrix.inverse()

Наконец, мы можем составить простой проект, который продемонстрирует возможности нашего пакета, SwiftyEigen. Приложение позволит вносить значения в матрицу 2x2 и вычислять обратную матрицу. Для этого, создаем новый iOS проект в Xcode, перетаскиваем папку с пакетом из Finder в project navigator, чтобы добавить локальную зависимость, и добавляем фреймворк SwiftyEigen в общие настройки проекта. Далее пишем UI и радуемся:

Смотрите полный проект наGitHub.


Ссылки

Спасибо за внимание!

Подробнее..

Категории

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

© 2006-2021, personeltest.ru