Безопасная коммуникация

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

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

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

Представление данных

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

Например, данные некоторой записи должны располагается в памяти специфическим образом, чтобы соответствовать требуемой структуре файла. Допустим, речь идет о структуре Key из главы «Безопасное создание объектов»:

type Key is limited record
   Issued: Date;
   Code: Integer;
end record;

где тип Date, в свою очередь, имеет следующий вид:

type Date is record
   Day: Integer range 1 .. 31;
   Month: Integer range 1..12;
   Year : Integer;
end record;

Предположим, что наша целевая машина использует 32-х битные слова из четырех байт. Принимая во внимания ограничения диапазона, день и месяц свободно помещаются в один байт каждый и для года остается 16 бит (мы игнорируем «проблему 32768 года»). Вся запись красиво ложится в одно слово. Выразим это следующим образом:

for Date use record
   Day at 0 range 0 .. 7;
   Month at 1 range 0 .. 7;
   Year at 2 range 0 .. 15;
end record;

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

for Key use record
   Issued at 0 range 0 .. 31;
   Code at 4 range 0 .. 31;
end record;

В качестве следующего примера рассмотрим тип Signal из главы «Безопасные типы данных»:

type Signal is (Danger, Caution, Clear);

Пока мы не укажем другого, компилятор будет использовать 0 для представления Danger, 1 для Caution и 2 для Clear. Но в реальной системе может потребоваться кодировать Danger как 1, Caution как 2 и Clear как 4. Мы можем потребовать использовать такую кодировку, написав спецификатор представления перечислимого типа:

for Signal use (Danger => 1, Caution => 2, Clear => 4);

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

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

for Signal'Size use 8;

Для Ады 2012 возможна альтернативная запись в виде спецификации аспекта прямо в определении типа:

type Signal is (Danger, Caution, Clear)
   with Size => 8;

Развивая дальше этот пример, допустим, мы хотим чтобы переменная The_Signal этого типа находилась по адресу 0ACE. Мы можем добиться этого:

The_Signal: Signal;
for Signal'Address
   use System.Storage_Elements.To_Address (16#0ACE#);

Значение атрибута 'Address должно быть типа Address, который обычно является приватным. Для преобразования целочисленного значения к этому типу здесь вызывается функция To_Address из пакета System.Storage_Elements.

Эквивалентный способ с использованием аспекта в Ада 2012 выглядит так:

The_Signal: Signal
  with Address => System.Storage_Elements.To_Address (16#0ACE#);

Корректность данных

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

Возьмем для примера опять тип Signal. Мы можем заставить компилятор использовать нужное нам кодирование. Но если по какой-то причине окажется, что значение не соответствует заданной схеме (например, пара бит были изменены из-за ошибки накопителя), то проверить это каким-либо условием будет невозможно, поскольку это выводит наc за область определения типа Signal. Но мы можем написать:

if not The_Signal'Valid then

Атрибут 'Valid применим к любому скалярному объекту. Он возвращает True, если объект содержит корректное, с точки зрения подтипа объекта, значение и False — в противном случае.

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

type Byte is range 0 .. 255;
for Byte'Size use 8;

--  Или, в Ада 2012
type Byte is range 0 .. 255 with Size => 8;

function Byte_To_Signal is new
    Unchecked_Conversion(Byte, Signal);

затем:

Raw_Signal: Byte;
for Raw_Signal'Address use To_Address(16#ACE#);

--  Или, в Ада 2012
Raw_Signal: Byte with Address => To_Address(16#ACE#);
The_Signal: Signal;

case Raw_Signal is
   when 1 | 2 | 4 =>
      The_Signal := Byte_To_Signal (Raw_Signal);
   when others =>
      ... -- Значение не корректно
 end case;

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

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

Взаимодействие с другими языками

Многие большие современные системы написаны на нескольких языках программирования, каждый из которых больше подходит для своей части системы. Части с повышенными требованиями к безопасности и надежности могут быть написаны на Аде (например, с использованием SPARK), графический интерфейс на C++, какой-нибудь сложный математический анализ на Fortran, драйвера устройств на C, и т. д.

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

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

Средства взаимодействия в своей основе используют директивы компилятору (pragma). Предположим, существует функция next_int на языке C и мы хотим вызвать ее из Ады. Достаточно написать:

function Next_Int return Interfaces.C.int;

pragma Import (C, Next_Int);

Эта директива указывает, что данная функция имеет соглашение о вызовах языка C, и что в Аде не будет тела для этой функции. В ней также возможно указать желаемое внешнее имя и имя для редактора связей, если необходимо. Предопределенный пакет Interfaces.C предоставляет объявления различных примитивных типов языка C. Использование этих типов позволяет нам абстрагироваться от того, как типы языка Ада соотносятся с типами языка C.

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

procedure Action(X,Y: in Interfaces.C.int);

pragma Export (C, Action);

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

type Response is access procedure (X,Y: in Interfaces.C.int);
pragma Convention (C, Response);

procedure Set_Click(P: in Response);
pragma Import(C, Set_Click);

procedure Action(X,Y: in Interfaces.C.int);
pragma Convention(C, Action);
...
Set_Click(Action'Access);

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

Потоки ввода/вывода

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

В качестве простого примера, рассмотрим файл, содержащий значения Integer, Float и Signal. Все типы имеют специальные атрибуты 'Read и 'Write для использования потоков. Для записи мы просто напишем:

S: Stream_Access := Stream(The_File);
...
Integer'Write(S, An_Integer);
Float'Write(S, A_Float);
Signal'Write(S, A_Signal);

и в результате получим смесь значений в файле The_File. Для экономии места опустим детали, скажем только, что S обозначает поток, связанный с файлом.

При чтении мы напишем обратное:

Integer'Read(S, An_Integer);
Float'Read(S, A_Float);
Signal'Read(S, A_Signal);

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

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

type Root is abstract tagged null record;

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

type S_Integer is new Root with record
   Value: Integer;
end record;

type S_Float is new Root with record
   Value: Float;
end record;

и так далее. Запись примет следующий вид:

Root'Class'Output(S, (Root with An_Integer));
Root'Class'Output(S, (Root with A_Float));
Root'Class'Output(S, (Root with A_Signal));

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

При чтении мы могли бы написать:

Next_Item: Root'Class := Root'Class'Input(S);
...
Process(Next_Item);

Процедура Root'Class'Input читает тег из потока и затем вызывает нужный 'Read атрибут для чтения всего объекта, а затем присваивает полученное значение переменной Next_Item. Далее мы можем обработать полученное значение, например, вызвав диспетчеризируемую подпрограмму Process. Пусть ее задачей будет присвоить полученное значение нужной переменной, согласно типу.

Для начала объявим абстрактную процедуру для базового типа:

procedure Process(X: in Root) is abstract;

затем конкретные варианты:

overriding procedure Process(X: S_Integer) is
begin
   An_Integer := X.Value; -- достаем значение из обвертки
end Process;

Безусловно, процедура Process может делать все, что мы захотим с полученным значением.

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

Фабрики объектов

Мы только что видели, как стандартный механизм потоков позволяет нам обрабатывать значения различных типов, поступающих из файла. Лежащий в основе механизм чтения тега и затем создания объекта соответствующего типа стал доступен пользователю в Аде 2005.

Допустим, мы обрабатываем геометрические объекты, которые мы обсуждали в главе «Безопасное ООП». Различные типы, такие как Circle, Square, Triangle и т. д. наследуются от базового типа Geometry.Object. Нам может понадобиться вводить эти объекты с клавиатуры. Для окружности нам понадобиться две координаты и радиус. Для треугольника — координаты и длины трех сторон. Мы могли бы объявить функцию Get_Object для создания объектов, например для окружности:

function Get_Object return Circle is
begin
   return C: Circle do
      Get(C.X_Coord); Get(C.Y_Coord); Get(C.Radius);
   end return;
end Get_Object;

Внутренние вызовы Get это стандартные процедуры для чтения значения примитивных типов с клавиатуры. Пользователь должен ввести какой-то код, чтобы обозначить, какого рода объект он хочет создать. Предположим, что ввод окружности обозначается строкой "Circle". Также предположим, что у нас уже есть функция для чтения строки Get_String.

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

generic
   type T(<>) is abstract tagged limited private;
   with function Constructor return T is abstract;
function Generic_Dispatching_Constructor(The_Tag: Tag)
   return T'Class;

Эта настраиваемая функция имеет два параметра настройки. Первый определяет конструируемый класс типов (в нашем случае Geometry.Object, от которого происходят Circle, Square и Triangle). Второй - это диспетчеризируемая функция для создания объектов (в нашем случае Get_Object).

Теперь мы настроим ее и получим функцию создания геометрических объектов:

function Make_Object is new
  Generic_Dispatching_Constructor(Object, Get_Object);

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

Мы могли бы определить ссылочную переменную для хранения вновь созданных объектов:

Object_Ptr: access Object'Class;

Если тег хранится в переменной Object_Tag (имеющую тип Tag из пакета Ada.Tags, там же объявлена и функция Generic_Dispatching_Constructor), то мы вызовем Make_Object так:

Object_Ptr := new Object'(Make_Object(Object_Tag));

и получим новый объект (например окружность), чьи координаты и радиус были введены с клавиатуры.

Чтобы закончить пример, нам осталось научиться преобразовывать строковые коды объектов, такие как "Circle", в теги. Простейший случай сделать это — определить атрибут 'External_Tag:

for Circle'External_Tag use "Circle";

for Triangle'External_Tag use "Triangle";

тогда прочесть строку и получить тег можно так:

Object_Tag: Tag := Internal_Tag(Get_String);

Конечно, использовать отдельную переменную Object_Tag не обязательно, поскольку мы можем объединить эти операции в одну:

Object_Ptr := new Object'(Make_Object(Internal_Tag(Get_String));

Напоследок следует заметить, что приведенный код немного упрощен. На самом деле настраиваемый конструктор имеет вспомогательный параметр, который мы опустили.