Недоря А.Е. - Диссертация. Глава 4

Введение    Глава 1    Глава 2    Глава 3    Глава 4    Заключение    Литература    Приложения


4. Система Мифрил

В данной главе мы рассмотрим принципы построения, структуру и возможности расширения системы Мифрил (см. также [32]). Название системы взято из [33]. Мифрил - это название подлинного серебра (the True Silver), которое добывалось гномами в недрах Мории. Удивительно легкий и прочный метал обогатил и прославил гномов.

Мы полагаем, что слово Mithril сконструировано Толкиеном из словосочетания mythical rill (мифический источник), и это расшифровка очень подходит в качестве имени РПС.

Мы предполагаем, что система Mithril будет использоваться как база для разработки переносимого прикладного ПО. Для того чтобы упростить разработку прикладного ПО необходимо решить несколько важных задач:

1) Необходимо создать комфортную обстановку для программиста (языки, отладка, поддержка проекта);

2) Система должна включать набор средств для создания пользовательского интерфейса (окна, мышь, конструктор интерфейсов);

3) Система должна поддерживать одинаковую обстановку на разных платформах (т.е. на разных машинах и под разными ОС).

С точки зрения пользователя, система практически не отличается от системы Oberon. При запуске системы пользователь видит на экране окно стандартного вывода и окно (System.Tool), содержащее набор основных команд (в том числе команду создания нового окна). Каждая команда M.P состоит из имени модуля M и имени процедуры без параметров P. Пользователь запускает команду, нажимая среднюю кнопку мыши на тексте команды. При активации команды система вызывает процедуру P из модуля M (загружая модуль M, если этот модуль не был загружен в систему).

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

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

4.1. Принципы построения и структура системы

Система состоит из трех частей: динамической поддержки (run time support, RTS), ядра системы и оболочки. Кроме этого система включает в себя набор драйверов для взаимодействия с операционной системой и/или аппаратурой.

Динамическая поддержка (RTS) - это неотъемлемая часть реализации языка Оберон-2, содержащая операции выделения памяти, сборки мусора и финализации объектов. В нашей системе RTS написан на языке Модула-2. При переносе системы RTS (как правило) переписывается для повышения эффективности сборки мусора.

Ядро системы определяет набор базовых понятий (типов): постоянный объект, файл, окно, текст, и т.д. (см. 4.1.2). При запуске ядро выполняет действия, необходимые для конфигурации системы, в том числе действия по установке драйверов.

Стандартная оболочка системы реализует минимальный набор понятий, определяющих интерфейс пользователя (в первую очередь текстовые окна) и набор команд. Расширение оболочки может выполняться двумя способами: расширение набора команд (динамическое расширение) и расширение некоторого базового типа (например, расширение окна до графического окна).

4.1.1. Влияние сборки мусора на построение системы

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

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

В некоторых случаях мы можем спроектировать дескриптор ресурса таким образом, что возвращение этого ресурса сводится к возвращению памяти, занимаемой дескриптором, и, следовательно, финализация для такого ресурса не нужна. Таким образом реализованы файлы в системе Оберон/Ceres. Но при переносе системы под Unix (см. [14]) возникла необходимость финализации файлов, так как число одновременно открытых файлов в системе Unix ограничено.

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

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

TYPE Untraced = UNTRACED POINTER TO X;


Мы, вслед за [13], реализуем эту возможность с помощью специальных указаний компилятору (системных флагов). При описании типа можно указать системный флаг (целое число), который определяет специфику данного типа. Эта возможность используется не только для получения нетрассируемых указателей, но и в тех случаях, когда для интерфейса с ОС или аппаратурой необходимо изменить способ размещения типа (см. также [15]).


Пример:

TYPE Untraced = POINTER TO [1] X;


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

Для того чтобы в разных частях системы не нужно было пользоваться низкоуровневой возможностью мы (на нижнем уровне системы) определяем понятие линейного нетрассируемого списка с операцией вставки. Каждый такой список содержит процедуру финализации (пустую, если финализация не нужна). Глобальным объектом сборщика мусора является нетрассируемый список таких списков (мета-список). Сборщик мусора перед возвращением памяти обходит список и вызывает процедуру финализации для всех "мусорных" объектов. Идея совместной организации механизма финализации и нетрассируемых ссылок описана в [34].

Модули системы, реализующие некоторый ресурс, должны создать заголовок списка и указать процедуру финализации, а при создании нового дескриптора ресурса – вставить его в этот список.

4.1.2. Возможности расширения и иерархия базовых типов

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

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

Кратко перечислим основные понятия системы Мифрил (подробнее см. 4.3). Мы будем приводить список понятий (типов) в следующей нотации

Тип (БазовыйТип [,Расширение] )


Здесь Тип есть ИмяМодуля.ИмяТипа, БазовыйТип - это имя типа или NIL (если сам тип является базовым), а Расширение может принимать одно из следующих значений:

Class - тип используется только как базовый для расширения;

Leaf  - тип является листом в иерархии типов, то есть расширение его или запрещено или не поддерживается.


Если Расширение не указано, то тип является конкретным, то есть его методы реализованы, и в то же время он может быть расширен. Заметим, что на языке Оберон мы можем описать тип, расширение которого запрещено, вне модуля в котором этот тип определяется.


 Иерархия типов системы Мифрил


Objects.Object (NIL,Class)


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


Errors.Error (Objects.Object)

Errors.Exception (Objects.Object)


Определяют контекст ошибки и реакцию на ошибку.


Channels.Channel (Objects.Object,Class)

Channels.Rider (Objects.Object,Class)


Определяют понятие канала ввода/вывода (файл, терминал, сеть, ...) и объекта доступа (см. также [34]).


XRiders.Rider (Channels.Rider,Leaf)


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


Storage.Object (Objects.Object,Class)


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


Files.File (Channels.Channel,Leaf)


Файл.


Fonts.Font (Storage.Object,Class)


Определяет абстрактный фонт.


Texts.Text (Storage.Object)

Texts.Elem (Storage.Object)


Текст со встроенными абстрактными литерами (элементами).


Screens.Screen (Storage.Object,Class)


Определяет стандарт драйвера экрана (см. 4.1.3).


Windows.Window (Storage.Object)


Окно.


Перечисленный набор типов является базовым. Оболочка системы в основном расширяет типы Windows.Window и Texts.Elem. Расширение типов Fonts.Font и Screens.Screen задают конкретизацию этих понятий и используются для настройки системы на структуру фонта и на вид экрана.

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

Мы использовали следующие принципы организации интерфейса объекта:


1) Методы используются при описании абстрактного класса. Для каждой операции используется отдельный метод. Дополнительный метод (handle) используется для передачи сообщений.

2) Операции оповещения реализуются также как в системе Оберон, то есть через сообщения.

3) Операции, специфичные для типов-листов, оформляются в виде статических процедур.

4) Мы старались исключить использование процедурных переменных, заменяя их парой объект-метод, так как процедурные переменные потенциально опасны при выгрузке модуля; кроме того использование объекта вместо процедурного значение позволяет передавать контекст операции (см. также итерацию директории в 4.3.6).

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

При итерации особое внимание необходимо уделять целостности структуры. Что произойдет, если во время итерации директории будут создаваться или удаляться файлы?

Для обеспечения целостности итерация может выполняться на копии структуры или просто выдавать копию структуры (вернее ее публичной части). В каждом конкретном случае (см. 4.3) мы постараемся дать обоснование выбранному методу.

4.1.3. Повышение переносимости и адаптируемости

Для повышения переносимости системы необходимо выдержать разделение ее на переносимые и системно-зависимые (или машинно-зависимые) части. Мы предполагаем, что система Мифрил будет работать в основном над некоторой стандартной ОС, или некоторым стандартным окружением (например, X Windows). Часть модулей ядра (Файлы, Загрузчик) зависят от системы и должны быть модифицированы при переносе. Для таких модулей важно спроектировать интерфейс, независимый от платформы. Интерфейс должен соответствовать двум (противоречивым) требованиям:

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

Рассмотрим подробнее, как эти требования могут быть удовлетворены на примере модуля Files. Остановимся на двух различиях в реализации файлов в системах Unix и MS-DOS.

В ОС Unix (и в ОС Excelsior) принято отделять понятие физического файла от его именования. Одному физическому файлу может соответствовать несколько имен. Операция именования файла (т.е. добавления еще одного имени) и операция удаления имени отделены от операций создания/удаления файла.

В MS-DOS каждый файл изначально именован и нет возможности выделить операции над именами.

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

Другое различие в этих системах связано с понятием защиты файла, которая отсутствует в MS-DOS. Мы полагаем, что операции модификации битов защиты файла являются достаточно специфичными и могут быть реализованы в отдельных системно-зависимых модулях. Поэтому такие операции не входят в интерфейс модуля Files.

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

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

Ядро системы определяет два абстрактных драйвера: драйвер экрана (Screens.Screen) и драйвер фонта (Fonts.Font). Конкретный драйвер экрана реализует графические примитивы (точка, линия, окружность, символ) для конкретного экрана. Каждое окно содержит ссылку на драйвер того экрана, на котором оно расположено, и выполняет все графические операции через операции этого драйвера. Это позволяет получить полностью независимый от аппаратуры механизм управления окнами.

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

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

4.2. Динамическая поддержка

Модуль динамической поддержки реализует операции выделения памяти, финализации и сборки мусора. Реализация модуля для систем Mithril/Excelsior и Mithril/MS-DOS выполнена А. Хапугиным.

Для выделения памяти и для сборки мусора используется информация о типовой системе, порожденная компилятором с языка Оберон-2. Модуль динамической поддержки неявно импортируется в любой модуль на языке Оберон-2. При инициализации любого модуля вызывается процедура, которая по информации, порожденной компилятором, строит некоторое внутреннее представление, удобное для использования.

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

Модуль содержит большой набор процедур выделения памяти, включающий в себя:

  • выделения памяти под запись;
  • выделение памяти под статический массив;
  • выделение памяти под динамический n-мерный массив;
  • выделение системной памяти (т.е. памяти, которая не обслуживается сборщиком мусора).

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

Для сборки мусора используется mark-and-sweep алгоритм, близкий к алгоритму, описанному в [35]. Алгоритм состоит из двух фаз: фазы маркировки и фазы освобождения памяти. В фазе маркировки алгоритм обходит все доступные по указателям объекты, начиная обход с глобалов всех модулей и с локалов на процедурном стеке. Все доступные объекты помечаются. После этого выполняется финализация объектов, которые содержатся в одном из списков финализации (см. 4.1.1) и не помечены. В фазе освобождения памяти все непомеченные объекты возвращаются в список свободной памяти.

Сборка мусора может запускаться системой периодически или при нехватке некоторого ресурса (не обязательно памяти). Так, например, многие ОС (e.g. Unix) накладывают ограничения на число одновременно открытых файлов, и сборка мусора может быть запущена при открытии файла.

4.3. Ядро системы

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

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

4.3.1. Объекты и финализация

Как уже говорилось, тип Objects.Object является корнем иерархии типов (см. 4.1.2). Кроме этого типа, в модуле Objects определяется еще два часто используемых типа. Тип Message - это базовый тип для всех сообщений (в стиле системы Оберон), а тип String определяет динамическую строку.


Из описаний модуля Objects:

TYPE

  Object = POINTER TO ObjectDesc;

  ObjectDesc = RECORD END;

  Message = RECORD END;

  String = POINTER TO ARRAY OF CHAR;


Модуль Closure определяет понятие нетрассируемого списка (4.1.1) и операции над ним. Тип Trailer задает голову такого списка, а тип Link - его элемент. Заметим, что Link - это нетрассируемый указатель на запись.

TYPE

  Close = PROCEDURE (obj: Objects.Object);

  Link = POINTER TO [1] RECORD

    obj-: Objects.Object;

    next-: Link;

  END;

  Trailer = POINTER TO RECORD (Objects.ObjectDesc)

    link-: Link;

  END;

 

PROCEDURE new_trailer(c: Close): Trailer;

(* Создание нового списка с указанной процедурой финализации *)

 

PROCEDURE insert(x: Trailer; o: Objects.Object);

(* Добавление объекта к списку *)


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

4.3.2. Реакция на ошибки

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

Контекст ошибки - это объект, содержащий поле - код ошибки и метод, позволяющий получить в строке текст сообщения об ошибке.

TYPE

  Error = POINTER TO ErrorDesc;

  ErrorDesc = RECORD

    code*: LONGINT;    -- код ошибки

    PROCEDURE (e: Error) perror(VAR msg: ARRAY OF CHAR);

    (* выдает в строку текст сообщения об ошибке *)

  END;


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

Введение этого типа позволяет в широких пределах варьировать обработку ошибки. Могут быть использованы следующие варианты:

1) Трансляция ошибки

  PROCEDURE DoIt(VAR res: Errors.Error);

  BEGIN

    ...

    Try(res);

    IF res#NIL THEN RETURN END;

    ...

  END DoIt;


Здесь и в следующих примерах:


PROCEDURE Try(VAR res: Errors.Error); ... END Try;


2) Реакция на код ошибки

  PROCEDURE Find(....): BOOLEAN;

  BEGIN

    Try(res);

    IF res=NIL THEN RETURN TRUE

    ELSIF res.code=Errors.no_entry THEN RETURN FALSE

    ELSE HALT(res.code);    -- аварийное завершение команды

    END;

  END Find;


3) Выдача сообщения об ошибке

  Try(res);

  IF res#NIL THEN

    res.perror(msg);

    print(msg);

  END;


4) Разбор контекста ошибки

  Try(res);

  IF res#NIL THEN

    WITH res: MyError DO ....

    | res: Files.Error DO ...

    ELSE HALT(res.code)

    END;

  END;


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

Второе понятие, определяемое этим модулем, это реакция на ошибку.

TYPE

  Exception = POINTER TO ExceptionDesc;

  ExceptionDesc = RECORD

    PROCEDURE (x: Exception) raise(e: Error);

  END;

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

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

Приведем пример на базе модуля Files:

TYPE

  File = POINTER TO FileDesc;

  FileDesc = RECORD

    res*: Errors.Error;

    exc*: Errors.Exception;

    ....

  END;

  Error = RECORD (Errors.Error)

    name*: FileName;

  END;

 

PROCEDURE set_length(f: File; len: LONGINT);

  VAR e: Error; error_code: LONGINT;

BEGIN

  error_code:=try_set_length();

  IF error_code#0 THEN    -- не получилось установить длину

    NEW(e);

    e.name:=f.name;

    f.res:=e;

    IF f.exc=NIL THEN     -- реакция не определена

      HALT(e.code);       -- реакция по умолчанию

    ELSE

      f.exc.raise(e);     -- реакция, определенная пользователем

    END;

  END;

END set_length;

4.3.3. Каналы и объекты доступа

Данный раздел базируется на идеях, высказанных в работах [6, 34]. Основная идея заключается в разделении операций передачи данных и операций форматирования данных при реализации подсистемы ввода/вывода. Мы будем называть объекты, определяющие передачу данных, - каналами (файл, терминал, ...), а объекты, определяющие операции форматирования (структуру данных), - объектами доступа.

Такое разделение позволяет независимо расширять как каналы, так и объекты доступа и получить систему, в которой любой объект доступа может использоваться вместе с любым каналом. Мы поддерживаем схему N:1, то есть множество объектов доступа может быть связано с одним каналом, но каждый объект доступа связан с не более чем одним каналом. Объект доступа может взаимодействовать с каналом через операции, определенные в абстрактных классах. В некоторых случаях канал должен иметь возможность ассоциировать с объектом доступа некоторые дополнительные атрибуты. Для этого вводиться дополнительный тип (Link), задающий связь между каналом и объектом доступа.

Тип Channel определяет (абстрактный) тип канала, тип Rider - абстрактный объект доступа. Метод attach присоединяет объект доступа к каналу.


Из описаний модуля Channels:

TYPE

  Channel = POINTER TO ChannelDesc;

  ChannelDesc = RECORD (Objects.ObjectDesc)

    res*: Errors.Error;

    exc*: Errors.Exception;

 

    PROCEDURE (c: Channel) attach(r: Rider);

      (* присоединение объекта доступа к каналу *)

    PROCEDURE (c: Channel) get_bytes(r: Rider;

                 VAR x: ARRAY OF SYSTEM.BYTE; pos,len: LONGINT);

      (* чтение из канала неструктурированных данных *)

    PROCEDURE (c: Channel) put_bytes(r: Rider;

                 VAR x: ARRAY OF SYSTEM.BYTE; pos,len: LONGINT);

      (* запись в канал неструктурированных данных *)

    PROCEDURE (c: Channel) get_pos(r: Rider): LONGINT;

      (* текущая позиция объекта доступа в канале *)

    PROCEDURE (c: Channel) set_pos(r: Rider; pos: LONGINT);

      (* изменение текущей позиции *)

END;

 

Link = POINTER TO LinkDesc;

LinkDesc = RECORD END;

Rider = POINTER TO RiderDesc;

RiderDesc = RECORD (Objects.ObjectDesc)

  res*: Errors.Error;

  exc*: Errors.Exception;

  base-: Channel;

  link-: Link;

  PROCEDURE (r: Rider) get(VAR x: SYSTEM.BYTE);

  PROCEDURE (r: Rider) get_int(VAR i: LONGINT);

  PROCEDURE (r: Rider) get_real(VAR x: REAL);

  PROCEDURE (r: Rider) get_str(VAR s: ARRAY OF CHAR);

  ...

  PROCEDURE (r: Rider) put(x: SYSTEM.BYTE);

  PROCEDURE (r: Rider) put_int(x: LONGINT);

  PROCEDURE (r: Rider) put_real(x: REAL);

  PROCEDURE (r: Rider) put_str(s: ARRAY OF CHAR);

  ...

END;

Модуль XRiders определяет стандартный объект доступа, который читает и пишет данные в переносимом бинарном формате.

Тип Files.File расширяет тип Channel (см. 4.3.6).

4.3.4. Загрузчик

Модуль Loader реализует операции загрузки/выгрузки модуля, динамический вызов команды и динамического выделения памяти. Операция динамического выделения памяти, то есть выделения памяти с указанием типа записи в виде пары (ИмяМодуля, ИмяТипа) отсутствует в системе Оберон и была предложена в работе [36].

Из описаний модуля Loader:

TYPE

  Module = POINTER TO ModuleDesc;

  ModuleDesc = RECORD (Objects.ObjectDesc)

  END;

 

PROCEDURE lookup(name: ARRAY OF CHAR; VAR m: Module;

                                      VAR res: Errors.Error);

(* Возвращает по имени модуль. Если модуль не загружен,

   то пытается загрузить его.

*)

 

PROCEDURE unload(name: ARRAY OF CHAR; VAR res: Errors.Error);

(* Выгружает модуль *)

 

PROCEDURE call(m: Module; cmd: ARRAY OF CHAR;

                      VAR res: Errors.Error);

(* Вызов команды *)

 

PROCEDURE new(m: Module; type: ARRAY OF CHAR;

                         VAR res: Errors.Error): Objects.Object;

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


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


PROCEDURE parse(command: ARRAY OF CHAR; VAR e: Entry);


Здесь тип Entry определен как:

TYPE

  Entry = RECORD

    module: Name;

    entry : Name;

    new : BOOLEAN;

    res : Errors.Error;

  END;


Строка command может быть представлена как

ИмяМодуля "." ИмяКоманды

или

ИмяМодуля "." "^" ИмяТипа

Во втором случае, строка обозначает операцию динамического выделения памяти для указателя на тип, задаваемые парой (ИмяМодуля, ИмяТипа). Поле new при этом выставляется в TRUE.

Данный модуль является системно-зависимым. Версии модуля для версий Mithril/Excelsior и Mithril/MS-DOS реализованы А. Хапугиным.

4.3.5. Постоянные объекты

Постоянными объектами мы будем называть объекты, для которых определены операции записи и восстановления. В системе Оберон понятие постоянного объекта не выделено. Поэтому такие объекты определяются в каждом конкретном случае (например, Элементы в редакторе Write [26] или графические объекты в редакторе Condor [31]). Мы полагаем, что понятие постоянного объекта позволяет унифицировать и упростить разработку различных приложений.

Постоянные объекты определяются в модуле Storage. Для такого объекта определены методы записи и восстановления атрибутов объекта, метод инициализации и метод, выдающий имя команды создания объекта.


Из описаний модуля Storage:

TYPE

  Object = POINTER TO ObjectDesc;

  ObjectDesc = RECORD (Objects.ObjectDesc)

    PROCEDURE (o: Object) externalize(r: XRiders.Rider);

      (* записывает атрибуты объекта в объект доступа *)

    PROCEDURE (o: Object) allocator(VAR s: ARRAY OF CHAR);

      (* возвращает строку команды создания объекта *)

    PROCEDURE (o: Object) constructor;

      (* инициализирует объект *)

    PROCEDURE (o: Object) internalize(r: XRiders.Rider);

      (* читает атрибуты объекта из объекта доступа *)

  END;

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

OBJ_TAG команда_создания_объекта длина атрибуты_объекта

или

NIL_TAG

если записывается объект со значением NIL.


Процедура put_object записывает произвольный объект в объект доступа. Процедура get_object - восстанавливает объект.


PROCEDURE put_object(r: XRiders.Rider; o: Object);

PROCEDURE get_object(r: XRiders.Rider; VAR o: Object;

                                       VAR res: Errors.Error);


Если модуль, имя которого определено в команде, не удалось загрузить, то процедура пропускает атрибуты объекта и возвращает ошибку. Дополнительная процедура get_header используется в тех случаях, когда пропускать атрибуты объекта не надо (например, при чтении элемента в тексте; см. 4.3.8).

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

Модуль Files определяет тип File, являющийся расширением типа Channels.Channel и набор процедур открытия, создания, закрытия файлов, доступа к атрибутам файла и итерации директории. Заметим, что операции чтения и записи определены только для объекта доступа. К файлу можно присоединить произвольный объект доступа, при этом методы attach и set_pos (см. 4.3.3) присоединяют специальный объект связи (Link) к объекту доступа.

Из описаний модуля Files:

  File = POINTER TO FileDesc;

  FileDesc = RECORD (Channels.ChannelDesc)

  END;

  Link = POINTER TO LinkDesc;

  LinkDesc- = RECORD (Channels.LinkDesc)

    iolen-: LONGINT; (* длина последней операции *)

    fpos-: LONGINT; (* позиция в файле *)

    eof-: BOOLEAN; (* признак конца файла *)

  END;

 

PROCEDURE create(dir: File; path,mode: ARRAY OF CHAR): File;

PROCEDURE open(dir: File; path,mode: ARRAY OF CHAR): File;

(* Если dir=NIL, то файл открывается/создается

   на текущей директории.

*)

 

PROCEDURE close(f: File);

(* Закрытие файла *)


Расширение типа File не поддерживается, хотя и не запрещается. Все операции над файлом определены как статические процедуры. Приведем пример, чтения массива целых чисел из файла.

PROCEDURE read(path: ARRAY OF CHAR; VAR ints: ARRAY OF LONGINT);

  VAR f: Files.File; R: XRiders.Rider; i: LONGINT;

BEGIN

  f:=Files.open(NIL,path,"r");

  IF f.res#NIL THEN (* реакция на ошибку *) END;

  NEW(R);

  f.attach(R);            -- присоединили объект доступа к файлу

  FOR i:=0 TO LEN(ints)-1 DO

    R.get_longint(ints[i])

  END;

  Files.close(f);

END read;

По умолчанию для файла устанавливается реакция на ошибку (Exception, см. 4.3.2), которая вызывает прекращение выполнения команды для всех ошибок, кроме ошибок открытия и создания файла.

Для итерации директории определяется специальный тип итератора. Процедура iter_dir вызывает метод entry для каждого файла в директории.

TYPE

  Iter = RECORD

    PROCEDURE (VAR i: Iter) entry(name: ARRAY OF CHAR;

                                          class: SET);

  END;

 

PROCEDURE iter_dir(dir: File; VAR iter: Iter);

(* Итератор директории; вызывает метод iter.entry для

   каждого файла в директории.

*)

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

TYPE

  Node = POINTER TO NodeDesc;

  NodeDesc = RECORD

    name : Name;

    class: SET;

    next : Node;

  END;

  IterList = RECORD (Files.Iter)

    head: Node;

  END;

 

PROCEDURE (i: Iter) entry(name: ARRAY OF CHAR; class: SET);

  VAR n: Node;

BEGIN

  NEW(n); COPY(name,n.name); n.class:=class;

  n.next:=i.head; i.head:=n; -- ввязали в линейный список

END entry;


На этом примере видно важное отличие использования в итерации объекта вместо процедурного значения. Кроме действия (метода) объект содержит некоторый контекст. При использовании процедуры нам бы пришлось определять этот контекст (в данном случае переменную head) на глобальном уровне.

Данный модуль является системно-зависимым. Для систем Mithril/Kronos и Mithril/MS-DOS он реализован на базе библиотеки BIO, которая является стандартной библиотекой ОС Excelsior и была перенесена в среду MS-DOS при переносе компилятора.

4.3.7. Фонты

Модуль Fonts определяет абстрактный тип Font. Каждый фонт имеет атрибуты: высоту, максимальную ширину символа и некоторые другие. Конкретный фонт определяется как расширение абстрактного. Каждый конкретный фонт должен реализовать два метода: char_attr и width, которые возвращают атрибуты символа. Каждый драйвер экрана работает с некоторым подмножеством конкретных фонтов.


TYPE

  Font = POINTER TO FontDesc;

  FontDesc = RECORD (Storage.ObjectDesc)

    h: INTEGER;           -- высота фонта

    w: INTEGER;           -- максимальная ширина символа

    name: Objects.String; -- имя фонта

    PROCEDURE (f: Font) char_attr(ch: CHAR;

                                     VAR dx,x,y,w,h: INTEGER);

    (* возвращает атрибуты символа *)

    PROCEDURE (f: Font) width(ch: CHAR): INTEGER;

    (* возвращает ширину символа *)

  END;


Процедура this реализует поиск фонта по имени. Все загруженные фонты связаны в нетрассируемый список (см 4.3.1). Неиспользуемый фонт будет удален из списка при выполнении операции сборки мусора. Если фонт не загружен, то процедура this пытается его загрузить, вернее прочитать его из файла с именем, построенным стандартным образом по имени фонта. Приведем схему реализации этой процедуры:

VAR fonts: Closure.Trailer; -- нетрассируемый список всех фонтов

 

PROCEDURE this(name: ARRAY OF CHAR): Font;

  VAR l: Closure.Link; f: Font;

BEGIN

  l:=fonts.link;

  WHILE l#NIL DO

    IF l.obj(Font).name=name THEN RETURN l.obj(Font) END;

    l:=l.next;

  END;

  RETURN read_font(name)

END this;

 

PROCEDURE read_font(name: ARRAY OF CHAR): Font;

  VAR f: Files.File; R: XRiders.Rider; o: Objects.Object;

BEGIN

  f:=Files.open(NIL,name,R);

  NEW(R); f.attach(R);

  Storage.get_object(R,o);     -- читаем объект из файла

  WITH o: Font DO              -- проверяем, что прочитали фонт

    Closure.insert(fonts,o);   -- присоединяем его к списку

    RETURN o

  END;

END read_font;


Так как фонт читается из файла, как объект, то после чтения мы получаем некоторое расширение (конкретизацию) фонта. Система может одновременно использовать разные виды фонтов с различным представлением. Для подключения нового фонта к системе достаточно оформить его в виде объекта в стандарте модуля Storage.

Специальный механизм реализован в данном модуле для поддержки так называемых "сессий". Рассмотрим операцию записи текста, в котором используется много различных фонтов. Для каждого фрагмента текста нам необходимо указать фонт, который для него используется. Так как размер данных для фонта достаточно велик, то для указания фонта мы будем записывать его имя. Если один фонт используется для различных фрагментов, то объем записываемой информации можно существенно уменьшить, если пронумеровать все фонты, используемые в данном тексте, и записывать имя фонта только при первом использовании, а при следующих указывать только его локальный номер. Для ведения такой локальной нумерации мы использует метод, аналогичный методу, описанному в [36].

Кроме уменьшения объема информации сессия ускоряет чтение текста, так как повторное обращение к некоторому фонту выполняется за константное время (не содержит поиска).

4.3.8. Тексты

Модуль Texts определяет понятие текста, близкое к соответствующему понятию системы Оберон, но с двумя существенными изменениями:

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

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

Система Мифрил (вслед за системой Оберон) поддерживает достаточно сложное понятие текста. Каждая литера в тексте обладает своим набором атрибутов, а именно цветом, вертикальным смещением относительно базовой линии и принадлежит к некоторому фонту.

Кроме типа Text модуль определяет тип Elem (абстрактную литеру), объекты доступа (чтения - Reader, Scanner и записи - Writer) и объект (Buffer), позволяющий работать с фрагментом текста, как с атомарным объектом. Структура текста скрыта в данном модуле.

Из описаний модуля Texts:

  Buffer = POINTER TO BufferDesc;

  BufferDesc = RECORD (Objects.ObjectDesc)

    len-: LONGINT;              -- текущая длина

 

    PROCEDURE (b: Buffer) append(a: Buffer);

      (* добавление буфера *)

    PROCEDURE (b: Buffer) copy(a: Buffer);

      (* копирование буфера *)

    PROCEDURE (b: Buffer) open;

      (* инициализцаия (пустого) буфера *)

  END;


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

  Text = POINTER TO TextDesc;

  TextDesc = RECORD (Storage.ObjectDesc)

    len-: LONGINT;              -- длина текста

 

    PROCEDURE (T: Text) open;

      (* инициализация (пустого) текста *)

    PROCEDURE (T: Text) append(a: Buffer);

      (* добавление буфера к концу текста *)

    PROCEDURE (T: Text) insert(pos: LONGINT; i: Buffer);

      (* вставка буфера в позицию pos *)

    PROCEDURE (T: Text) delete(beg,end: LONGINT);

      (* удаление фрагмента текста [beg..end[ *)

    PROCEDURE (T: Text) change_looks(beg,end: LONGINT; sel: SET;

                     font: Fonts.Font; col: SET; voff: INTEGER);

      (* изменение атрибутов фрагмента текста *)

    PROCEDURE (T: Text) save(pos,end: LONGINT; b: Buffer);

      (* копирование фрагмента текста в буфер *)

  END;


Текст - это массив литер с номерами в диапазоне [0..len-1]. Операции над текстом позволяют вставить буфер в указанную позицию, удалить фрагмент текста, изменить атрибуты фрагмента и скопировать фрагмент текста (с атрибутами) в буфер. Внутреннее представление текста реализовано в виде двусвязного списка фрагментов текста с одинаковыми атрибутами, что позволяет достаточно быстро выполнять все операции над текстом.

Важно заметить, что текст является постоянным объектом, то есть его можно записать в файл и прочитать, как целое.

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

  Noticer = POINTER TO NoticerDesc;

  NoticerDesc = RECORD (Objects.ObjectDesc)

    PROCEDURE (n: Noticer) notify(T: Text; op: INTEGER;

                                      beg,end: LONGINT);

      (* сообщение об изменении текста в диапазоне [beg..end[;

         op принимает значения insert, delete, replace.

      *)

  END;

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

Рассмотрим далее объекты доступа к тексту:

  Reader = POINTER TO ReaderDesc;

  ReaderDesc = RECORD (Objects.ObjectDesc)

    text-: Text;

    pos-: LONGINT;         -- текущая позиция в тексте

    eot*: BOOLEAN;         -- признак конца текста

    col*: SET;             -- цвет последней прочитанной литеры

    voff*: INTEGER;        -- вертикальное смещение

    font*: Fonts.Font;     -- фонт

    elem*: Elem;

 

    PROCEDURE (r: Reader) open(T: Text; pos: LONGINT);

      (* устанавливает объект чтения в позицию pos *)

    PROCEDURE (r: Reader) read(VAR ch: CHAR);

      (* читает очередной символ из текста *)

  END;


Объект чтения Reader позволяет прочитать из текста литеру и узнать ее атрибуты (поля col, voff, font). Если прочитанная литера является элементом, то соответствующий объект выставляется в поле elem. После чтения литеры текущая позиция увеличивается.

Дополнительный объект чтения (Scanner) выполняет при чтении лексический анализ и позволяет получить число (записанное в одном из принятых в Модуле-2 и Обероне-2 формате), строку, идентификатор или специальную литеру.

  Writer = POINTER TO WriterDesc;

  WriterDesc = RECORD (Objects.ObjectDesc)

    buf-: Buffer;

    col*: SET;

    voff*: INTEGER;

    font*: Fonts.Font;

    PROCEDURE (w: Writer) open;

    PROCEDURE (w: Writer) element(e: Elem);

    PROCEDURE (w: Writer) write(ch: CHAR);

    PROCEDURE (w: Writer) print(fmt: ARRAY OF CHAR;

                                       SEQ x: BYTE);

    PROCEDURE (w: Writer) set_color(col: SET);

    PROCEDURE (w: Writer) set_font(f: Fonts.Font);

    PROCEDURE (w: Writer) set_offset(voff: INTEGER);

  END;


Объект записи позволяет добавить к буферу литеру, элемент или последовательность литер. Процедуры set_* позволяют установить атрибуты литер.

Если W - объект записи, а T - текст, то последовательность вызовов:

  W.open;                -- инициализация пустого объекта записи

  W.set_color(red);      -- изменили цвет

  W.print('Hello, ');

  W.set_color(yellow);   -- изменили цвет

  W.print('world!');

  T.insert(10,W.buf);

вставит в текст в позицию 10 предложение "Hello, world!", причем первое слово будет изображено красным, а второе – желтым цветом. После вставки операция insert вызовет сообщение нотификации

  n.notify(T,insert,10,24)

для всех объектов, присоединенных к тексту T (то есть для всех объектов, изображающих этот текст).

Осталось рассказать об абстрактных литерах (элементах):

  Elem = POINTER TO ElemDesc;

  ElemDesc = RECORD (Storage.ObjectDesc)

    dx,w,h: INTEGER;

    alien-: Objects.String;

    text-: Text;

    PROCEDURE (e: Elem) copy(VAR x: Elem);

      (* копирование элемента *)

    PROCEDURE (e: Elem) handle(VAR M: Objects.Message);

      (* управление элементом *)

  END;

Элемент является постоянным объектом и обладает геометрическими размерами. Строка alien содержит текст команды создания элемента, если при чтении текста не удалось загрузить модуль, реализующий данный элемент. При записи такого "чужого" элемента будут скопированы атрибуты объекта, что позволяет при восстановлении модуля, реализующего объект, восстановить изображение и поведение элемента. Набор методов для изображения элементов определяется в модуле Elements. В целом реализация элементов близка к описанной в [26] (см. также 3.3).

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

PROCEDURE attach(T: Text; n: Noticer);

(* Добавляет к тексту объект нотификации. *)

 

PROCEDURE broadcast(T: Text; pos,end: LONGINT;

                               VAR m: Objects.Message);

(* Посылает сообщение всем элементам в диапазоне [beg..end[. *)

 

PROCEDURE open(T: Text; fname: ARRAY OF CHAR);

(* Открывает текст, хранящийся в файле с именем fname.

   Если этот файл не содержит информации об атрибутах,

   то считается, что все литеры текста имеют некоторые

   стандартные атрибуты.

*)

 

PROCEDURE store(T: Text; f: Files.File; pos: LONGINT;

                                    VAR len: LONGINT);

(* Записывает текст в файл, начиная с позиции pos. *)

4.3.9. Оконная система

Оконная (под)система в системе Мифрил реализована А. Никитиным. Оконная система достаточно подробно описана в работе [37]. В данном разделе мы перечислим только основные особенности системы.

Ядро подсистемы состоит из двух модулей: Windows и Screens. Модуль Screens определяет тип Screen - абстрактный драйвер экрана. Расширение (конкретизация) экрана реализует операции рисования графических примитивов, изменения палитры и работы с курсором для конкретного вида графического устройства. Система позволяет одновременно работать на нескольких физических экранах. Каждому экрану соответствует некоторое окно. Все окна-экраны расположены на мета-окне.

Модуль Windows определяет базовый тип окна (Window). Каждое окно может содержать подокна. Иерархия окон ничем не ограничена. Каждое окно имеет имя, и модуль определяет операции доступа к окну по пути, аналогично поиску файла в иерархической файловой системе. Окно является постоянным объектом, при записи окна записываются и все его подокна. Механизм именования позволяет исключить необходимость в явных ссылках на подокно и обеспечить восстановление всей иерархии окон при чтении из файла.

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

4.3.10. Инициализация и основной цикл системы

В предыдущих пунктах (4.3.1-4.3.9) мы описали основные понятия системы. Модуль Kеrnel создает среду исполнения и интерфейс пользователя на базе этих понятий. Запуск системы Мифрил происходит при запуске данного модуля. При инициализации модуля происходит создание драйвера первичного экрана и создание окна, соответствующему этому стандартному экрану. После этого модуль выполняет набор команд, задающих конфигурацию системы. Стандартная (минимальная) конфигурация состоит из двух окон: окна стандартного вывода (System.Log) и окна, содержащего набор команд (System.Tool). Заметим, что оба этих окна являются текстовыми (см. 4.4.2) расширениями Windows.Window и реализованы выше уровня ядра.

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

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

4.4. Оболочка системы

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

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

4.4.1. Поддержка разработки пользовательского интерфейса

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

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

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

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

Исследования показывают, что для различных приложений часть приложения, реализующего пользовательский интерфейс, может составлять от 29 до 88% (см. например [38]). Поэтому возможность отделить приложение (в виде набора акторов) от интерфейса является очень важной и позволяет резко ускорить разработку приложений.

Кроме селекторов, оболочка реализует стандартную панель, состоящую из заголовка, меню, рабочего окна и рамки, рабочие органы которой позволяют изменять размеры и местоположение окна. Заголовок, меню и рабочее окно являются окнами и могут быть установлены после создания панели. Как правило, меню и рабочее окно являются текстовыми окнами (см. 4.4.2).

4.4.2. Текстовые окна

Мы уже несколько раз упоминали особую роль текстовых окон в системе. Текстовые окна реализуются модулем TWindows. Текстовые окна изображают некоторый текст, возможно, содержащий элементы. Для изображения элемента текстовое окно посылает элементу сообщения, определенные в модуле Elements. Рисование строки выполняется в два прохода по тексту. На первом проходе вычисляется высота и ширина строки, при этом элементу посылается сообщение "приготовься к рисованию", и элемент может изменить свои размеры. На втором проходе обычные символы рисуются, и каждому элементу посылается сообщение "нарисуй себя".

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

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

4.4.3. Текстовые элементы

Как показано в 3.3, текстовые элементы могут быть расширены различными способами, существенно увеличивая возможности редактора. В системе Мифрил в настоящее время реализованы два расширения элементов - элементы пометки и командные элементы.

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

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

В дальнейшем мы планируем реализовать большую часть элементов, описанных в [26], в первую очередь элементы, поддерживающие свойства гипертекста.

4.4.4. Основные наборы команд

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

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

Каждая команда может иметь аргументы. Доступ к аргументам команды полностью аналогичен доступу, принятому в системе Оберон.

4.5. Редактор формул, как пример разработки приложения

В данном пункте мы опишем структуру некоторого простого редактора математических формул. Мы будем представлять формулу в виде элемента в тексте. Каждая формула состоит из литер различных алфавитов и специальных символов, таких как корень, интеграл, сумма, деление и так далее. Базовый уровень текста в системе позволяет нам получить текст, состоящий из литер различных алфавитов. Для этого достаточно иметь набор нужных фонтов (например, фонт с греческим алфавитом). Следовательно, нам достаточно определить изображения специальных символов.

Следующий важный вопрос - это способ задания формулы. Мы полагаем, что наиболее удобным является представление формулы на некотором простом функциональном языке, например:

div("a+b","c+d")

                            a+b

должно быть отображено в    ---.

                            c+d


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

Перейдем теперь к реализации такого редактора. С точки зрения текста формула является элементом. Элемент содержит объект (модель), описывающий внутреннюю структуру формулы. Формула состоит из различных объектов, являющихся расширениями некоторого абстрактного объекта, назовем его частью (Piece). Такой объект является постоянным объектом (мы должны уметь хранить формулы в файле), и над ним должна быть определена операция рисования и операция подготовки к рисованию (далее мы объясним это подробнее).

TYPE

  Piece = POINTER TO PieceDesc;

  PieceDesc = RECORD (Storage.Object)

    w,h: INTEGER;

    PROCEDURE (p: Piece) prepare;

    PROCEDURE (p: Piece) draw(v: Windows.Window; x,y: INTEGER);

  END;


Очевидным расширением такого абстрактного объекта, является объект, содержащий некоторый текст.

TYPE

  TextPiece = POINTER TO TextPieceDesc;

  TextPieceDesc = RECORD (PieceDesc)

    text: Texts.Text;

  END;


При выполнении операции prepare объект вычисляет размеры объемлющего прямоугольника, а операция draw изображает текст в прямоугольнике, левый нижний угол которого задан точкой (x,y). В описании формулы текстовая часть задается строкой, например "a+b". Важно заметить, что при создании такого объекта символы строки копируются вместе со своими атрибутами.

Рассмотрим теперь определение специального символа, на примере операции деления, изображаемой горизонтальной чертой.

TYPE

  Div = POINTER TO DivDesc;

  DivDesc = RECORD (PieceDesc)

    up: Piece;    -- верхняя часть (над дробной чертой)

    dw: Piece;    -- нижняя часть (под дробной чертой)

  END;


Реализуем методы подготовки и рисования:

PROCEDURE (d: Div) prepare;

BEGIN

  d.up.prepare;              -- вычислили размеры верхней части

  d.dw.prepare;              -- вычислили размеры нижней части

  d.w:=max(d.up.w,d.dw.w);   -- ширина деления равна ширине

                             -- большей части

  d.h:=d.up.h+d.dw.h+dH;     -- высота деления равна сумме высот

                             -- плюс некоторая константа

END prepare;

 

PROCEDURE (d: Div) draw(v: Windows.Window; x,y: INTEGER);

  VAR Y: INTEGER;

BEGIN

  d.dw.draw(v,x+(d.w-d.dw.w) DIV 2,y);

  -- нарисовали нижнюю часть

  d.up.draw(v,x+(d.w-d.up.w) DIV 2,y+d.dw.h+dH);

  -- нарисовали верхнюю часть

  Y:=y+d.dw.h;

  wnd.line(v,x,Y,x+d.w-1,Y);

  -- нарисовали черту между частями

END draw;


Эти две процедуры полностью реализуют рисование деления, разве что в настоящем редакторе формул желательно вычислять ширину разделяющей линии в зависимости от высоты частей. Определив деление, мы можем строить произвольные формулы, состоящие из делений, например: div("a+b",div("c+d","x*y")). Также просто реализуются операции записи и чтения объекта.

PROCEDURE (d: Div) externalize(r: XRiders.Rider);

BEGIN

  Storage.put_object(d.up);

  Storage.put_object(d.dw);

END externalize;

 

PROCEDURE (d: Div) internalize(r: XRiders.Rider);

  VAR o: Objects.Object; res: Errors.Error;

BEGIN

  Storage.get_object(r,o,res); IF res#NIL THEN ... END;

  d.up:=o(Piece);

  Storage.get_object(r,o,res); IF res#NIL THEN ... END;

  d.dw:=o(Piece);

END internalize;


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

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

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

- строка :

создается объект TextPiece и вставляется в список операндов;

- число  :

вставляется в список операндов;

- функция:

вызывается команда, определенная именем функции; эта команда должна создать объект, являющийся расширением Piece; после чего вызывается метод инициализации объекта с параметром - списком аргументов. Готовый объект вставляется в список операндов.


Описывая тип Piece (см. выше) мы опустили метод инициализации объекта. Полное описание этого типа выглядит так:

TYPE

  Piece = POINTER TO PieceDesc;

  PieceDesc = RECORD (Storage.Object)

    w,h: INTEGER;

 

    PROCEDURE (p: Piece) prepare;

    PROCEDURE (p: Piece) draw(v: Windows.Window; x,y: INTEGER);

    PROCEDURE (p: Piece) init(n: Node);

  END;


Здесь тип Node определен как:

  Node = POINTER TO NodeDesc;

  NodeDesc = RECORD

    class: (piece,int,real);

    piece: Piece;

    int : LONGINT;

    real : LONGREAL;

    next : Node;

  END;

При разборе описания: div("a+b","c+d") будет построен список из двух узлов, содержащих части, соответствующие текстам "a+b" и "c+d", и этот список будет передан методу инициализации объекта типа "Div".

PROCEDURE (d: Div) init(n: Node);

BEGIN

  IF n=NIL THEN error("мало аргументов"); RETURN END;

  IF n.class#piece THEN error("не тот аргумент"); RETURN END;

  d.up:=n;

  n:=n.next;

  IF n=NIL THEN error("мало аргументов"); RETURN END;

  IF n.class#piece THEN error("не тот аргумент"); RETURN END;

  IF n.next#NIL THEN error("лишний аргумент"); RETURN END;

END init;


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

Головной модуль редактора формул реализует единственную команду "Compile", которая разбирает описание формулы, создает элемент и вставляет его в позицию курсора. Методы элемента транслируются в методы корневой части формулы. Ядро редактора, включающее текстовые части и процедуру разбора, состоит из 300 строк (!) на языке Оберон-2.

4.6. Методы переноса системы

В настоящее время выполнен перенос системы в среду MS-DOS для старших моделей PC. Для переноса использовались переносимые компиляторы с генерацией для i386/486 [24].

Основной сложностью была реализация специальной утилиты – загрузчика (DOS extender), который переводит процессор в защищенный режим и обеспечивает доступ к базовым функциям операционной системы. Реализация такого загрузчика была выполнена О. Шатохиным.

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

  • драйвера экрана. Использовалась графическая библиотека, перенесенная с OS Excelsior;
  • драйвера фонтов;
  • драйвера ввода (мышь, клавиатура);
  • файловой системы.

Не считая графической библиотеки, которая была перенесена независимо, при переносе пришлось написать 1400 строк на Модуле-2 (модуль динамической поддержки) и около 2000 строк на Обероне-2 (динамический загрузчик, файловая система, драйверы).

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

4.7. Выводы

Система Оберон является первым примером переносимой расширяемой системы. Разработка системы Мифрил проводилась на основе анализа достоинств и недостатков системы Оберон. С нашей точки зрения, основными достоинствами этой системы (унаследованными системой Мифрил) являются:

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

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

Основной причиной такого ограничения расширяемости является проблема эффективности (см. [34]). Использование сообщений (расширяемых записей) для реализации расширяемых объектов достаточно дорого по времени исполнения. Переход к языку Оберон-2 и использование методов (type-bound procedure) позволил нам сделать расширяемыми практически все объекты системы.

Перечислим другие недостатки системы Оберон:

  • механизм финализации реализован в ограниченном объеме, и не может быть использован в расширениях;
  • система не поддерживает постоянные объекты, приводя к тому, что различные приложения вынуждены определять такие объекты;
  • слабый уровень графики, неадекватный графический интерфейс;
  • реализация только неперекрывающихся окон (tiling windows), что усложняет (вернее делает невозможным) использование системы на мониторах с малым разрешением;
  • проблемы с переносимостью, ввиду отсутствия четкого разделения между системно-зависимыми и системно независимыми компонентами.

Все эти недостатки существенно сказываются на распространении системы. При проектировании системы Мифрил мы пытались преодолеть недостатки, сохраняя достоинства системы.


Основные отличия системы Мифрил:

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

Разработка базового набора классов системы Мифрил в настоящее время завершена. В следующих версиях системы мы планируем добавить понятия процесса и возможности (квази-)параллельного исполнения.


Введение    Глава 1    Глава 2    Глава 3    Глава 4    Заключение    Литература    Приложения