Всем доброго времени суток! Это моя история о том, как я портировал исходный код одной фанатской Windows-игры о Марио с Visual Basic 6 на C++, и с какими трудностями я столкнулся в процессе создания порта.

Немного об оригинальной игре

Super Mario Bros. X (или коротко SMBX) - это фанатская игра по мотивам вселенной Марио, созданная в 2009 году американцем Эндрю Спинксом (который позже прославился как создатель игры Terraria). Эта фанатская игра была его первым опытом в разработке игр. В ней он познавал азы игростроя. Игра создавалась с использованием Visual Basic 6.

Главное меню игры Super Mario Bros. X версии 1.3
Главное меню игры Super Mario Bros. X версии 1.3

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

Даже не смотря на полное прекращение разработки игры в 2011 году, она была востребована, и продолжала широко использовалаться сообществом. Игра также привлекла внимание разработчиков-энтузиастов и хакеров, которые создавали для неё вспомогательные инструменты, а также делали попытки модифицировать и расширить игру. Самыми известными из них являются набор разработки из тулкита Moondust Project (изначально называвшимся PGE Project), а также, библиотека LunaLua (изначально известная как LunaDll), расширяющая функционал игры посредством dll-инъекции.

Исходный код игры долгое время был закрытым. Однако, всё изменилось, когда 2 февраля 2020 года на форуме были опубликованы исходные коды игры.

Начало работ

Слухи о возможной публикации исходных кодов игры были ещё в 2016м году, однако, этого не произошло. Это дало мне идею переписать игру на C++, чтобы с ней удобно было возиться. Но, поскольку, исходников не было, идея слегла в долгий ящик. С выходом исходников в феврале 2020-го года, я буквально достал эту идею из глубокой ямы забвения. Первым делом, я запустил свою виртуальную машину с Windows XP, где я и развернул среду VB6, в которой затем я открыл проект игры, и, после нескольких исправлений ошибок компиляции, успешно запустил игру из исходного кода.

Работы начались с исследования устройства кода игры и его структуры, а также с замены кода воспроизведения аудио с древнего MCI на мою специальную сборку SDL Mixer X, созданную для работы в VB6-проектах. Таким образом, я решил проблему работы игры на Linux под Wine, а также значительно ускорил загрузку игры и уменьшил потребление ею памяти.

Инструментарий

Сначала я использовал самописный кусок кода на JavaScript, которым я через регулярные выражения парсил определения переменных, массивов и функций. Затем, я использовал бесплатную версию "VB to C++ Converter" от Tangible Software Solutions, чтобы относительно точно преобразовывать куски кода в C++ (Я пользовался режимом Snifit, поскольку конвертер был ориентирован на VB.NET, значительно отличавшийся от VB6. Также не обошлось без огромного числа дополнительных ручных манипуляций и макросов). Часть кода была переписана мною вручную, особенно на начальных этапах. Много модулей я писал с нуля (либо заимствовал из других моих проектов), чтобы обеспечить конечный проект всей необходимой функциональностью. Весь процесс разработки я вёл на Linux Mint, используя среду разработки Qt Creator, параллельно с CLion. Также, я использовал виртуальные машины с Windows XP и Windows 7 для запуска некоторых зависимых инструментов, а также VisualStudio Code для просмотра VB6-проекта игры из под Linux-системы.

Особенности Visual Basic и различия с C++

Кроме явного синтаксического и функционального различия между языками, имеется огромное число тонких не интуитивных различий, которых невозможно заметить без прямого взаимодействия и без чтения документации. Я сам лично мало работал с Visual Basic, и большую часть знаний о нём я узнал из поведения написанных программ и MSDN-документации (для Visual Basic 6, нужно качать редакцию 2001 года, к счастью, она присутствует на archive.org в свободном доступе).

Благодарю @firehacker за уточнения и за расширенные объяснения.

Видимость переменных и функций

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

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

Структуры

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

' Определение структуры
Public Type Controls
    Up As Boolean
    Down As Boolean
    Left As Boolean
    Right As Boolean
    Jump As Boolean
    AltJump As Boolean
    Run As Boolean
    AltRun As Boolean
    Drop As Boolean
    Start As Boolean
End Type

' Определение одноимённой переменной
Controls As Controls

С одной стороны, такое возможно и в C/C++ тоже (если не забывать указывать ключевое слово struct перед именем структуры). Однако, одноимённые, но разнотипные, объекты сильно вредят наглядности, а также создают неоднозначности в некоторых ситуациях, например, при попытке определить локальную переменную Controls ko без ключевого слова struct, вылезет ошибка компиляции error: expected ‘;’ before ‘ko’. При использовании sizeof(Controls) также создаётся неоднозначность, хотим ли мы узнать размер переменной Controls, или размер типа структуры Controls.

Из-за этого, я решил добавлять всем именам структур окончание "_t", чтобы устранить все неоднозначности:

struct Controls_t
{
    bool Up = false;
    bool Down = false;
    bool Left = false;
    bool Right = false;
    bool Jump = false;
    bool AltJump = false;
    bool Run = false;
    bool AltRun = false;
    bool Drop = false;
    bool Start = false;
};

extern Controls_t Controls;

Массивы с явным диапазоном

В Visual Basic предусмотрена возможность создавать массивы с различными диапазонами индексов, и не обязательно от 0 до N-1, а например, от 1 до 5, от -1000 до +1000, и т.п.:

Public BlockSwitch(1 To 4) As Boolean

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

template <class T, long begin, long end>
class RangeArr
{
    static constexpr long range_diff = begin - end;
    static constexpr size_t size = (range_diff < 0 ? -range_diff : range_diff) + 1;
    static const long offset = -begin;
    T array[size];

public:
    RangeArr()
    {}

    ~RangeArr()
    {}

    RangeArr(const RangeArr &o)
    {
        for(size_t i = 0; i < size; i++)
            array[i] = o.array[i];
    }

    RangeArr& operator=(const RangeArr &o)
    {
        for(size_t i = 0; i < size; i++)
            array[i] = o.array[i];
        return *this;
    }

    void fill(const T &o)
    {
        for(size_t i = 0; i < size; i++)
            array[i] = o;
    }

    T& operator[](long index)
    {
        assert(index <= end);
        assert(index >= begin);
        assert(offset + index < static_cast<long>(size));
        assert(offset + index >= 0);
        return array[offset + index];
    }
};

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

template <class T, long begin, long end, T defaultValue>
class RangeArrI
{
    static constexpr long range_diff = begin - end;
    static constexpr size_t size = (range_diff < 0 ? -range_diff : range_diff) + 1;
    static const long offset = -begin;
    T array[size];

public:
    RangeArrI()
    {
        for(size_t i = 0; i < size; i++)
            array[i] = defaultValue;
    }

    ~RangeArrI()
    {}

    RangeArrI(const RangeArrI &o)
    {
        for(size_t i = 0; i < size; i++)
            array[i] = o.array[i];
    }

    RangeArrI& operator=(const RangeArrI &o)
    {
        for(size_t i = 0; i < size; i++)
            array[i] = o.array[i];
        return *this;
    }

    void fill(const T &o)
    {
        for(size_t i = 0; i < size; i++)
            array[i] = o;
    }

    T& operator[](long index)
    {
        assert(index <= end);
        assert(index >= begin);
        assert(offset + index < static_cast<long>(size));
        assert(offset + index >= 0);
        return array[offset + index];
    }
};

Таким образом, выше представленный пример на C++ будет определён следующим образом:

extern RangeArrI<bool, 1, 4, false> BlockSwitch;

К счастью (или к сожалению), Эндрю не использовал в своём коде динамических массивов, хотя, с другой стороны, по сравнению с std::vector в С++, динамические массивы в Visual Basic 6 были не очень удобными в работе.

В моих шаблонах я также применил assert-ы, поскольку они позволяют отлавливать ошибки выхода за пределы диапазона массива в коде (в Visual Basic 6 типичная ошибка это "Runtime Error 9: Subscript out of range"). И тем самым, иметь возможность легко отлаживать их, не допуская последующей порчи памяти и возникновения SIGSEGV.

Upd: В комментариях, @Izaron-ом предложена альтернативная реализация шаблона, используя 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);
    }
};

Опциональные аргументы-ссылки

В Visual Basic, в функциях и процедурах, по умолчанию (если явно не указать ByVal или ByRef), аргументы передаются по принципу ссылок: их можно изменить непосредственно из кода функции:

Public Sub addValue(number As Integer)
  number = 42
End Sub

В C++, по умолчанию, аргументы передаются по значению. Нужно явно указывать, что аргумент передаётся по ссылке:

void addValue(int &number)
{
  number = 42;
}

При портировании кода с Visual Basic 6 я столкнулся со следующим явлением:

Public Sub addValue(step As Integer, Optional number As Integer = 0)
  number = step
End Sub

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

В C++ из-за того, что я упустил тот факт, что аргументы в VB это ссылки, словил баг, что значение, переданное по опциональному аргументу, не обновилось в соответствии с кодом:

void addValue(int step, int number = 0)
{
  number = step;
}

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

void addValue(int step, int &number)
{
  number = step;
}

void addValue(int step)
{
  int dummy = 0;
  addValue(step, dummy);
}

Upd: В комментариях, @Izaron-ом предложена альтернативная реализация концепции через шаблоны:

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;
}

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

Округление чисел

В Visual Basic принципиально отличается политика округления чисел, чем от C++:

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

  • Округление идёт нестандартным образом, через x86-инструкцию FRNDINT, которая округляет серединные значения по типу 0.5 к ближайшему чётному целому. То есть, было 15.5, округление будет в 16, если было 42.5, то округлится к 42м.

Почему я обратил на это внимание? Потому что физика в игре во многом зависит от частей кода, использующего округление. Если округление делать не правильно (не так, как оно делалось в Visual Basic), то в итоге, физика будет искажена (Например, будет искажено движение пресса-давилки на уровне "Dungeon of Pain" в эпизоде "The Invasion 2").

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

const double power10[] =
{
    1.0,
    10.0,
    100.0,
    1000.0,
    10000.0,

    100000.0,
    1000000.0,
    10000000.0,
    100000000.0,
    1000000000.0,

    10000000000.0,
    100000000000.0,
    1000000000000.0,
    10000000000000.0,
    100000000000000.0,

    1000000000000000.0,
    10000000000000000.0,
    100000000000000000.0,
    1000000000000000000.0,
    10000000000000000000.0,

    100000000000000000000.0,
    1000000000000000000000.0
};

double vb6round(double x, int deimals);

int vb6Round(double x)
{
    return static_cast<int>(vb6Round(x, 0));
}

static SDL_INLINE double toNearest(double x)
{
    int round_old = std::fegetround();
    if(round_old == FE_TONEAREST)
        return std::nearbyint(x);
    else
    {
        std::fesetround(FE_TONEAREST);
        x = std::nearbyint(x);
        std::fesetround(round_old);
        return x;
    }
}

double vb6Round(double x, int decimals)
{
    double res = x, decmul;

    if(decimals < 0 || decimals > 22)
        decimals = 0;

    if(SDL_fabs(x) < 1.0e16)
    {
        decmul = power10[decimals];
        res = toNearest(x * decmul) / decmul;
    }

    return res;
}

Таким образом, получилась весьма точная реализация округления, соответствующая поведению Visual Basic 6.

Таймеры

В игре использовались встроенные функции и глобальные переменные, отвечающие за время: глобальная функция Timer, возвращающая Single-значение (аналог float в C++) секунд, прошедших с полуночи, и WinAPI-функция Sleep для различных задержек.

Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

К счастью, SDL2 легко заменяет эти функции на собственные SDL_GetTicks() и SDL_Delay(). Однако, имеется вопрос более серьёзный, чем просто выжидание времени и задержка: игра старается жёстко выдерживать частоту обновления в 65 кадров в секунду. Если специально не стараться, игра будет работать с частотой 67 кадров в секунду без вертикальной синхронизации. Это большая проблема для спидраннеров, использующих внешние таймеры для честного измерения времени прохождения игры.

Чтобы исправить проблему, я нашёл код, который выдерживает определённую частоту, и доработал его. Получился целый модуль, реализующий логику жёсткой выдержки частоты в 65 кадров в секунду. Пришлось отдельно повозиться, поскольку в Linux и macOS уже были нужные функции, считающие нановремя, а в Windows с этим большая проблема. Однако, спустя время я нашёл универсальное решение, которое будет идеально работать и на миллисекундах.

Боль логических выражений

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

  • В C++ присутствуют раздельные логические операторы &&, ||, !, != и побитовые операторы &, |, ~, ^. Булевый тип соответствует 0 (false) и 1 (true).

  • В Visual Basic, операторы And, Or, Not, Xor, побитовые и прямо эквивалентны операторам &, |, ~, ^. Булевый тип представлен в виде полной инверсии целого числа: 0x0000 в качестве True и 0xFFFF в качестве False.

  • Вследствие использования логических операторов, в С++ происходит постепенный расчёт значений и постепенное наложение. То есть, мы имеем выражение if(A < 5 && Array[A] == 1), и в случае, когда выражение A < 5 ложное, выражение Array[A] == 1 рассчитываться не будет.

  • В Visual Basic, члены выражения рассчитываются сразу, поскольку выполняется побитовый рассчёт всей формулы. То есть, в логическом выражении if A < 5 And Array(A) = 1 Then будут всегда рассчитываться оба члена (Код эквивалентен if((A < 5 ? 0xFFFF : 0) & (Array[A] == 1 ? 0xFFFF : 0)). Здесь произойдёт ошибка из-за того, что индекс A выходит за пределы размера массива. Таким образом, при портировании на C++ я случайно исправил баг, рушивший игру. Из-за этой особенности языка в коде можно встретить большое число лазаний из if() { if {} if { .... } }, которая вместе с тем, что выглядит неуклюже, является необходимым костылём над особенностью обработки логических выражений в Visual Basic.

  • При преобразовании сложных логических выражений из Visual Basic в C++ может возникнуть путаница там, где не использовались скобки, а также всплывать предупреждения компилятора. Решается легко добавлением скобок вокруг ключевых логических групп. Также нужно учитывать, что в логических выражениях на VB очень легко перепутать побитовый рассчёт между логическим выражением, из-за чего можно исказить конечный результат: получить один бит вместо целочисленного значения.

Также было явно заметно, что Эндрю на тот момент абсолютно не имел никакого понятия о Select Case (аналога оператора switch()), и поэтому в его коде было чрезвычайное злоупотребление конструкциями if else if else if else... .

Кошмарный спагетти-полиморфизм

Фрагмент спагетти-кода, отвечающего за логику НИП разных типов
Фрагмент спагетти-кода, отвечающего за логику НИП разных типов

Ни для кого не секрет, что в Visual Basic реализация классов была сильно ограниченной и прямо завязанной на технологии COM. Эндрю решил не использовать её вовсе. Однако, Эндрю даже не стал создавать раздельные функции для разбиения логики разнотипных объектов. Вместо этого, он создал цепь громоздких божественных функций, каждая из которых содержала огромнейшие массивы кода, разбивающие логику разнотипных объектов через цепь if else if else if else. Ещё одна особенность кода игры, это очень и очень длинные однострочные логические и арифметические выражения, почти никакого переноса кода использовано не было. Также Эндрю не позаботился об использовании перечислений, чтобы наглядно именовать каждый тип элементов, а просто использовал сырые числовые значения, чтобы обозначить тот или иной элемент (а их там сотни разных!). Большую часть кода я преобразовал достаточно точно, сохранив исходную логику. Часть этого всего "спагетти" мне пришлось разбить на секторы, вынося в отдельные функции, чтобы тем самым сохранить наглядность.

Поддержка текста и преобразований строк в числа

Visual Basic 6, даже не смотря на использование юникода в своём ядре, и даже на то, что строки базируются на COM-типе BSTR с юникодом, стандартными функциями работы с файловой системой, отображения в интерфейсе, и чтения-записи текстовых строк (если открывать файл как текстовый), по факту, не умеет ничего, кроме локалезависимых ANSI-кодировок и очень ограниченной поддержки UTF16 из-за того, что средам VB4-VB6 и программам, собранных на них, требовалось работать на Windows 9x, не поддерживающих юникод. Из-за этого возникают серьёзные проблемы при работе игры на компьютерах по всему миру. Поэтому, нельзя именовать игровые файлы на кириллице, иначе игра не заработает на компьютере в Китае. И наоборот. Я решил использовать в игре UTF8, поскольку эта кодировка является универсальной и повсеместной, хоть и очень неудобной для задач посимвольной обработки текста, требующих сканировать всю строку. Большинство операционных систем используют именно её в своих файловых системах. Отличается лишь Windows, которая предпочитает использовать локалезависимые ANSI-кодировки (на уровне ядер 9x) и UTF16 (на уровне ядер NT). Из-за чего, в функциях взаимодействия с Windows я применил прямое преобразование между UTF8 и UTF16, чтобы продолжать использовать UTF8 внутри игры, и UTF16 при обращении к функциям самой Windows.

Что касается преобразований строк в числа, здесь они тоже локалезависимы: Эндрю, как положено по американским стандартам, использовал точку в качестве разделителя целой и десятичной частями чисел. Из-за этого, если в полях файлов присутствовали числа с плавающей точкой, игра падала, выдавая ошибку "Runtime Error 13" лишь потому, что по локальному стандарту (например, в России, в Германии и других странах), в качестве десятичного разделителя требуется запятая. Эта проблема требовала от игроков менять настройки стандартов, чтобы указывать точку, либо убирать числа с плавающей точкой из игровых файлов. В итоге, мне пришлось реализовать обёртку, которая позволяет преобразовать числа как с точкой, так и с запятой, чтобы обеспечить полную совместимость со всеми.

Графика

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

Public Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, ByVal X As Long, ByVal Y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, ByVal ySrc As Long, ByVal dwRop As Long) As Long
Public Declare Function StretchBlt Lib "gdi32" (ByVal hdc As Long, ByVal X As Long, ByVal Y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, ByVal ySrc As Long, ByVal nSrcWidth As Long, ByVal nSrcHeight As Long, ByVal dwRop As Long) As Long
Public Declare Function CreateCompatibleBitmap Lib "gdi32" (ByVal hdc As Long, ByVal nWidth As Long, ByVal nHeight As Long) As Long
Public Declare Function CreateCompatibleDC Lib "gdi32" (ByVal hdc As Long) As Long
Public Declare Function GetDC Lib "user32" (ByVal hWnd As Long) As Long
Public Declare Function SelectObject Lib "gdi32" (ByVal hdc As Long, ByVal hObject As Long) As Long
Public Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As Long
Public Declare Function DeleteDC Lib "gdi32" (ByVal hdc As Long) As Long
Public Declare Function GetWindowDC Lib "user32.dll" (ByVal hWnd As Long) As Long
Главная форма игры и код, рисующий на ней игровую сцену из текстуры.
Главная форма игры и код, рисующий на ней игровую сцену из текстуры.

Игра позволяла работать с форматами GIF, JPEG и BMP. К несчастью, отрисовать с нормальной прозрачностью через GDI была сложная задача, Эндрю решил её методом битовой маски. То есть, простыми словами, картинка разбивается на две части: основная и маска. Основная часть это обычная картинка, но с полностью чёрным фоном.

Фронтальная часть картинки - обязательный чёрный фон, который превратится в фон через ИЛИ
Фронтальная часть картинки - обязательный чёрный фон, который превратится в фон через ИЛИ

Маска - это чёрно-белая карта, отображающая пиксели с прозрачностью и без.

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

Сам процесс отрисовки состоит из двух этапов:

' Отрисовка маски, используя побитовое И 
' между каждым пикселем целевой поверхности и маски.
' Белые пиксели оставят фон нетронутым, чёрные закрасят его
BitBlt targetDCSurface, X, Y, W, H, mask.hdc, 0, 0, vbSrcAnd

' Отрисовка переднего плана поверх маски на ту же поверхность,
' используя побитовое ИЛИ между каждым пикселем поверхности и маски.
' Там где чёрные пиксели, цвет не изменится, а там где цветные, будут побитого
' смешаны с фоном. Если на фоне уже нарисован чёрный силует, то передний план
' отрисуется поверх него без цветовых искажений.
BitBlt targetDCSurface, X, Y, W, H, front.hdc, 0, 0, vbSrcPaint

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

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

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

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

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

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

void bitmask_to_rgba(FIBITMAP *front, FIBITMAP *mask)
{
    unsigned int x, y, ym, img_w, img_h, mask_w, mask_h, img_pitch, mask_pitch;

    BYTE *img_bits, *mask_bits, *FPixP, *SPixP;
    RGBQUAD Npix = {0x00, 0x00, 0x00, 0xFF};   /* Цвет целевого пикселя */
    BYTE Bpix[] = {0x00, 0x0, 0x00, 0xFF};   /* Чёрный пиксель-заглушка */
    unsigned short newAlpha = 0xFF; /* Рассчитанное значение альфа-канала */

    BOOL endOfY = FALSE;

    if(!mask)
        return; /* Ничего не делать */

    img_w  = FreeImage_GetWidth(front);
    img_h  = FreeImage_GetHeight(front);
    img_pitch = FreeImage_GetPitch(front);
    mask_w = FreeImage_GetWidth(mask);
    mask_h = FreeImage_GetHeight(mask);
    mask_pitch = FreeImage_GetPitch(mask);

    img_bits  = FreeImage_GetBits(front);
    mask_bits = FreeImage_GetBits(mask);
    FPixP = img_bits;
    SPixP = mask_bits;

    ym = mask_h - 1;
    y = img_h - 1;

    while(1)
    {
        FPixP = img_bits + (img_pitch * y);
        if(!endOfY)
            SPixP = mask_bits + (mask_pitch * ym);

        for(x = 0; (x < img_w); x++)
        {
            Npix.rgbBlue = ((SPixP[FI_RGBA_BLUE] & 0x7F) | FPixP[FI_RGBA_BLUE]);
            Npix.rgbGreen = ((SPixP[FI_RGBA_GREEN] & 0x7F) | FPixP[FI_RGBA_GREEN]);
            Npix.rgbRed = ((SPixP[FI_RGBA_RED] & 0x7F) | FPixP[FI_RGBA_RED]);
            newAlpha = 255 - (((unsigned short)(SPixP[FI_RGBA_RED]) +
                               (unsigned short)(SPixP[FI_RGBA_GREEN]) +
                               (unsigned short)(SPixP[FI_RGBA_BLUE])) / 3);

            if((SPixP[FI_RGBA_RED] > 240u) // Почти белый
               && (SPixP[FI_RGBA_GREEN] > 240u)
               && (SPixP[FI_RGBA_BLUE] > 240u))
                newAlpha = 0;

            newAlpha += (((unsigned short)(FPixP[FI_RGBA_RED]) +
                          (unsigned short)(FPixP[FI_RGBA_GREEN]) +
                          (unsigned short)(FPixP[FI_RGBA_BLUE])) / 3);

            if(newAlpha > 255)
                newAlpha = 255;

            FPixP[FI_RGBA_BLUE]  = Npix.rgbBlue;
            FPixP[FI_RGBA_GREEN] = Npix.rgbGreen;
            FPixP[FI_RGBA_RED]   = Npix.rgbRed;
            FPixP[FI_RGBA_ALPHA] = (BYTE)(newAlpha);
            FPixP += 4;

            if(x >= mask_w - 1 || endOfY)
                SPixP = Bpix;
            else
                SPixP += 4;
        }

        if(ym == 0)
        {
            endOfY = TRUE;
            SPixP = Bpix;
        }
        else
            ym--;

        if(y == 0)
            break;
        y--;
    }
}

На заметку: Я использовал библиотеку FreeImage (а конкретно FreeImageLite, мой усечённый форк) для загрузки и предварительной обработки графики у себя.

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

Саму отрисовку я делаю с использованием SDL_Renderer, сделав конечный результат проще и гибче. SDL2 поддерживает множество интерфейсов графических ускорителей: и OpenGL, и DirectX, и Metal. Также сохраняется поддержка программной отрисовки в случае, если невозможно задействовать один из аппаратных методов отрисовки.

Звук и музыка

Игра использовала для воспроизведения музыки и звука старинный интерфейс MCI, существующий ещё со времён Windows 3.1.

Public Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" (ByVal lpstrCommand As String, ByVal lpstrReturnString As String, ByVal uReturnLength As Integer, ByVal hwndCallback As Integer) As Integer

Этот интерфейс позволял с помощью одной функции посылать текстовые команды системе для открытия аудио-файлов и управления ими: воспроизведение, приостановка, повтор, громкость, и т.п. Самый большой недостаток подобного интерфейса заключался в том, что требовалось доустанавливать в систему дополнительные пакеты кодеков. Из-за того, что игра открывает десятки аудиофайлов через MCI, во время работы игры, системный трей засорялся значками FFDshow и подобными (если не было отключено в настройках соответствующих кодеков). Также сам процесс подобной загрузки был чрезвычайно долгим. По факту, через интерфейс работали форматы MP3, WAV, WMA, а также криво-косо MIDI.

Системный лоток, засорённый значками кодека Lav
Системный лоток, засорённый значками кодека Lav

Я решил избавиться от такого недоразумения как MCI ещё 5 лет назад. Мною, на базе хакерского расширения LunaLua, было реализовано использование библиотеки SDL2_mixer (а затем форка SDL Mixer X), которая работала с музыкой и звуковыми эффектами намного лучше, а также совершенно не требовала установки каких либо внешних кодеков, поскольку все нужные библиотеки уже были включены в её состав. Вместе с этим была добавлена поддержка большого числа новых форматов: и OGG Vorbis, и FLAC, и огромного числа форматов трекерной музыки, и улучшенная поддерка MIDI. Также появилась возможность играть дублирующие звуковые эффекты параллельно.

Файловая система

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

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

Клавиатура, мышь, геймпады

Для работы с клавиатурой, в игре использовались функции WinAPI на базе Virtual Key. Из-за этого возникали проблемы при переключении раскладок, а также возникали трудности на клавиатурах QWERTZ и AZERTY, популярных в Германии и Франции. Чтобы решить все эти проблемы, я применил возможности библиотеки SDL2, переориентировав игровое управление на сканкоды. Они привязаны к физическим клавишам и абсолютно не зависимы от текущей раскладки.

Для поддержки геймпадов в игре использовались возможности библиотеки WinMM:

Public Declare Function joyGetPosEx Lib "winmm.dll" (ByVal uJoyID As Integer, pji As JOYINFOEX) As Integer
Public Declare Function joyGetDevCapsA Lib "winmm.dll" (ByVal uJoyID As Integer, pjc As JOYCAPS, ByVal cjc As Integer) As Integer
Public Type JOYCAPS
    wMid As Long
    wPid As Long
    szPname As String * 32
    wXmin As Long
    wXmax As Long
    wYmin As Long
    wYmax As Long
    wZmin As Long
    wZmax As Long
    wNumButtons As Long
    wPeriodMin As Long
    wPeriodMax As Long
    wRmin As Long
    wRmax As Long
    wUmin As Long
    wUmax As Long
    wVmin As Long
    wVmax As Long
    wCaps As Long
    wMaxAxes As Long
    wNumAxes As Long
    wMaxButtons As Long
    szRegKey As String * 32
    szOEMVxD As String * 260
End Type
Public Type JOYINFOEX
    dwSize As Long
    dwFlags As Long
    dwXpos As Long
    dwYpos As Long
    dwZpos As Long
    dwRpos As Long
    dwUpos As Long
    dwVpos As Long
    dwButtons As Long
    dwButtonNumber As Long
    dwPOV As Long
    dwReserved1 As Long
    dwReserved2 As Long
End Type
Public JoyNum As Long
Public MYJOYEX As JOYINFOEX
Public MYJOYCAPS As JOYCAPS
Public CenterX(0 To 7) As Long
Public CenterY(0 To 7) As Long
Public JoyButtons(-15 To 15) As Boolean
Public CurrentJoyX As Long
Public CurrentJoyY As Long
Public CurrentJoyPOV As Long

К счастью, в библиотеке SDL2 есть значительно более мощные подсистемы SDL Joystick и SDL GameController, которые позволяют использовать тысячи различных моделей геймпадов через всевозможные интерфейсы, работает и на Linux, и на macOS, и на Android. Также поддерживается горячее подключение и отключение игровых контроллеров прямо во время игры.

Что касается мыши, использовались стандартные события главной формы Form_MouseDown, Form_MouseMove и Form_MouseUp с последующей записью состояния кнопок и координат указателя в глобальные переменные, используемые в коде самой игры. В обновлённой игре, я делаю аналогичное, но через SDL_PollEvent().

Итог

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

Проект движка я назвал TheXTech по принципу: "The Super Mario Bros. X Tech".

Главное меню игры Super Mario Bros. X на базе TheXTech 1.3.5.2
Главное меню игры Super Mario Bros. X на базе TheXTech 1.3.5.2

В итоге, игра, изначально созданная жёстко под Windows на платформозависимом языке и на процессорах x86, превратилась в кроссплатформенную игру, которая работает не только на других операционных системах (Windows, Linux, macOS, Haiku, Android, Emscripten), но и на других процессорах (ARM, PowerPC, MIPS, и др.). Вместе с этим, игра наконец получила полноценную поддержку 64-битных процессоров и ARM-архитектуры. Энтузиасты также портируют игру на консоли (уже есть примеры, реализованные на 3DS и PS Vita).

В новом движке игры я не стал реализовывать встроенный редактор, и не стал реализовывать начальную форму для запуска игры или редактора, где показывалась веб-страница с новостями проекта (ныне просто сайт Nintendo), а также, была возможность отключить звук и пропуск кадров. Вместо этого, я решил реализовать интерфейс для настройки игры через аргументы командной строки, а также выделить конфигурационный INI-файл, который легко отредактировать, чтобы настроить игру под себя. А для создания новых уровней и эпизодов я решил рекомендовать девкит из набора Moondust Project, редактор которого я доработал, чтобы обеспечить его тесной интеграцией с игрой (поддержка специфичных полей, прямой запуск теста уровня, и т.п.).

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

Материалы

Исходный код оригинальной игры как есть

Мой полигон, мод оригинала

Репозиторий с кодом созданной игры

Комментарии (70)


  1. Izaron
    09.10.2021 23:38
    +7

    Круто! Про особенности переноса с 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;
    }


    1. Wohlstand Автор
      10.10.2021 00:41
      +2

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

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

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

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


  1. sedyh
    10.10.2021 01:02
    +1

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


  1. da-nie
    10.10.2021 08:02

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


    1. SnakeSolid
      10.10.2021 10:00
      +1

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

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


      1. da-nie
        10.10.2021 10:22

        Спасибо.

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


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

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


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

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


        1. SnakeSolid
          10.10.2021 11:39

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


    1. Wohlstand Автор
      10.10.2021 11:50
      +4

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

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

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

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

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

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

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

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

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

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

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

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

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

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


      1. da-nie
        10.10.2021 12:18

        Спасибо за информацию!


  1. ewgeniy2004
    10.10.2021 09:25

    Скажите вы пробовали проект скормит в Gambas?


    1. ZhilkinSerg
      10.10.2021 11:11

      Так проект же на C++.


      1. Wohlstand Автор
        10.10.2021 11:18

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


    1. Wohlstand Автор
      10.10.2021 12:09

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


  1. TakashiNord
    10.10.2021 11:07
    +2

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

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


    1. Wohlstand Автор
      10.10.2021 15:59
      +2

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

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


  1. forthuser
    10.10.2021 11:12

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


    1. Wohlstand Автор
      10.10.2021 11:17
      +1

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


  1. Gromushka
    10.10.2021 16:18

    Сколько у людей свободного времени...


    1. Chuvi
      10.10.2021 17:09
      +4

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


    1. perfect_genius
      01.11.2021 20:40

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


  1. lunacyrcus
    10.10.2021 18:19
    +3

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

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

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

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


    1. Wohlstand Автор
      10.10.2021 19:45
      +1

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

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

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


      1. firehacker
        11.10.2021 16:11
        +5

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

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


        Всё совершенно не так. В 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» (чекбокс), которую можно снять, и в этом случае проект будет всегда компилироваться полностью ?


        1. Wohlstand Автор
          11.10.2021 16:41

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


          1. firehacker
            14.10.2021 01:16

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


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


        1. perfect_genius
          31.10.2021 14:31

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


          1. firehacker
            31.10.2021 14:48

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


            1. perfect_genius
              01.11.2021 13:13

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


              1. firehacker
                01.11.2021 15:51

                Во-первых, у 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-ок.


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


      1. firehacker
        14.10.2021 01:14
        +1

        (Продолжение вот этого (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.


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


    1. Wohlstand Автор
      10.10.2021 19:52
      +1

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

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


  1. gus26
    10.10.2021 23:21

    А где бинарник игры можно скачать для винды? Или, надо самому собирать из сорцов?


    1. Wohlstand Автор
      11.10.2021 00:02

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


  1. napa3um
    11.10.2021 17:27

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


    1. Wohlstand Автор
      11.10.2021 17:31

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


      1. napa3um
        11.10.2021 18:20

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


        1. Wohlstand Автор
          11.10.2021 19:02

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

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

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

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


        1. Wohlstand Автор
          11.10.2021 19:05

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


          1. napa3um
            11.10.2021 19:23

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


            1. Wohlstand Автор
              11.10.2021 19:50

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

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

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

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

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

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

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


  1. Ingulf
    12.10.2021 08:17

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

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


    1. Wohlstand Автор
      12.10.2021 16:25

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

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


  1. Oxyd
    13.10.2021 22:40
    +1

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

    $ yay -Sua --batchinstall thextech-supermariobrosx


    1. Wohlstand Автор
      13.10.2021 23:09
      +1

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


      1. Oxyd
        14.10.2021 07:37

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


  1. firehacker
    14.10.2021 06:45
    +2

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


    Судя по всему, не только автор портируемой программы был новичком в 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 байта на символ, длина хранится перед строкой.


    1. forthuser
      14.10.2021 07:57

      1. firehacker
        14.10.2021 09:48
        +1

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

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


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


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


        1. forthuser
          15.10.2021 06:26

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

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

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

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

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

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

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

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


    1. Wohlstand Автор
      14.10.2021 13:04

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

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

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

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

      Так и есть, с 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 всё же не идеален, поскольку у него присутствует такое понятие как суррогатные пары, которые позволяют закодировать один символ двумя единицами.


  1. da-nie
    16.10.2021 15:49

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


    1. Wohlstand Автор
      16.10.2021 15:58

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

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

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

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

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


      1. Wohlstand Автор
        16.10.2021 16:00

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


      1. da-nie
        16.10.2021 16:36

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


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

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


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


        1. Wohlstand Автор
          16.10.2021 16:39

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

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

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

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


          1. da-nie
            16.10.2021 16:46

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


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

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


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

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


            1. Wohlstand Автор
              16.10.2021 17:01

              WinAPI

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

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

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


              1. da-nie
                16.10.2021 17:12

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


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

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


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

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


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


                1. Wohlstand Автор
                  16.10.2021 17:32

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

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

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

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


                  1. da-nie
                    16.10.2021 17:40
                    +1

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

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


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


                    1. Wohlstand Автор
                      16.10.2021 17:49

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

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

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

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


                      1. da-nie
                        16.10.2021 19:47

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


                1. firehacker
                  16.10.2021 18:40
                  +2

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

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


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


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


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


                  1. da-nie
                    16.10.2021 19:55

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


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

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


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

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


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

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


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

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


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


                    1. Wohlstand Автор
                      16.10.2021 20:42

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

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


                      1. da-nie
                        16.10.2021 20:46

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


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


      1. perfect_genius
        04.11.2021 11:52

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


        1. Wohlstand Автор
          04.11.2021 12:46

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


  1. Andrey2008
    24.11.2021 21:23

    1. Wohlstand Автор
      24.11.2021 21:25
      +1

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