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

Для интерпретируемых языков эта связь естественным образом сохраняется, так как интерпретатор собственно и «выполняет» исходный текст программы, т.е. имеет к нему непосредственный доступ.

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

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

60 лет назад, при разработке языка PL/1 был предложен оператор вывода с именами. Если «обычный» оператор вывода, например:

put list(x,y);

выводил что-нибудь вроде

1280 1024

то подобный же оператор вывода, но с именами:

put data(x,y);

выводил соответственно:

X= 1280 Y= 1024

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

Реализация такого вывода с именами в компиляторе довольно громоздкая, но принципиально несложная, так как компилятор имеет доступ к таблицам имен всех объектов программы. Он сам такие таблицы и составляет.

Однако кроме оператора вывода с именами PUT DATA был предложен и «зеркальный» ему оператор GET DATA. Применительно к приведенному выше примеру, если есть текстовый файл f.dat с содержимым:

X= 1280 Y= 1024

То оператор:

get file(f) data(x,y);

должен присвоить указанные значения в переменные x и y.

Я упоминал об операторе GET DATA в первой части и упоминал, что в используемом мною компиляторе он реализован упрощенно. Вот теперь и попробуем разобраться, почему пришлось упростить реализацию.

В операторе PUT DATA порядок вывода естественным образом определяется порядком перечисления выводимых элементов. Казалось бы, в «зеркальном» операторе GET DATA надо указывать тот же порядок, но уже для вводимых элементов. Но если подумать, то ведь это не обязательно. Если я напишу оператор:

get file(f) data(y,x);

вместо

get file(f) data(x,y);

то, поскольку в самих данных указано, что есть что, переменные x и y все равно должны получить правильные значения. Да, черт возьми, я вообще могу не указывать список ввода! И всегда писать просто:

get file(f) data;

в смысле: введи из файла f переменные, а какие, ты и сам из данных узнаешь.

И вот для компилируемых языков здесь начинаются проблемы. Даже не будем касаться такой мелочи реализации, как необходимость хранить весь файл данных в памяти целиком, из-за необходимости просматривать его многократно в случае «нарушения» порядка следования элементов. Главное, для реализации такого оператора GET DATA необходимо в выполняемом модуле (или отдельно от него) хранить списки всех имен из исходного текста. А скорее, из многих исходных текстов, так как выполняемый модуль обычно получается соединением нескольких объектных модулей. И это еще не все. Для языка с блочной структурой и вложенными подпрограммами связь имени со значением, например, X= 1280, будет зависеть от того, где стоит в программе оператор GET DATA относительно описания переменной X, т.е. нужно хранить и области видимости всех имен.

Именно поэтому реализация GET DATA была так упрощена. При выполнении упрощенного оператора ввода файл с данными читается последовательно и все от начала очередного имени до очередного символа «равенство» просто пропускается. По существу имена никак не используются, GET DATA превращается в обычный GET LIST, а чтобы не ошибиться в порядке ввода, я в своих программах единственный экземпляр списка элементов держу в отдельном файле и вставляю его и в операторы PUT DATA и в упрощенные операторы GET DATA с помощью препроцессорного оператора %INCLUDE. В результате при изменениях список и ввода и вывода всегда соответствует друг другу и операторы PUT DATA и GET DATA остаются «зеркальны» друг другу. Что выведено одним, может быть прочитано другим.

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

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

Поэтому мы пошли по пути автоматического включения в каждый выполняемый модуль небольшого интерактивного отладчика. Его возможности, конечно, скромные. Почти на уровне знаменитого Debug из MS-DOS. Разумеется, с дополнением аппаратных контрольных точек по чтению/записи/исполнению, которых во времена процессоров 8086 еще не было. Но, главное, у отладчика остается связь с именами из исходных текстов. И можно осмысленно контролировать значения именно переменных заданного типа, а не просто участков памяти в 16-тиричном формате.

Для сохранения связи имени с адресом используется побочный продукт работы редактора связей – таблица всех имен из всех модулей, которая автоматически записывается в текстовый файл. Удобно затем этот файл включить как «ресурс» прямо в выполняемый модуль формата EXE. Тогда во время выполнения программы без IDE всегда под рукой есть возможность выйти в интерактивный отладчик, поставить контрольные точки, посмотреть (и даже исправить) значения переменных. Отладчик работает в своем стандартном консольном окне, не мешая другим графическим окнам программы. А если запустили программу через FAR, то можно считать, что отладчик работает в его окне, что удобно и привычно. Но, повторю, базой такой схемы сопровождения и отладки программы является сохранение связи между именами и адресами после компиляции и сборки выполняемого модуля.

И помимо отладки есть еще одна область, где важно сохранить связь имен с адресами. Это интерактивный ввод в Windows. Я застал еще времена перфокарт, когда в «пакетном» режиме в каком-нибудь БЭСМ-Алголе требовалось писать оператор ввода

INPUT(X,Y,Z); 

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

Но скоро (для меня в 1979 году) ввод действительно стал интерактивным и тот же самый оператор INPUT при выполнении программы уже вызывал приглашение на терминале ко вводу с клавиатуры.

Немного отвлекаясь в сторону: я несколько лет работал за терминалом Videoton-340, который по телефонному каналу был связан с БЭСМ-6. Этот терминал можно, например, увидеть в кабинете Людмилы Прокофьевны в фильме «Служебный роман», и современные зрители часто воспринимают его как какую-то экзотическую вычислительную машину. Недавно эта тема неожиданно опять всплыла в сети совершенно идиотским образом. Некий нелюбитель советской власти начал уверять, что эта дорогущая вычислительная машина за 438 000 рублей (почему именно за 438?) специальными людьми с охраной и специальным транспортом каждый день привозилась на съемочную площадку из Главного управления статистики и вечером увозилась, увеличивая расходы на фильм. На самом деле на съемках был всего лишь «телевизор с клавиатурой» венгерского производства, рублей так за 200 и причем необязательно исправный …

Но те времена прошли, и вот появился интерактивный ввод в Windows. Мне очень нравится подход разработчиков Windows к оформлению интерактивного ввода. Я могу независимо от своей программы на «языке ресурсов» описать в отдельном файле вид панелей со всякими стандартными «радиокнопками» и полями текстового ввода. А затем отдельным компилятором ресурсов преобразовать это описание в двоичный вид и с помощью все того же редактора связей включить все это прямо в EXE-файл. В своей программе я все эти «диалоги» собираю универсальным циклом в единую структуру, обращаюсь к единственной стандартной WinAPI PropertySheet и получаю на экране красивый интерактивный ввод в виде набора «вкладок», каждая из которых представляет заданную мною панель с полями для ввода.

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

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

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

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

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

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

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


  1. rukhi7
    31.05.2023 07:56

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

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

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