Реализация языка высокого уровня через WinAPI

Первая публикация 30.12.2017

Меня всегда интересовала связь между реализацией языка программирования высокого уровня и операционной средой. Конкретно, через какие именно WinAPI реализуется связь языка с операционной системой Windows?

На первый взгляд, связь со средой в каждой конкретной программе своя и зависит от назначения программы. Например, для программы с «оконным» интерфейсом связь с Windows будет через многочисленные «оконные» API, а связь для программы, рисующей графику, – через API GDI и т.д.

Но среди этих тысяч API, вероятно, можно выделить некоторый минимальный базовый набор, который реализует собственно язык, а не прикладные программы. Определить этот базовый набор легко, когда компилятор работает по следующей схеме: операторы языка переводятся сразу в «нативные» команды процессора, а если таких команд нет (они являются слишком громоздкой последовательностью «простых» команд или выполняют взаимодействие с «внешним миром»), то компилятор генерирует системный вызов, т.е. вызов подпрограммы из системной библиотеки, являющейся, неотъемлемой частью языка.

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

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

Вызовы API в системной библиотеке

У языка высокого уровня PL/1-KT, компилятор которого реализован для среды Win64, я проанализировал все вызовы WinAPI в системной библиотеке. Оказалось, что могут быть вызваны 27 разных API (см. таблицу).

AllocConsole FormatMessage GetStdHandle Sleep
CharToOEM GetCommandLine GlobalMemoryStatus TerminateThread
CloseHandle GetCurrentDirectory MapViewOfFile UnMapViewOfFile
CreateFile GetFileSize ReadFile VirtualAlloc
CreateFileMapping GetLastError RTLAddFunctionTable VirtualFree
CreateThread GetLocalTime SetFilePointer WriteFile
ExitProcess GetShortPathName SetUnhandledExceptionFilter  

Однако остается вопрос, насколько необходим каждый вызов именно для самого языка. На мой взгляд, здесь должен быть применен принцип «вещей альпиниста» – не то, что может пригодиться, а то, без чего нельзя обойтись.

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

Подключение стандартного окна ввода-вывода

В языке PL/1 определен стандартный (текстовый) ввод-вывод, поэтому потребовались API AllocConsole и GetStdHandle. Вообще говоря, подключение текстовой «консоли» может быть определено в самом формате PE-файла, поэтому ее вызов вряд ли так необходим. А вот API, определяющее стандартные файлы ввода-вывода (в PL/1 это файлы SYSIN и SYSPRINT), совершенно необходимо для реализации.

Выделение памяти

В PL/1 имеется аппарат работы с памятью в виде операторов ALLOCATE/FREE. Однако три следующих API GlobalMemoryStatus, VirtualAlloc и VirtualFree используются только один раз в начале работы программы для определения и задания стартовых размеров «кучи» для последующей работы ALLOCATE/FREE. Интересно, что эта «куча» уже первоначально может быть несвязной и составленной из списка несмежных сегментов (из-за загрузки системных библиотек). При этом максимально возможный суммарный размер кучи, определяется рекурсивными вызовами VirtualAlloc/VirtualFree, т.е. методом проб и ошибок. А определение физического размера памяти в компьютере нужно только для реализации одного специального режима, когда «куча» не должна превосходить размер физического ОЗУ (для повышения эффективности). Таким образом, для реализации нужна лишь одна VirtualAlloc, а две другие обеспечивают пусть и полезные, но избыточные возможности.

Работа с файлами

Развитые средства файлового обмена в PL/1 сводятся к вызовам лишь шести API: CloseHandle, CreateFile, GetFileSize, ReadFile, WriteFile и SetFilePointer. Можно проследить однозначную связь между этими API и операторами обмена в PL/1. Некоторое исключение составляет GetFileSize, которое, во-первых, можно заменить использованием SetFilePointer, а во-вторых, оно нужно для обеспечения главным образом, перехвата исключительных ситуаций, хотя в данной реализации и имеется прямое ее использование оператором типа X=LENGTH(F), где X – целая переменная, а F – переменная типа «файл».

Несколько особняком стоят еще три API, реализующие «отображение файла на память»: CreateFileMapping, MapViewOfFile и UnMapViewOfFile. Это очень удобная возможность работы с файлами изначально, конечно, никак не была представлена в PL/1, хотя операторы вроде READ FILE(F) SET(P), где P – переменная типа указатель, являются прообразом такой работы.

Перехват исключений

Может быть, не все знают, что впервые обработка исключений появилась в PL/1 полвека назад. Естественно, в системной библиотеке есть вызов двух API на эту тему:

RTLAddFunctionTable и SetUnhandledExceptionFilter.

Причем необходимость использования обоих вызывает большие сомнения. Задать постоянный обработчик всех исключений проще прямо в формате PE-файла, а не с помощью RTLAddFunctionTable. А поскольку такое задание перехватчика обычно включает практически все случаи, второе API, по-моему, вообще не требуется.

Работа с параллельными потоками

Поскольку в PL/1 предусмотрено паралелльное выполнение с помощью оператора TASK, используются два API: CreateThread и TerminateThread.

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

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

В PL/1 предусмотрено, что главная (MAIN) подпрограмма может иметь параметр, для получения которого требуется API GetCommandLine.

Запрос времени

Встроенные функции DATE и TIME в PL/1 реализуются с помощью единственного API GetLocalTime.

Пауза

В стандарте PL/1 это довольно тонкое место, но действительно в языке предусмотрен оператор DELAY, который и реализуется с помощью API Sleep, а кроме того, пауза нужна для реализации оператора языка WAIT при выполнении параллельных потоков.

Оператор STOP

Данный оператор в PL/1 однозначно отображается с помощью API ExitProcess.

Вспомогательные API

В системной библиотеке PL/1 обнаружилось еще пять вызовов следующих API: CharToOEM, FormatMessage, GetCurrentDirectory, GetLastError и GetShortPathName.

Три API GetLastError, CharToOEM и FormatMessage дают развернутую информацию об ошибке, причем даже на русском языке. Хотя в PL/1 и предусмотрена стандартная реакция на исключения, для которых нет обработчиков (обычно это окончание работы с выводом сообщения), конкретный вид сообщений в стандарте PL/1, конечно, не оговорен и эти API не реализуют необходимые свойства языка. Хотя на практике информация от этих API, безусловно, помогает в поиске ошибок.

API GetCurrentDirectory и GetShortPathName выдают некоторую информацию о среде Windows и тоже напрямую не относятся к языку. Например, отголосок MS DOS в виде GetShortPathName позволяет просто сократить и стандартизировать информацию в случае ошибки работы с файлом, умещая ее в одну строку. Конечно, в другой среде строго повторять это не требуется.

Заключение

Итак, реализация языка довольно высокого уровня PL/1 (у многих до сих пор в головах остались мифы о его невероятной сложности) свелась к реализации всего лишь горстки API, в пределе следующей:

GetStdHandle, VirtualAlloc, CloseHandle, CreateFile, ReadFile, WriteFile, SetFilePointer, CreateThread, GetCommandLine, GetLocalTime, Sleep и ExitProcess.

Эти 12 API позволяют реализовать довольно развитые возможности, намекая при этом на минимально необходимый набор API в любой операционной системе, например, даже во вновь разрабатываемой.

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

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

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

1

Автор публикации

не в сети 8 часов

admin

3
Комментарии: 28Публикации: 173Регистрация: 13-06-2019
Авторизация
*
*

пять + 9 =

Регистрация
*
*
*

2 + двадцать =

Генерация пароля

восемнадцать + двадцать =

Перевести »
Прокрутить вверх
Scroll to Top