Оберон для ZX Spectrum
Тонкости при разработке на Обероне
в среде ZXDev
Oleg N. Cher, VEDAsoft Oberon Club
Концепция среды XDev (и её подсистемы
ZXDev,нацеленной на разработку для Спект─
рума) формировалась в течение нескольких
лет, но обрела свою реализацию в виде пер─
вой версии сравнительно недавно ─ в конце
января 2015 г. Основные особенности:
1.В качестве входного языка применяется
язык Оберон и его надмножества ─ языки
Оберон-2 иКомпонентный Паскаль(из после─
днего поддержаны только некоторые фичи).
2.Кодогенерация реализована через тран─
сляцию ОберонавСис последующим вызовом
сишного компилятора. В качестве основного
используется компилятор SDCC, но путём
редактирования сборочных скриптов возможно
подключение и других компиляторов, напри─
мер, z88d.
3.Мультитаргетность. В репозитории про─
екта XDev(https://github.com/Oleg-N-Cher/
XDev ) есть подсистемы для таргетов MSX,
MS-DOS, Windows (32/64 bit), Linux, нахо─
дящиеся в разной степени готовности. Не
публиковались,но также намечены подсистемы
для разработки под NES/Nintendo, Java ME и
Android. Уже зарелижена первая версия под─
системы ZXDev с набором библиотек в комп─
лекте,хотя и не очень богатым, и примерами
простых игр на Обероне ─
https://sourceforge.net/projects/bb-xdev
Оберон - это очень компактный модульный
язык программирования (компонентный,объек─
тно-ориентированный - в зависимости от ди─
алекта) со структурной парадигмой, строгой
типизацией и автоматическим управлением
памятью,спроектированный для универсально─
го применения (в том числе системного, где
он заодно играет и роль скриптового язы─
ка ─ ОС ETH Oberon, A2/Bluebottle ). Это
квинтэссенция творчества классика програм─
мирования доктора Никлауса Вирта, автора
Паскаля иМодулы-2. В противовес современ─
ным средствам "основного потока" (main
stream) ─ большим языкам, ещё и наращивае─
мым дальнейшим усложнением,Оберон основан
на самых ключевых понятиях информатики ─
модуль, процедура, структура данных (за─
пись).
Оберон-парадигма так же подвержена фра─
гментации,как и другие области IT. Поэтому
и в Оберон-парадигме есть диалекты, напри─
мер,Oberon-07 (сверхминималистичная реви─
зия Оберона-1 ), OberonX (математическое
расширение),Active Oberon (многопоточный
диалект),Оберон-2 (с расширенными средст─
вами ООП), а также особенно любимый мною
Компонентный Паскаль (надмножествоОберо─
на-2 для промышленного применения).
Беззнаковые вычисления
Язык Оберон не имеет беззнаковых типов
данных, это подобно тому,как процессор Z80
не имеет беззнаковых регистров или ячеек
памяти. Но их значения могут интерпретиро─
ваться как беззнаковые числа и быть подве─
ргнуты беззнаковым операциям.
Это также похоже на языкФорт ─ значе─
ния на стеке не обладают типом - могут
трактоваться как знаковые, так и беззнако─
вые (или даже как двойные - занимающие два
слова). И хотя большинство стековых опера─
ций знаковые, но наряду со знаковым умно─
жением (* ) есть и беззнаковое умножение
(U* ), так же и со сравнением, и с деле─
нием, и т.п. В общем, системного програм─
миста не должно смутить отсутствие беззна─
ковых типов,ему достаточно знать,что ZXDev
имеет три типа для целочисленных данных
размером в 1,2 и 4 байта. Рассмотрим,каким
способом можно определить эффективные без─
знаковые операции в ZXDev.
Беззнаковое сравнение байтов. Этот трюк
я придумал в процессе работы над портом
игры "Дурак", где применяется много знако─
вых сравнений байтов на больше-меньше, ко─
торые можно свободно изменить на беззнако─
вые сравнения, тем самым повысив эффектив─
ность кода.
Знаковое сравнение:
IF a > b THEN ...
Беззнаковое сравнение:
IF CHR( a ) > CHR( b ) THEN ...
Здесь знаковые значения приводятся к
беззнаковому типуCHAR и потом сравнивают─
ся.
Сложение и вычитание реализуется одина─
ково для знаковых и беззнаковых.А вот сей─
час попробуем реализовать эффективные (без
накладных расходов) операции беззнакового
деления и умножения для байтов и слов. По─
добным образом можно реализовывать любые
другие операции, отсутствующие в языке
(ведь всего не предусмотришь):
MODULE UMath; IMPORT SYSTEM, B := Basic;
VAR a, b: SHORTINT;
PROCEDURE -UMultBytes (a, b: SHORTINT):
SHORTINT
"( ((CHAR)a) * ((CHAR)b) )";
PROCEDURE -UMultWords (a, b: INTEGER):
INTEGER
"(((unsigned int)a)*((unsigned int)b))";
PROCEDURE -UDivBytes (a, b: SHORTINT):
SHORTINT
"( ((CHAR)a) / ((CHAR)b) )";
PROCEDURE -UDivWords (a, b: INTEGER):
INTEGER
"(((unsigned int)a)/((unsigned int)b))";
BEGIN (*$MAIN*)
B.Init;
(* Приводим, т.к. значение >
SIZE(SHORTINT): *)
a := SYSTEM.VAL(SHORTINT, 255);
b := SYSTEM.VAL(SHORTINT, 255);
(* Печатает: 65025 (255*255): *)
B.PRWORD( UMultBytes(a, b) );
B.PRLN;
(* Печатает: 1 ( (-1)*(-1) ): *)
B.PRWORD( a * b );
B.PRLN;
a := SYSTEM.VAL(SHORTINT, 255); b := 5;
(* Печатает: 51 (255 DIV 5): *)
B.PRWORD( UDivBytes(a, b) );
B.PRLN;
(* Печатает: 0 (-1 DIV 5): *)
B.PRWORD( a DIV b );
B.Quit
END UMath.
Обратите внимание на странный на первый
взгляд результат процедур ─UMultiBytes и
остальных ─ он того же размера,что и аргу─
менты. Это должно значить,что переполнение
при умножении даст потерю разрядности ре─
зультата, однако же на практике этого не
происходит,потому что изнутри эти операции
устроены так:
#define UMath_UDivBytes(a, b)
( ((CHAR)a) / ((CHAR)b) )
#define UMath_UDivWords(a, b)
(((unsigned int)a)/((unsigned int)b))
#define UMath_UMultBytes(a, b)
( ((CHAR)a) * ((CHAR)b) )
#define UMath_UMultWords(a, b)
(((unsigned int)a)*((unsigned int)b))
Здесь я применил при описании результа─
та короткий тип (байт),чтобы результат был
совместим с коротким типом (в случае при─
сваивания результата переменной длиной в 1
байт) без удлинения, т.е.:
short:=UMultBytes(short1,short2);
вместо характерного дляОберона явного
SHORT() для уменьшения разрядности типа
(угадайте, какой вариант более эффекти─
вен?):
short:=SHORT(UMultBytes(short1,short2));
Но если тип результата имеет среднюю
разрядность (слово), то,как видите,старший
разряд результата не теряется:
integer:=UMultBytes(short1,short2);
(ВОбероне обязательно явное указание
SHORT() для уменьшения мощности числового
типа; так сделано,чтобы было легче контро─
лировать возможное искажение результата в
случае приведения бОльших типов к мень─
шим).
Битовые вычисления
Вирт попытался придать работе с битами
более привлекательный вид, согласующийся с
математическими абстракциями, поэтому биты
машинного слова представлены как множество
целых чисел ─ номеров отдельных битов
(http://oberoncore.ru/library/wirth_sets).
Для Оберона вместо универсальных множеств
были выбраны множества небольших целых
чисел. Тип SET в Обероне можно рассматри─
вать как битовый набор, спроектированный
так,чтобы быть независимым от порядка сле─
дования байтов платформы (так называемый
byte order: most significant byte ─ MSB, и
least significant byte ─ LSB). Именно поэ─
тому Оберон не поощряет насильственного
приведения целых к множествам и наоборот,
т.к.такое системное приведение типа ─ опе─
рация достаточно низкоуровневая,чтобы учи─
тывать порядок байтов, и её использование
может привести к непредсказуемым последст─
виям на платформах с разным порядком сле─
дования байтов, хотя, конечно, для Z80 это
некритично.
Размер типаSET в Обероне зафиксирован
в соответствии с современными процессорами
и составляет 4 байта (в GPCP есть тип
LONGSET = 8 байт), но в ZXDev мы можем в
конфиге Ofront.par указать произвольный
размер множеств, и я настоятельно рекомен─
дую 1 байт, что наиболее эффективно для
процессора Z80.
ОперацияMOD (остаток от целочисленного
деления) при соответствующем делителе бу─
дет оптимизирована до соответствующего ей
логического AND, например, обероновское
a MOD 8 будет транслировано в сишноеa&7.
Эквивалент логических побитовых опера─
ций для целыхa и b:
a AND b = ORD(BITS(a) * BITS(b))
a XOR b = ORD(BITS(a) / BITS(b))
a OR b = ORD(BITS(a) + BITS(b))
NOT a = ORD(-BITS(a))
Где ORD - это преобразование битового
множества в целое, аBITS ─ целого во мно─
жество. Идеология Оберона не поощряет ра─
боту с целыми как с битами и наоборот, ибо
это, по мнению Вирта, ведёт к неряшливому
использованию типов и нивелирует преимуще─
ства строгой типизации, поэтому функций
BITS() иORD(set) в стандарте Оберона нет,
но есть вКомпонентном Паскале (и в XDev
тоже).
IF 0 IN set THEN(* if(set & 1) ... *)
(* if (set & 0x23) ... *)
IF BITS(23H) * set # {} THEN(* ... *)
(* if (set & 0x23) ... *)
IF {0, 1, 5} * set # {} THEN(* ... *)
Последний вариант,как мне кажется,более
наглядно показывает, что в наборе проверя─
ется состояние битов №№0,1 и 5. Пусть вас
не вводит в заблуждение некоторая вычур─
ность записи битовых операций,особенно это
кажущееся "умножить" ─ машкод получается
что надо:
; if ((0x23 & _set) != 0x0) {
ld a,(#_set + 0)
and a, #0x23
jr Z, ...
Таким образом, наОбероне можно сделать
достаточно низкоуровневую программу,напри─
мер, эмулятор Спектрума.
Константные массивы
Для включения ресурсов и двоичных дан─
ных прямо в кодОберон не предлагает ниче─
го лучше,чем поэлементное присваивание.И я
очень благодарен Олегу Комлеву (Saferoll)
за его труд по добавлению в ZXDev нестан─
дартного языкового расширения ─ констант─
ных массивов. Дадим ему слово:
Saferoll:
Что удалось сделать по константным мас─
сивам на данный момент - май 2015 года.
1)Константные массивы любой вложеннос─
ти.
2)Типы элементов - линейка целых типов
(включаяBYTE),BOOLEANилиCHAR.Все эти
типы в C-исходнике становятся целыми конс─
тантами.
3)Если массив состоит изCHARилиBYTE,
то элементы можно указывать либо как пере─
чень символов в скобках('f',20X,"7"),ли─
бо в виде строки"ab"без лишних скобок.Но
строка обязательно подразумевает в конце
символ0Х,для него тоже должно быть место
в массиве!
Ставить в кавычках меньше символов мож─
но,тогда символы после0Xмогут быть запо─
лнены мусором - зависит от реализации Си-
компилера. Поэтому лучше считать,что неис─
пользуемый строкой остаток массива запол─
нен неопределёнными символами.
Пустую строку "" можно указывать для
любого массиваARRAY N OF CHAR(илиARRAY
N OF BYTE).
Примеры:
TYPE
MsgStr = ARRAY 3, 7 OF CHAR;
CONST
Way = MsgStr("Hello","Error","Try");
TYPE
Labirint = ARRAY 3, 16 OF CHAR;
CONST
Map = Labirint(
"...o..##...oo12",
"...o..##...ooЗ5",
"...o..##...oo78"
);
Пока не сделано: экспорт константных
массивов (с этим мы ещё не разобрались),
возможность опускать размер массива, чтобы
Ofront автоматически его рассчитал по ко─
личеству элементов, и указание$на фикси─
рованный символьный массив. Чую, что тут
опять полезут проблемы с путаницей "сим─
вол или строка". Также есть куда развивать
реализацию в смысле эффективности. Но то,
что сделано сейчас, уже весьма полезно.
Совместное использование Оберона и Си
В Оберон-программы можно вставлять про─
извольные части кода, написанные на языке
Си (и встроенном ассемблере).Есть несколь─
ко способов (http://zx.oberon2.ru/forum/
viewtopic.php?f=10&t=202 ),которые я крат─
ко перечислю.
1. Прямая вставка сишного файла
в Оберон-программу
IMPORT SYSTEM;
PROCEDURE -includemain
'#include "Main.c"';
// --- Main.c ---
void main (void) {
Basic_Init();
Laser_InitScroll(65392);
Laser_InitSprites(Rsrc_SprStart, 4769);
...
Basic_Quit();
}
Разумнее всего будет вставлять так от─
дельные функции, хотя может оказаться, что
этот способ имеет гораздо более широкие
возможности. Просто нужно учитывать то,что
Оберон-модуль как-то должен знать про этот
сишный код, чтобы уметь с ним взаимодейст─
вовать.
Как мы знаем, Оберон-строки являются
нуль-терминированными, но в отличие от си─
шных к ним прикреплено значение максималь─
ной длины строки.Если процедуры для работы
со строками всегда будут действовать в
рамках этой длины ─ код всегда будет рабо─
тать корректно. Но можно ли работать на
Обероне со строками целиком в сишном стиле
без дополнительного поля макс. длины? Ко─
нечно, можно. Вот мы опишем вызовы сишных
функций в обёртке обероновских процедур, и
они могут даже не совпадать по параметрам
(см., например,IntToStr ):
TYPE
(* C-like null-terminated string: *)
CString = SYSTEM.PTR;
PROCEDURE -includestdlib
"#include <stdlib.h>";
PROCEDURE -includestring
"#include <string.h>";
PROCEDURE -Length (
str: CString): INTEGER
"strlen((char*)str)";
PROCEDURE -CopyStr (dest, src: CString)
"strcpy((char*)dest, (char*)src)";
PROCEDURE -IntToStr (
n: INTEGER; str: CString)
"_itoa(n, (char*)str, 10)";
PROCEDURE -UIntToStr (
u: INTEGER; s: CString)
"_uitoa((unsigned int)u, (char*)s, 10)";
PROCEDURE -Concat (dest, src: CString)
"strcat((char*)dest, (char*)src)";
Пример использования:
IMPORT SYSTEM, B := Basic;
CONST
MaxIntSize = 7;(* ~-12345~ + 0X. *)
VAR
num: SHORTINT;
strBuf: ARRAY MaxIntSize OF CHAR;
BEGIN
num := B.RND(1, 4);
UIntToStr(num, SYSTEM.VAL(
CString, SYSTEM.ADR(strBuf))
);
Concat(SYSTEM.VAL(CString,
SYSTEM.ADR(strBuf)),
SYSTEM.VAL(CString,
SYSTEM.ADR(" is my number"))
);
2. Биндинг
Чтобы Оберон умел взаимодействовать с
кодом наСи ─ нужно как-то к нему прикре─
питься. Необходимо сделать описание интер─
фейса сишной библиотеки в стиле Оберон-мо─
дуля,чтобы другие модули могли вызывать из
него процедуры, брать значение констант и
т.д. Для этого мы должны подготовить бин─
динг-связку, в которой будет описан интер─
фейс чужеродного модуля,все константы,типы
и процедуры (в случае XDev ─ с пустыми те─
лами). Замечу,что это обычная практика для
связки модульных языков с сишными библио─
теками (применяется не только вОберонах,
но и в языкахАда, Модула-2, Модула-3 ).
XDev содержит достаточные средства для
создания биндингов,учитывающие возможность
использовать разные модели вызова функций,
замену одних вызовов другими,описание про─
тотипов функций и т.д. С их помощью соз─
даны биндинги к WinAPI и libSDL для XDev/
WinDev. Приведу пример простого биндинга,
отсылая за деталями к форуму
http://zx.oberon2.ru/forum/
viewtopic.php?f=10&t=94.
MODULE Input; IMPORT SYSTEM;
CONST
Backspace* = OCX;
Enter* = ODX;
Escape* = "E";
Space* = " ";
(* Arrows *)
Up * = "Q";
Down * = "A";
Right * = "P";
Left * = "O";
TYPE
Key* = CHAR;
(** Returns the number of keystrokes
in the keyboard input buffer. *)
PROCEDURE Available* (): SHORTINT;
BEGIN RETURN 0 END Available;
(** Read a key from the keyboard buffer.
Blocks if no key is available. *)
PROCEDURE Read* (): Key;
BEGIN RETURN 0X END Read;
PROCEDURE RunMe5OHz* ; END RunMe5OHz;
END Input.
Транслятор сгенерирует из этого биндин─
га сишный файл с пустыми телами функций, а
также более нужные нам 1) заголовочный
файлObj/Input.h, который будет подключен
при компиляции, и 2) символьный файл Sym/
Input.sym, подобный таким же,сгенерирован─
ным для родных Оберон-модулей. В нём хра─
нится закодированное представление интер─
фейса модуля. Автосгенерированную пустую
сишную реализацию мы игнорируем, подменяя
при компиляции на реализацию, написанную
ручками (на Си или асме), которая хранится
в папке/C и реализует заявленную в интер─
фейсе функциональность.
3. Биндинг с использованием готового
сишного стандартного заголовка
и самодельного заголовка-переадресовщика
Допускаю и такую вариацию: при необхо─
димости можно игнорировать и автосгенерён─
ный Оберон-транслятором заголовок(*.h).
Делается модуль-биндинг на Обероне (для
получения символьного файла), а при компи─
ляции подключается сделанный вручную заго─
ловочный сишный файл, из которого1) уже в
свою очередь инклюдится стандартный сишный
заголовок, не адаптированный к оберонскому
манглированию имён префиксом и т.п.;и2) в
котором описывается переадресация Оберон-
процедур в сишные функции или даже макро─
сы. Подобным образом устроен биндинг к
библиотеке trdos.lib (см. в дистрибутиве
XDev ).
(про сопряжение с ассемблером см.следующую
статью)
Other articles: