Пользователь
Информация
- В рейтинге
- 4 951-й
- Откуда
- Петропавловск, Северо-Казахстанская обл., Казахстан
- Зарегистрирован
- Активность
Специализация
Software Developer, Embedded Software Engineer
Pure C
Assembler
X86 asm
Win32 API
Visual Basic
MySQL
Git
OOP
Electronics Development
Reverse development
Во-первых, у 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-ок.
В общем-то, я не против, но это отдельная работа.
А этот длиннокоммент я просто написал на одном дыхании, даже не перечитывая его. Там и опечатки, и какие-нибудь дублирование одного и того же могут встречаться, и какие-нибудь логические ошибки.
Не планировал писать статью, собирался написать рядовой комментарий.
Получилось то, что получилось.
Зачем вам одинаковость времени между тиками для обеспечения плавности и безрывковости анимации?
Замеряйте на каждом игровом тике DeltaTime от предыдущего игрового тика и умножайте DeltaTime на Speed, чтобы вычислить перемещение объекта на экране.
Для анимации путём смены кадра откажитесь от идеи показывать новый кадр каждый игровой тик (для чего может понадобиться чётко вывенное время между тиками). Меняйте кадр исходя из того, какой кадр сейчас стоит показывать исходя из времени от начала показа анимационной последовательности. Ещё более плавную картинку может дать альфа-блендинг между двумя фреймами анимации и показ интерполированной версии анимационного времени исходя из точного значения времени анимации.
Используйте мультимедийный таймер Windows вместо GetTickCount — он наиболее точный. А если используете GetTickCount, озаботьтесь, чтобы маска афинности потока к ядрам процесса была такой, чтобы поток не попадал разным ядрам. А то иногда отрицательный DeltaTime получается между тиками со всеми вытекающими.
Что это означает или какой вывод из этого должен быть сделан?
Я просто впервые слышу про Rosetta Code и не знаю, что количество решённых задач должно означать.
Картинка ваша не грузится, кстати.
Теперь что касается самой статьи.
Судя по всему, не только автор портируемой программы был новичком в VB, раз не знал даже о конструкции
Select Case
, но и вы его очень поверхностно знаете и может быть второй раз в жизни видите.При этом вы делаете много спорных, дискредитирующих VB или просто неверных утверждений. Понятно, что по сравнению с каким-нибудь новомодным Python-ом, упоминания которого лезут из каждой дырки — из ваканский, из job-offer-ов, из бесконечной рекламы курсов по Python-у, на 2021 год язык VB можно назвать «мёртвым», а мёртвые сраму не имут, как гласит известная поговорка. Но мы на техническом ресурсе, а вы пишите статью, а не простой комментарий, и делать неточные и неверные заявления непозволительно в статье, не делая хотя бы пометку, что язык, с которого вы портировали, вы знаете плохо и мало. Или вы считаете, что вряд ли кто-то в 2021 году посмотрит в сторону этого продукта, и не играет роли, в каком свете вы его выставите? Я и 20 лет видел такие нападки на VB: благодаря им инстумент приобрёл репутацию недоязыка для зелёных программистов и несерьёзных проектов, при этом в большинстве холиваров большинство доводов против было просто мифами или следствием чьего-то незнания. Но с одной стороны холивара на защите VB стояли действительно зелёные новички, которым нечего было противопоставить оппонента в силу своей малообразованности — они и VB-то сами знали едва-едва, не говоря уже о полном отсутствии знаний других языков, низкоуровневого понимания работы всех этих вещей. С другой стороны были зачастую технически грамотные и опытные люди, но грамотные во всём чём угодно, кроме VB, о котором они могли судить и заявлять только по где-то услышанным чужим заявлениям, зачастую совершенно неверным.
Начнём с того, что название «Visual Basic» официально и общепринято пишется через пробел, у вас оно везде написано слитно и даже в ключевых словах/тегах статьи, что, очевидно, влияет на возможность находить эту статью поискам по тегов (ваша статья — единственная с тегом «VisualBasic», остальные статьи на эту тему на сайте идут с тегом «Visual Basic»).
Теперь непосредственно по поводу изложенных в статье мыслей.
Очень странное заявление! Примерно как заявление, что «все люди на Земле способны рожать детей, исключение составляют лишь мужчины».
Вообще-то все функции, процедуры, переменные и константы (то, о чём вы пишите) должны, в соответствии с правилами хорошего тона, быть объявлены либо с ключевым словом Public, либо с ключевым словом Private. Как можно назвать ключевое слово «Private» всего лишь каким-то там исключением, если это, блин, одна из двух доступных опций, и сущность будет соответственно, либо видимой из других модулей, либо невидимой? Вообще-то, кстати, не из двух, потому что есть ещё ключевое слово «Friend» и тогда сущность объектного модуля будет видна из своего проекта, но не видна из чужих проектов.
Из соображений совместимости с кодом, переносимым из QBasic, где никаких ключевых слов Private и Public не было. Там переменные уровня модуля объявлялись так же, как локальные переменные в процедурах: с помощью ключевого слова Dim, но могли быть объявлены глобальные переменные уровня модуля — с помощью ключевого слова Global. То же самое касается объявления процедур без указания ключевых слов Private/Public/Friend. Так вот из соображений переносимости кода, возможность объявлять таким образом переменные и процедуры была оставлена в VB.
Это не значит, что ей нужно пользоваться. Но, тем не менее, переменные уровня модуля, объявленные с ключевым словом Global, как ни странно, видны из других модулей. А переменные уровня модуля, объявленные через ключевое слово Dim, вопреки вашим словам, не видны из других модулей. О каком единственном лишь исключении с ключевым словом Private вы говорите?
Отбросим Friend, применимое только для объектных модулей, поговорим об обычных модулях.
Существует 4 способа объявить переменную уровня модуля в модуле:
И ровно ровно половина даёт видимую извне модуля переменную, половина даёт невидимую.
Что касается процедур, то да, без указания Private/Public процедура будет по умолчанию видна из других модулей.
Из ваших слов просто складывается впечатление, что в VB совершенно нет концепции ограничения видимости и доступа к переменным и функциям, и только какое-то жалкое ключевое слово «Private» дали для сокрытия чего-то там. Между тем, есть Public/Friend/Private, и их очень желательно использовать во всех случаях; а у переменных, объявленных без указания видимости, зона видимости ограничена модулем — они не видны из других модулей.
Напротив, это про C/C++ можно сказать, что все переменные и функции, объявленные в «модуле» являются видимыми из всех остальных модулей, и лишь исключение в виде storage-class'а «
static
» у переменных и функций делает так, что в объектном модуле на выходе компилятора в таблцие символов сущность не будет присутствовать в виде «экспортируемой» наружу сущности, и значит из других модулей с переменной или с функцией не получится слинковаться, потому что линкер не найдёт соответствующую сущность. «private», «public», «protected» в C++ появился только для членов классов и типов, для просто «глобальных» переменных и обычных функций никакого инструментария для указания видимости нет (кроме «static»).И не надо говорить, что сущность может считаться условно приватной, если в другом файле она не объявлена в заголовочном файле. Не говоря уже о том, что сами по себе заголовочные файлы — чистая условность — всё что в них содержится может быть вставлено и в сам top-level файл-исходник, публичность/приватность сущности должна предопределяться из того модуля, где она находится, а не из того места, где ей кому-то внезапно захотелось попользоваться. В Си (не Си++), я напомню, можно обратиться к функции, находящейся в другом модуле, вообще не упоминая в данном модуле (напрямую или через включение заголовочного файла) её прототип — за исключением не-cdecl-функций.
Почему это стало проблемой? Да, VB позволяет.
Но и Си позволяет, потому что у struct-ов и union-ов одно пространство имён, а у переменных — другое, и они друг другу не мешают:
По какой-то причине не устраивает писать
struct Controls
в обозначении типа (я не представляю ни одной такой причины, кроме использования для портирования слепой автозамены, не учитывающей контекст, которая не сможет понять, гдеControls
надо заменить наControls
, а где наstruct Controls
)?В С++ это тоже работает, но в отличие от Си, можно даже не писать
struct
в обозначении типа:Так работает:
А некоторые компиляторы позволяют даже так:
Пытался придумать, где может возникнуть проблем, на ум приходит только то, что выражение
sizeof(Controls)
может быть неоднозначным, если где-то в зоне видимости есть переменная Controls с типом, отличным отstruct Controls
, но в случае портирования такой ситуации возникнуть не может, потому что VB-шный Len() и LenB() не может быть применён к идентификатору структуры.Почему «как ни странно»?
Как ни странно, в VB аргументы могут передаваться хоть по значению, хоть по ссылке — в зависимости от того, как программисту нужно:
И хорошим тоном является указывать способ передаче аргумента явно.
Нет, особенность состоит в не в том, что VB6 по другому обрабабывает логические выражения, а в том, что в VB операторы And, Or, Not, Xor и другие являются побитовыми, и им соответствуют не
&&
,||
,!
и!=
, а&
,|
,~
и^
. И тогда всё встаёт на свои места: правый операнд тоже вычисляется всегда, даже тогда, когда из результата вычисления левого операнда ясен результат всего выражения.Зато разница состоит в другом: в том, что в C++
true
это не полная противоположностьfalse
, а единичка. Единичка отличается от нуля не всеми битами, а только младшим битом. Поэтому использование булевых значений вперемешку с числовыми с использованием побитовых операций может иметь непредвиденный эффект:А использовать при портировании с VB на C++ нужно именно побитовые операторы, потому что не ясно, использовались ли они в VB-шном коде для осуществления логических операций или же для каких-то побитовых манипуляций.
Но это лечится оборачиванием в cpp-исходнике всех «логических выражений» (типа сравнений) функцией, которая true превращает в
~0
. Этой же функцией следует заменить VB-шную псевдофункцию CBool (не знаю, была ли она в оригинальном коде).Не понятно, почему ситуация с
(num > 0) && (array[num - 1])
названа болью логических выражений. Это при обратном портировании с С++ на VB создавало бы проблемы при механистическом переводе, а при портировании с VB на C++ это не должно давать никаких проблем.Для меня секрет, что такое полноценное понятие классов. Вам так хотелось очернить 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 это абсолютно беспроблемная ситуация:Предлагаю подумать, как это будет выглядеть на C++? И ведь, что самое интересное, на С++ проблема решится пародоксально легко, если в одном из интерфейсов переименовать Reset в ResetThisObject. Спрашивается: почему проектанты разных интерфейсов, которые могут не знать друг друга, и которым не обязательно быть знакомым с тем, кто собирается имплементировать интерфейс, должен договариваться друг с другом с той целью, чтобы не возникло конфликта имён? Но корень проблемы концептуальный: что наследование и поддержка интерфейса — это два принципиально разных явления, и попытка сэмулировать концепцию поддержки интерфейсов через наследование абстрактного класса это дырявая абстракция.
В VB у классов есть концепция членов по умолчанию: это могут быть и свойства и методы. В C++ это кое-как можно получить только путём перегрузки операторов. Назначение какого-нибудь метода как члена-по-умолчанию позволяет в VB поиметь класс, экземпляры которого будут «прикидываться» функциеями. Ссылки на экземпляры таких классов внезапно становятся эквивалентными указателям на функции в C/C++, только это типо- и значение- безопасные указатели. Наличие возможности делать параметрические свойства сама по себе интересная, но одновременно с наличием возможности делать какое-то свойство свойством по умолчанию позволяет делать объекты, прикидывающиеся массивами или контейнерами любого толка. Легко делаются объекты, ведущие себя как PHP-массивы (а там это key-value словари по своей сути, причём key это число или словарь, а value — что угодно). Наличие синтаксиса
foo!bar
позволяет сделать класс, экземпляры которого будут вести себя как объекты в безклассовом JS (когда-то JS был таким): такие объекты можно будет во время исполнения наделять любыми нужными свойствами. Более того, вкупе с классами, которые могут обёртывать функции и претворяться функции, объекты можно будет наделять не только произвольными свойствами, но и произвольными методами.Список можно продолжать.
Это дезинформация. Начнём с того, что в 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 функций и работать с юникодом.
Никто, в конце-концов, не мешал сохранить в файл строку в юникоде, обернув её просто в байтовый массив:
В таком виде строка в файле будет сохранена в юникоде (
UTF-16UCS-2) без BOM-а.По крайней мере, если сишные strlen(), strstr() и substr() уж точно не поддерживают юникод, то VB-шные Len(), InStr() и Mid$() полноценно юникодные.
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 байта на символ, длина хранится перед строкой.
Вот вторая часть того, что я первоначально хотел сюда написать одним постом:
https://habr.com/ru/post/582566/#comment_23589108
Пришлось вырезать этот кусок из-за лимита на длину коммента, а затем текст и вовсе был утрачен и восстановлен склеиванием из кусков, оставшихся на правах мусора в файле подкачки.
(Продолжение вот этого (https://habr.com/ru/post/582566/#comment_23578554) комментария. Из-за лимита в 55 тысяч символов на длину комментария, пришлось вынуть большой кусок из первого поста, чтобы у первого поста мог бы быть шанс на логический и складный финал — раз вырезанный кусок решено было оформить вторым комментом, решил расписать его несколько подробнее и полнее, но затем произошла череда неудач, и пост пришлось по кусочкам восстанавливать из крупиц, которые сохранились лишь в файле подкачки. Поэтому публикую с запозданием)
Отдельно хочется подчеркнуть, что BSCR-форма представления кода (из которой при компиляции проекта генерируется P-код процедур) с её уклоном в сторону использования обратной польской нотации очень удобна, чтобы с малыми тредозатратами сгенерировать по BSCR-представлениюю выражений P-код (байт-код виртуальной машины), который бы вычислял/выполнял эти вражения.
Лет 15 назад tyomitch написал цикл из 4 статей, посвящённых проблемам кодогенерации, которые мне тогда очень понравились.
К сожалению, в этих статьях половину значимости играли картинки с диаграммами и схемами: его личный сервер с этими картинками давно мёртв и вряд ли он когда-нибудь озаботится воссозданием этих картинок.
Одна из основных идей той серии статей: конвертация выражения из общеприятной у людей формы записи (инфиксная запись) в обратную польскую нотацию сама по себе составляет чуть ли не половину работу по генерации кода для стековой вычислительной машины — элементам ОПН могут быть прозрачным образом сопоставлены инструкции стековой машины. А я напоминаю, что эта конвертация выполняется даже не при попытки скомпилировать/запустить проект, а уже на этапе ввода каждой новой строчки кода в IDE (когда текстовое представление строки кода конвертируется в BSCR-представление).
Возьмём для примера вот такую строку кода на VB:
(Я специально взял одну строчку, а не целую процедуру для упрощения рассмотрения, чтобы оставить «за скобками» рассмотрение генерации пролога/эпилога процедуры.)
Уже непосредственно в момент ввода этой строчки в редактор кода она сначала будет преобразована из текста в древовидное представление (PCR):
Поскольку я ничего особо не писал о PCR-дерева и о типах PCR-узлов — пост и так огромный получился — вот небольшая шпаргалка по этому PCR-дереву:
Квадратные скобки здесь олицетворяются узлы PCR-дерева, у слеши, соединяющие узлы, — рёбра (связи) между узлами. Шестнадцатеричное число внутри квадратных скобок означают тип PCR-узла. Как и у BSCR-сущностей, у PCR-узлов бывают подтипы, но здесь это не показано. У узлов также есть флаговое поле, и значение флагового поля здесь не показано, чтобы не загромождать схему.
Let <assignee> = <assignment>
. Во многих ЯП есть разделение на присвоение и сравнение (=
vs.==
в C/C++,:=
vs.=
в Паскале) — изначально в бейсиках для этого разделения использовалась конструкция с ключевым словомLet
, но в целом VB умеет отличать присвоение от сравнения по контексту. О том, что это обычное присвоение, и ключевое словоLet
опущено, говорит значение флагового поля PCR-узла, равное0x0004
. Если бы ключевое слово не было опущено, этого бита во флаговом поле бы не было. В конструкцияхLSet ... = ...
,RSet ... = ...
,Set ... = ...
флаговое поля принимало бы другие значения. У узла типа 2Ch обязательно есть два дочерних: левый соответствует PCR-поддереву, описывающему то, чему присваивается значение (в простейшем случае это какая-то переменная, то есть упоминание её идентификатора), а правый — описанию выражения, которое присваивается. Левая ножка этого PCR-узла может ссылаться только на ограниченное пожмножество возможных PCR-деревьев (например, там не может быть PCR-дерево, описывающее выражение типаfoo+bar
)По PCR-представлению можно было бы написать отдельную статью, потому ограничимся лишь этими отрывочными сведениям: описывать все типы узлов и особенности построения деревьев (особенно когда у узла не 2, а 5 ног) не хватит места.
Парсер VB-кода устроен таким образом, что он строит подобное дерево за один проход по цепочке символов входной строки кода, на лету токенизируя её и строя дерево. Нет никакой ни рекурсии, ни цикла с несколькими прогонами. Чуть-чуть токенизировали — достроили дерево, ещё чуть вперёд токенизировали — ещё достроили. То есть парсер однопроходный, но за раз он обрабатывает только одну строку, которая, впрочем, с одной стороны может иметь несколько statement-о в своём составе (можно хоть весь модуль в одну длиннющую строку уместить), а с другой стороны одна логическая строка может быть представлена несколькими физическими строчкам, если использовался символ переноса строки.
Вторым этапом по запомненным местам переноса строки (если таковые были), выделенной метке, PCR-дереву (которое может описывать один statement или несколько statement-ов), комментарию строится BSCR-представление всей строки. В случае, если строка была изначально синтаксически некорректная, строится особое BSCR-представление некорректной строки (такие подсвечиваются красным в редакторе кода).
В нашем случае строка корректна, меток, комментариев и переноса строк нет. Поэтому BSCR строится только на базе PCR-дерева. Для вышепоказанного исходного кода и вышепоказанного PCR-дерева строится вот такое BSCR-представление:
Вот прямо в виде такой непрерывной цепочки 16-битных сущностей будет представлена вышеприведённая строка кода
Напомню, что каждый блок в квадратных скобках означает 16-битное число, а записи [#a] или [#foo] означают 16-битное число, содержащее индекс NAMEREF-структуры, описывающей идентификаторы «a» и «foo» в проектно-глобальном словаре идентификатор.
Для удобства ту же самую цепочку BSCR-сущностей я запишу не в виде строчки, а в столбик, оставляя при этом параметры на одной строке с заголовком BSCR-сущности, и каждой строке добавив пояснение:
Уже в таком виде прослеживается, что последовательность BSCR-сущностей практически один-к-одному соответствует гипотетической последовательности команд для стековой вычислительной машины, которая осуществляла бы обозначенное в коде действие.
И в таком полуготовом виде хранятся все строки VB-кода. Что по сути остаётся сделать компилятору при преобразовании BSCR-представления в P-код для виртуальной машины?
Я сейчас, конечно, буду очень сильно упрощать, опуская отвлекающие детали.
Компилятору остаётся разрешить (resolve) обращения к идентификаторам, разобравшись, что за ними стоит — возможно это переменная, а может быть константа, а может быть это свойство или функция, не принимающая аргументов.
Наличие в словаре идентификаторов какого-то имени вовсе не означает, что в зоне видимости, к которой относится компилируемая строка кода, упомянутый идентификатор что-то вообще означает: возможно переменная с таким именем есть, о она находится в другом модуле и вообще приватная и мы её не видим.
Разобравшись со всеми идентификаторами и определившись, на что именно каждый из них ссылается, убедившись, что у нас есть доступ и видимость в каждом конкретном случае, и что не возникает неоднозначности (или мы знаем, как разрешить неоднозрачность), компилятору предстоит предстоит разобраться с типами выражений на каждом шаге вычисления, убедиться в совместимости типов, в возможности произвести неявные преобразования типов там, где это требуется, сгенерировать эти действия по нужным преобразованиям.
Для простоты представим, что «a», «b», «c», «d» и «foo» являются локальными переменные и имеют тип Variant. Это избавит нас (мы сейчас олицетворяем себя с компилятором) от необходимости заботиться от неявных приведениях типов на этапе кодогенерации, но не избавит от проверки совместимости типов и приведении «к общему типу» виртуальную машину, которая будет, выполняя наш байт-код, манипулировать VARIANT-значениями, внутри которых могут храниться гетерогенные величины.
Некоторые инструкции P-кода, о которых следует знать, прежде чем мы двинемся дальше (напоминаю, что наша виртуальная машина обожает манипулировать данными на стеке, и не использует концепцию регистров):
Теперь я покажу вам, в какой P-код реально скомпилируется строка
foo = (a + 5) * (b - 4) Xor (c / d)
, написав инструкции P-кода напротив соответствующих BSCR-сущностей BSCR-представления этого кода:Отсюда видно, что скомпилированная в P-код строка кода (а P-код является конечной формой существования кода при работе VB-проекта в режиме отладки под IDE и при работе всех VBA-проектов) практически один в один соответствует её BSCR-представлению, а именно в виде BSCR-представление хранится код внутри IDE на от момента его попадания в IDE до момемента его сохранения и/или закрытия IDE.
Лишь BSCR-сущностям, которые обозначают взятие подвыражения в скобки, в P-коде не соответствует ничего. В инфиксной записи взятие в скобки подвыражений не играет никакой роли, кроме обозначения порядка вычисления подвыражений в выражении. По сути дела скобки переопределяют порядок вычисления выражения по сравнению с порядком его прочтения (слева направо) и приоритетом операторов. В обратной польской нотации эту роль уже выполняет сам по себе порядок записи элементов обратной польской записи: порядок прочтения автоматически соответствует правильному порядку вычисления. Поэтому в BSCR-представлении сущности, кодирующие взятие в скобки, нужны лишь для правильной реконструкции VB-кода в виде человеко-читаемого текста, а для последующей кодогенерации эти сущности не используются. Это не должно удивлять: BSCR-представление кода является многоцелевым — с одной стороны оно представляет собой подспорье для последующей кодогенерации в конечную форму существования кода (P-код виртуальной машины), с другой стороны оно, содержа инормацию о местах разлома строки на части и о комментариях, является компактной формой хранения исходного VB-кода, то есть позволяет из BSCR воссоздать первоначальный вид исходного текста для его отрисовки на экране одновременно с раскраской синтаксиса, копирования в буфер обмена, осуществления поиска по нему (Find / Replace), сохранения в файл.
Эта иллюстрация показывает, что VB-код, будучи загруженным или написанным в редакторе кода, хранится в памяти среды разработки в некотором промежуточном состоянии, из которого можно и реконструировать первоначальное состояние (движенеим влево из серединки), или получить готовый к выполнению код (движением вправо).
Здесь есть некоторое количество упрощение: если между стадией кода как текста (как цепочки букв) и стадией кода как PCR-дерево, и между стадией PCR-дерева и стадией BSCR-данных на самом деле нет никаких промежуточных состояний, то конечно же промежуточные стадии между BSCR-представлением кода и скомпилированным P-кодом просто не показаны здесь. Так же здесь полностью игнорируется, что VB4, VB5, VB6 умеет при создании EXE-файла компилировать проекты не только в P-код, но и Native-код (то есть машинный код x86), что является, вообще-то говоря, режимом по умолчанию.
Тут стоит сделать отступление и прояснить в общих чертах устройство компилятора C/C++ от компании Microsoft, известного как CL.EXE.
Этот компилятор поставляется и в комплекте Platform SDK или DDK/WDK для Windows, и входит в состав Microsoft Visual C++. Благодаря второму факту многие люди часто называют этот компилятор компилятором Visual C++, хотя непонятно, почему эта чисто-консольная утилита без какого-либо GUI должна иметь в своём названии слово Visual, ошибочно приписываемое ей от названия среды разработки, вместе с которой она поставляется, но которая не является составной частью эксклюзивно именно этой среды разработки как продукта.
Компилятор CL.EXE состоит из двух половинок:
При создании Standalone VB (то есть VB как самостоятельного продукта, в противовес VBA, которое можно привязать к любому, например к программам из комплекта Office), который должен был уметь генерировать EXE-файлы, содержащие Native-код, Microsoft позаимствовали бэкенд C2 — если вместе с компилятором CL он шёл как DLL-модуль, то в комплекте с VB (например VB6) он стал поставляться уже как EXE-файл:
При этом концепция с фронтендом и бэкендом сохранена: роль фронтенда берёт на себя среда разработки VB. Она не генерирует Native-код (машинный код x86, главным образом) непосредственно сама: она передаёт промежуточный результат своей работы в бэкенд C2 путём сохранения IL-данных (intermediate language) в файлы, которые скармливаются C2, а уже C2 генерирует типичные объектные файлы COFF (.obj-файлы), которые поступают затем на вход линкеру.
Вдумчивый читатель спросит: являются ли 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-данными.
Когда программист введёт в редакторе кода новую строчку, для примера — такую:
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.
Какой ещё инструмент разработки
интерпретирует код по мере его выполненияустроен таким чудесным образом?Всё потому что кто-то не знает сам, но охотно плодит и тиражирует популярные мифы, которые незаслуженно бросали и бросают тень на отличный, в общем-то, продукт.
Всё совершенно не так. В 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-дерево не является конечной формой представления строки после распарсивания: если первый этап (главным образом это построение 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-сущность — это 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-сущность имеет следующий формат:
Младшие 10 бит 16-битной сущности определяют тип сущности, от чего зависит интерпретация сущности и следующих по соседству с ней данных. Старшие 6 бит определяют подтип сущности или некие дополнительные флаги. У большинства BSCR-сущностей никаких подтипов нет, и это поле (старшие 6 бит) содержат нули и ни на что не влияют.
Для наглядности я теперь буду использовать форму
[xxxxx]
для обозначения BSCR-сущности, у которых нет подтипов или флагов, и форму[xxxxx/yy]
для BSCR-сущности типа xxxxx с флагом yy.Тогда для конструкций
Option ...
предусмотрены следующие способы BSCR-представления:Как можно видеть отсюда, 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.
Так, например, вот такая строка кода:
в BSCR-представлении будет кодироваться вот так:
Или, если не использовать наше соглашение о записи BSCR-сущностей, а использовать простой hex-дамп:
Можете вставить вышеприведённую строчку кода в модуль 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-сущность, соответствующая самой конструкции.
Начнём с простых примеров.
Три разных конструкции, существующие в синтаксисе VB для создания циклов, позволяющие задать условие (в виде выражения, которое будет вычисляться и проверять на каждой итерации), по которому осуществляется прекращение/продолжение работы цикла.
В BSCR-формате они кодируются сущностями [0x62], [0x61] и [0xF4] соответственно. Эти BSCR-сущности сами по себе не имеют параметра (который шёл бы после самой сущности), но вот конструкции, кодируемые этими сущностями, зависят от выражения, и в BSCR-кодировании конструкции целиком BSCR-кодирование выражения будет предшествовать BSCR-сущности, кодирующей тип конструкции.
То есть в BSCR-представлении эти три конструкции будут выглядеть так:
Как же кодируется BSCR-представление выражений в данном? Во-первых, любые выражения (а не только в контексте условия цикла Do/While) кодируются единым образом, так что в только что заданном вопросе можно смело убрать словосочетание «в данном случае». Во-вторых, давайте поговорим о концепции выражений.
Что такое выражения и из чего они могут состоять? В простейшем случае выражение является атомарным и состоит из одного лишь упоминания литерала (числовой, текстовой или булевой константы), либо упоминания идентификатора (имени переменной или константы, свойства или функции, не требующей аргументов). С использованием операторов, скобок, или обращений к функциям или параметрическим свойствам из атомарных выражений могут быть составлены составные выражения. Впрочем, с использованием операторов, скобок и обращений к функциям сложные выражения могут быть составлены и из других сложных выражений.
Начнём с примеров кодирования простых выражений. Для кодирования булевых литералов (логических констант True и False) в BSCR-предусмотрена сущность [0xB7/u], где u — 0 или 1, в зависимости от того, False или True мы кодируем.
Таким образом, строка кода, представляющая собой типичный пример бесконечного цикла, в BSCR-представлении будет выглядеть вот так:
Как я уже писал выше, сложные выражения могут быть образованы с других выражений (атомарных или составных) и операторов: унарные операторы образуют операции с участием одного операнда, бинарные — с использованием двух операндов, операндами при этом являются другие выражения (атомарные или составные).
Каждый оператор имеет свою BSCR-сущность, кодирующую его. При этом, в случае с операторами в BSCR-форме сначала следует BSCR-запись операнда или операндов, и лишь после него/них следует BSCR-сущность самого оператора.
Так, например, для оператора
Xor
используется BSCR-сущность [0x02], а для оператораOr
— [0x03].Таким образом, строка кода
под капотом будет закодирована не иначе как
Числой литерал, являющейся целочисленной константной, укладывающейся в диапазон типа Integer, кодируется в BSCR следующим образом:
где [W:val] — непосредственно само число в виде знакового двухбайтового значения.
Таким образом, строка
While 1 And 2
будет закодирована какЕсли выражение взять в скобки:
While (1 And 2)
, то это будетЕсли его немного усложнить:
BSCR-представление будет таким:
Не трудно догадаться, если таким образом можно закодировать сколь угодно сложное выражение, если знать BSCR-сущности для всех операторов (выше приведена табличка), для кодирования всех типов литералов, для кодирования обращения к идентификаторам и обращения к функциям или параметрическим свойствам.
Упоминание идентификатора (например переменной, константы, непараметрического свойства) в коде в BSCR-виде кодируется сущностью [0x20/fff], где fff — флаги, например 0x20 в случае, если упоминаемый идентификатор должен быть взят в квадратные скобки.
В VB предусмотрена возможность использовать идентификаторы, нарушающие собственные правила VB в отношении идентификаторов — для этого идентификатор берётся в квадратные скобки. Это жизненно необходимо при работе с объектами/интерфейсами/функциями, имплементированными на других ЯП с другими правилами в отношении идентификаторов, а также для работы с объектами, чьи имена могут содержать пробелы и другие непозволительные для идентификаторов символы.
Типичный пример: если в Excel мы имеем лист, названный «Summary», то в VBA-макросе мы можем написать
для удаления этого листа. Но если лист называется «Our $$$», то мы можем выкрутиться из ситуации вот так:
Что, впрочем, является просто альтернативой менее компактной формы записи
Однако если мы работаем с COM-объектом, реализованном, к примеру, на С++ и имеющим имена свойств, нормальные для С++, например, имеющим свойство «__hidden_prop», нарушающее правила VB в отношении идентификаторов, потому что в VB идентификатор не может начинаться на символ подчёркивания, то единственный способ работать с этим свойством — обрамить его упоминание в квадратные скобки:
После этой 16-битной сущности [0x20/fff] обязательно следует 16-битная сущность, являющаяся индексом идентификатора в глобальной коллекции идентификатором.
Идентификаторы, в отличие от строковых литералов или комментариев, не попадают в BSCR-представление напрямую. Вместо этого парсер кода, разбирая строку кода сразу же после её внесения в проект, когда он встречает нечто, что должно быть идентификатором, пытается найти идентификатор в коллекции идентификаторов, либо, если его там нет, добавляет в коллекцию идентификаторов новый идентификатор.
Всякий раз, когда в коде встречается упоминание идентификатора, в BSCR-представлении этого кода будет фигурировать сущность [0x20/f], после которой будет идти индекс идентификатора.
Такая архитектура является причиной того, что в VB в принципе нельзя в одной процедуре объявить переменную
foo
, а в другойFOO
. Написание идентификатора не может быть разным в разных процедурах, оно будет одинаковым в пределах всего проекта, потому что представление кода (BSCR-представление) обращается к идентификаторам по их индексам, а коллекция идентификаторов глобальна для всего проекта.Кроме того, менеджер базы данных идентификаторов не имеет механизма контроля за использованием идентификатора и сборки мусора, поэтому идентификаторы в коллекцию заносятся при первом же появлении где-либо в коде, но никогда не удаляются из коллекции, даже если в коде проекта не осталось ни одного упоминания. Следствием такого подхода является забавный баг, проявляющийся тем, что если маниакально переименовывать какую-нибудь переменную или константу, всякий раз меняя её имя на ранее не использованное, то число таких попыток не может превысить 32 тысячи раз — причина в исчерпании свободных индексов для идентификаторов, ведь обращение к идентификаторам из BSCR-представления кода осуществляется именно по их индексам. Разумеется, перезапуск IDE вызывает парсинг кода и заполнение коллекции идентификаторов с чистого листа, поэтому ограничение на количество попыток переименования идентификатора сбрасывается перезапуском IDE.
Так вот, с учётом того, что мы знаем, как осуществляется BSCR-кодирование упоминания идентификатора в коде, можно показать, как кодируются следующие конструкции:
(здесь 101, 102, 103 и 104 — случайно выбранные индексы идентификаторов
bSomeFlaaag
,fBaaz
,foo
иzulu
.Ещё одним автоматическим следствием такого подхода является возможность найти в памяти IDE этот словарь идентификаторов, поменять в нём идентификатор, из-за чего все упоминания идентификатора в коде поменяются разом — во всех модулях проекта. Одна правка в одном месте в памяти может изменить тысячи упоминаний какого-нибудь «популярного» идентификатора в коде.
Если с While/Do всё более менее понятно, стоит сказать, что больштинство подобных Control Structures (конструкций) используют похожую схему BSCR-кодирования — отличие только в коде типа BSCR-сущности.
Например, конструкция
If <condition> Then
кодируется как<expr_repr> [0x9C]
.Таким образом, вот такой код:
внутри VB IDE, под капотом среды разработки никогда не будет храниться в текстовом виде, в том, в каком его видит на экране программист, или в том, в каком код хранится будучи сохранённым в файл. Он будет храниться в виде BSCR-представления этих строк кода, а именно — вот так:
Или то же самое, но без использования компактной формы записи, а в виде хекс-дампа:
Тот же самый код можно уместить в одну длинную строку, используя символ разделения стейтментов (двоеточие):
И тогда она будет кодироваться в BSCR-представлении так:
Исходя из прочитанного, вы должны понимать, почему если в редактор кода VB IDE вставить вот такой код:
то среда автоматически исправит его на
Это не какая-то дополнительная логика по fancy-фикации кода, которую можно было бы закомментировать в исходниках самого VB и получить поведение, при котором среда не исправляла бы регистр, не удаляла бы ненужные пробелы, не привода бы по разному написанные идентификаторы к единому виду в плане регистра символов. Это не дополнительная фича. Это неизбежное следствие его архитектуры, и чтобы этого не было, нужно не отключить/закомментировать что-то в коде самой среды, а наоборот, нужно было бы написать очень много дополнительного кода.
VB IDE неизбежно удаляет лишние пробелы и приводит регистр символов ключевых слов к правильному, а регистр символов к единообразному ровно по той причине, что исходный код не хранится под капотом IDE как текст и как код, а хранится в интерпретированном (сразу же после загрузки кода или сразу же после ввода кода) виде, в бинарном виде — в виде BSCR, и в BSCR попросту не предусмотрено место под хранение числа избыточных пробелов и флагов исковерканности ключевых слов. BSCR компактен и не хранит лишней информации, а поскольку код, отображаемый в редакторе кода, всего лишь воссоздаётся по BSCR в момент отрисовки, он выглядит в fancy-фицированном/канонизированном виде.
Тем не менее, для некоторых конструкций в BSCR всё же предусмотрено хранение информации о числе пробелах, точнее об отступах:
As <typename>
) в объявлении переменных, констант и членов User-Defined-типов (структур), но не в объявлении аргументов и типа возврата процедур.Так что вот в таком коде избыточные пробелы (нужные для выравнивания и красивого оформления) убраны не будут — эти выравнивания запоминаются в BSCR:
Кстати говор, внимательный читатель должен задаться вопросом: если всем хорошо известно, что VB почти в любом месте кода разрешает воспользоваться символом переноса строки и вместо
написать
или даже
то как кодируется в 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 есть два режима запуска проекта:
VB IDE (точнее движок EB/VBA — подробнее об этих терминах читайте тут) компилирует проект по-процедурно. В случае использования простой опции «Start», VB практикует ленивый и экономный до ресурса подход on-demand компиляции процедур, иначе называемый JIT-компиляцией. Он не пытается скомпилировать процедуру, пока кто-нибудь не попытается вызвать эту процедуру.
Это позволяет не компилировать процедуры, которые никто никогда не вызовет, и очень значительно сокращает временнУю задержку на запуск проекта из под IDE. Проект может быть гигантским и иметь очень много кода, но запуск проекта будет происходить сверхбыстро — и не только на современных компьютерах, но и на очень ограниченных компьютерах образца 90-х годов.
Как только происходит попытка вызвать процедуру, которая ещё не скомпилирована, VB быстренько компилирует её и спокойно продолжает работу проекта совершенно незаметно для программиста. Однако, если процедура, до которой дошло дело, имеет ошибку, из-за которой компиляция процедуры вообще невозможна (например: обращение к нигде не объявленной переменной при задействованной директиве
Option Explicit
). В этом случае on-demand подход к компиляции процедур перестаёт быть заметным для пользователя: ошибка становится очевидной не в момент нажатия кнопки «Start», а в момент, когда процедуру попытались вызвать.Я серьёзно полагаю, что именно это наблюдение, что в режиме «Start» некоторые серьёзные ошибки в процедурах «всплывают» только в момент захода выполнения внутрь процедуры, стал основанием для наивных людей считать, что VB IDE интерпретирует VB-код непосредственно в момент выполнения процедуры. Тот факт, что в момент запуска проекта на исполнение VB не находит таких ошибок, а обнаруживает их в последний момент, когда программа уже частично поработала, вкупе с тем фактом, что в момент приостановки работы проекта («пауза») можно серьёзно правит код, заставляет людей делать догадку, что «руки» у среды доходят до кода только когда код исполняется, а при запуске VB IDE ни коим образом не анализирует код и уж точно не компилирует его (иначе ошибка отлавилась бы в момент запуска?)
Но это совершенно ошибочная позиция. Причина неотлова подобных ошибок на ранних стадиях — это использовани on-demand/JIT подхода к компиляции. Это фишка, фича, а не недостаток или баг. Если же вместо опции «Start» (F5) запускать проект опцией «Start With Full Compile» (Ctrl+F5), on-demand подход использоваться не будет. VB IDE попытается скомпилировать абсолютно все процедуры, какие только есть в проекте, и найдёт все ошибки компиляции, какие только имеются, и уж точно не даст проекту начать кое-как работать, если хоть в одном месте есть compile error.
Просто это дольше и не так эффективно.
В настройках IDE существует опция «Compile On Demand» (чекбокс), которую можно снять, и в этом случае проект будет всегда компилироваться полностью перед запуском, а значит и полностью проверяться.
Я не зря выше упомянул, что у каждой процедуры (но в более общем случае — у каждого scope-а) есть своя структура, содержащая dirty-флаг. Если какая-то процедура была единожды скомпилирована, но с тех пор в её исходный код не вносилось никаких правок, то в перерывах между запусками проекта результат компиляции кода этой процедуры не теряется и не выбрасывается. Процедура, которая не менялась между запусками проекта, при перезапуске проекта не перекомпилируется повторно.
Теперь о том, что представляет собой процесс компиляции.
Как и другие схожие платформы и решения, такие, например, как Java, дотнет, PHP и т.п., VB располагает своим собственным байт-кодом (со своей собственной системой команд) и своей собственной виртуальной машиной, которая выполняет этот байт-код. По сравнению с кодом на языке VB этот байт-код является низкоуровневым, но по сравнению с машинными командами архитектуры x86 (или любой другой, под которую скомпилирован VB) он является весьма высокоуровневым, потому что одна инструкция машинного кода VB может делать работу, эквивалентную тысячам инструкций процессора, десяткам системных вызовов.
В терминологии самого VB его собственный машинный код называется P-кодом.
VB IDE никогда в принципе не занимается ни какой интерпретацией, в том значении, в каком этот термин актуален для интерпретируемых языков, а абсолютно всегда компилирует процедуру из BSCR-представления и служебных структур в P-код, причём целиком, и только затем выполняет P-кодное воплощение процедуры на своей виртуальной машины.
Даже команды, которые пишутся в Immediate Pane — и те сперва компилируются в P-код, и только затем этот P-код отдаётся на выполнение.
Что представляет собой P-код и как устроен процесс выполнения P-кода виртуальной машиной? Ведь можно предположить, что за P-кодом может скрываться некий код на чуток менее высокоуровневом языке, чем VB, но код, существующий в виде текста (например, как ассемблерный листинг), а виртуальная машина идёт по строчкам-инструкциям этого низкоуровневого листинга, интерпретирует их и пытается выполнить. В таком ключе исполнение кода VB-проекта, пусть он и скомпилирован в P-код, можно было бы всё равно называть интерпретацией, коль скоро P-код представляет собой текст и его нужно парсить и интерпретировать? Увы, но, к счастью, подобное предположение не имеет ничего общего с реальностью.
P-код представляет собой никакой не текст, а бинарный код, во многом похожий на машинный код x86 или большинства аппаратныз процессорных архитектур. P-кодные инструкции — это короткие последовательности байтов, начинающиеся с байта, кодирующего сам тип инструкции — с опкода — вслед за которым идут закодированные параметры команды P-кода. Определение количества и смысла параметров команды (инструкции P-кодной машины) зависит от опкода и выполняется в зависимости от его значения.
Выполнение P-кода виртуальной машиной устроено очень простым и эффективным способом:
tblByteDisp
.То есть виртуальная машина читает первый байт первой инструкции скомпилированной процедуры — он является опкодом, а дальше по значению этого байта берёт адрес из соответствующей ячейки таблицы и передаёт управление туда. Таблица устроена так, что для каждой инструкции в ней содержится адрес кода виртуальной машины (машинного кода, исполняемого аппаратно, то есть процессором), который отвечает за исполнение P-кодной инструкции.
Этот код делает всю специфичную для конкретно взятой P-кодной инструкции работу — извлекает и декодирует параметры инструкции (если они имеются, конечно), сдвигает ESI (который в рамках виртуальной машины используется как аналог EIP, но для инструкций виртуальной машины), выполняет полезную работу, подчищает за собой (если требуется), после чего (к этому моменту) оказывается, что ESI указывает уже не следующую инструкцию. В конце-концов обработчик инструкции делает
Этот код определяет опкод следующей (на данный моменит уже легитимно говорить «текущей») инструкции и передаёт управление на её обработчик. Если в нашем P-коде у нас было две одинаковых инстуркции подряд, то обработчиком следующей инструкции окажется этот же самый обработчик.
Подобно тому, как в архитектуре x86 для всех форм кодирования всех существующих инструкций не хватило 256 возможных комбинаций однобайтового опкода, так и в случае P-кода количество всех форм кодирования всех существующих P-кодных команд не умещается в набор из 256 возможных значений. По этой причине для некоторых команд используются однобайтовые опкоды, но существуют также и команды с двухбайтовым опкодом. В x86 в своё время для этого был выделен псевдо-префикс расширения набора опкодов 0Fh. В P-коде двухбайтовые опкоды начинаются на FBh, FCh, FDh, FEh, FFh. По этой причине помимо таблицы tblByteDisp с 256 элементами-указателями существует ещё большая таблица tblDispatch, в которой 256+256+256+256+70=1094 ячейки — таблица состоит из 5 подтаблиц, каждая из которых отвечает за префиксы FBh, FCh, FDh, FEh, FFh.
P-кодная виртуальная машина VB — стековая. У неё нет как таковой концепции регистрового файла или регистров, как, например, у x86. Все манипуляции над значениями она предпочитает выполнять на стеке. В этом смысле она чем-то похоже на идеологию работу инструкций сопроцессора x87 — его инструкции оперируют своим собственным стеком плавающих чисел. Если обычная инструкция ADD архитектуры x86 принимает два операнда, при этом оба операнда содержат слагаемые, а результат помещается в регистр или ячейку памяти, обозначенную первым операндом, то в случае P-кодной системы команд тоже есть инструкция, которая складывает два числа, но она вообще не принимает никаких явных параметров: она предполагает, что операнды уже лежат на стеке к моменту вызова команды — она извлекает из стека два числа, складывает их, и результат тоже кладёт на стек. При этом виртуальная машина VB в качестве стека использует обычный стек, тот же самый, которым манипулируют машинные инструкции общего назначения, тот, что предоставляемый потоку операционной системой, и указатель на который содержится в регистре ESP.
Поскольку VB — типобезопасный язык, который должен оградить программиста от выстрела в ногу, инструкция, складывающая два числа, не просто выполнит арифметику над числами, но и проверить совместимость типов, а также тот факт, не произошло ли в результате выполнения операции сложения переполнение, и если произошло — сгенерирует ошибку.
Например, опкод
FB 8E
соответствует инструкции AddUI1, которая выполняет сложение двух значений типа Byte. Реализация кода этой инструкции в виртуальной машине выглядит так:Первая инструкция извлекает из стека второе слагаемое, вторая инструкция не извлекая из стека первого слагаемого, сразу добивается того, что на верхушке стека будет лежать сумма двух слагаемых, которые были на стеке до выполнения этой инструкции. Третья инструкция проверяет, не произошло ли переполнение беззнакового 1-байтового числа, и если да — то выполнение улетает в код, генерирующий ошибку.
Следующие интрукции прочитывают опкод следющей инструкции в EAX, сдвигает указатель не текущий декодируемый байт (ESI), и, основываясь на значении опкода (в ргеистре EAX), выбирает подходящий обработчик следующей инструкции и передаёт туда управление.
В целом, все обработчики P-кодных инструкций работают схожим образом.
В данном примере показана целочисленная арифметика над типизированными переменными — это, пожалуй, самое простое из всех задач, которые могут стоять перед виртуальной машиной. Операции над Variant-переменными или объектами с использованием позднего связывания — вот примеры того, где за одной двухбайтной инструкцией P-кода может стоять сотня, а то и тысяча машинных инструкций процессора.
Изначально движок EB/VBA был устроен так, что компилирование VB-кода в P-код и его исполнение виртуальной машиной было единственным возможным способом работы. Как минимум, P-код давал такое преимущество, как кроссплатформенность: документ MS Office (например, книга Excel) содержащий макросы на языке VB, мог бы быть создан под ОС Windows на платформе x86, а затем открыт под версией MS Office для MacOS на платформе PowerPC — при этом код макросов работал бы там без перекомпиляции, поскольку один и тот же платформо-независимый байт-код выполнялся бы соответствующим билдом виртуальной машины под кокнертную аппаратно-программную платформу.
В случае Standalone VB, была добавлена возможность компиляции проекта не только в P-код, но и в Native-код (машинный код под целевую аппаратную платформу, главным образом 386+) — однако только при компиляции проекта в EXE-файл. В режиме отладки проекта под IDE проект, то есть его процедуры, всегда компилируется в P-код.
Когда-то давно, когда накопители были маленькими, P-код давал значительное преимущество в том, насколько компактными получались результирующие исполняемые файлы: там, где с использованием Native-кода потребовалось бы с полсотни инструкций, можно было обойтись одной P-кодной инструкцией. Платить за это приходилось производительностью: P-код был более медленным.
С современными процессорами с большими и быстрыми кешами данных и инструкций ситуация стала ещё более интересной: P-кодные билды стали часто обыгрывать Native-кодные билды по той причине, что одна и та же программа, один и тот же алгоритм в P-кодном воплощении становился намного компактнее и охотно умещался в кеше данных процессора. Наиболее часто используемые P-коды команды, точнее их Native-кодные реализации в составе виртуальной машины стали полностью умещаться в кеше инструкций: если для полностью Native-кодной программы, где какая-нибудь процедура вызывается впервые и имеет размер в 100 условных машинных инструкций, пришлось бы делать множество чтений из памяти, и к тому же в код этой процедуры гарантированно отсутствовал бы в кеше инструкций (поскольку процедура вызывается впервые), то для P-кодного вариантй той же процедуры её размер в P-кода составлял бы условные 10—15 P-кодных инструкций. Да, их тоже пришлось бы вычитывать из памяти, но количество чтений было бы на порядок меньшим, зато с большой вероятностью эти P-кодные инструкции вызывали бы исполнение уже неоднократно поработавших фрагментов с имплементацией работы этих P-кодных команд, уже попавших и в кеш инструкций, а потому выполняющихся намного быстрее, чем аналогичный по функциональности Native-код.
Подытожим:
Когда после осмысления и осознания всех этих фактов я вижу, как кто-то утверждает, что VB6 IDE или VBA IDE выполняет VB-код путём его построчной интерпретации прямо в момент выполнения процедур, буквально как какой-нибудь bash-интерпретатор интерпретирует шелл-скрипт (работая с ним как с текстом), у меня глаза наливаются кровью от злости.
Сосед с перфоратором — теперь и на Марсе.
Пробежавшись по абзацам про Thunder, Silver, Ruby, Алана Купера сложилось впечатление, что вы вдохновлялись вот этой статьей. Угадал?
Но мимо этого я не мог пройти:
Вы серьёзно? For Each Джоэль позаимствовал не из C-Sharp'а, а из CSH — скриптового языка для написания шелл-скриптов, который появился на 32 года раньше Шарпа. C# и csh это не одно и то же. Да и как можно было допустить мысль, что до си-шарпа такой конструкции ни в одном другом языке не было?
В конце концов, даже если не знать ничего про csh, как при создании Excel Basic'а (который позже стал VBA и VB), которое происходило на рубеже 80-х и 90-х, можно было позаимствовать что-то из C#, до появления которого оставалось ещё 10 лет? Это как писать, что Аристотель позаимствовал что-то из трудов Ленина.
Чьи требования?
До сих пор думаю, что это был такой «экспресс-тест на психопата и неадеквата»; при этом интересно, ко всем ли они применяют такой подход, или что-то в моём поведении или образе им сходу показалось подозрительным.
Уже забыл, как в точности провоцировался конфликт, но общая формула создания конфликтной ситуации примерно следующая:
2. Вы окидываете взглядом кабинет, и видите, что рядом со столом, за котором сидит врач и медсестра, нет стула, предназначенного для вас.
3. «Куда садиться?» — спрашиваете вы.
4. «Вот сюда» — вам показывают рукой на то место, где по идее должен стоять стул.
5. «Но здесь нет стула», возражаете вы.
6. «У тебя глаз или нет или как?» — надменно начинает хамить врачиха, перейдя на «ты».
7. «В смысле?»
8. «Ты сюда в кабинет заходил, не видел, что в коридорчике стоит стул?»
9. «Ну...»?
10. «Он тебе что? Не годится??? Или тебе какой-то особенный стул нужен?»
11. «Эээ....»
12. «Ну так пройди тиуда, возьми, переставь его сюда и сядь! В чём проблема? Или мы тебе ещё стулья должны подносить, как барину? Тут лакеев нет, знаешь ли?»
13. Но дальше хамские интонации отключаются и разговор продолжается в более-менее мирном русле.
По-моему ещё был какой-то аналогичный подкол на тему заполнения бумаг (вписать в бланк свои паспортные данные я должен был сам, они бы только подпись поставили), не помню к чему именно относилась фраза по лакеев, но сама фраза запомнилась очень хорошо.
В общем, спектакль заключался в том, чтобы максимально хамски и оскорбительно вести себя по отношению к «пациенту». Я сходу предположил, что прямо сейчас передо мной разыгрывается спектакль и что надо не поддаться на провокации. А так они ведут себя таким образом, чтобы по максимуму вызвать у тебя желание развернуться и, хлопнув дверью, уйти со словами «да подавитесь вы своей справкой».
После нескольких дежурных вопросов, включая вопрос «чем занимаетесь?», разговор продолжился в духе того, что а вот у моей дочки/подруги/сестры есть такие-то проблемы с компьютером. «Чем это может быть вызвано?».
Дальше набор симптомов компьютерной проблемы на ходу переиначивался, дополнялся новыми подробностями, задавались уточняющие вопросы, которые были всё глупее и глупее. Возможно замысел был в том, что если человек выдержал испытание хамством, он может «взорваться» на стадии «тупых и назойливых вопросов».
Исходники далеко не полные. Нет, к примеру, кода oleaut32.dll. Это, конечно, не относится к описанным здесь проблемам, но и части ядерных вещей недосчитаешься.
Про стюардессу не поддерживаю
Вы это про Windows или про nix-системы говорите?
Если про Windows, то что сейчас, что 20 лет назад, ничего подобного не практиковалось.
Когда вы выделяете 1 гигабайт памяти с помощью VirtualAlloc, создаётся структура MMVAD (она является узлом дерева, всё дерево хранит всю карту выделенных или зарезервированных областей виртуального АП отдельно взятого процесса, точнее «пользовательской» части всего АП), в которую заносятся параметры выделения (база, размер, параметры защиты страниц).
Никаких страниц физической памяти или файла подкачки, ассоциированных со страницами этого гигабайтного региона, в момент выделения памяти не выделяется.
Единственное, что есть такой счётчик как «page file quota», от которого минусуется количество выделенных страниц — этот счётчик не даёт всем процессам в сумме выделить больше страниц, чем теоретически может обеспечить файл подкачки.
Когда вы попробуете записать тот пресловутый 1 байт, произойдёт page fault, который ОС разрулит незаметным для процесса образом — странице виртуального АП, в которую был записан 1 байт, начнёт соответствовать некая страница физической памяти. Эта страница физической памяти будет выбрана из списка заблаговременно заготовленных занулённых страниц, а если таких готовых страниц на данный момент нет — будет занулена прямо сейчас (что дольше, чем если брать из простаивающих занулённых).
В случае, если ОС будет испытывать недостаток страниц физпамяти, страница физпамяти, соответствующая странице виртуального АП, в которую был записан 1 байт, будет сброшена в файл подкачки, а при необходимости затем подгружена.
Все остальные остальные страницы этого гигабайтного региона по прежнему будут существовать только номинального — не более, чем как числа в структуре MMVAD. Никаких страничных фреймов физической памяти или страничных фреймов файла подкачки им (без необходимости) соответсововать не будет.
Я прекрасно знаю об этом, но в XP поддержка физпамяти больше 4 Гб принудительно зарезана. Где только я говорил про PAE, не могу понять? В комментариях к этой публикации я PAE не упоминал — разве что в комментах к другим статьям мог где-то сказать про PAE в рамках развенчания мифа про лимит в 4 Гб для 32-битных систем, налагаемый, якобы, архитектурой.
Вот я сижу на 32-битной ОС, объём поддерживаемой физпамяти ограничен числом чуть меньшим, чем 4 Гб. При этом у меня огромный файл подкачки, размещённый на специально выделенном ради этого SSD-диске. Естественно у меня процессов, выдеряющих огромные объёмы, сумма которых больше, чем объём всей физпамяти на порядок.
Вам не приходило в голову, что и 16-битный софт под DOS кому-то может быть интересно или нужно реверсить хотя бы потому, что люди хотят функциональность перенести на более современные рельсы? Вот есть софт под DOS, который управляет станком, а хочется, чтобы был подо что-то современное. Но ни документации, ни исходников, ничего.
Понимаете мысль?
Это какая-то терминологическая демагогия пошла. Общепринято, что более гибкий и многозначный термин «память» используют часто как синоним понятия «адресное пространство».
В любом случае, вы написали
Можете сделать процесс, который резервирует 1 Гб (или вообще все свободные регионы) своего адресного пространства (после чего засыпает или зацикливается), и запустить 200 таких процессов. Никакие системные ресурсы не исчерпаются. Будет потрачено 200 фрагментов неподкачиваемого пула ядра на структуры MMVAD_SHORT. Ровно столько же было бы потрачено, если бы процесс закоммитил 200 несмежных 4-килобайтных страниц (800 кб в общей сумме).
Не могу говорить о всех операционных системах на свете, но к Windows ваши слова точно не применимы.
Во-первых, похоже, что вы путаете резервирование (
MEM_RESERVE
) и выделение (MEM_COMMIT
) страниц. Прочитайте статью об устройстве виртуальной памяти в Windows, которую tyomitch написал в далёком 2006 году. Если не хотите читать, то коротко: разница между резервированием и выделением страниц такая же, как между бронированием номеров в гостинице и заселением в них. Вы не сможете создать дефицит чистого постельного белья в городе, даже если забронируете все номера всех гостиниц в нём (но не станете никем из заселять).Во-вторых, даже если вы не резервируете, а именно выделяете какое-то количество страниц с помощью VirtualAlloc(), последняя лишь создаёт (или обновляет) структуру MMVAD_SHORT, описывающую диапазон страниц, но совершенно никак не обновляет таблицы страниц, не инициализируя новые PDE или PTE.
Инициализация/создание новых PDE/PTE происходит лишь при первом обращении к соответствующей странице. Когда происходит первый page fault при попытке доступа к ней. То есть каталог страниц обновляется только по мере необходимости (on demand).
Не верите: можете раздобыть утёкшие исходники ядра и прочитать от корки до корки исходники ядерной функции
NtAllocateVirtualMemory
(VirtualAlloc
лишь переходник к ней). Единственным исключением является вызовVirtualAlloc
с флагомMEM_PHYSICAL
.