Pull to refresh

Comments 72

Круто! Про особенности переноса с VisualBasic на C++ - полноценный RangeArr можно получить "бесплатно", унаследовав от std::array

template<class T, long Begin, long End>
class range_arr : public std::array<T, End - Begin + 1> {
public:
    using underlying_t = std::array<T, End - Begin + 1>;
    using typename underlying_t::reference;
    
    constexpr reference operator[](long pos) {
        return underlying_t::operator[](pos - Begin);
    }
    constexpr reference operator[](long pos) const {
        return underlying_t::operator[](pos - Begin);
    }
};

Про опциональные ссылки - можно по умолчанию ссылаться на "свалку", если не будет проблем с многопоточностью (пример не идеальный):

template<typename T>
T dummy;

template<typename T>
T& dummy_ref(T value) {
    dummy<T> = value;
    return dummy<T>;
}

void addValue(int step, int& number = dummy_ref<int>(0)) {
    number = step;
}

полноценный RangeArr можно получить "бесплатно", унаследовав от std::array

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

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

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

Замечательная игра с очень широкими возможностями по программированию своих карт, выпущенная задолго до Super Mario Maker (целых 6 лет).

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

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

Если нужно чтобы были произвольные размеры тайлов, то отсечение выполняется, например, через rTree. Еще можете почитать про Broad phase (например тут - https://research.ncl.ac.uk/game/mastersdegree/gametechnologies/physicstutorials/6accelerationstructures/Physics%20-%20Spatial%20Acceleration%20Structures.pdf) - это про физику; но по аналогии с поиском пересечений объектов можно понять как построить пересечение с экраном.

Спасибо.

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


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

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


Эх, с деревом вариант-то я знаю. Но очень сильно сомневаюсь, что в 80-х его использовали в платформерах типа Commander Ken и подобных.

Я надеялся, что там есть какой-то простой (пусть и неочевидный) и эффективный алгоритм. Но, видимо, нет. Жаль.

Если мне не изменяет память, то на старых приставках использовалось что-то вроде сжатия RLE по колонкам или по строкам, в зависимости от того, горизонтальный или вертикальный скроллинг на уровне. Индекс строки определяется из позиции персонажа, по ней распаковывается вся колонка и производятся расчеты. Иногда данные хранятся непосредственно в видеобуфере. Можно понять ка это работает, например, по этой статье - https://habr.com/ru/post/354774/ .

Если интересно что-то в общем, то @SnakeSolid уже ответил на вопрос.

В оригинальном SMBX, Эндрю почти не делал оптимизацию вообще, за исключением блоков, где он применил линейную тайловую сортировку:

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

  • Блоки используют одномерную тайловую оптимизацию (работает по горизонтали):

    • Создаётся два массива диапазоном -8000 и +8000

    • Весь массив блоков предварительно сортируется по Y, группируясь по X

    • Первый массив хранит индексы начала группы на каждый столбец шириной в 32 пикселя и высотой в бесконечность

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

    • Первичный поиск идёт следующим образом: из первого массива запрашивается значение с интексом (x / 32) - 1, а из второго массива запрашивается значение с индексом ((x + w) / 32) + 1. Мы получаем границы поиска - индекс первого блока группы fBlock, и индекс последнего блока группы lBlock.

    • Дальше, проходимся по массиву блоков от fBlock по lBlock, и попарно проверяем коллизию с блоками.

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

    • Данный метод не позволяет безопасно и свободно перемещать блоки по секции без пересортировки всего массива: по вертикали в рамках группы перемещать их можно, однако, если двигать по горизонтали, потребуется пересортировка массива, поскольку блоки переходят из одной группы X в другую. Эндрю у себя в коде совсем отключает эту оптимизацию, если с помощью движущихся слоёв перемещать блоки по-горизонтали. Из-за этого возникает проблема, названная "проблемой доктора Пеппера" (в честь имени уровня, где эта ошибка чётко воспроизводится).

В моём порту по большей части пока что используется всё то, что сделал Эндрю, за исключением карты мира: я реализовал использование квадратного дерева (реализацию LooseQuadTree), чтобы ускорить поиск видимых элементов. В планах применить квадратное дерево и на уровнях по остальным местам. На ветке devel, мой друг-соразработчик проделал работу по замене линейной тайловой сортировки на квадратное дерево, тем самым полностью исправив "проблему Доктора Пеппера".

Тайловый поиск хоть и просто реализуется, но он не очень эффективен, если много движущихся объектов, приходится их часто перерегистрировать между ячейками. А также, размерностью массива прямо задаётся максимальный предел размера игрового поля. Я предпочитаю квадратное дерево, поскольку оно позволяет свободно перемещаться объектам, нахоящимся в нём, а также не ограничивает максимальный размер игрового поля. Я раньше использовал R-дерево в другом моём проекте, однако, у него есть недостаток: элемент нельзя свободно перемещать, его можно лишь удалить и добавить заново (с новыми координатами). Из-за этого проседала производительность. Квадратное дерево работает эффективней.

Мой портированный проект да, на C++. @ewgeniy2004 имеет в виду код оригинальной игры как есть (который на VB6).

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

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

У меня сие геройство в 30 лет пропало.

Благодарю за комплимент!

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

А, соберётся ли порт игры в рамках близкого инструментария к VB6,
а именно в Visual Studio 6?

Порт игры требует компилятор, поддерживающий стандарт C++11. То есть, для семейства MSVC это 2015 (версия 2013 имеет прблемы с constexpr). Для семейства MinGW где-то ориентировчно 4.8 минимум, но в основном используются наборы версии 5 и старше.

UFO just landed and posted this here

Вам же хватило времени на написание этого коммента?

Неужели вы заняты с утра до вечера, 7 дней в неделю?

UFO just landed and posted this here

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

частенько ронялась вся IDE

В моём случае IDE роняется тогда, когда я инициализирую внешнюю C-библиотеку (я сделал обёртку вокруг SDL2 и MixerX, которую я запилил для замены звукового ядра в оригинальной игре), и когда я прерву программу на середине работы, тут IDE и разобьётся вдребезги, потому что аудиопоток, созданный на стороне SDL2, не был корректно погашен. Даже не было специальной ловушки, чтобы корректно гасить подобные библиотеки прямо во время работы IDE, и это больно... Тут есть место архитектурным ошибкам: исполняемую среду надо строго выносить наружу, и взаимодействовать с обработчиком по межпроцессу, и тогда если что-то сдохнет на стороне, это не побьёт IDE. Также я сталкивался с тем, что если на стороне C-библиотеки (которую я создал для того, чтобы пришпилить к VB6-проекту некоторые библиотеки и кусочки кода на C или C++), допущена ошибка по типу вылез за пределы памяти, или не правильно оформил соглашение вызова публичной функции, IDE начнёт баговать глючить, если переживает подкапотный кавардак. В итоге, отлаживать игру приходится строго в режиме с отключённым звуком совсем, потому что иначе если прервать игру во время работы звука, сдохнет вся среда.

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

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


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


VB IDE под капотом, то есть внутри себя, в своей памяти вообще не хранит исходный код модулей проекта в виде текста — ни в виде текста целиком (то есть в том виде, в каком код хранится в .bas/.cls/.frm/.ctl-файлах на диске), ни в виде крупных фрагментов этого текста, ни в виде отдельных строк, ни в виде отдельных токенов (с некоторыми исключениями — о них далее). Она хранит исходный код (и работает с ним) в компактном бинарном виде, значительно обработанным при том по сравнению с исходным текстом. В этом легко убедиться: если присоединиться к IDE отладчиком (типа OllyDbg) или снять дампа памяти, то в адресном пространстве, хоть всё целиком его прошерсти, не удастся найти ни кода целиком, ни отдельных строк.


Для простоты будем считать, что мы только что написали в редакторе кода новую строку кода и нажимаем Enter. Сразу же в этот момент строка (line) кода, которая, кстати, может быть многострочковой(!) строкой (multi-row line) благодаря наличию возможности переноса строки (символом _), распарсивается — сперва на основе строки (как цепочки букв) строится древовидная бинарная структура данных, которую условимся называть PCR-деревом или просто PCR. Затем на основе PCR-дерева формируется опять же бинарная, но уже не древовидная, а линейная структура — назовём её BSCR, а также в некоторых случаях создаётся ряд дополнительных бинарных структур в памяти IDE.


Чуть более точное, но длинное и нудное описание

На самом деле любая строка в VB состоит из трёх частей:
[МЕТКИ] [СТЕЙТМЕНТЫ] [КОММЕНТАРИИ]
Эти части опциональны — любая из них может отсутствовать, в том числе и все три (что даёт нам просто пустую строку). Но если какие-либо есть, то они обязаны идти именно в таком порядке — комментариев /* в духи Си */ здесь не бывает; комментарий (будь то Rem или '), если и имеется, то идёт самым последним в строке.


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


Таким образом на первом этапе на вход поступает строка (line) в виде строчки (row) или набора строчек (rows), если в строке осуществлялся перенос строки. На выходе же образуется:


  • Сведения о метке в начале строки (если она вообще была)
  • PCR-дерево для стейтмента или стейтментов (если их несколько и они разделены символом :).
  • Сведения о комментарии.

Таким образом, PCR-дерево описывает не всю строку целиком, а только среднюю «часть», то есть только стейтменты. С учётом того, что метка и комментарии могут присутствовать, а стейтментов не быть, PCR-дерево может оказаться пустым деревом. Например, вот такая строка является синтаксически корректной, но в ней нет стейтментов и PCR-дерево будет пустым:


20                       ' test

или даже такая «многострочковая» строка:


mylabel:       ' multi-line _
                 comment is _
                 used here.

PCR-дерево не является конечной формой представления строки после распарсивания: если первый этап (главным образом это построение PCR-дерева) прошёл успешно (что обычно означает, что в строке не было синтаксических ошибок, «незакрытых» скобок и тому подобного) проводится некая проверка PCR-дерева на корректность (что довершает проверку корректности синтаксиса и даже чуть-чуть захватывает зону ответственности семантического контроля), после чего древовидное PCR-представление новоиспечённой строки кода трансформируется в опять же бинарное, но уже не древовидное, а линейное представление — назовём его BSCR.


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


Если PCR-дерево состоит из 40-байтовых узлов (нод), каждая из которых имеет такие поля как «тип ноды», «флаги» и до 6 параметрических полей, суть и смысл которых зависит от типа конкретно взятого узла (в большинстве случаев это адреса, то есть указатели на дочерние узлы), которые могут как угодно лежать в памяти, BSCR-цепочка состоит из следующих строго друг за другом (без дырок и промежутков) 16-битных (то есть двухбайтных) сущностей, в череду которых вплетаются включения, кодирающие либо бинарное представление литералов (числовых и строковых констант), либо какие-то параметры сущностей, при этом всё выравнивается по 16-битной/двухбайтовой границе. При этом в BSCR-цепочке никогда не бывает никаких указателей/адресов (но бывают индексы), в результате чего BSCR-блоки можно свободно перемещать по памяти, не корректируя никакие указатели, а также их можно рассекать и раздвигать, вставляя в середину новые BSCR-сущности.


Выше я написал, что если текстовое представление строки кода удалось превратить в древовидное PCR-представление, вторым шагом сразу же по PCR-представлению строится BSCR-представление. На самом деле, BSCR-представление строки генерируется даже если редактору кода подсунули чуть-чуть некорректную или абсолютно некорректную строку: для этого используется специально выделенная для таких случаев BSCR-сущность типа «некорректная строка кода». Этот как раз тот исключительный случай, когда вся строка целиком в своём первозданном виде копируется в BSCR-представление кода, и только в этом исключительном случае при попытке отыскать что-то в памяти IDE у вас получится найти образец исходной строки. Подобная строка, парсинг которой закончился ошибкой, в редакторе кода затем показывается красным цветом — до тех пор, пока программист не предпримет попытку поправить её, после чего жизненный цикл строки кода начнётся с самого начала по пути
ТекстоваяСтрокаPCR-деревоBSCR-представление


PCR-дерево является временным форматом представления строки и после формирования BSCR-представления строки сразу же уничтожается.


BSCR же является долгосрочным способом существования/хранения/обработки исходного VB-шного кода внутри VB IDE. Ещё раз: исходный VB-шный код в виде сырого текста внутри IDE не хранится! Ни одним целным блоком. Ни как массив отдельных строк. Ни как массив отдельных токенов. Каждая строка кода представляется последовательностью 16-битных BSCR-сущностей. В такую последовательность вшиты строковые литералы (да и числовые) и комментарии, если они есть в данной строки. Исключения составляют синтаксически некорректные строки, подсвечиваемые при отображении красным — в таком случае используется специальная BSCR-сущность, вслед за которой идёт некорректная строка в своём первозданном виде.


Итак, IDE не хранит исходный код в виде текста. Когда редактору коду нужно отрисовать на экране ту часть (потенциально гораздо более объёмного) исходного кода, которую сейчас должен видеть пользователь, она по бинарному BSCR-представлению реконструирует текстовое представление только лишь той части всего кода, которую нужно отрисовать. При этом же происходит и подсветка синтаксиса (включая подсветку красным некорректных строк). То же самое происходит, когда нужно сохранить исходный код в файл или скопировать в буфер обмена — текстовое представление кода вновь воссоздаётся, но лишь на короткое время, т.е. на время отрисовки или записи в файл. Первоисточником исходного кода для IDE является именно бинарное BSCR-представление.


VB6 IDE при сохранении проекта записывает всё в файлы, имеющие текстовый формат (.bas/.cls/.frm/.ctl) — в момент сохранения по чисто бинарному BSCR-представлению реконструируются текстовое человеко-читаемое представление кода. В момент открытия проекта и загрузки файлов IDE обрабатывает строки одну за другой, распарсивая каждую точно так же, как если бы каждая последующая строка просто писалась с нуля в редакторе, после чего нажимался бы Enter, с той лишь единственной разницей, что вывод ошибок (жалоб на ошибки синтаксиса) подавлен, в результате чего если в сохранённом файле были некорректные строки, то они сразу молча станут красными, без большого числа выводимых сообщений (при этом и для режима интерактивной правки кода можно отключить вывод сообщений об ошибках синтаксиса).


А вот VBA IDE при сохранении записывает в файл BSCR-представление, не конвертируя его в текст. Поэтому если в Excel-евском или Word-овском файле (или базе Access) открыть редактор VBA и написать там любой код, после открытия .xls/.doc-файла в блокноте или hex-редакторе там не удастся найти ни одной строчки VB-кода. Ни визуально, ни используя поиск. Это не потому, что код зашифрован или сжат. Это потому, что VB IDE разбирает код (в виде текста) не в момент запуска
этого кода, не в момент исполнения, а в момент попадания кода в саму IDE, и в случае VBA в MS Office это бинарное представление кода (BSCR) записывается прямо в файл, откуда потом и загружается.


Немного подробнее о BSCR с примерами кодирования
На самом деле довольно-таки много для неподготовленного. Готовы?

BSCR-сущность — это 16-битное число. Из BSCR-сущностей составляется BSCR-представление строк кода. Некоторые BSCR-сущности имеют параметры, которые в BSCR-представлении следуют за сущностью как 16-битное число.


Чему в коде соответствует BSCR-сущность? Каждому ключевому слову или токену — своя сущность? Нет, одна BSCR-сущность соответствует скорее логической сущности из кода, и такими сущностями могут быть разнородные вещи: это и отдельно взятое число (числовой литерал), и комментарий, и control structure.


Начнём с простых примеров:
Строчка Option Explicit в BSCR-представлении кодируется как 0x10CD.
Строчка Option Compare Text кодируется как 0x08CD.
Строчка Option Base 1 кодируется как 0x04CD.


Не нужно быть особо внимательным, чтобы заметить похожее 0xCD в младшем байте 16-битной BSCR-сущности. На самом деле BSCR-сущность имеет следующий формат:


typedef struct
{
    USHORT Type:10;
    USHORT Subtype:6; // or flags
} BSCR_ENTITY;

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


Для наглядности я теперь буду использовать форму [xxxxx] для обозначения BSCR-сущности, у которых нет подтипов или флагов, и форму [xxxxx/yy] для BSCR-сущности типа xxxxx с флагом yy.


Тогда для конструкций Option ... предусмотрены следующие способы BSCR-представления:


[0xCD/0] — Option Base 0
[0xCD/1] — Option Base 1
[0xCD/2] — Option Compare Text
[0xCD/7] — Option Compare Database
[0xCD/3] — Option Compare Binary
[0xCD/4] — Option Explicit
[0xCD/5] — Option Private Module

Как можно видеть отсюда, BSCR-сущности соответствуют не отдельно взятым ключевым слвоам или токенам, а «единицам смысла».


Каждая конструкция или то, что называется control structure, кодируется своей отдельной BSCR-сущностью.


Например, End Function это [0x69], End If это [0x6B], End Property это [0x6D], End Select — [0x6E] (флаги, как видно, не используются вообще).


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


Как я уже писал выше, одна line кода может иметь несколько statement-ов. Целые процедуры в VB можно записать в одну строчку, используя символ двоеточия (:).


Разделителю двух statement-ов соответствует своя BSCR-сущность, при этом она имеет параметр, означающий, на какой колонке должен начинаться следующий за разделителем statement, то есть, грубо говоря, какой отступ относительно начала строки он должен иметь. Если statement должен начинаться сразу же после предыдущего (не упуская из виду двоеточие и следующий за ним пробел), этот параметр имеет значение 0.


Так, например, вот такая строка кода:


Option Explicit: Option Compare Text: Option Base 1

в BSCR-представлении будет кодироваться вот так:


[0xCD/4]  [0x46][0]  [0xCD/2]   [0x46][0]  [0xCD/1]

Или, если не использовать наше соглашение о записи BSCR-сущностей, а использовать простой hex-дамп:


0x10CD 0x0046 0x0000 0x08CD 0x0046 0x0000 0x04CD

Можете вставить вышеприведённую строчку кода в модуль VB- или VBA-проекта, после чего подключиться к процессу VB/VBA отладчиком и попробовать в памяти найти хотя бы строчку «Option Explicit» — уверяю вас, вы не найдёте её там, или же найдёте, но это будет мусор, который можно затереть чем угодно, и это ни на что не повлияет.


Зато вы гарантированно найдёте в памяти ту последовательность байтов, которая показана на вышеприведённом hex-дампе. Более того, если вы поменяете в ней 0x08CD ([0xCD/2]) на 0x10CD ([0xCD/4), в редакторе кода тотчас же строчка поменяется на
Option Explicit: Option Explicit: Option Base 1


Если же вы замените 0x0046 0x0000 на 0x0046 0x0020, то второй Statement будет начинаться на 32-й колонке:
Option Explicit: Option Explicit: Option Base 1


Если первоначальную строчку вставить в VBA-проект в Excel и сохранить книгу, а затем открыть .xls-файл, те же самые байты вы найдёте внутри него, но ни за что не найдёте там стрчоку «Option Explicit» или «Option Compare Text», потому что, как я уже писал, в .xls-файл сохранится BSCR-представление кода как есть, без конвертации в текстовое представление.


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


Начнём с простых примеров.


Do While <condition>
Do Until <condition>
While <condition>

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


В BSCR-формате они кодируются сущностями [0x62], [0x61] и [0xF4] соответственно. Эти BSCR-сущности сами по себе не имеют параметра (который шёл бы после самой сущности), но вот конструкции, кодируемые этими сущностями, зависят от выражения, и в BSCR-кодировании конструкции целиком BSCR-кодирование выражения будет предшествовать BSCR-сущности, кодирующей тип конструкции.


То есть в BSCR-представлении эти три конструкции будут выглядеть так:


<expr_representation> [0x62]
<expr_representation> [0x61]
<expr_representation> [0xF4]

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


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


Начнём с примеров кодирования простых выражений. Для кодирования булевых литералов (логических констант True и False) в BSCR-предусмотрена сущность [0xB7/u], где u — 0 или 1, в зависимости от того, False или True мы кодируем.


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


VB-код: While True
BSCR:   [0xB7/1] [0xF4]

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


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


Так, например, для оператора Xor используется BSCR-сущность [0x02], а для оператора Or — [0x03].


Табличка BSCR-сущностей для всех унарных и бинарных операторов VB
exprrepA exprrepB [0x00] — Implication Operation       (Example: A Imp B)
exprrepA exprrepB [0x01] — Eqv Operation               (Example: A Eqv B)
exprrepA exprrepB [0x02] — Xor Operation               (Example: A Xor B)
exprrepA exprrepB [0x03] — Or Operation                (Example: A Or B)
exprrepA exprrepB [0x04] — And Operation               (Example: A And B)
exprrepA exprrepB [0x05] — Equal                       (Example: A = B)
exprrepA exprrepB [0x06] — Not Equal                   (Example: A <> B)
exprrepA exprrepB [0x07] — Greater or Equal            (Example: A <= B)
exprrepA exprrepB [0x08] — Greater or Equal            (Example: A >= B)
exprrepA exprrepB [0x09] — Less Than                   (Example: A < B)
exprrepA exprrepB [0x0A] — Greater Than                (Example: A > B)
exprrepA exprrepB [0x0B] — Plus Operation              (example: A + B)
exprrepA exprrepB [0x0C] — Minus Operation             (example: A - B)
exprrepA exprrepB [0x0D] — Mod Operation               (example: A Mod B)
exprrepA exprrepB [0x0E] — Integral Division Operation (example: A \ B)
exprrepA exprrepB [0x0F] — Multiplication Operation    (example: A * B)
exprrepA exprrepB [0x10] — Division Operation          (Example: A / B)
exprrepA exprrepB [0x11] — Concatenation Operation     (Example: A & B)
exprrepA exprrepB [0x12] — Like Operation              (example: A Like B)
exprrepA exprrepB [0x13] — Power Operation             (example: A ^ B)
exprrepA exprrepB [0x14] — Is Operation                (example: A Is B)
exprrep [0x15] — Not Operation                         (example: Not expr)
exprrep [0x16] — Unary Minus                           (example: -expr)
exprrep [0x17] — Abs Operation                         (example: Abs(expr))
exprrep [0x1D] — Bracketed Expression                  (example: (expr))
exprrep [0x1E] — Hash-prefixed expression              (example: #expr)

Таким образом, строка кода


While True Or False

под капотом будет закодирована не иначе как


[0xB7/1] [0xB7/0] [0x03] [0xF4]

Числой литерал, являющейся целочисленной константной, укладывающейся в диапазон типа Integer, кодируется в BSCR следующим образом:


[0xAC] [W:val]

где [W:val] — непосредственно само число в виде знакового двухбайтового значения.


Таким образом, строка While 1 And 2 будет закодирована как


[0xAC] [0x0001] [0xAC] [0x0002] [0x04] [0xF4]

Если выражение взять в скобки: While (1 And 2), то это будет


[0xAC] [0x0001] [0xAC] [0x0002] [0x04] [0x1D] [0xF4]

Если его немного усложнить:


While (1 And 2) Xor False

BSCR-представление будет таким:


[0xAC] [0x0001] [0xAC] [0x0002] [0x04] [0xB7/0] [0x02] [0xF4]

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


Упоминание идентификатора (например переменной, константы, непараметрического свойства) в коде в BSCR-виде кодируется сущностью [0x20/fff], где fff — флаги, например 0x20 в случае, если упоминаемый идентификатор должен быть взят в квадратные скобки.


Подробнее про квадратные скобки в VB

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


Типичный пример: если в Excel мы имеем лист, названный «Summary», то в VBA-макросе мы можем написать


Sheets!Summary.Delete

для удаления этого листа. Но если лист называется «Our $$$», то мы можем выкрутиться из ситуации вот так:


Sheets![Our $$$].Delete

Что, впрочем, является просто альтернативой менее компактной формы записи


Sheets.Item("Our $$$").Delete

Однако если мы работаем с COM-объектом, реализованном, к примеру, на С++ и имеющим имена свойств, нормальные для С++, например, имеющим свойство «__hidden_prop», нарушающее правила VB в отношении идентификаторов, потому что в VB идентификатор не может начинаться на символ подчёркивания, то единственный способ работать с этим свойством — обрамить его упоминание в квадратные скобки:


foo = some_foreign_object.__hidden_prop ' Будет ошибка синтаксиса
foo = some_foreign_object.[__hidden_prop] ' Нормально

После этой 16-битной сущности [0x20/fff] обязательно следует 16-битная сущность, являющаяся индексом идентификатора в глобальной коллекции идентификатором.


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


Всякий раз, когда в коде встречается упоминание идентификатора, в BSCR-представлении этого кода будет фигурировать сущность [0x20/f], после которой будет идти индекс идентификатора.


Такая архитектура является причиной того, что в VB в принципе нельзя в одной процедуре объявить переменную foo, а в другой FOO. Написание идентификатора не может быть разным в разных процедурах, оно будет одинаковым в пределах всего проекта, потому что представление кода (BSCR-представление) обращается к идентификаторам по их индексам, а коллекция идентификаторов глобальна для всего проекта.


Кроме того, менеджер базы данных идентификаторов не имеет механизма контроля за использованием идентификатора и сборки мусора, поэтому идентификаторы в коллекцию заносятся при первом же появлении где-либо в коде, но никогда не удаляются из коллекции, даже если в коде проекта не осталось ни одного упоминания. Следствием такого подхода является забавный баг, проявляющийся тем, что если маниакально переименовывать какую-нибудь переменную или константу, всякий раз меняя её имя на ранее не использованное, то число таких попыток не может превысить 32 тысячи раз — причина в исчерпании свободных индексов для идентификаторов, ведь обращение к идентификаторам из BSCR-представления кода осуществляется именно по их индексам. Разумеется, перезапуск IDE вызывает парсинг кода и заполнение коллекции идентификаторов с чистого листа, поэтому ограничение на количество попыток переименования идентификатора сбрасывается перезапуском IDE.


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


While bSomeFlag                  
[0x20] [101] [0xF4]

While bSomeFlag Or fBaaz         
[0x20] [101] [0x20] [102] [0x03] [0xF4]

While (foo And 15) And Not zulu  
[0x20] [103] [0xAC] [0xF] [0x1D] [0x20] [104] [0x15] [0x04] [0xF4]

(здесь 101, 102, 103 и 104 — случайно выбранные индексы идентификаторов bSomeFlaaag, fBaaz, foo и zulu.


Ещё одним автоматическим следствием такого подхода является возможность найти в памяти IDE этот словарь идентификаторов, поменять в нём идентификатор, из-за чего все упоминания идентификатора в коде поменяются разом — во всех модулях проекта. Одна правка в одном месте в памяти может изменить тысячи упоминаний какого-нибудь «популярного» идентификатора в коде.


Если с While/Do всё более менее понятно, стоит сказать, что больштинство подобных Control Structures (конструкций) используют похожую схему BSCR-кодирования — отличие только в коде типа BSCR-сущности.


Например, конструкция If <condition> Then кодируется как <expr_repr> [0x9C].


Таким образом, вот такой код:


If fNeedTotalTermination Or bUnrecoverableError Then
    End
Else
    Exit Sub
End If

внутри VB IDE, под капотом среды разработки никогда не будет храниться в текстовом виде, в том, в каком его видит на экране программист, или в том, в каком код хранится будучи сохранённым в файл. Он будет храниться в виде BSCR-представления этих строк кода, а именно — вот так:


[0x20] [501] [0x20] [977] [0x03] [0x9C]
[0x67]
[0x64]
[0x7C]
[0x6B]

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


0x0020 0x01F5 0x0020 0x03D1 0x0003 0x009C
0x0067
0x0064
0x007C
0x006B

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


If fNeedTotalTermination Or bUnrecoverableError Then: End: Else: Exit Sub: End If

И тогда она будет кодироваться в BSCR-представлении так:


[0x20] [501] [0x20] [977] [0x03] [0x9C] [0x46] [0] [0x67] [0x46] [0] [0x64] [0x46] [0] [0x7C] [0x46] [0] [0x6B]

Исходя из прочитанного, вы должны понимать, почему если в редактор кода VB IDE вставить вот такой код:


iF    foo             xOr       FOO                tHEn

то среда автоматически исправит его на


If FOO Xor Foo Then

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


VB IDE неизбежно удаляет лишние пробелы и приводит регистр символов ключевых слов к правильному, а регистр символов к единообразному ровно по той причине, что исходный код не хранится под капотом IDE как текст и как код, а хранится в интерпретированном (сразу же после загрузки кода или сразу же после ввода кода) виде, в бинарном виде — в виде BSCR, и в BSCR попросту не предусмотрено место под хранение числа избыточных пробелов и флагов исковерканности ключевых слов. BSCR компактен и не хранит лишней информации, а поскольку код, отображаемый в редакторе кода, всего лишь воссоздаётся по BSCR в момент отрисовки, он выглядит в fancy-фицированном/канонизированном виде.


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


  • Информация об отступах после двоеточия там, где оно разделяет стейтменты,
  • Информация об отступе начала комменария
  • Информация об отступе перед спецификатором типа (As <typename>) в объявлении переменных, констант и членов User-Defined-типов (структур), но не в объявлении аргументов и типа возврата процедур.
  • Информацию об отступе продолжения строки после символа переноса строки.

Так что вот в таком коде избыточные пробелы (нужные для выравнивания и красивого оформления) убраны не будут — эти выравнивания запоминаются в BSCR:


Type person
    age             As Integer
    Weight          As Single
    DOB             As Data    ' <---- коммент

    password                   As String * 16
End Type

Sub test()
    Dim x       As Long
    Dim foo     As Long
    Dim baza    As Single
    Dim delim   As Single
    Dim message As String

    Const rough_pi      As Double = 3
    Const module        As String = "fiction"

    Stop:                      Stop: Stop:    Stop
End _
        Sub

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


End If

написать


End _
If

или даже


End _
 _
 _
        Sub

то как кодируется в BSCR-виде форма записи конструкции End If с использованием переноса или нескольких переносов, если учесть, что вся конструкция End If целиком кодируется одной единственной BSCR-сущностью [0x6B]?


Разгадка такова: информация о переносах строки хранится в BSCR-представлении этой строки, но она хранится отдельно от «смысловой нагрузки». Для любой строки BSCR-представление её смысловой нагрузки записывается абсолютно независимо от того, были ли в этой строке переносы строки (хоть через каждое слово), или же строка была введена без единого переноса. Сведения о переносах (если они имели место) записывается в BSCR-цепочку отдельной сущностью [0xA6], вслед за которой идёт информация о местах в строке, где длинную строку при последующей реконструкции (для рендеринга на экран или сохранения в файл) нужно целенаправленно разбить на подстрочке и повставлять символы переноса строки (нижнее подчёркивание) при отображении. При этом «координаты мест разлома» запоминаются в не символах, а в токенах, поэтому тот факт, что происходит неминуемое и неизбежное удаление избыточных пробелов, не приводит к тому, что места разлома длинной строки на несколько строчек уползают в середину токенов и портят строку.


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


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


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


Когда пользователь (программист) осуществляет ввод очередной строки, помимо того, что из строки вычленяются метки, statement-ы анализируется и по ним строится PCR-дерево, выделяется комментарий, а затем строка переписывается в BSCR-форму, которая включает в себя информацию о метке в начале строки, информацию о местах переноса строки, смысловую нагрузку statement-ов, информацию о комментарии (если он есть) — помимо всего этого, создаются или модифицируются определённые блоки (большине структуры), если выясняется, что новоиспечённая строка модифицирует текущий scope, то есть если имеющиеся в ней конструкции относятся к объявлению начала новой процедуру, начала нового энума или user-defined типа.


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


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


Что же происходит, когда пользователь VB IDE нажимает кнопочку Run? У VB есть два режима запуска проекта:


  • Start
  • Start With Full Compile

VB IDE (точнее движок EB/VBA — подробнее об этих терминах читайте тут) компилирует проект по-процедурно. В случае использования простой опции «Start», VB практикует ленивый и экономный до ресурса подход on-demand компиляции процедур, иначе называемый JIT-компиляцией. Он не пытается скомпилировать процедуру, пока кто-нибудь не попытается вызвать эту процедуру.


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


Как только происходит попытка вызвать процедуру, которая ещё не скомпилирована, VB быстренько компилирует её и спокойно продолжает работу проекта совершенно незаметно для программиста. Однако, если процедура, до которой дошло дело, имеет ошибку, из-за которой компиляция процедуры вообще невозможна (например: обращение к нигде не объявленной переменной при задействованной директиве Option Explicit). В этом случае on-demand подход к компиляции процедур перестаёт быть заметным для пользователя: ошибка становится очевидной не в момент нажатия кнопки «Start», а в момент, когда процедуру попытались вызвать.


Я серьёзно полагаю, что именно это наблюдение, что в режиме «Start» некоторые серьёзные ошибки в процедурах «всплывают» только в момент захода выполнения внутрь процедуры, стал основанием для наивных людей считать, что VB IDE интерпретирует VB-код непосредственно в момент выполнения процедуры. Тот факт, что в момент запуска проекта на исполнение VB не находит таких ошибок, а обнаруживает их в последний момент, когда программа уже частично поработала, вкупе с тем фактом, что в момент приостановки работы проекта («пауза») можно серьёзно правит код, заставляет людей делать догадку, что «руки» у среды доходят до кода только когда код исполняется, а при запуске VB IDE ни коим образом не анализирует код и уж точно не компилирует его (иначе ошибка отлавилась бы в момент запуска?)


Но это совершенно ошибочная позиция. Причина неотлова подобных ошибок на ранних стадиях — это использовани on-demand/JIT подхода к компиляции. Это фишка, фича, а не недостаток или баг. Если же вместо опции «Start» (F5) запускать проект опцией «Start With Full Compile» (Ctrl+F5), on-demand подход использоваться не будет. VB IDE попытается скомпилировать абсолютно все процедуры, какие только есть в проекте, и найдёт все ошибки компиляции, какие только имеются, и уж точно не даст проекту начать кое-как работать, если хоть в одном месте есть compile error.


Просто это дольше и не так эффективно.


В настройках IDE существует опция «Compile On Demand» (чекбокс), которую можно снять, и в этом случае проект будет всегда компилироваться полностью перед запуском, а значит и полностью проверяться.


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


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


Как и другие схожие платформы и решения, такие, например, как Java, дотнет, PHP и т.п., VB располагает своим собственным байт-кодом (со своей собственной системой команд) и своей собственной виртуальной машиной, которая выполняет этот байт-код. По сравнению с кодом на языке VB этот байт-код является низкоуровневым, но по сравнению с машинными командами архитектуры x86 (или любой другой, под которую скомпилирован VB) он является весьма высокоуровневым, потому что одна инструкция машинного кода VB может делать работу, эквивалентную тысячам инструкций процессора, десяткам системных вызовов.


В терминологии самого VB его собственный машинный код называется P-кодом.


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


Даже команды, которые пишутся в Immediate Pane — и те сперва компилируются в P-код, и только затем этот P-код отдаётся на выполнение.


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


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


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


  • Внутри виртуальной машины существует большая таблица, где каждый элемент является указателем (адресом) на обработчик отдельной инструкции. Назовём эту таблицу таблицей диспетчеризации, а адрес начала этой таблицы в памяти обозначим за tblByteDisp.
  • Для каждого существующего опкода P-кодной инструкции в этой таблице содержится указатель на код, реализующий исполнение этой инструкции, при этом значение опкода соответсвуте индексу ячейки этой таблицы.
  • Для значений, которым не соответствует никакая инструкция P-кода, в таблице содержит адрес обработчика некорректных инструкций.
  • В начале выполнения P-кодной процедуры виртуальная машина делает следующее:
    mov esi, entry_point_of_pcode_procedure
    movzx al, byte[esi]
    jmp [tblByteDisp + eax*4]

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


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


movzx al, byte[esi]
inc esi
jmp [tblByteDisp + eax*4]

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


Подобно тому, как в архитектуре x86 для всех форм кодирования всех существующих инструкций не хватило 256 возможных комбинаций однобайтового опкода, так и в случае P-кода количество всех форм кодирования всех существующих P-кодных команд не умещается в набор из 256 возможных значений. По этой причине для некоторых команд используются однобайтовые опкоды, но существуют также и команды с двухбайтовым опкодом. В x86 в своё время для этого был выделен псевдо-префикс расширения набора опкодов 0Fh. В P-коде двухбайтовые опкоды начинаются на FBh, FCh, FDh, FEh, FFh. По этой причине помимо таблицы tblByteDisp с 256 элементами-указателями существует ещё большая таблица tblDispatch, в которой 256+256+256+256+70=1094 ячейки — таблица состоит из 5 подтаблиц, каждая из которых отвечает за префиксы FBh, FCh, FDh, FEh, FFh.


P-кодная виртуальная машина VB — стековая. У неё нет как таковой концепции регистрового файла или регистров, как, например, у x86. Все манипуляции над значениями она предпочитает выполнять на стеке. В этом смысле она чем-то похоже на идеологию работу инструкций сопроцессора x87 — его инструкции оперируют своим собственным стеком плавающих чисел. Если обычная инструкция ADD архитектуры x86 принимает два операнда, при этом оба операнда содержат слагаемые, а результат помещается в регистр или ячейку памяти, обозначенную первым операндом, то в случае P-кодной системы команд тоже есть инструкция, которая складывает два числа, но она вообще не принимает никаких явных параметров: она предполагает, что операнды уже лежат на стеке к моменту вызова команды — она извлекает из стека два числа, складывает их, и результат тоже кладёт на стек. При этом виртуальная машина VB в качестве стека использует обычный стек, тот же самый, которым манипулируют машинные инструкции общего назначения, тот, что предоставляемый потоку операционной системой, и указатель на который содержится в регистре ESP.


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


Например, опкод FB 8E соответствует инструкции AddUI1, которая выполняет сложение двух значений типа Byte. Реализация кода этой инструкции в виртуальной машине выглядит так:


lblEX_AddUI1:
    pop   eax
    add   byte[esp], al
    jb    lbl_ThrowOverflowError
    xor   eax, eax
    mov   al, byte[esi]
    inc   esi
    jmp   [tblByteDisp + esi*4]

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


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


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


Изначально движок EB/VBA был устроен так, что компилирование VB-кода в P-код и его исполнение виртуальной машиной было единственным возможным способом работы. Как минимум, P-код давал такое преимущество, как кроссплатформенность: документ MS Office (например, книга Excel) содержащий макросы на языке VB, мог бы быть создан под ОС Windows на платформе x86, а затем открыт под версией MS Office для MacOS на платформе PowerPC — при этом код макросов работал бы там без перекомпиляции, поскольку один и тот же платформо-независимый байт-код выполнялся бы соответствующим билдом виртуальной машины под кокнертную аппаратно-программную платформу.


В случае Standalone VB, была добавлена возможность компиляции проекта не только в P-код, но и в Native-код (машинный код под целевую аппаратную платформу, главным образом 386+) — однако только при компиляции проекта в EXE-файл. В режиме отладки проекта под IDE проект, то есть его процедуры, всегда компилируется в P-код.


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


С современными процессорами с большими и быстрыми кешами данных и инструкций ситуация стала ещё более интересной: P-кодные билды стали часто обыгрывать Native-кодные билды по той причине, что одна и та же программа, один и тот же алгоритм в P-кодном воплощении становился намного компактнее и охотно умещался в кеше данных процессора. Наиболее часто используемые P-коды команды, точнее их Native-кодные реализации в составе виртуальной машины стали полностью умещаться в кеше инструкций: если для полностью Native-кодной программы, где какая-нибудь процедура вызывается впервые и имеет размер в 100 условных машинных инструкций, пришлось бы делать множество чтений из памяти, и к тому же в код этой процедуры гарантированно отсутствовал бы в кеше инструкций (поскольку процедура вызывается впервые), то для P-кодного вариантй той же процедуры её размер в P-кода составлял бы условные 10—15 P-кодных инструкций. Да, их тоже пришлось бы вычитывать из памяти, но количество чтений было бы на порядок меньшим, зато с большой вероятностью эти P-кодные инструкции вызывали бы исполнение уже неоднократно поработавших фрагментов с имплементацией работы этих P-кодных команд, уже попавших и в кеш инструкций, а потому выполняющихся намного быстрее, чем аналогичный по функциональности Native-код.


Подытожим:


  • В момент ввода новой строки кода в редактор кода, эта новая строка тут же распарсивается: сначала по ней строится PCR-дерево, затем по PCR-дереву (или по факту неудачной попытки его построения) строится BSCR-представление строки. BSCR уже содержит выражения, записанные с использованием идеологии обратной польской нотации, что позволяет прозрачно построить по такой записи последовательность команд для некоей стековой машины, вычисляющей выражения. Приоритеты операторов и всё такое уже учтено. Строка, введённая человеком как текст, сразу после нажатия клавиши Enter (или перехода каретки на др. строку) перестаёт существовать как текст, и начинает существовать как цепочка бинарных сущностей и ассоциированных бинарных структур. Лишь фрагменты введённой строки, такие как строковые литераы или комменты, становятся частями BSCR-представления, сохраняющими свой исходный вид (но без участия символов переноса строки для перенесённых комментов).
  • Загрузка кода модуля из файла должна рассматриваться как поочерёдное добавление в изначально пустой редактор кода одной строки за другой подряд.
  • Пока код модулей содержится в проекте и IDE работает, код всё время внутри IDE хранится не как код, а как совокупность в немалой степени обработанных бинарных структур и цепочек.
  • IDE не умеет интерпретировать и выполнять код в принципе. Она выполняет только скомпилированные в P-код процедуры, натравливая на последовательность P-кодных комманд виртуальную машину — конечный автомат по своей сути.
  • IDE компилирует все процедуры в момента старта проекта, либо оттягивает момент компиляции каждой отдельной взятой процедуры до тех пор, пока её реально не попытается кто-то вызвать — в зависимости от предпочтений и желаний пользователя IDE (то есть программиста).
  • Единожды скомпилированная из BSCR-представления в P-код процедура не перекомпилируется каждый раз при каждом запуске проекта: если в код процедуры не вносились правки, и dirty-флаг не был установлен, результат компиляции в P-код живёт между запусками проекта и повторно используется.
  • В остановленном состоянии IDE позволяет править код процедур. В таком случае при попытке продолжить выполнение она их просто перекомпилирует. Она даже позволяет править код процедуры, которая выполняется сейчас или чьи фреймы находятся на стеке вызовов. При этом IDE пытается перекомпилировать процедуру так, чтобы в своём новом исполнении она была совместимо по layout-у стекового фрейма со старой версией. Если это невозможно, среда предлагает пользователю или перезапустить проект с нуля, или отказаться от правки и откатить всё как было, или продолжить правку и довести её до ума.

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

Очень интересное объяснение, действительно, я абсолютно не правильно сказал на счёт "интерпретируется". Благодарю за подробности! Такой комментарий вообще заслуживает быть полноценной статьёй. А так, я и подозревал, что код при исполнеии уже хранится в преобразованной форме концептуально больше похожий на Java или .NET, а вовсе не на bash и т.п., с которыми его сравнивали.

Вот вторая часть того, что я первоначально хотел сюда написать одним постом:
https://habr.com/ru/post/582566/#comment_23589108


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

Зачем вы выложили статью в комментарий? О_о
Хабр не пропустил в публикацию?

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

Почему бы тогда теперь не оформить в статью?

Во-первых, у VB и VBA не очень хорошая репутация (и совершенно напрасно — это тот случай, когда не инструмент красит человека, а человек инструмент — похожим образом у DAW «FL Studio» есть репутация недо-программы для недомузыкантов, хотя как DAW она не хуже других). Так что я просто опасаюсь, что какашками закидают и скажут «да кому интересен этот продукт, последний релиз которого состоялся 23 года назад».


Во-вторых, тем не менее, я последние годы потратил на глубочайший реверс-инжерининг VB и VBA и в связи с этим пишу курс статей «VB Internals» (могу дать ссылки, если интересует). В нём пока только 2 опубликованных параграфа, и несколько почти готовых.


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


А генерация P-кода JIT-компилятором, оптимизация, анатомия виртуальной машины и отладчика, генерация x86-кода — это вообще ближе к парагрфу номер 200.


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


С другой стороны, я конечно могу именно про эту уникальную особенность VB/VBA сделать статью, дескать, а вы и не знали героя в своём отечестве, что существует такая IDE, которая начинает компилировать код уже в момент его ввода в редактор, и которая настолько lazy/just-in-time/on-demand/инкрементально-компилирующая, что иной раз при нажатии кнопки Compile вообще ничего внутри не происходит — все резултаты компиляции уже готовы.


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


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

(Продолжение вот этого (https://habr.com/ru/post/582566/#comment_23578554) комментария. Из-за лимита в 55 тысяч символов на длину комментария, пришлось вынуть большой кусок из первого поста, чтобы у первого поста мог бы быть шанс на логический и складный финал — раз вырезанный кусок решено было оформить вторым комментом, решил расписать его несколько подробнее и полнее, но затем произошла череда неудач, и пост пришлось по кусочкам восстанавливать из крупиц, которые сохранились лишь в файле подкачки. Поэтому публикую с запозданием)


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


Лет 15 назад tyomitch написал цикл из 4 статей, посвящённых проблемам кодогенерации, которые мне тогда очень понравились.


Ссылки на эти статьи

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



Одна из основных идей той серии статей: конвертация выражения из общеприятной у людей формы записи (инфиксная запись) в обратную польскую нотацию сама по себе составляет чуть ли не половину работу по генерации кода для стековой вычислительной машины — элементам ОПН могут быть прозрачным образом сопоставлены инструкции стековой машины. А я напоминаю, что эта конвертация выполняется даже не при попытки скомпилировать/запустить проект, а уже на этапе ввода каждой новой строчки кода в IDE (когда текстовое представление строки кода конвертируется в BSCR-представление).


Возьмём для примера вот такую строку кода на VB:


  foo = (a + 5) * (b - 4) Xor (c / d)

(Я специально взял одну строчку, а не целую процедуру для упрощения рассмотрения, чтобы оставить «за скобками» рассмотрение генерации пролога/эпилога процедуры.)


Уже непосредственно в момент ввода этой строчки в редактор кода она сначала будет преобразована из текста в древовидное представление (PCR):


             [2Ch]
            /let=\
           /      \
          /        \
       [74h]      [22h]
        foo       /Xor \
                 /      \
                /        \
               /          \
              /            \
           [18h]          [87h]
          /  *  \         (...)
       [87h]   [87h]          \
       (...)   (...)           \
       /           \           [19h]
    [16h]        [17h]        / c/d \  
   / a+5 \      / b-4 \      /       \
[74h]  [01h]  [74h]  [01h] [74h]     [74h]
  a      5      b      4     c         d

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


Шпаргалка по PCR-дереву

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


  • На вершине иерархии дерева PCR находится узел типа 2Ch, олицетворяющий конструкцию присвоения вида Let <assignee> = <assignment>. Во многих ЯП есть разделение на присвоение и сравнение (= vs. == в C/C++, := vs. = в Паскале) — изначально в бейсиках для этого разделения использовалась конструкция с ключевым словом Let, но в целом VB умеет отличать присвоение от сравнения по контексту. О том, что это обычное присвоение, и ключевое слово Let опущено, говорит значение флагового поля PCR-узла, равное 0x0004. Если бы ключевое слово не было опущено, этого бита во флаговом поле бы не было. В конструкциях LSet ... = ..., RSet ... = ..., Set ... = ... флаговое поля принимало бы другие значения. У узла типа 2Ch обязательно есть два дочерних: левый соответствует PCR-поддереву, описывающему то, чему присваивается значение (в простейшем случае это какая-то переменная, то есть упоминание её идентификатора), а правый — описанию выражения, которое присваивается. Левая ножка этого PCR-узла может ссылаться только на ограниченное пожмножество возможных PCR-деревьев (например, там не может быть PCR-дерево, описывающее выражение типа foo+bar)
  • Узел типа 74h означает упоминание в коде какого-либо идентификатора. Флаги узла используются для того, что запомнить, были ли при упоминании идентификатора использованы квадратные скобки. У этого узла параметрическое поле указывает уже не на какое-то PCR-поддерево, а содержит указатель на структуру NAMEREF, которая олицетворяет элемент словаря уникальных идентификаторов в проекте, внутри которой содержится сам идентификатор, его LHash, его индекс в таблице идентификаторов (эти индексы и только они разрешены для упоминания в BSCR, тогда как в PCR можно использовать указатели, так как PCR короткоживущая структура и за время, пока оно существует, словарь идентификаторов (коллекция NAMEREF-ов) гарантированно никуда не переедет и не реорганизуется. В нашем примере таких узлов в дереве 5 — по количеству переменных (а может быть и не переменных, а констант или свойств или функций, не ожидающих ни одного обязательного аргумента — в общем случае, просто идентификаторов), на которые есть отсылки в исходной строке.
  • Узел типа 87h означает нечто, взятое в скобки. У его единственный линк — это указатель на поддерево, описывающее то, что взято в скобки (это может быть какое-то подвыражение: составное или атомарное)).
  • Узлы типа 16h, 17h, 19h, 22h означают операции с участием бинарных операторов сложения, вычитания, деления и Xor соответственно. Каждый такой узел PCR-дерева имеет два линка: левый на поддерево, описывающее левый операнд (чем бы он там ни был), правый — на поддерево, описывающее правый операнд (чем бы он там ни являлся).
  • Узел типа 01h описывает целочисленный литерал (целочисленную числовую константу). Поле подтипа PCR-узла определяет более точный тип литерала: 3 для булевых констант, 6 для констант типа Integer (отображаются без TDC, хотя их родной TDC — символ процента), 8 для типа Long (отображаются с TDC в виде символа «амперсанд»). Флаговое поле определяет нотацию числа: без флагов это просто десятичное число, с флагом 0x0008 — число, префиксированное октоторпом, с флагом 0x4000 и 0x8000 — число в oct- и hex- формах соответственно.

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


Парсер VB-кода устроен таким образом, что он строит подобное дерево за один проход по цепочке символов входной строки кода, на лету токенизируя её и строя дерево. Нет никакой ни рекурсии, ни цикла с несколькими прогонами. Чуть-чуть токенизировали — достроили дерево, ещё чуть вперёд токенизировали — ещё достроили. То есть парсер однопроходный, но за раз он обрабатывает только одну строку, которая, впрочем, с одной стороны может иметь несколько statement-о в своём составе (можно хоть весь модуль в одну длиннющую строку уместить), а с другой стороны одна логическая строка может быть представлена несколькими физическими строчкам, если использовался символ переноса строки.


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


В нашем случае строка корректна, меток, комментариев и переноса строк нет. Поэтому BSCR строится только на базе PCR-дерева. Для вышепоказанного исходного кода и вышепоказанного PCR-дерева строится вот такое BSCR-представление:


[0x20][#a] [0xAC][5] [0x0B] [0x1D] [0x20][#b] [0xAC][4] [0x0C] [0x1D] [0x0F] [0x20][#c] [0x20][#d] [0x10] [0x1D] [0x02] [0x27][#foo]

Вот прямо в виде такой непрерывной цепочки 16-битных сущностей будет представлена вышеприведённая строка кода


foo = (a + 5) * (b - 4) Xor (c / d)

Напомню, что каждый блок в квадратных скобках означает 16-битное число, а записи [#a] или [#foo] означают 16-битное число, содержащее индекс NAMEREF-структуры, описывающей идентификаторы «a» и «foo» в проектно-глобальном словаре идентификатор.


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


Компактный    |
hex-дамп      |            Смысл 
BSCR-данных   |
--------------+--------------------------------
[0x20][#a]    | Упоминание идентификатора «a»
[0xAC][5]     |          Упоминание числа «5»
[0x0B]        |       Применение оператора +
[0x1D]        |        Взятие в скобки   (...)
[0x20][#b]    | Упоминание идентификатора «b» 
[0xAC][4]     |          Упоминание числа «4»
[0x0C]        |       Применение оператора -
[0x1D]        |        Взятие в скобки   (...)
[0x0F]        |       Применение оператора *
[0x20][#c]    | Упоминание идентификатора «c»
[0x20][#d]    | Упоминание идентификатора «d»
[0x10]        |       Применение оператора /
[0x1D]        |        Взятие в скобки   (...)
[0x02]        |      Применение оператора Xor
[0x27][#foo]  | Констр-ция присвоения вида foo = ...

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


И в таком полуготовом виде хранятся все строки VB-кода. Что по сути остаётся сделать компилятору при преобразовании BSCR-представления в P-код для виртуальной машины?


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


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


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


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


Для простоты представим, что «a», «b», «c», «d» и «foo» являются локальными переменные и имеют тип Variant. Это избавит нас (мы сейчас олицетворяем себя с компилятором) от необходимости заботиться от неявных приведениях типов на этапе кодогенерации, но не избавит от проверки совместимости типов и приведении «к общему типу» виртуальную машину, которая будет, выполняя наш байт-код, манипулировать VARIANT-значениями, внутри которых могут храниться гетерогенные величины.


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


  • FLdRf — кладёт на стек ссылку на Variant-переменную. Ld расшифровывается как load, Rf как reference.
  • LitVarI2 — кладёт на вершину стека Variant-значение, содержащее числовое значение типа I2 (Integer в VB, signed short в С/С++). Lit расшифровывается как Load immediate.
  • FStVar — берёт Variant-значение с вершины стека и сохраняет её значение в локальную переменную. St расшифровывается как store.
  • AddVar — берёт с вершины стека два операнда (подразумевая, что они имеют тип Variant) производит сложение, результат кладёт обратно на стек.
  • SubVar — то же самое, но производит вычитание.
  • DivVar — то же самое, но производит деление.
  • XorVar — то же самое, но производит побитовое исключающее ИЛИ.

Теперь я покажу вам, в какой P-код реально скомпилируется строка foo = (a + 5) * (b - 4) Xor (c / d), написав инструкции P-кода напротив соответствующих BSCR-сущностей BSCR-представления этого кода:


Компактный    |                                      |               |
hex-дамп      |            Смысл                     | P-код         |
BSCR-данных   |                                      |               |
--------------+--------------------------------------+ --------------+
[0x20][#a]    | Упоминание идентификатора «a»        | FLdRf     a   | 
[0xAC][5]     |          Упоминание числа «5»        | LitVarI2  5   |
[0x0B]        |       Применение оператора +         | AddRef        |
[0x1D]        |        Взятие в скобки   (...)       |               |
[0x20][#b]    | Упоминание идентификатора «b»        | FLdRf     b   |
[0xAC][4]     |          Упоминание числа «4»        | LitVarI2  4   |
[0x0C]        |       Применение оператора -         | SubVar        |
[0x1D]        |        Взятие в скобки   (...)       |               |
[0x0F]        |       Применение оператора *         | MulVar        |
[0x20][#c]    | Упоминание идентификатора «c»        | FLdRf     c   |
[0x20][#d]    | Упоминание идентификатора «d»        | FLdRf     d   |
[0x10]        |       Применение оператора /         | DivVar        |
[0x1D]        |        Взятие в скобки   (...)       |               |
[0x02]        |      Применение оператора Xor        | XorVar        |
[0x27][#foo]  | Констр-ция присвоения вида foo = ... | FStVar    foo |

Отсюда видно, что скомпилированная в P-код строка кода (а P-код является конечной формой существования кода при работе VB-проекта в режиме отладки под IDE и при работе всех VBA-проектов) практически один в один соответствует её BSCR-представлению, а именно в виде BSCR-представление хранится код внутри IDE на от момента его попадания в IDE до момемента его сохранения и/или закрытия IDE.


Лишь BSCR-сущностям, которые обозначают взятие подвыражения в скобки, в P-коде не соответствует ничего. В инфиксной записи взятие в скобки подвыражений не играет никакой роли, кроме обозначения порядка вычисления подвыражений в выражении. По сути дела скобки переопределяют порядок вычисления выражения по сравнению с порядком его прочтения (слева направо) и приоритетом операторов. В обратной польской нотации эту роль уже выполняет сам по себе порядок записи элементов обратной польской записи: порядок прочтения автоматически соответствует правильному порядку вычисления. Поэтому в BSCR-представлении сущности, кодирующие взятие в скобки, нужны лишь для правильной реконструкции VB-кода в виде человеко-читаемого текста, а для последующей кодогенерации эти сущности не используются. Это не должно удивлять: BSCR-представление кода является многоцелевым — с одной стороны оно представляет собой подспорье для последующей кодогенерации в конечную форму существования кода (P-код виртуальной машины), с другой стороны оно, содержа инормацию о местах разлома строки на части и о комментариях, является компактной формой хранения исходного VB-кода, то есть позволяет из BSCR воссоздать первоначальный вид исходного текста для его отрисовки на экране одновременно с раскраской синтаксиса, копирования в буфер обмена, осуществления поиска по нему (Find / Replace), сохранения в файл.


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


Здесь есть некоторое количество упрощение: если между стадией кода как текста (как цепочки букв) и стадией кода как PCR-дерево, и между стадией PCR-дерева и стадией BSCR-данных на самом деле нет никаких промежуточных состояний, то конечно же промежуточные стадии между BSCR-представлением кода и скомпилированным P-кодом просто не показаны здесь. Так же здесь полностью игнорируется, что VB4, VB5, VB6 умеет при создании EXE-файла компилировать проекты не только в P-код, но и Native-код (то есть машинный код x86), что является, вообще-то говоря, режимом по умолчанию.


Немного подробностей о том, как происходит компиляция в Native-код

Тут стоит сделать отступление и прояснить в общих чертах устройство компилятора C/C++ от компании Microsoft, известного как CL.EXE.


Этот компилятор поставляется и в комплекте Platform SDK или DDK/WDK для Windows, и входит в состав Microsoft Visual C++. Благодаря второму факту многие люди часто называют этот компилятор компилятором Visual C++, хотя непонятно, почему эта чисто-консольная утилита без какого-либо GUI должна иметь в своём названии слово Visual, ошибочно приписываемое ей от названия среды разработки, вместе с которой она поставляется, но которая не является составной частью эксклюзивно именно этой среды разработки как продукта.


Компилятор CL.EXE состоит из двух половинок:


  • Фронтенда С1 — задача которого состоит в том, чтобы принять на вход исходный текст на языке Си (в этом случае используется фронтенд C1.DLL) или C++ (используется C1XX.DLL), провести лексический разбор, препроцессинг (обработка директив препроцессора), синтаксический разбор, семантический разбор, проверку на наличие ошибок и предупреждений. Иными словами, фронтент делает всю ЯП-специфичную работу, но не делает абсолютно ничего платформо-специфичного.
  • Бэкенда C2 — задача которого получить представление компилируемой программы в абстрагированном от конкретного языка программирования виде и сделать завершающие этапы компиляции, которые как раз таки зависят от целевой программной платформы и, в особенности, от аппаратной архитектуры, то есть все низкоуровневые оптимизации, генерацию машинного кода и т.д.

image


При создании Standalone VB (то есть VB как самостоятельного продукта, в противовес VBA, которое можно привязать к любому, например к программам из комплекта Office), который должен был уметь генерировать EXE-файлы, содержащие Native-код, Microsoft позаимствовали бэкенд C2 — если вместе с компилятором CL он шёл как DLL-модуль, то в комплекте с VB (например VB6) он стал поставляться уже как EXE-файл:


image


При этом концепция с фронтендом и бэкендом сохранена: роль фронтенда берёт на себя среда разработки VB. Она не генерирует Native-код (машинный код x86, главным образом) непосредственно сама: она передаёт промежуточный результат своей работы в бэкенд C2 путём сохранения IL-данных (intermediate language) в файлы, которые скармливаются C2, а уже C2 генерирует типичные объектные файлы COFF (.obj-файлы), которые поступают затем на вход линкеру.


Интересный вопрос касательно IL на входе бэкенда C2

Вдумчивый читатель спросит: являются ли IL-представление программы, которое поступает на вход бэкенда C2 обычным байт-кодом (P-кодом) VB? Или может быть на вход C2 поступает BSCR-представление кода и сериализованное представление вспомогательных структур?


Ни то, ни другое не является правдой. IL-представление модуля (обычного модуля, модуля класса, формы или чего угодно в составе VB-проекта) не является ли P-кодом, ни BSCR-данными. IL-представление — это некая третья форма представления данных о коде модуля.


Интересно ли, как устроено IL-представление и какое место оно занимается на длинном пути превращения исходного кода в исполняемый код? Написать ли об этом статью? Как вы думаете, генерируется ли IL-представление из P-кода, сгенерированного на основе BSCR-представления, или же на основе BSCR-представления генерируется IL-представление, которое может либо поступить на вход бэкенда C2, либо (своими силами) преобразовывается в P-код?


При создании же исполняемого файла в режиме генерации в P-код, бэкенд C2 не используется: среда разработки сама производит на свет объектные файлы COFF (файлы .obj), которые и в этом случае тоже поступают на вход линкеру.


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


Когда программист введёт в редакторе кода новую строчку, для примера — такую:


Public Sub Main

VB сперва разбирает его в PCR-дерево, затем, анализируя тип PCR-узла, соответствующего statement-у (если на строке кода был один statement, то это будет корневым узлом PCR-дерева), понимает (по типу 0xB7), что в этом месте исходника будет находиться не просто абы какая строка кода, а начало новой процедуры. Поняв это, VB не только создаст для этой строки на базе PCR-дерева соответствующее BSCR-воплощение — это происходит для абсолютно любой строки кода, какой бы они ни была, иначе она потом не будет отображаться в редакторе кода. VB также создаёт особый блок в памяти (большую структуру), олицетворяющую процедурную сущность и её зону видимости (scope).


Такие блоки создаются не только для процедур, но и для Enum-ов и Type-ов.


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


Если добавляется простая строка вроде foo = 123 или вообще пустой строки, это особо ни на что не влияет. Однако, если в момент внесения новой строки в какое-то место кода VB понимает, что новоиспечённая строка является со строкой определения новой переменной или константы (по анализу типа PCR-узла в корне PCR-дерева (или соответствующего поддерева — для многоstatement-овых строк кода)), опять же создаётся не только BSCR-представление такой строки, но и вспомогательные блоки для только что созданных констант или переменных, и между такими блоками, и блоками, соответствующими разным scope-ам, выстраиваются определённые ассоциативные связи.


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


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


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


Благодаря этому классическиский VB обладал самым быстрым, отзывчивым и точном механизмом IntelliSense-подсказок и автодополнений и подсказок по выражениям, выводящимся по наведению мышки. Люди всегда отмечали, что монструозные проекты, в которых было под сотню модулей (а такие проекты были весьма частым явлением в enterprise-секторе разработки), могут заметно долго загружаться в момент открытия проекта, но феноменально быстро запускаются и компилируются. Ещё бы: в момент загрузки проекта VB разбирает и обрабатывает каждую новую строчку открываемого файла, так, словно эта строчка добавляется в редактор кода. Создаётся BSCR-представление всего исходника и пишется «топографическая карта» исходного кода каждого из модулей. При запуске же отладки, а большинство людей не отключали on-demand компиляцию, полноценной компиляции именно в момент запуска подвергалась только процедура Main. Остальные процедуры компилировались в момент первого обращения к ним. Логично предположить, что при запуска отладки монструозного проекта, мало кто собирается использовать программу таким образом, что зависит абсолютно каждую строчку проекта хоть раз да поработать, каждую процедуру — хоть раз быть вызванной. Обычно отладка монструозного проекта предполагала испытание небольшой части функциональности, например, недавно добавленной. Фоновая компиляция кода, продолжающаяся во время работы запущенного проекта по мере того, как вызываются всё новые и новые процедуры, вызывала совершенно незначительные задержки в работе запущенного проекта. И даже если человек собрался «дёрнуть» каждую заложенную в программе фичу, стоит помнить, что между перезапусками одного и того же проекта, если процедуры не модифицировались, VB сохраняет результат компиляции процедур в P-код и не перекомпилирует их каждый раз. Так что даже при использовании Start With Full Compile, медленным будет только первый запуск. Последующие запуски даже через Start With Full Compile будут буквально мгновенными, так как весь продукт (пусть и монструозный) уже прошёл компиляцию.


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


Грубо говоря, у вас вообще не будет смысла и нужды периодически проводить перепись населения, если в вашей стране идеально работают ЗАГСы и пограничный контроль. Если вы не пропускаете ни одного факта рождения нового человека или чьей-то смерти, если вы точно знаете о каждом въехавшем в страну и покинувшем её, и если вы начинали с нуля — вам незачем тратить огромные силы на пересчёт миллионов людей. Всегда проще обрабатывать дифференциальные данные о малых изменениях общей картины и поддерживать понимание общей картины во всегда актуальном состоянии, чем каждый раз перестраивать общую картину с нуля, отбрасывая предыдущие данные, только потому, что с прошлого раза могли произойти маленькие или не очень изменения, которые мы упустили непосредственно в момент из совершения. Именно так устроен VB.


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

firehacker
Не знаю, приходят ли здесь уведомления о новых ЛС (которые "Диалоги"), если Вас не затруднит, загляните пожалуйста.

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

У китайцев есть несколько подобных игр, и они даже вместо GDI запилили использование DirectX 9 там. Среди игр прошлого полно VB-игр от самих Microsoft (пакет Best Of Windows Entertainment Pack), там по большей части логические и карточные игры, если не считать Chip's Challenge, представляющую из себя бродилку с головоломками. При этом, они были созданы с более старыми версиями VB, по моей памяти, это где-то версия 3 (работало прямо на борту Windows 3.1).

UFO just landed and posted this here

В Вики-разделе репозитория есть ссылки на сборки под разные платформы, в т.ч. и под винду (x86_64, ARM64 и x86).

Кажется, для того, чтобы вдохнуть новую жизнь в данный проект, его надо портировать не на Ц++, а на HTML5 :3

Через Emscripten легко собирается для работы в браузере через WebAssembly. Недостаток работы из браузера, это невозможность легко добавлять в игру свои ресурсы или эпизоды без полной пересборки игры с созданием пака ресурсов. Создать возможность "выгрузки" даёт лишь временный эффект, "выгруженные" данные будут летать в воздуже, а если их объём гигабайт? (Есть такие эпизоды тяжёлые). Веб-версия хороша в качестве демки, при этом, игра полноценная и её можно пройти целиком.

Нет, JS/TS должен быть в исходниках, а не мусором компиляции. Социальная часть проекта именно в этом [предполагается мною] :3.

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

Я сам был вебером, пока учился в универе, однако, мне веб сильно надоел тем, что было (на тот момент) полно неудобных инструментов разработки (это сейчас мы имеем JetBrains, VisualStudio Code, Atom, и кучу всевозможных фрэймворков и под JavaScript, и под PHP, т.п.), а также, по вебу у меня не было масштабных серьёзных проектов. Мой первый настоящий масштабный проект был начат именно на C++.

Почему я для себя выбрал C++?

Мне этот язык полюбился за его широкие возможности, и особенно за то, что он среди тех, которые умеют создавать монолитные, лёгкие и независимые исполняемые файлы (системные библиотеки не считаются). Я пробовал и смотрел много разных решений, но ни одно из них мне не понравилось, поскольку либо результат получался громоздским и тяжеловесным, либо зависел от кучи ещё более тяжеловесных бибилотек и сред исполнения, и т.п. И как раз мне подвернулось в универе изучить C++. Сначала я его не особо использовал, кроме как для решения лаборатоных работ и курсовых. Однако, к 2014 году у меня созрела идея создать проект, который вроде бы как хобби, но благодаря нему, я начал профессиональную карьеру программиста C++, и мне это сыграло на руку.

P.S. Я ещё слыхал, что некто задумал выпустить порт "Super Mario Bros. X Java Edition", перенеся код игры на язык Java, однако, проект отменили. Подробностей не нашёл, только упоминание факта.

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

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

Главная идея именно возможность играть автономно, без интернета совсем. Также, это критично важно для сохранения истории, ведь, наши будущие внуки и правнуки потом смогут играть в эти игры. DOS-игры как раз самые живучие в этом плане, и всё благодаря проекту DosBox, который позволяет им работать даже на ARMах. Всё, что создавалось онлайн, веб, очень быстро умирает, если теряет аудиторию и прибыль, что ествественно вынуждает компанию-владельца гасить сервера (в The Matrix Online уже точно не поиграешь, лишь надежда на реверс-разработчиков, которые параллельно пилят альтернативный сервер, чтобы воскресить игру, но пока ещё очень и очень сыро). Можно лишь хранить исходники игры, что, пусть каждый сам соберёт себе сервер и запустит сие чудо. Куда проще запустить сборку автономного приложения в таком случае. По факту, игра будет ориентирована на сисадминов, которые готовы развернуть у себя игру на сервере, чтобы поиграть.
Ред.: Забыл упомянуть ситуацию с гибелью Adobe Flash. Хоть это и действительно очень убогая в плане безопасности и стабильности технология, но на её базе было построено большое количество крупных проектов, которые ныне полностью погибли (кроме тех единиц, кто успел перенести свою империю на HTML5).

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

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

Немного про мой основной проект

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

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

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

А почему же не частичная специализация?

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

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

Есть в AUR. откуда прекрасно собралось и работает под Manjaro.
Кому интересно, пакет называется thextech-supermariobrosx Ставить:

$ yay -Sua --batchinstall thextech-supermariobrosx

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

О спасибо! Да многие китайцы вообще молодцы. longpanda, автор ventoy, например. Или создатель OpenResty.

Теперь что касается самой статьи.


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


При этом вы делаете много спорных, дискредитирующих VB или просто неверных утверждений. Понятно, что по сравнению с каким-нибудь новомодным Python-ом, упоминания которого лезут из каждой дырки — из ваканский, из job-offer-ов, из бесконечной рекламы курсов по Python-у, на 2021 год язык VB можно назвать «мёртвым», а мёртвые сраму не имут, как гласит известная поговорка. Но мы на техническом ресурсе, а вы пишите статью, а не простой комментарий, и делать неточные и неверные заявления непозволительно в статье, не делая хотя бы пометку, что язык, с которого вы портировали, вы знаете плохо и мало. Или вы считаете, что вряд ли кто-то в 2021 году посмотрит в сторону этого продукта, и не играет роли, в каком свете вы его выставите? Я и 20 лет видел такие нападки на VB: благодаря им инстумент приобрёл репутацию недоязыка для зелёных программистов и несерьёзных проектов, при этом в большинстве холиваров большинство доводов против было просто мифами или следствием чьего-то незнания. Но с одной стороны холивара на защите VB стояли действительно зелёные новички, которым нечего было противопоставить оппонента в силу своей малообразованности — они и VB-то сами знали едва-едва, не говоря уже о полном отсутствии знаний других языков, низкоуровневого понимания работы всех этих вещей. С другой стороны были зачастую технически грамотные и опытные люди, но грамотные во всём чём угодно, кроме VB, о котором они могли судить и заявлять только по где-то услышанным чужим заявлениям, зачастую совершенно неверным.


Начнём с того, что название «Visual Basic» официально и общепринято пишется через пробел, у вас оно везде написано слитно и даже в ключевых словах/тегах статьи, что, очевидно, влияет на возможность находить эту статью поискам по тегов (ваша статья — единственная с тегом «VisualBasic», остальные статьи на эту тему на сайте идут с тегом «Visual Basic»).


Теперь непосредственно по поводу изложенных в статье мыслей.


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

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


Вообще-то все функции, процедуры, переменные и константы (то, о чём вы пишите) должны, в соответствии с правилами хорошего тона, быть объявлены либо с ключевым словом Public, либо с ключевым словом Private. Как можно назвать ключевое слово «Private» всего лишь каким-то там исключением, если это, блин, одна из двух доступных опций, и сущность будет соответственно, либо видимой из других модулей, либо невидимой? Вообще-то, кстати, не из двух, потому что есть ещё ключевое слово «Friend» и тогда сущность объектного модуля будет видна из своего проекта, но не видна из чужих проектов.


Из соображений совместимости с кодом, переносимым из QBasic, где никаких ключевых слов Private и Public не было. Там переменные уровня модуля объявлялись так же, как локальные переменные в процедурах: с помощью ключевого слова Dim, но могли быть объявлены глобальные переменные уровня модуля — с помощью ключевого слова Global. То же самое касается объявления процедур без указания ключевых слов Private/Public/Friend. Так вот из соображений переносимости кода, возможность объявлять таким образом переменные и процедуры была оставлена в VB.


Это не значит, что ей нужно пользоваться. Но, тем не менее, переменные уровня модуля, объявленные с ключевым словом Global, как ни странно, видны из других модулей. А переменные уровня модуля, объявленные через ключевое слово Dim, вопреки вашим словам, не видны из других модулей. О каком единственном лишь исключении с ключевым словом Private вы говорите?


Отбросим Friend, применимое только для объектных модулей, поговорим об обычных модулях.


Существует 4 способа объявить переменную уровня модуля в модуле:


Global foo As String  ' *
Dim foo As String
Public foo As String ' *
Private foo As String

И ровно ровно половина даёт видимую извне модуля переменную, половина даёт невидимую.


Что касается процедур, то да, без указания Private/Public процедура будет по умолчанию видна из других модулей.


Из ваших слов просто складывается впечатление, что в VB совершенно нет концепции ограничения видимости и доступа к переменным и функциям, и только какое-то жалкое ключевое слово «Private» дали для сокрытия чего-то там. Между тем, есть Public/Friend/Private, и их очень желательно использовать во всех случаях; а у переменных, объявленных без указания видимости, зона видимости ограничена модулем — они не видны из других модулей.


Напротив, это про C/C++ можно сказать, что все переменные и функции, объявленные в «модуле» являются видимыми из всех остальных модулей, и лишь исключение в виде storage-class'а «static» у переменных и функций делает так, что в объектном модуле на выходе компилятора в таблцие символов сущность не будет присутствовать в виде «экспортируемой» наружу сущности, и значит из других модулей с переменной или с функцией не получится слинковаться, потому что линкер не найдёт соответствующую сущность. «private», «public», «protected» в C++ появился только для членов классов и типов, для просто «глобальных» переменных и обычных функций никакого инструментария для указания видимости нет (кроме «static»).


И не надо говорить, что сущность может считаться условно приватной, если в другом файле она не объявлена в заголовочном файле. Не говоря уже о том, что сами по себе заголовочные файлы — чистая условность — всё что в них содержится может быть вставлено и в сам top-level файл-исходник, публичность/приватность сущности должна предопределяться из того модуля, где она находится, а не из того места, где ей кому-то внезапно захотелось попользоваться. В Си (не Си++), я напомню, можно обратиться к функции, находящейся в другом модуле, вообще не упоминая в данном модуле (напрямую или через включение заголовочного файла) её прототип — за исключением не-cdecl-функций.


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

Почему это стало проблемой? Да, VB позволяет.


Но и Си позволяет, потому что у struct-ов и union-ов одно пространство имён, а у переменных — другое, и они друг другу не мешают:


Показать код на Си
struct Controls
{
    short Up;
    short Down;
    short Left;
    short Right;
    short Jump;
    short AltJump;
    short Run;
    short AltRun;
    short Drop;
    short Start;
};

struct Controls Controls;

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


В С++ это тоже работает, но в отличие от Си, можно даже не писать struct в обозначении типа:


Показать два примера кода на C++

Так работает:


struct Controls
{
    short Up;
    short Down;
    short Left;
    short Right;
    short Jump;
    short AltJump;
    short Run;
    short AltRun;
    short Drop;
    short Start;
};

Controls Controls;

А некоторые компиляторы позволяют даже так:


struct Controls
{
    short Up;
    short Down;
    short Left;
    short Right;
    short Jump;
    short AltJump;
    short Run;
    short AltRun;
    short Drop;
    short Start;
};
typedef struct Controls Controls;
Controls Controls;

Пытался придумать, где может возникнуть проблем, на ум приходит только то, что выражение sizeof(Controls) может быть неоднозначным, если где-то в зоне видимости есть переменная Controls с типом, отличным от struct Controls, но в случае портирования такой ситуации возникнуть не может, потому что VB-шный Len() и LenB() не может быть применён к идентификатору структуры.


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

Почему «как ни странно»?


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


Public Sub addValue(ByVal number As Integer) ' void addValue(short number);
Public Sub addValue(ByRef number As Integer) ' void addValue(short &number);

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


Ещё одна особенность, которая заключается в том, что VisualBasic 6 и C++ по разному обрабатывают логические выражения:

Нет, особенность состоит в не в том, что VB6 по другому обрабабывает логические выражения, а в том, что в VB операторы And, Or, Not, Xor и другие являются побитовыми, и им соответствуют не &&, ||, ! и !=, а &, |, ~ и ^. И тогда всё встаёт на свои места: правый операнд тоже вычисляется всегда, даже тогда, когда из результата вычисления левого операнда ясен результат всего выражения.


Зато разница состоит в другом: в том, что в C++ true это не полная противоположность false, а единичка. Единичка отличается от нуля не всеми битами, а только младшим битом. Поэтому использование булевых значений вперемешку с числовыми с использованием побитовых операций может иметь непредвиденный эффект:


nObjects = 32
If nObjects And (2 = 2) Then ' это сработает, потому что 32 & 0xFFFF ==> 32

nObjects = 32;
if(nObjects & (2 == 2)) // это не сработает, потому что 32 & 1 ===> 0

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


Но это лечится оборачиванием в cpp-исходнике всех «логических выражений» (типа сравнений) функцией, которая true превращает в ~0. Этой же функцией следует заменить VB-шную псевдофункцию CBool (не знаю, была ли она в оригинальном коде).


Не понятно, почему ситуация с (num > 0) && (array[num - 1]) названа болью логических выражений. Это при обратном портировании с С++ на VB создавало бы проблемы при механистическом переводе, а при портировании с VB на C++ это не должно давать никаких проблем.


Ни для кого не секрет, что в VisualBasic полностью отсутствовало полноценное понятие классов, а реализация классов,

Для меня секрет, что такое полноценное понятие классов. Вам так хотелось очернить VB, что у вас ни «полностью отсутствует понятие классов», ни «отсутствует полноценное понятие классов», а «полностью отсутствует полноценное». Даже не знаю, как это назвать. Двойная абсолютизация?


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


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


Во многих ЯП с классами отсутствует множественное наследование, а в C++ оно есть. Значит ли это, что во всех этих языках полностью нет полноценных классов? У спорткара отсутствует ковш, поэтому с точки зрения водителя бульдозера Феррарри — неполноценное автотранспортное средство, а с точки зрения владельца спорткара все бульдозеры неполноценны, ведь они даже до 100 км/ч не разгоняются.


Часть вещей, которые умеют классы в С++, очевидно, в VB недоступны. Но зато в VB есть ряд вещей, которых нет в C++:


  • Нет понятия интерфейсов как таковых, хотя в COM понятие интерфейса и класса — это два раздельных понятия. В самом VB каждый класс в то же время является интерфейсом, и нельзя сделать интерфейс, не являющийся при этом классом, но зато в VB можно импортировать интерфейсы, описанные в TLB (созданной на свет каким угодно инструментом). Для примера, в PHP тоже есть раздельное понятие класса и интерфейса. В C++ в качестве интерфейса предполагается использовать абстрактный класс, но отсюда возникает ряд проблем.


  • Предлагаю подумать над ситуацией, когда из разных источников происходят интерфейсы IFoo и IBar, имеющие совершенно разные смыслы и предназначения, и по счастливому стечению обстоятельств оба имеют метод Reset(). И нужно иметь класс CObject, который имплементирует оба интерфейса, и значит реализуют метод Reset для каждого из интерфейсов (при этом для каждого из интерфейсов метод должен делать совершенно своё). Например один интерфейс отвечает за возможность перечислить дочерние подэлементы родительского объекта (родительский объект для этого имплементирует интерфейс энумерации) и метод Reset просто сбрасывает курсор перечисления на начало списка. А второй интерфейс отвечает за какое-нибудь соединение с чем-нибудь или какой-то длинный процесс и просто сбрасывает или отменяет этот длинный процесс. На VB это абсолютно беспроблемная ситуация:


    Implements IFoo
    Implements IBar
    Private Sub IFoo_Reset()
    '   реализация IFoo::Reset
    End Sub
    Private Sub IBar_Reset()
    '   реализация IBar::Reset
    End Sub

    Предлагаю подумать, как это будет выглядеть на C++? И ведь, что самое интересное, на С++ проблема решится пародоксально легко, если в одном из интерфейсов переименовать Reset в ResetThisObject. Спрашивается: почему проектанты разных интерфейсов, которые могут не знать друг друга, и которым не обязательно быть знакомым с тем, кто собирается имплементировать интерфейс, должен договариваться друг с другом с той целью, чтобы не возникло конфликта имён? Но корень проблемы концептуальный: что наследование и поддержка интерфейса — это два принципиально разных явления, и попытка сэмулировать концепцию поддержки интерфейсов через наследование абстрактного класса это дырявая абстракция.


  • В VB у классов есть концепция членов по умолчанию: это могут быть и свойства и методы. В C++ это кое-как можно получить только путём перегрузки операторов. Назначение какого-нибудь метода как члена-по-умолчанию позволяет в VB поиметь класс, экземпляры которого будут «прикидываться» функциеями. Ссылки на экземпляры таких классов внезапно становятся эквивалентными указателям на функции в C/C++, только это типо- и значение- безопасные указатели. Наличие возможности делать параметрические свойства сама по себе интересная, но одновременно с наличием возможности делать какое-то свойство свойством по умолчанию позволяет делать объекты, прикидывающиеся массивами или контейнерами любого толка. Легко делаются объекты, ведущие себя как PHP-массивы (а там это key-value словари по своей сути, причём key это число или словарь, а value — что угодно). Наличие синтаксиса foo!bar позволяет сделать класс, экземпляры которого будут вести себя как объекты в безклассовом JS (когда-то JS был таким): такие объекты можно будет во время исполнения наделять любыми нужными свойствами. Более того, вкупе с классами, которые могут обёртывать функции и претворяться функции, объекты можно будет наделять не только произвольными свойствами, но и произвольными методами.


    Dim blnk as BlankObject
    blnk!SubObject = New BlankObject
    blnk!SubObject!DoJob = AnotherObject.FlexibleMethod
    foo = blnk!SubObject!DoJob("test", IO_FOO, True)

  • Список можно продолжать.



VisualBasic 6 не умеет ничего, кроме локалезависимых ANSI-кодировок и очень ограниченной поддержки UTF16.

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


На вашем месте мне в связи с этим следовало бы заявить, что С++ не умеет ничего: ни локаленезависимые ANSI-кодировки, ни UTF-16. Но я не любитель таких громких заявлений.


Так вот, в VB имеет встроенный тип String, и, для вас это будет сюрпризом, за этим типом стоят исключительно юникодные строки. Все строки хранятся в памяти в юникоде, манипуляции с ними происходят в юникоде. При вызове методов объектов, включая внешние объекты, реализованные не на VB, а на чём угодно (на том же C++, например), строки по прежнему передаются и принимаются в юникоде. Потому что таковы правил технологии COM и тамошнего типа BSTR.


Другое дело, что помимо самого языка есть ещё набор стандартных функций, часть из которых вынужденна взаимодействовать с системой. Например функция MsgBox взаимодействует с системой (вызывает WinAPI-функцию MessageBox), функция Kill, удаляющая файл, тоже должна взаимодействовать с системой. Проблема в том, что VB4—VB6 должен был работать на Windows 9x сам по себе, и VB-программы в скомпилированном виде, должны были работать на Windows 9x, и в этих самых 9x-системах юникод не поддерживался. Большинство W-версий WinAPI-функций не могли работать.


Поэтому реализация той части «встроенных функций» VB, которые вынужденны взаимодействовать с ОС: вывод сообщений, встроенные контролы, работа с файлами — чтобы это хоть как-то работало под 9x, во всех местах взаимодействия VB с API операционной системы, юникод пробразуется в однобайтовую кодировку, а при обратном движении — наоборот.


Текст, сохраняемый или читаемый в/из файлы встроенными средствами языка вынужденно сохраняется в однобайтовой кодировке, потому что если бы он сохранялся как есть в юникоде — его бы открыли под какой-нибудь Windows 95 или 98 и ужаснулись бы: блокнот не показал бы непонятно что. Да и большинство текстовых файлов, существующих на дисках в тот момент, были не юникодными, так что VB-программы (если бы они ожидали прочитать юникод), читали бы что попало.


Стандартная библиотека Си тоже имеет функции printf() и strlen(), расчитанные на однобайтовые кодировки. Повод ли это говорить, что Си не умеет ничего? Это лишь говорит об стандартной библиотеки, но не об ограниченности самого языка. В случае VB никто не мешал использовать библиотеки, чья объектная модель предоставляла бы все нужные возможности, и которые взаимодействовали бы взаимодействовали с системой используя юникодные версии WinAPI-функций. Например, использовать библиотеку FSO для работы с файловой системой. Никто не мешал напрямую использовать W-версии WinAPI функций и работать с юникодом.


Никто, в конце-концов, не мешал сохранить в файл строку в юникоде, обернув её просто в байтовый массив:


Dim b() As Byte
Open "test.txt" For Binary as #1
b = "Привет, я люблю юникод"
Put #1, , b
Close #1

В таком виде строка в файле будет сохранена в юникоде (UTF-16 UCS-2) без BOM-а.


По крайней мере, если сишные strlen(), strstr() и substr() уж точно не поддерживают юникод, то VB-шные Len(), InStr() и Mid$() полноценно юникодные.


Я решил использовать в игре UTF8, поскольку эта кодировка является универсальной и повсеместной. Большинство операционных систем используют именно её в своих файловых системах. Отличается лишь Windows, которая предпочитает использовать локалезависимые ANSI-кодировки и UTF16. Из-за чего, в функциях взаимодействия с Windows я применил прямое преобразование между UTF8 и UTF16, чтобы продолжать использовать UTF8 внутри игры, и UTF16 при обращении к функциям самой Windows.

UTF-8 — это жуткий костыль. Это худшая из возможных кодировок для работы со строками, потому что для определения длины строки придётся просканировать всю строку от начала до конца. Потому что для одной строки вместо 1 теперь появляется 2 показателя: длина строки в символах и размер данных строки в байтах. Для хранения и передачи через сеть она, конечно, весьма оптимальная, особенно с позиции какого-нибудь американца, у которого текст почти полностью состоит из символов, умещающиеся в нижние 128 кодовых точек, и изредка содержащие какие-нибудь экзотические символы: перерасход место в таком случае получается почти что никакой по сравнению с той же UTF-16 или, упаси господи, UTF-32.


Большинство операционных систем — это, видимо, юникс-подобные операционные системы, на момент создания и в первые годы существования которых никого даже близко не волновала проблема поддержки юникода, при этом была написана огромная база кода, переписать которую разом не так-то просто. И как гениальны выход из ситуации попался юникод, который позволял манипулировать текстами используя функционал, заточенные под манипулирование однобайтными кодировками — изменения нужно было сделать лишь в тех местах, где осуществлялся вывод текста. Какой-нибудь grep мог быть соединён с awk, и оба, написанные без всякой задней мысли о юникоде, могли корректно обработать текстовый файл в UTF-8, если grep-у подсунуть паттерн в UTF-8 — важно было бы только то, чтобы терминал корректно отобразил пользователю конечный выхлоп.


Между тем, Windows NT с самого своего появления была юникодной изнутри. Ядро Windows NT использует исключительно юникод для хранения всех строк. User-mode библиотеки Windows NT тоже используют юникод. ANSI-версии WinAPI-функций только и делают, что конвертируют ANSI в Юникод и передают это нормальным полноценным юникодным реализациям.


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


То же самое делает и VB: его тип String этот тот же самый COM-овский тип BSTR — юникодная строка, 2 байта на символ, длина хранится перед строкой.

Интересно, что на ресурсе rosettacode
на языке Viual Basic всего 113 решённых задач
на языке Viual Basic Net 393 решений задач

Что это означает или какой вывод из этого должен быть сделан?


Я просто впервые слышу про Rosetta Code и не знаю, что количество решённых задач должно означать.


Картинка ваша не грузится, кстати.

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

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

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

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

Java и Kotlin где то рядом, по Racket вообще отсечка в таблице сравнения с ним.
Factor на 23-ем месте (конкатенативный язык программирования с функциональной направленностью)

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

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

P.S. Картинка image

Благодарю за замечания!

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

Отвечаю по каждому из замечаний:

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

Так и есть, с VB я в серьёз никогда и не работал, максимум делал пару-тройку экспериментов, пока был ребёнком, а потом взял лишь за необходимостью портировать код игры на C++.

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

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

Private/Public/Friend

Всё верно. Я по факту не правильно страктовал свою мысль. Про обращение к переменным и функциям внешних модулей я лишь хотел обозначит то, что для доступа к публичным модулям из других не надо делать никаких предварительных деклараций, включений заголовков, импортов, и т.п. и что неявное определение таких ссылок функций и переменных в VB это основная особенность. В чистом C (до стандарта С99) возможно вызывать какую-нибудь функцию, не добавляя никаких предварительных деклараций. Однако, это очень плохой тон, и, в зависимости от компилятора и его флагов, можно получить соответствующее предупреждение "implicit declaration of function", или даже ошибку вовсе.

Почему это стало проблемой?

Возникает путаница в коде (особенно для человека, сильно теряется наглядность и множество неоднозначностей), и в C, и C++ чётко происходит ошибка, если помимо глобального поля, без ключевого слова "struct" попытаться определить локальную переменную с этим типом, будет ошибка "error: expected ‘;’ before ‘ko’":

struct Controls
{
    short Up;
    short Down;
    short Left;
    short Right;
    short Jump;
    short AltJump;
    short Run;
    short AltRun;
    short Drop;
    short Start;
};

Controls Controls;

int main()
{
    Controls ko; // <-- error: expected ‘;’ before ‘ko’
    return 0;
}

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

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

а в том, что в VB операторы And, Or, Not, Xor и другие являются побитовыми

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

Зато разница состоит в другом: в том, что в C++ true это не полная противоположность false, а единичка.

Так и есть, в С++ это 0 и 1, а в VB используется COMBOOL со значениями 0 (0x0000) и -1 (0xFFFF).

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

Я в коде чётко использовал либо && / ||, либо & / |, в зависимости от ситуации.

Ни для кого не секрет, что в VisualBasic полностью отсутствовало полноценное понятие классов, а реализация классов,

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

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

Та и есть, BRST в качестве носителя. Я ругался, по факту, на интерфейс IDE и на фактическое поведение созданных программ при приёме и выдаче текстовых данных, запрос путей к файлам, и т.п. Пока под капотом работает юникод, стандартные интерфейсы, используемые конкретно в VB6, используют именно локалезависимые ANSI-кодировки. Соответственно и нужно использовать расширенные возможности и нестандартный подход, чтобы нормально принимать и выдавать нормальные юникодные строки. В C/C++ стандартных способов работы с текстом большое множество. Самые базовые фукнции также локалезависимы. При этом имеется множество альтернативных функций, принимающих и работающих с юникодом, как стандартных (в т.ч. платформозависимых), так и сторонних. UTF8 мне нравится тем, что он позволяет создавать по большей части лаконичный код, который бы корректно работал с системными интерфейсами, не зависимо от того, на каком языке строчка. Так и есть, что для посимвольной работы с UTF8 много хитростей и сложностей, требующих сканировать всю строку и парсить биты, чего мне и приходится делать, когда я реализую функцию отрисовки текста на экране, для подсчёта посимвольной размерности строки, и т.п. UTF32 в этом плане гораздо удобней, потому что одно значение - один символ. Фреймворк Qt внутри себя и использует UTF32 под капотом QString, чем он чрезвычайно удобен. UCS-2 всё же не идеален, поскольку у него присутствует такое понятие как суррогатные пары, которые позволяют закодировать один символ двумя единицами.

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

Для плавности нужно делать следующее:

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

  • Взять желаемую длительность кадра (например, 16 миллисекунд), вычесть из неё время, затраченное на обработку всего, и если разница больше нуля, использовать как задержку

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

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

P.S. Библиотека, чей код я взял за основу для себя, чтобы жёстко выдерживать заданную частоту (которая крайне важна для сообщества спидраннеров): https://gitlab.com/torkel104/libstrangle

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


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

использовать как задержку


Задержку чем?

Задержку чем?

Время, которое ждать, используя SDL_Delay() / usleep() / Sleep().

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

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

SDL_Delay() / usleep() / Sleep().


А, у вас SDL… А я что-то подумал, что у вас чистый Си++ и WinAPI.

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


Не могу. Direct-X я отключил, так как буду портировать на тот же QNX, а GDI такого не умеет.

Вот в чём проблема: попробуйте походить и увидите микрорывочки. И это при том, что я трачу почти весь процессор на поддержание такта (как в способе Ламота ниже).
Я пробовал кучу способов (Андре Ламот в своей книжке лет 17 назад применял GetTickCount() — этот способ тоже не идеал).

WinAPI

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

Не могу. Direct-X я отключил, так как буду портировать на тот же QNX, а GDI такого не умеет.

А OpenGL как? Он есть почти везде, и я использую именно его первым делом. Без вертикальной синхронизации (не зависимо от используемого графического интерфейса) никогда не удастся отрисовать идеально плавно, потому что если частота монитора не совпадает с обновлением графики игры, картинка будут резаться пополам, чего будет видно на динамических сценах (особенно если фон движится вправо-влево). В QNX, как я покопал, OpenGL есть.

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


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

А OpenGL как? Он есть почти везде, и я использую именно его первым делом.


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

В QNX, как я покопал, OpenGL есть.


Он есть, но не у всех работает. :) Там с дровами вообще беда. В общем, лучше нативной отрисовки там ничего нет.

расслоение картинки будет

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

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

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

В GDI смысла в ней особого нет при использовании StretchDIBits. Всё равно контекст памяти буфера будет копироваться такой же функцией. А рисую я в своём собственном буфере.

надо выделить текстуру,


Текстуру? Там исходно ведь создаётся совместимый контекст памяти, в который отображается BITMAP, на котором идёт рисование в памяти. А дальше этот контекст памяти копируется на исходный контекст устройства.

Текстуру? Там исходно ведь создаётся совместимый контекст памяти, в который отображается BITMAP, на котором идёт рисование в памяти

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

В GDI смысла в ней особого нет при использовании StretchDIBits. Всё равно контекст памяти буфера будет копироваться такой же функцией. А рисую я в своём собственном буфере.

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

Спасибо, но всё это мне известно, поскольку с OpenGL я уже лет 20 как знаком. Правда, на версию 2 и выше я не переходил ввиду полной ненужности для моих задач.
Я как-то замерял время между тактами (вот не помню, для какого варианта синхронизации), так вот, оно плавает, хотя нагрузка, вообще говоря, нулевая.

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


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


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


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

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


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

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


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

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


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

Используйте мультимедийный таймер Windows


А вот этот вариант я не пробовал. Попробую. Спасибо.

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


Вот этого тоже не знал, спасибо.

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

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

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


Зависит от решаемой задачи и желания заморачиваться.

У вас есть идеи, почему он выбрал 65 кадров в секунду? Очень странное значение.

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

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

Sign up to leave a comment.

Articles